# Changelog

## 2.4.0

A minor release: one new entry, `@sonordev/contracts/schema-placeholders`, and the llms.txt sanitizers stop cutting mid-word. Every other entry is unchanged.

- **New entry `@sonordev/contracts/schema-placeholders`**: whether stored JSON-LD carries a template's unfilled slots, so the API that serves it and the site that renders it drop the same nodes.
  - `withoutSchemaPlaceholders(value)` returns `{ value, dropped, notes }`: the JSON-LD without its placeholder nodes (the same reference when nothing was found, `null` when nothing real is left), every node it removed with the reason and the evidence, and the AI note keys it stripped from nodes it kept.
  - `findSchemaPlaceholders(value)` returns just the nodes, and `describeSchemaPlaceholder(node)` turns one into a line a person can act on.
  - It finds reserved example domains (`example.com`, `example.net`, `example.org`, `.example`) in URLs, hosts and emails, whole-value template names ("Example", "Your Business Name"), placeholder phones, bracketed and JSX slots (`[Resident Name]`, `{plan.name}`, `{{business_name}}`), `REPLACE_WITH_` markers, and objects that are only a note. A URI template's `{search_term_string}`, a real name that contains "Example" and a 555 number outside the fiction range are left alone.
- **`llms`**: `clipAtWordBoundary(text, max)` cuts prose to a length without splitting a word. It ends at a sentence when one closes in the last 40% of the window, otherwise at the last whole word with an ellipsis, and never returns more than `max` characters. `sanitizeLlmsPublicSummary` and `sanitizeLlmsDisclaimerLine` now cut with it, so a long summary ends at a sentence or a whole word instead of mid-word. The limits are unchanged (400 and 280).

## 2.3.0

A minor release: one new entry, `@sonordev/contracts/error-page`. Every other entry is unchanged.

- **New entry `@sonordev/contracts/error-page`**: telling a rendered error page from a page's content, and copy that calls a working page missing from copy that describes it, so every writer and reader of page text answers the same way.
  - `looksLikeErrorPage(text, wordCount?)` is true when stored text is an error page: a 404 in its common wordings, a framework or server error, a status page, a "something went wrong" or "temporarily unavailable" screen or a maintenance notice. It reads how the text opens, decodes entities, doesn't depend on word boundaries (scraped text runs elements together, as in "404This page could not be found."), and leaves phone numbers, addresses and pages that only describe an error alone.
  - `usablePageText(text, wordCount?)` returns the text, or `null` for an error page.
  - `findErrorPageClaim(copy, pageText?)` and `describesErrorPage(copy, pageText?)` find generated copy that says the page itself is missing or unavailable ("Page Not Found", "this page is currently unavailable", "returns a 404", "a page-not-found message"). A phrase the page's own text really says is allowed, and copy about something unavailable on a working page isn't a claim about the page.
  - `findPageCopyClaim(fields, pageText?)` runs that check over every string in a set of fields, JSON-LD included, and returns the first `{ field, phrase }`.
  - `ERROR_PAGE_MAX_WORDS` and `ERROR_PAGE_CONTRACT_VERSION` are exported alongside.

## 2.2.0

A minor release: one new entry, `@sonordev/contracts/proposal-sitemap`. Every other entry is unchanged.

- **New entry `@sonordev/contracts/proposal-sitemap`**: how a website proposal's site plan counts its pages, so the app that writes a proposal, the API that checks it and the page that shows it state the same numbers.
  - `countSitemapPlan(plan)` counts core pages, the plan's own pages, re-published articles, pages carried over and pages added, each address once.
  - `sitemapTransformation(plan)` decides whether the plan's before-and-after comparison shows. It shows only when the build is bigger than today; a rebuild that keeps the same pages never reads as "N → 0".
  - `normalizeSitemapLabels(labels)` keeps the words a plan may use for pages that aren't services sold to industries.
  - `proposalPageCountIssues(sections)` and `describePageCountIssue(issue)` find page and URL counts stated in a proposal's headline, summary, pricing and plan copy that don't match the plan.

## 2.1.1

- Docs only: the README names the Sonor API and the dashboard where it named internal repositories. No code changes.

## 2.1.0

A minor release: one new entry, `@sonordev/contracts/voice`. Every other entry is unchanged.

- **New entry `@sonordev/contracts/voice`**: the house voice's contraction rule, so every app that checks or rewrites copy agrees on it.
  - `findUncontracted(text, options?)` finds full forms that should be
    contractions ("do not" to "don't", "it is" to "it's", "let us look" to
    "let's look") and returns each one with its position, the matched text and
    the suggestion, first-letter case kept.
  - It leaves alone what can't contract where it stands: a clause-final "it
    is" or "we are" (negations skip that guard, so "We do not." still becomes
    "We don't."), "what it is and why", "can not only", a "what is" question, a
    heading that opens with "What Is" (when `titleLike` is set), and "let us
    know" and "let us help". "let us" only contracts at the start of a sentence and
    before look, start, begin, consider, turn, see, review, compare, break,
    dig, recap, unpack, explore, examine, revisit, focus, go, talk, move, "get
    started", "step back" or "zoom out". "have" and "has" on their own are
    never touched, though "have not" and "has not" still contract.
  - `contractText(text, options?)` applies those findings and returns the new
    text.
  - `findContracted(text, options?)` is the reverse rule for question-form
    headings and FAQ questions: it finds contractions ("What's") and suggests
    the full form ("What is").
  - `VOICE_MASK` is the one character that stands in for text the rule must
    not touch.
- **Options**: `titleLike` marks a heading or title. `protectedRanges` takes
  half-open `[start, end)` ranges (code, quotations, link destinations) that
  are never rewritten and never read as a clause end, except a protected line
  break, which still ends its line. An infinite bound means
  "from the start" or "to the end". A malformed range (NaN, not a number,
  start after end), or a truthy value that isn't an array, protects the whole
  text, so a bad range can never cause code to be rewritten.
- **Lines**: multi-line text is read one line at a time. Nothing matches
  across a line break, and the end of a line ends a clause. A "\r" before a
  "\n" or at the very end of the text is part of the line ending. Hits carry
  offsets into the whole string.
- **Known limits**: the rule is deliberately plain. "must not only" and "will
  not only" contract, "need not" contracts even where "need" is a noun, "let
  us" is read by its position and next word rather than its meaning, a
  decimal, a domain or an initial hides a "what is" question, a straight
  apostrophe doesn't end a clause, only spaces and tabs join the words of a
  pair, all-caps input gets a mixed-case suggestion, and `findContracted`
  always expands "'s" to "is" and "'d" to "would" and expands "ain't" like any
  other "n't" word ("ai not"). The README lists each one with an example.
- Anything that isn't a string is read as no text, so a stray number or object
  can't throw.
- No new dependencies, and CommonJS and ESM builds like every other entry.

## 2.0.0

A major for one entry, `slots`, plus three new entries; every other entry is unchanged.

- **Slots contract v2**: `rich`, `link` and `list` content types beside
  `text`, with a structured `value` that the signature now covers.
  `SLOTS_CONTRACT_VERSION` is 2 and `signSlotContent` / `verifySlotContent`
  default to v2. Pass `1` as the version to keep signing and verifying the
  pre-v2 text-only form, which older site-kit releases verify.
- **`normalizeSlotValue(type, { content, value })`**: the one rule for what
  a valid slot of each type is, returning the canonical content and value to
  store and sign.
- **`parseSlotRichText(markdown)`**: the rich-text subset (paragraphs, line
  breaks, bold, italic, safe links, lists) as a small AST to render as
  elements, never HTML.
- **New entry `@sonordev/contracts/slot-content`**: the pure half of the
  slots contract (types, `normalizeSlotValue`, `isSafeSlotHref`,
  `parseSlotRichText`) with no `crypto`, so a browser can import it.
  `slots` re-exports all of it.
- **New entry `@sonordev/contracts/site-cache`**: how Sonor refreshes a
  site's cached pages. `SITE_CACHE_TAGS` (the tag names site-kit's fetches
  carry and the Sonor API expires), `SITE_REVALIDATE_PATH`, the webhook payload
  and its `ping`, and `isSiteRevalidatePong`.
- **New entry `@sonordev/contracts/site-edit`**: Sonor's Edit on page. The
  messages between a framed site and the dashboard, which origins may drive
  the overlay (`isSonorAppOrigin`), and `slotNeedles`, the visible text used
  to find a slot on the page.

## 1.0.0

First release. The rules site-kit 6.x published as `@sonordev/site-kit/*/contract`
(website, color, sites, seo-pages, seo-meta, llms, forms, fleet, slots,
portfolio), as a dependency-free package, so the Sonor APIs and the
dashboard import them instead of keeping copies.
