# Changelog

## 0.24.49

UNVERIFIED: this release has no Unreleased entry and the package-input comparison could not be made (git diff cd06da58af..bc1fc5c3ce failed (commit not in this checkout)). Whether package inputs changed is unknown; consumer action is unknown.
Check: `git diff cd06da58af bc1fc5c3ce -- apps/shared/content-ir-core`

## 0.24.48

- Routed kind components now receive partial values as they arrive, regardless of `partialReady`; `partialReady: false` no longer opts a kind out. If a component throws on a partial value, the boundary reports it, quarantines that kind for the session, and falls back to its loading skeleton. Consumer action: custom kind components must tolerate fields that have not arrived yet; do not rely on `partialReady: false` to delay rendering.

## 0.24.47

- **A `__kind` key is not a kind (content-ir rule 28, 2026-10-25).** `applyIrKindRoute` returns a
  block whose slug has no definition and no component row UNTOUCHED (plain JSON) instead of routing
  it to the generic floor; the `"unregistered"` `GenericFallbackReason` is removed. New
  `setKindSlugOracle` / `namesNoKind` (`surfaces`): `jsonKindSignal` answers `not_kind` and
  `valueCarriesKind` answers false for a slug the host measured as absent; with no oracle installed
  nothing changes. Consumer action: a host that wants the behaviour installs an oracle that answers
  "absent" only from a settled registry (rich-content does).
- **No kind is named for you (content-ir rule 28, 2026-10-25).** `convertAiSchemaToBlockFields` /
  `runSchemaConversion` no longer synthesize an item kind (`<schema>_<field singular>`) for an array
  of objects whose items declare no `__kind`. Such a field is now `json[]` with a warning
  ("holds objects with no declared kind — kept as JSON"); no child draft, no array binding, no
  `__kind` injected into the agent schema. Items that DECLARE a kind are unchanged. Consumer
  action: none for registered kinds that declare their item kinds; a Shape proposal that relied on
  derived child kinds now registers one kind and must declare item kinds to type its items.

## 0.24.46

PACKAGE INPUTS CHANGED WITH NO RELEASE NOTE. This release includes package inputs that nobody described in an Unreleased entry,
so they may include breaking API changes. Consumer action is UNKNOWN: read the diff before upgrading.
Diff: `git diff ef92845a81 6018c56140 -- apps/shared/content-ir-core`

Changed package inputs (1):
- `apps/shared/content-ir-core/react/KindInstanceRender.tsx`

## 0.24.45

PACKAGE INPUTS CHANGED WITH NO RELEASE NOTE. This release includes package inputs that nobody described in an Unreleased entry,
so they may include breaking API changes. Consumer action is UNKNOWN: read the diff before upgrading.
Diff: `git diff fb9f0f33da ef92845a81 -- apps/shared/content-ir-core`

Changed package inputs (11):
- `apps/shared/content-ir-core/kind-kit/KindAudioDefault.tsx`
- `apps/shared/content-ir-core/kind-kit/KindEmbedDefault.tsx`
- `apps/shared/content-ir-core/kind-kit/KindFile.tsx`
- `apps/shared/content-ir-core/kind-kit/KindHeaderBar.tsx`
- `apps/shared/content-ir-core/kind-kit/KindImageDefault.tsx`
- `apps/shared/content-ir-core/kind-kit/KindPanel.tsx`
- `apps/shared/content-ir-core/kind-kit/KindPanelGrid.tsx`
- `apps/shared/content-ir-core/kind-kit/KindVideoDefault.tsx`
- `apps/shared/content-ir-core/kind-kit/SortableList.tsx`
- `apps/shared/content-ir-core/kind-kit/StreamingSkeleton.tsx`
- `apps/shared/content-ir-core/kind-kit/TagList.tsx`

## 0.24.44

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff f1984cb2e2 fb9f0f33da -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.43

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 0a2b8c9356 f1984cb2e2 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.42

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff ece1023750 0a2b8c9356 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.41

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff fca9e3321e ece1023750 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.40

- `directives/item-summary` asks `isUuidShape` from `@ai-matrx/kit/uuid` instead of carrying its own anchored UUID regex.

## 0.24.39

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 7dd0861691 a53838052a -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.38

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 4d24e77604 7dd0861691 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.37

- **`source/html-elements.ts`: THE list of real HTML (`HTML_ELEMENT_NAMES`, `isHtmlElementName`, `isHtmlDoctypeLine`, `htmlRootOf`) (production regression, 2026-10-21).** The source tokenizer reads it before any custom XML container: a document, `<svg>` or non-allow-list element is one `html_block` island, never `xml_container`. Consumer action: none.

- **Glyph path data removed; the same exports now alias the one glyph home (one-canonical, glyph lane, 2026-10-21).** `kind-kit/icons.tsx, kind-kit/file-icons.tsx and react-directives/icons.tsx` defines no SVG any more: each export is `@ai-matrx/design-system/icons`'s glyph with this package's historical size and stroke. Names, props and sizes are unchanged. Consumer action: none.

## 0.24.36

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 4d06d99ff5 e0d747f7a1 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.35

- **Media kit part 1 (lane M1, 2026-10-21).** `KindImage` takes a web address (`"https://…"` or `{ url }`): it asks the host for the bytes with the new `fetch_image` action (image door, image-only, size-capped) and draws a local `blob:` URL, sizing the box from the natural size once loaded so the whole image always shows; Expand opens the address. New `KindFile` (`{ file_id, name?, mime_type?, size? }`): a compact card with type glyph or thumbnail, name, size and Open / Download / Share through `runAction`. New exports `toKindImageRef`, `loadKindImage`, `useKindImageSource`, `resetKindImageCache`, `KindFile`, `kindFileType`. Guards: `__tests__/kind-image.test.tsx`, `__tests__/kind-file.test.tsx`. Consumer action: hosts register `fetch_image` and `download` kind actions (matrx-frontend does).

- **One tree-line rule (Arman 2026-10-20, no duplication).** `source/tree-line.ts` (`isTreeLine`, `TREE_GLYPHS`, `ASCII_TREE_CONNECTOR`, `MIN_TREE_LINES`, exported from `/source`) replaces three copies that had drifted: rich-content's splitter `isTreeLine`, this tokenizer's box-glyph-only test (it never saw an ASCII tree, so Read drew a tree that Write left as prose), and the tokenizer's root walk-back regex (it read a table's `| a |` edge as a connector). The ASCII last branch `` `-- `` is now a connector (dashes then a space; `` `--flag` `` stays prose). `TREE_GLYPHS` left `source/constants.ts`. Guard: `__tests__/tree-line-vectors.test.ts` (generated corpus, Python twin runs the copy) and an ASCII tree document in `source-island-vectors`. Consumer action: import tree detection from `@ai-matrx/content-ir/source`.

- **Text taken out of its home keeps each block's own markdown (2026-10-19).** `unwrapKindEnvelopes` replaced a materialized `<artifact type=… id=…>` with its bare body, so Copy as Markdown of a chart answer wrote raw JSON (pasted as a code block, never a chart). The body is now written the way it renders through the new `artifactBodyAsMarkdown(type, body)`: a fence-promoted type gets its fence (```` ```chart ````, mermaid, svg, map, stats, diff, tasks, html, react …), an XML type its tag (`<timeline>`, `<flashcards>`, `<cooking_recipe>`, `<progress_tracker>` …), a kind value or other JSON a ```` ```json ```` fence, a table stays a table. Guard: rich-content `copy/__tests__/copy-keeps-the-block.test.ts` (every block type splits into the same block after the copy). Consumer action: none.
- **Each React context ships once in the whole dist (2026-10-18).** `/react` (unstamped) carried its own copies of `ContentIrHostContext`, `DirectiveHostContext` and `DirectiveScopeContext` beside the copies in `/host` and `/react-directives`, so a provider from one entry and a consumer from another silently read the default. The two context modules are now their own stamped entries, new subpaths `@ai-matrx/content-ir/host-provider` and `@ai-matrx/content-ir/directive-host`. Every other entry imports them by name (ESM and CJS each hold one instance; proven in a packed consumer). Every entry keeps its export names (runtime import and require). Guard: `../scripts/check-ships-once.mjs` now fails on any `createContext(` module in more than one file. Consumer action: none.

## 0.24.34

- **One tree-line rule (Arman 2026-10-20, no duplication).** `source/tree-line.ts` (`isTreeLine`, `TREE_GLYPHS`, `ASCII_TREE_CONNECTOR`, `MIN_TREE_LINES`, exported from `/source`) replaces three copies that had drifted: rich-content's splitter `isTreeLine`, this tokenizer's box-glyph-only test (it never saw an ASCII tree, so Read drew a tree that Write left as prose), and the tokenizer's root walk-back regex (it read a table's `| a |` edge as a connector). The ASCII last branch `` `-- `` is now a connector (dashes then a space; `` `--flag` `` stays prose). `TREE_GLYPHS` left `source/constants.ts`. Guard: `__tests__/tree-line-vectors.test.ts` (generated corpus, Python twin runs the copy) and an ASCII tree document in `source-island-vectors`. Consumer action: import tree detection from `@ai-matrx/content-ir/source`.

- **Text taken out of its home keeps each block's own markdown (2026-10-19).** `unwrapKindEnvelopes` replaced a materialized `<artifact type=… id=…>` with its bare body, so Copy as Markdown of a chart answer wrote raw JSON (pasted as a code block, never a chart). The body is now written the way it renders through the new `artifactBodyAsMarkdown(type, body)`: a fence-promoted type gets its fence (```` ```chart ````, mermaid, svg, map, stats, diff, tasks, html, react …), an XML type its tag (`<timeline>`, `<flashcards>`, `<cooking_recipe>`, `<progress_tracker>` …), a kind value or other JSON a ```` ```json ```` fence, a table stays a table. Guard: rich-content `copy/__tests__/copy-keeps-the-block.test.ts` (every block type splits into the same block after the copy). Consumer action: none.
- **Each React context ships once in the whole dist (2026-10-18).** `/react` (unstamped) carried its own copies of `ContentIrHostContext`, `DirectiveHostContext` and `DirectiveScopeContext` beside the copies in `/host` and `/react-directives`, so a provider from one entry and a consumer from another silently read the default. The two context modules are now their own stamped entries, new subpaths `@ai-matrx/content-ir/host-provider` and `@ai-matrx/content-ir/directive-host`. Every other entry imports them by name (ESM and CJS each hold one instance; proven in a packed consumer). Every entry keeps its export names (runtime import and require). Guard: `../scripts/check-ships-once.mjs` now fails on any `createContext(` module in more than one file. Consumer action: none.

## 0.24.33

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 11c83a3134 a63e242d70 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.32

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff c1a12e82cd 11c83a3134 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.31

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff e8c948b706 c1a12e82cd -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.30

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 3a39137302 e8c948b706 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.29

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 92167249a3 3a39137302 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.28

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff d6dd84595d 92167249a3 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.27

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff e56db81627 d6dd84595d -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.26

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 3172cf918c e56db81627 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.25
### Unreleased

- **Shared code ships once per side (2026-10-17).** The ESM build was `splitting: false`, so every entry inlined its own copy of every module it reached (58 modules in 2+ unstamped files, 19 in 2+ client files; `index.js` alone 297 KB). ESM now builds in two split passes: the kernel plus the unstamped React entries (`/react`, `/resolver`, `/route`) share `chunk-server-*.js` with no banner, and the "use client" entries share stamped chunks. CJS stays unsplit; declarations unchanged. ESM JS 1.16 MB -> 0.68 MB; every entry's export names identical (ESM and CJS). Guard: `../scripts/check-ships-once.mjs`, run in `build`. Consumer action: none.
- **A kind's own "none" settles it; no lookup waits forever (2026-10-17).** `ComponentResolver.hasSettledFor(kind)` is now also true when this kind's own single-kind read answered with no rows (a recorded cold miss) — before, a kind with no component waited for the whole list, forever on a path that never warmed it. New THE BOUNDED WAIT: the first `requestComponent` for a still-unsettled kind starts a bound (`ComponentResolverOptions.lookupBoundMs`, default exported `KIND_LOOKUP_BOUND_MS` = 10s); past it the kind settles (reported once through `reportError`, the kind repainted), so the route answers with the generic floor instead of an endless loader, and a late answer still lands. Guard: `__tests__/no-unregistered-verdict-before-the-kinds-own-lookup.test.ts`. Consumer action: none.

## 0.24.24

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 3ff38e77bf b5880bf964 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.23

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff dee0337e81 3ff38e77bf -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.22

Automatic changed-only republish (docs/metadata drift since the last published version; verified: `git diff 3260b81ccf dee0337e81 -- apps/shared/content-ir-core/src`
has no shipped-source changes outside tests, fixtures and docs). No consumer action required.

## 0.24.21
### Unreleased

- `KindDefinition.partialReady` doc: progressive is the default; `false` opts a kind out (matches `@ai-matrx/content-ir-react`). Consumer action: none.

## 0.24.20
### Unreleased

- Publish the generated `KIND_DISPOSITIONS` tuple from `/registry`; package CI
  verifies it against matrx-graph's canonical Python tuple. Consumer action:
  import the tuple instead of reading the private Aidream checkout.

## 0.24.19

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.18..npm/content-ir-core/v0.24.19 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.18

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.17..npm/content-ir-core/v0.24.18 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.17

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.16..npm/content-ir-core/v0.24.17 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.16

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.15..npm/content-ir-core/v0.24.16 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.15

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.14..npm/content-ir-core/v0.24.15 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.14

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.13..npm/content-ir-core/v0.24.14 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.13

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.12..npm/content-ir-core/v0.24.13 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.12

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.11..npm/content-ir-core/v0.24.12 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.11

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.10..npm/content-ir-core/v0.24.11 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.10

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.9..npm/content-ir-core/v0.24.10 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.9

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.8..npm/content-ir-core/v0.24.9 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.8

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.7..npm/content-ir-core/v0.24.8 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.7

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.6..npm/content-ir-core/v0.24.7 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.6

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.5..npm/content-ir-core/v0.24.6 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.5

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.4..npm/content-ir-core/v0.24.5 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.4

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.3..npm/content-ir-core/v0.24.4 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.3

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.2..npm/content-ir-core/v0.24.3 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.2
### ajv leaves the first screen

- The kind dual gate builds its ajv instance on first validation instead of at import. A module-level
  `new Ajv()` was a side effect that kept ajv (~34 kB gzip) in every bundle importing the root, even
  for `readEnvelope` alone (chat's message selectors). Behaviour is identical.

Consumer action: none.

## 0.24.1

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.24.0..npm/content-ir-core/v0.24.1 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.24.0 — one thinking-strip rule

- `stripThinking` / `stripThinkingStreaming` / `hasThinkingTags` are the ONE reasoning-block rule
  (alchemy's and print's copies now call it): `<think>` joins `<thinking>` and `<reasoning>`, and
  paragraph structure around a removed block survives — a block between blank lines leaves two
  paragraphs, one on its own line leaves a line break, one inside a line leaves nothing. Tags in
  code stay text, as before.

Consumer action: none. Output differs only in the newlines around a removed block.

## 0.23.0 — the one markdown-source home

- `/source` gains the delimiter guard (`guardMarkdownDelimiters`, `guardMathDelimiters`,
  `guardRunawayLinks`, `reportDelimiterViolations` + types) and the thinking-tag helpers
  (`stripThinking`, `hasThinkingTags`, `stripThinkingStreaming`), moved from `@ai-matrx/kit`. The
  guard's math rules are now THE rules of `source/math-pairs` (kit carried a private copy);
  `opensFlowBlock` is exported there.
- New subpath `/json-extract` (moved from `@ai-matrx/kit/json-extract`): JSON in model text,
  fenced or bare, complete or streaming. New dependencies: `json5`, `jsonrepair`.
- `/text-case` is gone: the implementation lives in `@ai-matrx/kit/text-case`, which this
  package now depends on.

Consumer action: `@ai-matrx/content-ir/text-case` → `@ai-matrx/kit/text-case`.

## 0.22.7

- `kindSchemaFromJsonSchema` / `convertAiSchemaToBlockFields`: a `$ref` to a `$def` that does NOT declare its kind (`__kind` const/enum) is a plain nested structure and now converts IN PLACE — `inline_object` for a field, a nullable `inline_object` for `anyOf: [$ref, null]`. It used to become `{type:"object", kind:"<ClassName>"}`, so the parser demanded `__kind === "<ClassName>"` of every pydantic sub-structure (they carry none) and the whole instance resolved `raw` the moment a kind's database schema reached the registry — the generic key/value fallback instead of the kind's component (Applet builder floating window, 2026-10-07). Live corpus: 397 optional-`__kind` defs across 134 active kinds, plus 89 defs with no `__kind`. A def that declares its kind is still a kind reference; array-item `$ref`s are unchanged.

**Consumer action:** `pnpm up @ai-matrx/content-ir@latest`. Nothing else — the kind registry's cold fetch derives through this converter.

## 0.22.6

- `startsLikeTableRow` / `isGrowingDelimiterRow` (the streaming forms of the table rule) now live in `source/gfm-table.ts` beside the rest of THE GFM table rule; the public exports are unchanged (AP-2: `check-table-readers` green with an empty baseline).

**Consumer action:** none.

## 0.22.5

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.22.4..npm/content-ir-core/v0.22.5 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.22.4

- The cross-package one-code-range gate is green against Rich Content's readable-copy path, which now consumes the canonical `fenceOpenerOf`, `closeFence`, and `FenceLines` contract instead of defining a private fence parser.

### Consumer action

- None required. The public Content IR API is unchanged.

## 0.22.2
### Unreleased

### Portable registry declarations

- Install the React type declarations required by the public registry contracts.
  The parser still has no React runtime dependency.
- Check the packed package from an empty TypeScript project through both ESM
  and CommonJS with library checking enabled and no ambient workspace types.

### Consumer action

- None required. Clean consumers no longer need to add `@types/react` manually.

## 0.22.1

First published release of the 0.22 line (the `npm/content-ir-core/v0.22.0` tag never published: the
`one-code-range-rule` census was red against the relocated chat tree; it now records the real tree).

## 0.22.0 (unpublished)

### New subpath `@ai-matrx/content-ir/surfaces` — kind-aware text surfaces (rich-content extraction)

- Moved verbatim from `@ai-matrx/chat` `utils/content-ir/**` (parked there by chat-package-move P14): the JSON-kind
  signal, embedded kind JSON, kind value → markdown, one-line and label projections, quoted-kind lift, escaped-kind
  recovery, image-alt unwrap, the instance-title rule, the legacy quiz envelope, the envelope parity predicate and the
  decision-answers ids. Pure TS, shared by both block splitters and the server-side shape doctors.
- `registerKindValueMarkdown` keeps its registration on `globalThis` (`Symbol.for("ai-matrx.content-ir.kind-value-markdown")`)
  and announces an unregistered host once on the console.
- Strict-mode fixes only (`noUncheckedIndexedAccess` assertions where the bound is already checked); no behavior change.

### `/source` `readXmlTag`: requires a `<` at `start`

- The chat copy carried the RC-B3r guard and this one did not: `description: >-` was read as a tag named `escription:`
  when a caller started one character early. Now one reader with the guard; the chat copy is deleted in rich-content
  phase 3.

### Consumer action

- None required. New code should import these helpers from `@ai-matrx/content-ir/surfaces`, not from `@ai-matrx/chat`.

## 0.21.2

### `/source`: a ```json fence's string state never crosses a line end (never-raw R11 H2)

- `FenceReader` (behind `closeFence`, `fenceLineKinds`, `findCodeRanges`, the XML / directive balance walkers) read
  JSON-string state across lines inside a ```json fence. A JSON string never holds a raw newline, so a body line
  that ends inside one (`{\"__kind\":\"note\"}` — escaped quotes, a cut value) swallowed the closing ``` and every
  following list item / quoted line. The state is now read per line; a backtick run inside a string on the same line
  is still text.

### Consumer action

- None required. matrx-frontend may drop its splitter workaround (`jsonLineForFenceReader` in
  content-splitter-core.ts) once on 0.21.2.

## 0.21.1

### `/source`: the tokenizer locks what the renderer draws (fuzz `source-covers-renderer`)

- A `~~~` island now ends at its closer. The 0.14.2 "renderer overflow" assumed the renderer did not know `~~~`
  (it has since matrx-frontend A4, 2026-09-30), and swallowed the next fence's opener into the tilde island,
  leaving a following ````` ````md ````` block editable.
- A JSON-family fence (unlabelled / json / jsonc / json5) opened on a quoted line (`> ```) is an island up to its
  own closer, which may carry no `>`: the renderer lifts such a region out of its quote (V1 quoted-kind-lift). A
  quoted fence of another language stays prose (source).

### Consumer action

- None required.

## 0.21.0

### `/source`: the markdown text helpers move in from matrx-frontend (chat-package-move P14)

- New on `@ai-matrx/content-ir/source`, unchanged in behavior: `unwrapCodeSpans`, `removeCodeSpans`,
  `replaceFences`, `soleFence`; `markdownToPlainText`, `mathToReadable`, `unwrapKindEnvelopes`; `cleanMarkdown`;
  `FENCE_META_KEY`, `splitFenceInfo`, `parseFenceMeta`, `parseLineRanges` (+ `FenceInfo`, `FenceMeta`);
  `DIRECTIVE_CONTAINER_OPEN`, `DirectiveContainerTracker`; `TITLED_IMAGE_LINE`; `isInsideInlineCode`,
  `indexOutsideInlineCode`, `hasUnclosedBacktickRun`; `initialXmlBalance`, `findBalancedXmlClose`
  (+ `XmlBalanceState`, `XmlBalanceOptions`); `findWholeTableStart`, `startsLikeTableRow`,
  `isGrowingDelimiterRow`, `trailingTableStart`.
- `splitFenceInfo` omits `language` / `meta` when empty instead of setting them to `undefined`.

### Consumer action

- matrx-frontend: import these from `@ai-matrx/content-ir/source` (the app's old modules re-export or are gone).

## 0.20.1

### `humanizeIdentifier`: already-human text, plurals, null

- Text that is already words (whitespace, no `_`) comes back as written: `"Customer's iPhone model"` and
  `"Version 2.0"` are no longer split and re-cased; only a lowercase first letter is capitalized.
- A plural acronym stays whole: `userIDs` / `user_ids` → "User IDs" (was "User I Ds" / "User Ids").
- `null` / `undefined` return `""` instead of throwing.
- Parity corpus rows added; the Python twin is now `matrx_utils.text_case.humanize_identifier`.

### Consumer action

- None required.


## 0.20.0

### `KindDefinition.schemaSource: "table"` + `table` facet (KINDS-GLUE wave 3 slice 4)

- A `table:<uuid>` kind ("a record of that Table") is answered by the host from the Table's live Fields, never a
  `kind_definition` row. `schemaSource` gains `"table"`; `KindDefinition.table?: TableKindFacet` carries the
  Table id, organization, name, title field, stamp, field ids, options-table ids and the store's refusal sentence.

### Consumer action

- None required; a host that switches on `schemaSource` exhaustively handles `"table"`.

## 0.19.28

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.27..npm/content-ir-core/v0.19.28 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.


## 0.19.27

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.26..npm/content-ir-core/v0.19.27 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.26
### `./text-case`: the one text-case implementation

- New subpath **`@ai-matrx/content-ir/text-case`** — `formatText` & friends plus `humanizeIdentifier` / `displayLabel` (THE identifier-to-words rule). `@ai-matrx/kit/text-case` re-exports it; `utils/text-case-converter.ts` (a byte-copy of kit's) is deleted.
- `titleCaseToken` and `formatBlockLabel` go through `humanizeIdentifier`: `plan_node_patch` → "Plan Node Patch".

### Consumer action

- None required; apps import through `@ai-matrx/kit/text-case`.
## 0.19.25

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.21..npm/content-ir-core/v0.19.25 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.24

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.21..npm/content-ir-core/v0.19.24 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.23
### Unreleased

- `/convert` **The control keys, one definition in two languages** (KINDS-GLUE wave 2 slice 1a). `withControlKeys(schema, { mode: "stored" | "authored", many, allowEdit })` is the byte-identical twin of aidream's `matrx_graph.kind_control_keys.with_control_keys` (shared golden `packages/matrx-graph/tests/fixtures/kind_control_keys_golden.json`): stored mode declares `_replaces` / `_new` / `_record_id` / `_records` optional and lifts the root's required Fields for a patch or a batch with one appended `allOf` branch; authored mode adds `_replaces`/`_new` always, `_record_id` only with `allowEdit`, and with `many` the root is the batch `{__kind, _records: [...]}`. `withControlKeyFields(kindSchema)` adds the four as optional render fields; `kindSchemaFromJsonSchema(kind, schema, { disposition })` applies it for `record` kinds. Consumer action: pass the registry row's `metadata.disposition` to `kindSchemaFromJsonSchema` (matrx-frontend `schema-source-kind-tables.ts`).

## 0.19.22

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.21..npm/content-ir-core/v0.19.22 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.21
### Unreleased

- `/core` **A MISSING FIELD NEVER DEGRADES A BLOCK** (Arman, 2026-09-28). `validateObjectAgainstSchema` / `validateObjectAgainstFields` no longer turn an absent `required` field into a `raw_fallback`: the block resolves, the snapshot fills the field blank (`emptyValueForFieldSchema`), and the gap rides the residue. Only a PRESENT value of the wrong type is still a contract violation. A pure prediction (`expectedRootKind` / parent-schema speculation, no `__kind`, no surface) claims an object only when it carries at least one of the kind's fields, so unrelated data is never hidden behind an empty component. Effect: legacy `json_root_key` payloads (`quiz_title`, `comparison`, `decision_tree`, `diagram`, `presentation`, `math_problem`) now resolve to their kind while STREAMING and in client-only parses, instead of showing "missing required field" until the server's saved copy arrived. Consumer action: kind bridges must read their legacy wrapped/renamed shape (matrx-frontend `unwrapLegacyRoot` in `features/content-ir/kinds/legacy-bridge-utils.ts`).

## 0.19.20
### Unreleased

- `/source` **The tokenizer's raw-HTML allow-list is the renderer's list** (verify-RC-B4 round 16): `ALLOWED_RAW_HTML_TAGS` (and `HTML_BLOCK_LEVEL_TAGS`) gain `details` and `summary`, which the screen's `rehypeSafeRawHtml` renders — a line-leading `<details>` is now an `html_block` island like `<table>`, not an inline `xml_tag`. Pinned by a shared vector: the list must equal the frontend-generated `rawHtmlTags` in `gfm-table-vectors.json` (content-ir and both Python twins). Island vectors: 1 of 896 changed; Python fixture copied.

## 0.19.19
### Unreleased

- `/source` **THE ONE DELIBERATE GFM DEVIATION** (verify-RC-B4 round 16 ruling: content never silently disappears): a line opening an HTML block whose element every sanitizer strips — `STRIPPED_HTML_BLOCK_NAMES` (`link`, `nav`, `header`, `footer`, `article`, `aside`, `main`, `form`, `iframe`, `title`, … the type-6 names neither the screen's raw-HTML allow-list nor print's GitHub schema keeps) — ends that block at its own line, so a table or list under it is read as markdown (`insideHtmlBlock`; new `opensStrippedHtmlBlock(trimmed)`). GFM keeps such lines as raw HTML the sanitizer then empties; a stored answer quoting `<link rel="canonical" …/>` above its content lost every list that way. Python twin moved; `tableStarts` vectors +4 (the frontend's GFM judge applies the deviation). Documented in FEATURE.md § `source/`.

## 0.19.18
### Unreleased

- `/source` **An unclosed HTML block tag owns nothing** (verify-RC-B4 round 9): a line-leading `<div>`, `<section>`, `<details>`… that never closes is no `xml_container` island running to the end of the text — GFM ends that HTML block at the blank line, so the text after it stays prose (the Visual editor showed a whole note swallowed as a "Section · div, not closed" island). The tag stays an inline `xml_tag` island; a closed container is still one island; an unclosed custom tag still owns the rest. Python twin moved; island vectors +5.

## 0.19.17
### Unreleased

- **Release note:** 0.19.16 was tagged but never reached npm — its tag points at a commit whose workspace lockfile (`apps/pnpm-lock.yaml`) was out of date, so every publish retry of that immutable tag fails at `pnpm install --frozen-lockfile`. The round-9 changes it carried ship in the next version.

## 0.19.16

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.14..npm/content-ir-core/v0.19.16 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.15

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.13..npm/content-ir-core/v0.19.15 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.14

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.12..npm/content-ir-core/v0.19.14 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.13

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.12..npm/content-ir-core/v0.19.13 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.12

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.11..npm/content-ir-core/v0.19.12 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.11

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.10..npm/content-ir-core/v0.19.11 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.10

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.9..npm/content-ir-core/v0.19.10 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.9
### Unreleased

- `/source` **Round 9 verifier findings (verify-RC-B4 R9-1, R9-4, R9-5):** a footnote definition (`[^1]: …`) ends a table (Python twin moved); a table never opens INSIDE an HTML block that opened above it with no blank line (`<details>` then `| a | b |` is raw HTML — types 1–5 end at their marker, 6–7 at the blank line, 7 cannot interrupt a paragraph); new THE link-reference rule — `collectLinkDefinitions(text)`, `findLinkDefinitionLines(text)`, `resolveReferenceLinks(fragment, defs)`, `normalizeLinkLabel` — so a cell or split block resolves `[Apple Support][1]` against the document's definitions, as GFM does (a definition cannot interrupt a paragraph or a row run). Vectors +2 ends, +11 starts, +1 document.
- `/source` **`isHtmlBlockTagName(name)`** (verify-RC-B4 round 9): the tag names CommonMark reads as an HTML BLOCK when they open a line (conditions 1 and 6: `div`, `details`, `section`, `script`, …; case-insensitive) — so a splitter can leave such a line to the markdown renderer, ending where GFM ends it, instead of opening a custom XML container that owns what follows.
- `/source` **A table ends where GFM ends it (verify-RC-B4 round 9):** `continuesTable` / `findTableEnd` end a table at an HTML block start (all seven CommonMark 4.6 conditions — `<script`/`<pre`/`<style`/`<textarea`, `<!--`, `<?`, `<!X`, `<![CDATA[`, a block tag name, or any other complete tag alone on its line such as `</artifact>`) and at indented code: a line 4+ columns past the table's container. New `startsHtmlBlock`, `lineIndent`, `tableContainerIndent` (the content column of the list item a table sits in); `continuesTable(line, containerIndent = 0)`. A lone `|` is a row (one empty cell) — `rowCells("|")` is `[""]`, no longer `[]`. Since 0.19.4 an HTML line or an indented line after a table was read as a table row. Shared vectors gain `tableEnds` (30 cases, each judged against remark-gfm in matrx-frontend); the Python twin moved with them.
- `/source` **A table never opens on a lazy continuation line (verify-RC-B4 round 9):** `opensTable` / `tableStartsAt` / `findTableStart` refuse a header that lazily continues a list item's or a block quote's paragraph (no blank line above, not indented to the item's content column, or missing the quote's `>`) — GFM reads it as paragraph text (`- item` then `| a | b |` is no table). Vectors gain `tableStarts` (15 cases, judged against remark-gfm).
- `/source` **A table opens exactly where GFM opens one (verify-RC-B4 round 9):** `tableStartsAt` requires the delimiter row to match the header's width for pipe-led headers too (`| a | b |` over `|---|` is no table), refuses a "delimiter" that is a list item (`- | -`), and refuses a header indented 4+ columns past its container (indented code). `startsPipelessTable` accepts a header with one pipe (`| a`) or none (`Notes` over `|---|` is a one-column table), as GFM does. Vectors gain 8 opening cases.

## 0.19.8

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.7..npm/content-ir-core/v0.19.8 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.7
### Unreleased

- `/source` **THE fence opener follows GFM (CommonMark 4.5):** a backtick run whose info string holds a backtick opens NO fence — "```` code ```` | b" and "``` `x` | b" are code spans. Applied at the one source (`parseFenceOpener` → `fenceOpenerOf`), so the tokenizer's islands, `findCodeRanges`, content cleanup and every splitter move together; `opensStrictFence` is now just `fenceOpenerOf(line) !== null`. Fence-nesting vectors regenerated (+5 openers, +2 documents); the Python twin moved with them.

## 0.19.6
### Unreleased

- `/source`: a table row opening with a backtick run whose info string holds a backtick ("```` code ```` | b") continues the table, as GFM reads it — fences in the table rule are judged by the new `opensStrictFence` (CommonMark 4.5), in the code-range owner. New `findInlineCodeSpans` / `mapInlineCodeSpans` for INLINE content (a table cell, a label) where a leading ``` is a code span, never a fence (verify-RC-B4 round 8).

## 0.19.5

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.19.4..npm/content-ir-core/v0.19.5 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.19.4
### Unreleased

- `/source` `continuesTable` / `findTableEnd`: a table runs exactly where GFM
  runs it (spec example 202) — to the first blank line or block start; a line
  without a pipe and a lone `|` are rows (verify-RC-B4 R7-2). The Python twin
  (`gfm_table_lines.continues_table`) moved with it.

## 0.19.3

Automatic republish — despite the generated note, this version carries source
changes: **RC-B3 round 4 close-out** (verify-RC-B3 § round 4, RC-B3 lane; swept
into aidream 57b7a956da by the local-work sweeper).

- `/source` streaming (D1): a footnote's lazy connector lines that a tree may
  still claim hold the note open; an inline island read out of a paragraph
  that reaches into provisional text is held; a note that took in provisional
  text (a `$$` that may still pair across a blank line) is held. Each pinned by
  a reproducer that fails when its hold alone is removed.
- D2: the provisional mark in the JSON-head branch (braces balancing on the
  open last line, unrecognized head) is deleted as redundant — planted
  mutation: 1,120 differing streamed prefixes, 0 settled-island changes over
  12,000 documents.
- `isSingleDollarMath` (D5): shell arithmetic `$((1+2))$` and PowerShell's
  `$_.Name$` / `$_[0]` are not math (vectors). Read-only measurement over 87,527
  assistant messages + 4,351 notes: 0 such spans in stored content.
- No consumer action required.

## 0.19.2
### Unreleased

- `/source`: **THE GFM table rule** (`source/gfm-table.ts`, verify-RC-B4) —
  `splitRowSegments` / `rowCells` (a `|` is a boundary unless an odd backslash
  run precedes it), `unescapeCellPipes`, `isGfmDelimiterRow`, `isPipeLedRow`,
  `startsPipelessTable`, `continuesTable`, `opensTable`, `tableStartsAt`,
  `findTableStart`, `findTableEnd`. Held to the shared vectors
  (`__tests__/gfm-table-vectors.json`, byte copy of matrx-frontend's and of the
  Python block detector's fixture). Consumers: `@ai-matrx/print` now; the
  matrx-frontend readers move onto it once this version is on npm.

## 0.19.1

Automatic republish — despite the generated note, this version carries source
changes: **the RC-B3 round 3 fix round** (verify-RC-B3 § round 3, RC-B3 lane;
aidream a8506eeeca, 4b92192527, aaef42333d).

- `/source` streaming: a trailing single `$` may be half of a `$$` (B3); a lone
  CR is half of a line break still arriving (B4); a block that opens in
  provisional text may still dissolve (a footnote or paragraph does not end
  there); front matter still waiting for its closing fence is undecided.
  Lone CR, tab, list-item and quote atoms joined the LLM-fragment generator.
- Holdbacks R4 (`$$` block may dissolve), R8 (spilled region), R10 (`\(`/`\[`
  opener) each pinned by a reproducer that fails when it alone is removed; the
  JSON open-tail holdback (R12) and a provisional-tree-root holdback were
  deleted as redundant — planted-mutation differentials over 12,000 documents
  showed 64 and 972 differing prefixes but 0 settled-island changes.
- Front matter behind a leading byte-order mark (`\uFEFF---`) is front matter.
- `isSingleDollarMath`: PHP/Perl member access is not math (`$this->$prop`,
  `$obj->$method`, `$class::$instance`). Measured read-only over 86,136
  assistant messages + 4,329 notes: 0 gained, 8 lost (6 shell-variable-chain,
  2 php-perl-member), every one intended.
- No consumer action required.

## 0.19.0 — kind validation behind a schema-source port (Matrx Alchemy ALC-13)

`/registry` (and the root) now export `createKindValidator(source, options?)`,
`SchemaSourcePort`, `SchemaUnavailableReason`, `KindValidationResult`,
`KindValidator` and `Json`. This is matrx-frontend's `validateAgainstKind`,
moved into the package with its database read inverted: the host implements
`SchemaSourcePort.kindSchema(kind, signal)` once and answers `{ schema,
version }` or `{ unavailable: "kind_not_registered" | "schema_unavailable" |
"schema_invalid" | "catalog_unreachable" }`. The verdict (`checked`, `ok`,
`errors`, `degradedReason`) and its messages are unchanged; a source that
throws reads as `catalog_unreachable`; an uncompilable schema reads as
`schema_invalid`. The schema cache (30 s default, `invalidate`,
`cachedSchema`) belongs to each validator instance, never to the module.

### Consumer action

matrx-frontend: implement `SchemaSourcePort` once over
`content_ir.kind_definition`, build one validator with `createKindValidator`,
repoint `features/surfaces/runtime/surface-writeback.ts` and
`build-tool-injection.ts` to it, and delete
`features/content-ir/registry/validate-against-kind.ts`.

### Also in 0.19.0 — the RC-B3 fix round (RC-B3 lane)

**Carries: RC-B3 fix round (verify-RC-B3 § 0.18.3) and RC-B3r R3 — builder:
the RC-B3 standard lane.** (aidream 1174edb640 — RC-B10, the print lane —
shipped in 0.18.3, and 094594d3a1 — the escaped-bracket rule, "Unreleased"
below — shipped in 0.18.4; both under generic "no source changes" notes.)

**`/source` streaming: settled means settled** (F3/F4). Found with the
verifier's generator (realistic model fragments, every prefix, with and
without a final newline), now permanent in `__tests__/source-attack.test.ts`
(`LLM-fragment documents…`; 12,000 documents in `SOURCE_ATTACK=long`) plus 30
pinned reproducers. `tokenizeSource(prefix, { streaming: true })` now also
holds back, as prose or as an open island:
- lines a box-drawing tree still forming at the end may claim as its root
  label (the renderer's walk-back), the open last line included;
- a paragraph cut short by a block that may still dissolve (a `$$` pair
  closing on the open line, an unbalanced JSON head, any opener still
  arriving on the open line) — and a footnote before one;
- a block island ending on the open last line (inlines there are covered by
  the tree-root hold);
- a nested-fence closer found only by the strict-CommonMark retry, a tilde
  fence whose inner block may still grow past it, braces that balance on
  the open last line;
- a code span that a blank-so-far or `>`-only open line may still close
  (`pendingCodeSpanStart`, `source/code-ranges.ts`);
- a block whose opener is provisional text, and a paragraph break there.
The two lone-HTML-tag holdbacks (M-N6/N7) collapsed into the open-tail block
rule. Each holdback turns the attack suite red on its own when removed
(planted mutations on a scratch copy). The H2 exception is unchanged.

**`/source` one-shot: a mid-line reasoning mention that never closes is prose**
(RC-B3r R3). `grep -c '<thinking>' log.txt > count.txt` no longer opens an
`xml_region` to the end of the text (the renderer shows prose). While a stream
is still arriving it stays an open region (its closer may yet come).

**`isSingleDollarMath` refuses shell and template variable chains** (F5):
`$USER$HOSTNAME`, `A=$A$B`, `df$col$sub`, `$name.$ext`, `PS1='$USER@$HOST'`,
`${a}${b}` stay text; `$n$th`, `$x_i$s` stay math. Measured read-only with
matrx-frontend `scripts/measure-single-dollar-rule.ts` over 85,325 assistant
messages and 4,317 notes: 0 spans gained, 3 lost (2 shell-variable-chain, 1
template-literal), each an intended false positive; notes unchanged. New
export `singleDollarAfter(text, close)` — the `after` argument the rule now
reads (the following name, `\n` marking the end of text). Shared vectors grew
(`__tests__/fixtures/single-dollar-vectors.json`, including the trailing-space
guard case `echo $HOME $PATH`).
- Consumer action: callers of `isSingleDollarMath` pass
  `singleDollarAfter(text, close)` instead of `text[close + 1]` (a one-character
  `after` still works; it just cannot see a variable chain).

## 0.18.6

Automatic republish. Despite the generated note, this version DID carry source
changes: most of the 0.19.0 work above (through aidream cd35751bea). 0.19.0
completes it (aidream d6241c5496, f1e9156e82).

## 0.18.5

Automatic republish. Despite the generated note, this version DID carry source
changes: the first half of the 0.19.0 work above (aidream edb3ec222d streaming
holds, 3a1dae805c + 9a6c8db0b2 variable-chain guard, 60d3f61803
`singleDollarAfter`). 0.19.0 supersedes it.

## 0.18.4

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.18.3..npm/content-ir-core/v0.18.4 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## Shipped in 0.18.4 (listed here before the release)

**`/source`: THE escaped-bracket rule** (verify-RC-B4 R3-2). `\[ … \]` is a
display-math island only when its content is real TeX — a command, `^ { } =
< > +`, or a subscript on a letter — and the `\[` is not an index on an
identifier. Everything else is CommonMark's escaped brackets and stays text:
citations `\[1\]`, dates `\[2026-09-25\]`, task boxes `- \[x\]`, `arr\[i\]`,
editorial `\[was\]`, placeholders `\[site name\]`. New export
`isEscapedBracketMath(content, before)`; the tokenizer asks it. Shared vectors:
`__tests__/fixtures/escaped-bracket-vectors.json` (7 of 11 red before).
No Python reader draws `\[ \]` math today (matrx-ai has no bracket-math
detector), so the vectors are the contract for any future twin.
- Consumer action: the renderer's math normalizer and the editor ask
  `isEscapedBracketMath` instead of their own bracket rules (matrx-frontend
  `components/markdown-core/math-normalizer.ts`).

## 0.18.3

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.18.2..npm/content-ir-core/v0.18.3 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.18.2 — 2026-09-25

**Carries: RC-B10 only.** Three helpers on THE one code-range rule, so every
consumer strips or reads code in one line instead of a private regex:
`mapCodeRanges(text, fn)` (rewrite each fence/span, prose kept byte for
byte), `codeSpanText(raw)` (CommonMark span text: runs off, one padding space
each side), `fenceParts(raw)` (opener, body, closed — list-marker line,
blockquote and the renderer's line-end closer handled).
- Consumer action: none — refresh to latest.

## 0.18.1 — 2026-09-25

**Carries: RC-B10 only** (the one code-range rule). No RC-B3 work is pending
after the 0.18.0 tag; 0.18.0 itself (RC-B3's single-dollar release) ALSO
carried RC-B10 stages a and b, swept onto main before that tag — recorded
here because its entry does not mention them.

**`/source`: THE one code-range rule is the renderer's rule** (RC-B10 ruling,
REGISTER rulings ledger). `source/code-ranges.ts` owns it: a backtick fence
opens where a line's trimmed text starts with ``` at ANY indent (list items at
any depth), after stripping blockquote `>` prefixes; a tilde fence opens at
≤ 3 spaces relative to its container (list content column); a backtick fence
closes by the nested-fence rule (with the strict retry) or the renderer's
line-end closer (`code````), and inside ```json a backtick run in a JSON
string is text; a blockquoted fence ends with its quote. Strict CommonMark's
"4+ spaces is not a fence" is rejected on purpose — users see what the
renderer's splitter draws. Phases follow the renderer: the splitter's
any-indent backtick fences first, then remark inside each prose segment
(blockquote, list-marker-line and tilde fences, code spans).
- New exports: `fenceOpenerOf`, `closeFence`, `FenceReader` (the streaming
  closer), `BacktickRuns` (the code-span pairing table), `fenceLineKinds`
  (per-line prose/open/body/close), types `FenceOpener`, `FenceLines`,
  `FenceEnd`, `FenceLineKind`.
- The tokenizer (0.18.0) and `XmlContainerTracker` (this release) read fences
  only through these; their private copies are deleted.
- Corpus: 136,662/136,662 byte-identical; every tokenizer backtick-fence
  island (17,518) is exactly one `findCodeRanges` fence (the 0.17 rule missed
  92); display-math pairing unchanged on every row. Renderer blocks ⊆
  tokenizer islands (matrx-frontend `source-covers-renderer`, container
  fences added). Vectors: `__tests__/fixtures/code-range-vectors.json` (25,
  incl. stored-row shapes with every word replaced) and the Python twin's
  `fence-nesting-vectors.json` (container + stored-row documents).
- Consumer action: none — refresh to latest.

## 0.18.0 — 2026-09-25

**`/source` `isSingleDollarMath`: `$FC$` is math.** (RC-B9 verifier, RC-B3.)
`where $FC$ is fixed costs.` drew literal dollars on screen and in every
export, because the rule wanted an operator, a lone variable or a TeX
command. It now follows Pandoc's `tex_math_dollars` delimiters (what
remark-math approximates, and how ChatGPT and Claude render): the opener is
followed by a non-space; the closer is preceded by a non-space and not
followed by a digit. `$5 and $10`, `$5-$10`, `$5/$10` stay currency. On top,
content that reads as something else is refused: an unescaped `%`; a closing
bracket or `/` first, an opening bracket last; a straight quote at either
end; `:` `;` `,` `/` last; a number glued to a following letter
(`$10$N9qo…`); a placeholder (`$...$`); and prose (two plain words, a
contraction, an abbreviation) unless a TeX command or operator is present.
Measured before choosing (matrx-frontend
`scripts/measure-single-dollar-rule.ts`, 85,102 assistant messages and 4,298
notes, read-only): 128 spans become math, each one a formula on review
(`$FC$`, `$VC$`, `$(0, 0)$`, `$2a$`, `$3x + 6 = 15$`, `$XY$`, `$p(doom)$`);
22 spans the old rule drew as math stop (jQuery `$("#id")`, template literals
`${id}:${…}`, a printf `%1$s`, broken TeX); 0 currency spans become math.
Pure Pandoc would have added 36 more false positives (shell `$HOME/…:$PATH`,
`$\ge$20% …**$`, `[$X] … [$Y]`, bcrypt costs). Shared vectors:
`__tests__/fixtures/single-dollar-vectors.json` (red on 0.17.0). No other
implementation of this rule exists (the Python server has none).

**New: `looksLikeOpenInlineMath(partial)`** — whether an UNCLOSED `$…`
already reads as a formula while a stream is still arriving. It keeps the
old signal rule (a TeX command or operator, a lone variable, a single-letter
shape; a leading digit needs a TeX command), because without a closer there
are no delimiter facts yet: `$35 per stop` keeps showing mid-stream.

Also carries the committed RC-B10 code-range work in `source/code-ranges.ts`
and `source/tokenize.ts`. The print lane owns its entry.

**Consumer action:** a streaming healer that asks `isSingleDollarMath` about
an unclosed `$…` tail must ask `looksLikeOpenInlineMath` instead. With the
wider rule, a price like `$35 per stop` would otherwise be held back until
the line ends. matrx-frontend `stream-heal.ts` is the one known caller.

## 0.17.0 — 2026-09-26

**`/source` streaming: what "settled" promises, pinned — and the one
documented exception** (verify-RC-B3 § Re-verification (0.16.0): G3, G6, H1,
H2). The verifier's mutations M1–M13 each turn
`__tests__/source-attack.test.ts` red ON ITS OWN — including the three
holdbacks the 0.16.0 suite could not see (M9 unclosed backtick, M10 open `{`
line, M12 pending `$$`), the `$$` open-tail rule (M11) and the held footnote
(M13). Unmutated: green in CI and in 10 consecutive long runs under 24 CPU
spinners.

- **G3** — the suite's bounds are counts, never the clock, so no test may be
  failed by the clock either: an explicit suite-wide timeout (15 min CI, 1 h
  long). 0.16.0's long mode died on vitest's default 5 s timeout while every
  work assertion held.
- **G6** — each streaming holdback is pinned directly and by a flicker
  message, and a new property checks, over random documents with and without
  a final newline, that every island a stream shows as settled is the one-shot
  island.
- **H1** — a footnote whose continuation is undecided (it ends the text, or
  the next line is still arriving — even one that currently looks like a
  block) is held open (`complete: false`), never shown settled.
- **H2 (chair ruling, 2026-09-26)** — a late orphan `</thinking>` /
  `</think>` / `</reasoning>` closer is the ONE documented exception: it folds
  the text before it into a thinking region retroactively. Consumers re-read
  the finished text one-shot when a stream completes (README § streaming
  mode); matrx-frontend's chat renderer does the same for its accumulator.
- The property found and fixed further settle-then-change classes, each now
  held back in streaming mode: a lone allowed HTML tag at any block start on
  the open line; an undecided single-`$` span (everything from its opener);
  a code span the final paragraph may still close (`pendingCodeSpanStart`,
  counting a single trailing newline as still-open) — held only for the `$$`
  readings it can change; `$$` pairs that a completing backtick / tilde run
  would re-pair. The paragraph's end is always computed exactly as one-shot
  computes it; only what is reported stops at the provisional point.

**New (`/source` `findCodeRanges`, `CodeRange`): THE ONE code-range rule**
(RC-B10 one-canonical). Fenced blocks and inline code spans by CommonMark —
a fence opens only at line start (≤3 spaces) and closes by the one
fence-nesting rule; a code span pairs equal backtick runs within one
paragraph. Moved out of `math-pairs` (with `pendingCodeSpanStart`) so every
package asks one function: `@ai-matrx/kit` delimiter-guard and
`@ai-matrx/chat` speakable text now import it. Shared vectors:
`__tests__/fixtures/code-range-vectors.json` (read by content-ir and kit).
Guard: `__tests__/one-code-range-rule.test.ts` fails on any NEW fence/code-span
detection in `apps/shared` (shrink-only census of the existing walkers).

**Consumer action:** streaming consumers re-tokenize one-shot on completion
(the H2 contract). Stored text is unaffected.

## 0.16.2

- **Fixed (`/source` `pairDisplayMath`, `pendingDisplayMathOpener`):** a
  `$$...$$` / `$$…$$` placeholder in prose was paired as display math. New
  `isMathPlaceholder(inner)`; such a pair is consumed whole as literal text.
  Same rule as `@ai-matrx/kit` `delimiter-guard`.
- **Fixed (`/source` `pairDisplayMath`): an inline triple backtick in prose
  hid every later `$$` formula.** Code is now found the CommonMark way — a fence
  opens only at line start (≤3 spaces), closes by the one fence-nesting rule
  (`classifyInnerFenceLine`), and a code span pairs equal backtick runs inside
  one paragraph. `tokenizeSource` math islands inherit it.
- Consumer action: none — refresh to latest.

## 0.16.1

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.16.0..npm/content-ir-core/v0.16.1 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.16.2 (also shipped)

**`/source` — a callout or `:::` directive nested in a list item stays in the
list's prose block** (verify-RC-B8 round 2, SAFETY). Guard:
`source-markdown-syntax-islands.test.ts` (3 cases red before).

**Consumer action:** none.

## 0.16.0 — 2026-09-25

**`/source` — a deterministic attack suite, and streaming that only ever
grows** (verify-RC-B3 § Re-verification (0.15.0): G3, G4, G5). The verifier's
planted mutations M1–M6 plus M7 (the trailing-`$` streaming guard removed)
and M8 (the footnote open-tail rule removed) each turn
`__tests__/source-attack.test.ts` red ON ITS OWN; unmutated code is green.

- **G3 — work, not wall-clock.** `tokenizeSource(text, { meter })` (diagnostic)
  counts every character the tokenizer's own loops visit; the attack suite adds
  native string / regex work (indexOf / includes / lastIndexOf spans, trim,
  split, replace, regex exec / test to where they stop) under a V8 cost model,
  and asserts work at 4× input stays under 6× — deterministic on any host. One
  generous wall-clock smoke bound remains (1 MB mixed in < 20 s). The meter
  immediately caught two real quadratics, both fixed: the new pending-`$$`
  scan (206 s at 1 MB of `$$\n`, now linear) — and it replaced a timing
  check that flaked 3 of 12 runs under load.
- **G4** — a streamed `$x$` that the next byte turns into currency (`$x$5`)
  never settles as math first; guarded by a message in the suite.
- **G5 — trailing islands only grow.** The suite asserts that a line-grown
  island (tree, callout, footnote, HTML block, directive) keeps its start and
  type and never shrinks, at every next prefix, over random documents. Fixed
  classes it found: a footnote absorbing an unterminated line that becomes
  the next note (`[^n]: short\n[`); a one-tag HTML line still arriving; a
  trailing lone CR (the first half of CRLF) counted as content; text after an
  unclosed backtick run while its paragraph can still grow; an open last line
  starting with `{`; and anything after a `$$` that could still find its
  partner (`pendingDisplayMathOpener` — the earliest pairable unpaired token).
  In streaming mode all of these are provisional prose until decided.
- Distinct-document counting: the structure attack counts the distinct
  documents it tokenized (mulberry32) and requires ≥ 95% of its rounds.

**Consumer action:** none for stored text. Streaming callers see fewer
islands near the tail until the text decides them.

## 0.15.1

- **Fixed (`/source` `pairDisplayMath`):** a stray `$$` in prose paired with
  the next paragraph's formula when a LaTeX command sat between them. A `$$`
  with text beside it never crosses a blank line (TeX's rule), checked on the
  untrimmed span; a `$$` alone on its line still opens a flow block that may
  span blank lines. `looksLikeDisplayMath(inner, flow?)` gains the `flow`
  flag. Same rule as `@ai-matrx/kit` `delimiter-guard` (one math dialect).
- Consumer action: none — refresh to latest.

## 0.14.3

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.14.2..npm/content-ir-core/v0.14.3 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.15.0 — 2026-09-25

**`/source` — the attack suite stands alone; the last twin drift and flicker
closed** (verify-RC-B3 § Re-verification (0.14.2): G1–G3, R4′, R2′). Proof:
the verifier's six planted mutations (M1 skip the in-range island check, M2
integrity default off, M3 streaming ignored, M4 code-point offset off by one,
M5 unbounded `}}` scan, M6 `islandEdit` accepts any span) each turn
`__tests__/source-attack.test.ts` red ON ITS OWN; unmutated code is green in
CI and in four consecutive `SOURCE_ATTACK=long` runs on a heavily loaded host.

- **G1** — the streaming check covers every complete island in every prefix,
  the LAST one included, keyed by span AND block/inline; only line-grown
  constructs (tree, callout, footnote, HTML block, directive) may grow as the
  trailing block. A third message carries the R2′ case and the extended syntax.
- **G2** — save verdicts are checked against an independent oracle (which
  islands survive in the expected text), exactly: `island_edit` vs
  `integrity` vs accepted; two-sided edits must map an island exactly (never
  through a deleted range); mangling must be `island_edit`; forged
  `island:true` spans must be `misaligned`.
- **G3** — timing compares the MINIMUM of interleaved runs at a 4× size step
  (linear ≈ 4×, quadratic ≈ 16×, bound 8×) and fails only if every one of
  three attempts breaches.
- **`island_edit` is now also the verdict** when an island inside the edited
  range survives only as ordinary text (its bytes matched a non-island copy,
  or the edit turned it into code) — previously reported as `integrity`.
  `integrity` now means an island OUTSIDE the edited ranges was disturbed.
- **R2′** — streaming: a `$$` block whose closer sits on the still-open last
  line is not settled (the rest of that line decides block vs inline); a
  closing `$` that is the last byte so far is not yet math.
- **R4′** — `FENCE_OPENER_WHITESPACE` + `parseFenceOpener`: the opener parse
  (JavaScript `trim()` / `/\s/`, written out — proven equal to `/\s/` over
  every BMP code point) shared with the Python twin, whose block detector now
  reads `markdown\u001f` as the renderer does. Vectors gained an `openers`
  section and four exotic-whitespace documents (old Python: red).

**Consumer action:** code that branches on `SourceSpliceError.code` treats a
carried island that no longer stands as `island_edit` (it was `integrity`).

## 0.14.2 — 2026-09-25

**`/source` — a tilde fence locks what the renderer draws inside it**
(verify-RC-B3 residual R5). The renderer's splitter does not know `~~~`
fences, so a ```` ``` ```` fence (or an XML section, a JSON block) that starts
inside one can run past the tilde closer — and the tokenizer had left that
overflow as editable prose. A tilde island now extends over every block the
renderer would detect starting inside it (the union of both readings). Found
by matrx-frontend's new `source-covers-renderer` property test (every fenced
block the renderer draws must sit inside one tokenizer island; red on 0.14.1,
green on 0.14.2 over 80k generated documents with tilde, nested-markdown,
CRLF and exotic-whitespace fences). Guard here: 3 cases in
`__tests__/source-verify-findings.test.ts`.

**Consumer action:** none.

## 0.14.1 — 2026-09-25

**Single-dollar math: an adversarial review of the widened rule** (RC-B2's
single-letter shapes, shipped in 0.14.0). Stored-text impact, every row diffed
0.13.0 → 0.14.x: 11 chat text parts gain 37 math spans, 0 lost; notes, agent
messages, templates and skills unchanged; every gained span is a real shape
(`f(x)`, `(x, y)`, `(-x, y)`, `f(x-y)`). Two synthetic false positives found
by probing (none in stored data) are closed: a contraction (`$I'm$` — a prime
is never followed by a letter) and an abbreviation (`$e.g. (x)$` — a lone
letter is never followed by a period) stay text. Guard: 6 new cases in
`__tests__/single-dollar-math.test.ts` (3 red before).

**Consumer action:** none.

## 0.14.0 — 2026-09-25

**`/source` — RC-B3 polish: the re-verification residuals, and the verifier's
attacks made permanent** (common-docs
`projects/rich-content-unification/evidence/verify-RC-B3.md` § Re-verification).
Each residual had a failing test first (R1–R3 red on 0.13.0; the new
cross-language vectors fail 10 cases on the old Python twin):

- **R1** — an insertion carrying an exact copy of the next island at its
  expected offset no longer causes a false `integrity` refusal: an island's
  candidate positions are scored by how much of the original text still
  surrounds them.
- **R2** — `tokenizeSource(text, { streaming: true })`: the last,
  still-unterminated line closes no fence (a bare ```` ``` ```` there may grow
  into ```` ```bash ````). Stored text omits the option.
- **R3** — `listIslands` loops instead of spreading, so a block with 150k
  inline islands saves without a `RangeError`.
- **R4** — the fence rule defines its whitespace: `FENCE_WHITESPACE`
  (CommonMark ASCII whitespace) and `trimFenceLine`, which every caller uses
  in place of a language's `trim()`. The vectors carry U+FEFF, U+0085,
  U+00A0, U+001C–U+001F and U+2028 lines; aidream matrx-ai's Python twin
  gained `FENCE_WHITESPACE` / `trim_fence_line` and passes them.
- **R5** — tilde and odd-whitespace fences: matrx-frontend's
  `source-covers-renderer` property test proves the tokenizer locks at least
  every fenced block the renderer draws (both now share `trimFenceLine`).
- **Permanent attack suite** — `__tests__/source-attack.test.ts`: seeded fuzz
  of rejoin identity, offsets (independently counted), surrogate safety,
  no-edit/identity saves, splice verdicts, island refusals, misaligned spans,
  streaming prefixes (settled islands never move) and pathological timing
  (budget + linear scaling), bounded in CI; `SOURCE_ATTACK=long` runs the
  verifier-scale version.

**Consumer action:** fence-line classification callers trim with
`trimFenceLine(line)` (matrx-frontend's splitters do as of this release).

**`/source` — islands for the extended markdown syntax** (RC-B8, common-docs
`projects/rich-content-unification/REGISTER.md`). The renderer now draws
callouts, directives, front matter, footnotes and wikilinks; an editor must
never re-serialize their grammar, so each is now an island:

- `directive` (block) — a generic-directive container `:::name[label]{attrs}`
  … `:::` with nesting counted (tabs, columns, details, figures, callouts,
  TOC), a leaf `::name` line (`meta.leaf`), or a MkDocs admonition
  `!!! type "Title"` / `??? type` with its indented body (`meta.syntax: "mkdocs"`).
  An unclosed container runs to the end with `complete: false` (streaming).
- `callout` (block) — a blockquote opening `> [!TYPE]` (GitHub alert /
  Obsidian callout; `meta.name`, `meta.fold`).
- `footnote_def` (block) — `[^id]: …` with its continuation lines (`meta.id`).
- `front_matter` now also recognizes TOML (`+++`); `meta.format` is `yaml` | `toml`.
- `wikilink` (inline) — `[[Page]]`, `[[Page|alias]]`, `![[Page]]`
  (`meta.target`, `meta.embed`); never across a line, never inside code.

CSV/TSV fences were already `fence` islands (`meta.lang`). Guard:
`__tests__/source-markdown-syntax-islands.test.ts` (7 of 10 red on 0.13.0).

**Consumer action:** an exhaustive `switch` over `IslandType` /
`BlockIslandType` gains the new members; nothing else changes (joinSource is
still byte-identical, the corpus check stays green).

**Single-dollar math: single-letter math shapes are math** (RC-B2 verification,
common-docs `projects/rich-content-unification/evidence/verify-RC-B2.md`).
`isSingleDollarMath` now also accepts a shape built only from single-letter
identifiers (primes, standard function names), digits, parentheses, commas and
`- * /` that applies, primes or juxtaposes them — `$f(g(x))$`, `$u v$`,
`$f'(x)$`, `$(x, y)$`, `$ln(x)$`. Words keep it text (`$Tipping (fee)$`), and
the pandoc spacing / digit rules still keep currency (`$5 and $10`, `$5-$10`)
as text. Guard: `__tests__/single-dollar-math.test.ts` (5 of 12 red before).

**Consumer action:** none — adopt the release; matrx-frontend's math
normalizer and `/source` tokenizer pick the rule up through the import.

## 0.13.0 — 2026-09-25

**`/source` — fixes from the RC-B3 independent verification**
(common-docs `projects/rich-content-unification/evidence/verify-RC-B3.md`).
Every finding got a failing test first (`__tests__/source-verify-findings.test.ts`,
14 of the 17 non-timing tests red on 0.12.0), then the fix:

- **THE nested-fence rule now lives here** (`source/fence-nesting.ts`, moved from
  matrx-frontend `components/markdown-core/fence-nesting.ts`): inside a
  ```` ```markdown ```` / `md` / `mdx` fence, ```` ```lang ```` opens a nested
  fence and a bare ```` ``` ```` closes the innermost one; a nested fence still
  open at the end of the text retries strict CommonMark. The tokenizer uses it,
  so it ends a nested markdown fence exactly where the renderer does (real row:
  note `f9b7e9a2-…`). Generated cross-language vectors
  `__tests__/fence-nesting-vectors.json`; aidream matrx-ai's Python twin runs a
  byte-identical copy and fails on drift.
- **Islands change only through `islandEdit(island, text)`.** A plain edit must
  carry every island inside its range back byte-identical and in order —
  otherwise `SourceSpliceError("island_edit")` names the island. Edits are
  diffed segment by segment between islands, so anchors inside an edited
  paragraph map exactly.
- **Integrity is refused by default.** `requireIntegrity` defaults to `true`;
  `false` is documented as tooling-only.
- **Linear time on pathological input.** Every closer search goes through an
  occurrence index; code spans use a per-paragraph run table; the single-`$`
  scan stops at the rule's 400-character limit; embedded `__kind` probing has a
  per-paragraph budget; `readXmlTag` takes a finder. 2 MB of `{{`: 21 s → 41 ms.
- **A tree's root label belongs to the tree island** (the renderer splitter's
  `findTreeBlockStart`).
- **`$$` follows the core's pairing rule** (`source/math-pairs.ts`, ported from
  `@ai-matrx/kit/delimiter-guard`): tokens outside code pair in order, prose
  pairs are not math, an unpaired `$$` is literal. `$$` at the end of a line
  pairs with a later closer, and a lone `$$` never locks the rest of a document.

### Consumer action

- `spliceSave` now THROWS on an island change (`island_edit`) and on disturbed
  integrity (`integrity`). Editors change islands with `islandEdit`; tooling
  that wants the report passes `requireIntegrity: false`.
- matrx-frontend: import the fence rule from `@ai-matrx/content-ir/source`; the
  local `components/markdown-core/fence-nesting.ts` is deleted.

## 0.12.1

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.12.0..npm/content-ir-core/v0.12.1 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.12.0 — 2026-09-23

**`/source` — the stored text is the truth, block by block** (rich-content
unification PLAN decision 6, register row RC-B3). New subpath
`@ai-matrx/content-ir/source` (also re-exported from the root):

- `tokenizeSource(text)` — the offset-preserving tokenizer. Blocks
  `{ kind: "prose" | "island" | "gap", islandType, start, end, startCp, endCp,
  raw, complete, meta, inlines }` cover the text exactly (the concatenation of
  `raw` IS the input — nothing trimmed or normalized). Every protected
  construct is an island: code fences (incl. special languages, the ```json
  string-aware close), bare typed / `__kind` / directive JSON, known XML
  regions, attribute XML (line-leading and mid-line), unknown XML containers
  (the renderer splitter's own balance tracker, ported), raw HTML blocks and
  tags, HTML comments and pinned anchors `<!--@a:id-->`, `$$` / `\(` / `\[`
  math and THE single-dollar rule, `{{variables}}`, `<matrxcite/>`, embedded
  `__kind` JSON, `[Image URL: …]` media refs, front matter, box-drawing trees.
  Islands inside a paragraph stay in that prose block as `inlines`.
  Streaming-tolerant: an unclosed construct is an island to the end of the
  text with `complete: false`; never throws. Offsets in UTF-16 and code points.
- `spliceSave(original, edits)` — save = splice. Block-aligned replacements;
  bytes outside the edits are copied, never rebuilt; no-op edits return the
  original string. Returns a change set narrowed to the bytes that differ
  (UTF-16 + code points) for anchor mapping (`mapPosition`, `mapRange`,
  ProseMirror StepMap semantics — STORE-DESIGN §3.19) and an integrity
  report naming any island outside the edit that the edit disturbed
  (`requireIntegrity: true` throws instead).
- `isSingleDollarMath` / `singleDollarMathEnd` — THE single-dollar math rule,
  moved here from matrx-frontend's math normalizer so the renderer and the
  tokenizer can never disagree.
- `readXmlTag`, `XmlContainerTracker`, `buildCodePointIndex`,
  `toCodePointOffset`, `toUtf16Offset`, `joinSource`, `listIslands`,
  `blockAt`, `blockEdit`, `blockRangeEdit`.

Proof: 39 tests (fixture corpus of every construct, 2,000 adversarial random
documents, every prefix of every fixture, 1,000 random single-block edits;
two planted mutations turned the suite red). matrx-frontend's read-only
`pnpm check:source-roundtrip` ran every stored row — notes, chat text parts,
agent and template messages, message templates, skills — 135,302 rows, all
byte-identical, all edit-local.

### Consumer action

None required — additive. matrx-frontend adopts `isSingleDollarMath` from
`/source` in its math normalizer.

## 0.11.7

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.11.6..npm/content-ir-core/v0.11.7 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.11.6

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.11.5..npm/content-ir-core/v0.11.6 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.11.5

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.11.4..npm/content-ir-core/v0.11.5 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.11.4

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.11.3..npm/content-ir-core/v0.11.4 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.11.3

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.11.2..npm/content-ir-core/v0.11.3 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.11.2

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.11.1..npm/content-ir-core/v0.11.2 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.11.1

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.11.0..npm/content-ir-core/v0.11.1 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.11.0 — 2026-09-08

**Kind Directives move into the kernel — `/directives`.** A directive
(`{"__kind": "directive_v1_<class>_<noun>", "items": [...]}`) is a kind
instance, and the grammar that recognises one lived only in matrx-frontend
(`features/content-ir/directives/`). Workflow Studio had no copy, so the agent
builder's envelope streamed into the Plan Room as raw text while the studio's
resolver 404'd on the slug. The whole pure half now ships here, ported
verbatim from the aidream source of record
(`matrx_graph/content_ir/directives.py`):

- `directives/grammar.ts` — `RESERVED_PREFIX`, the CLOSED class vocabulary,
  `CAPABILITY_BY_CLASS`, `buildDirectiveSlug` / `parseDirectiveSlug` /
  `isReservedDirectiveSlug`, THE POSITION LAW (`executesAtOutputRoot`,
  `resolvesInContent`), `isKindDirective` / `directiveSlugOf`,
  `buildKindDirective`, and `looksLikeDirectiveHead` (mid-stream recognition).
- `directives/decode.ts` — `decodeDirective` / `tryDecodeDirective` /
  `tryDecodeDirectiveContent` (JSON text or a parsed value), `DecodedDirective`,
  `DirectiveDecodeError`.
- `directives/legacy-shell.ts` — THE ONE read-only shim for the retired 4-key
  shell, reachable only through the decoder; its use counter lives on a
  `globalThis` `Symbol.for` slot so the root and `./directives` bundles count
  the same uses (the tarball canary proves it).
- `directives/display.ts` — the auto-view naming (`directiveDisplay`,
  `nounLabel`, `nounFamily`, `nounTitleColumn`) over an injectable
  `DirectiveNounCatalog`; degrades to a title-cased token, never a slug.
- `directives/item-summary.ts` — `itemTitle` / `itemSubtitle` / `itemFacts`
  (scalars only, THE UUID RULE).
- `directives/item-kind.ts` — THE DIRECTIVE⇄KIND SEAM:
  `directiveItemKindFromEdges` (reads the `items` edge of a directive
  KindDescriptor — the ONE kind endpoint serves every directive shape since
  aidream 2026-09-08) and `asKindInstance` (`__kind` FIRST, never overwritten).

Exported from the root and from the new `./directives` subpath.

### Consumer action

- **matrx-frontend**: delete `features/content-ir/directives/{grammar,decode,
  legacyShell,itemKind,itemSummary,nounDisplay}.ts` and import the same names
  from `@ai-matrx/content-ir` (`itemTitle` now takes the title COLUMN, not the
  noun; `directiveDisplay`/`nounLabel`/`nounFamily`/`nounTitleColumn` take an
  optional catalog lookup; `asKindInstance(item, kind)` takes the resolved
  kind). Retire `scripts/check-legacy-shim-containment.ts` (the shim is
  package-internal) and repoint `sync-directive-grammar` at the package.
- **Workflow Studio / dashboard**: nothing to delete (no copy existed).

## 0.10.4

Automatic changed-only republish (docs/metadata drift since the last tag — see
`git diff npm/content-ir-core/v0.10.3..npm/content-ir-core/v0.10.4 -- apps/shared/content-ir-core`).
No source changes intended and no consumer action required.

## 0.10.1

Automatic changed-only republish (docs/metadata drift since v0.10.0; dist verified
byte-identical to v0.10.0). No source changes and no consumer action required.

## 0.10.3 — 2026-08-31

**Bug fix — inline objects nest to any depth, on the streaming path too.**
`validateObjectAgainstFields` always recursed without limit, but
`resolveParentFieldSchema` looked a field up only through KIND-typed parents.
A second-level `inline_object` was therefore invisible mid-stream: it
registered no inline context and closed as an untyped object
(`Object is missing "__kind"`), degrading the whole instance to
`kindState: "raw"` — for a shape the kind's own schema declares. Found
declaring the true `lulu_print_job` shape after 0.10.2
(`costs.shipping_cost`); the same seam also gates `json`-any opacity and
`record` members nested inside an inline object.

Field lookup now resolves through an inline-object parent's field map first.
Guard: `__tests__/kind-parser.test.ts` § "inline objects nest to any depth"
(three-deep nesting resolves; chunking-invariant so the streaming path is
covered; a wrong type deep inside still degrades), proven failing on the
unfixed parser with three `Object is missing "__kind"` raws.

### Consumer action

None required — this only widens what validates. A mirror schema that flattened
a nested shape to `json` to survive can now declare the real `inline_object`.

## 0.10.2 — 2026-08-30

**Bug fix — `nullable` is a FIELD law, not a scalar law.** `validateFinalFieldValue`
consulted `nullable` only on scalar-shaped fields and `json[]`. Every other branch
(`object`, `inline_object`, `record`, `array`, `string[]`/`number[]`/`boolean[]`)
opened with a shape check that reads `null` as a type violation and returned an error
before `nullable` was ever looked at — so a field a kind legitimately declares
nullable, whose value is genuinely `null`, failed validation and degraded the WHOLE
instance to `kindState: "raw"`, rendering through the `generic_structured` fallback.
Reproduced live on `lulu_print_job` (`costs: null`, `estimated_shipping_dates: null`).

`nullable` now short-circuits at the top of `validateFinalFieldValue` for every field
type, and the same law was applied to the streaming placement path
(`validateValueAgainstField`), where a `null` arrives as a scalar token and every
shape check would equally have rejected it — the sibling of the same defect. Messages
for non-nullable nulls are unchanged. Guard: three cases in
`__tests__/kind-parser.test.ts` (all five shapes null on a nullable field stays typed;
chunking-invariant so the streaming path is covered; a null on a NON-nullable field
still degrades), proven failing on the unfixed parser before the fix landed.

### Consumer action

None required — this only widens what validates. Kinds that were declared `json` with
a comment to work around this bug (matrx-frontend `features/content-ir/kinds/print-kinds.ts`)
can go back to their true `object` / `inline_object` / `record` type with
`nullable: true`, and the ~23 existing sites already declaring `nullable: true` on an
object-shaped field stop being latent bugs.

## 0.10.0 — 2026-08-30

**Non-breaking.** Every existing `@ai-matrx/content-ir` import keeps working
unchanged — the root export is still the FULL surface. What is new is that the
package's four clean internal areas are now addressable directly.

- **Five new subpath exports — `/core`, `/session`, `/registry`, `/convert`,
  `/wire`.** Each is a real build entry (dual ESM + CJS with matching
  `.d.ts`/`.d.cts`), so a consumer that only reads wire contracts no longer
  pulls the streaming parser — or `ajv`, which rides `/registry` and
  `/convert` — into its graph. `.` re-exports all five, so nothing that
  imports the root changes.

  | Subpath | What it holds |
  |---|---|
  | `/core` | IR types + tree, JSON tokenizer, kind parser, snapshots, normalize, fingerprint, discriminator, envelope read/value/cache, region, schema structure |
  | `/session` | `ParseSession`, `openParseSession` / `getParseSession` / `disposeParseSession` |
  | `/registry` | kind registry types, storage transform, the dual gate |
  | `/convert` | kind ⇄ JSON Schema, the OpenAI structured-output converter |
  | `/wire` | partial-kind events, runtime wrapper readers, emit payload |

- **Fixed: the parse-session registry could silently SPLIT across module
  graphs.** `session/session-manager.ts` held its `Map` in a module-level
  variable. The package builds `splitting: false` in dual ESM/CJS format, so
  that module is duplicated into every bundle that reaches it — and with the
  new `/session` subpath, a host importing the root and the subpath would have
  gotten TWO registries: "one writer per stream identity" would stop being
  enforced across them and a second writer would open cleanly onto a live
  stream. The registry now lives on `globalThis` under
  `Symbol.for("ai-matrx.content-ir.parse-sessions")`. The tarball canary
  proves the slot across BOTH loaders by opening a session through `/session`
  and resolving it through the root; the guard was verified failing against
  the old module-level form before this release.

- **Contract locked: `expectedRootKind` is a PREDICTION, never a free pass.**
  Three new tests pin the behavior that was already correct but untested —
  a payload predicted into a kind whose required field is missing degrades to
  `kindState: "raw"` carrying a `raw_fallback` notice naming the field; a
  valid payload still promotes to `resolved`. No behavior changed.

### Consumer action (C28)

Nothing is required — this release is non-breaking and the root export is
unchanged. Two things are worth doing at your next working session:

1. **Prefer the area subpath over the root for new and touched imports**
   (`@ai-matrx/content-ir/wire` instead of `@ai-matrx/content-ir` when you
   only read wire contracts, and so on). Root stays supported indefinitely;
   the subpaths are the preferred imports because they keep the parser and
   `ajv` out of graphs that do not need them.

2. **🚨 Audit every place you gate a typed path on `envelope.root.kind`.**
   `kind` is PRESERVED on failure by design (KIND PRESERVATION, 0.x
   2026-08-29): a payload that fails its schema stays a *broken*
   `flashcard_set` rather than becoming an anonymous object. `status` says
   only that parsing finished. **`kindState` is the validity signal** —
   `"resolved"` is the only state a typed consumer may act on
   (`"unverified"` means no schema existed and still routes to a component;
   `"raw"` means checked and failed). A guard written as
   `if (root.kind !== "x" || root.status !== "complete") return null` reads a
   degraded root as a good one. Found in matrx-frontend's
   `generatedSetFromEnvelope` and fixed in the same session as this release.

## 0.9.0 — 2026-08-29

- **A nested `__kind` object now registers the CHILD's schema, not just a ref
  to it.** Arrays of `__kind` items have always drafted their item kind; an
  object-VALUED field declaring a `__kind` returned
  `{type:"object", kind}` and dropped the child's fields on the floor.

  The consequence was not a missing child — it was a broken PARENT. The parser
  had a field predicting `mx_child` with no schema registered for it, so the
  child degraded, the parent failed validation, and the parent fell to
  `broken-instance` and lost its component over a child the very same schema
  had fully described one line above.

  Found by matrx-frontend's new render matrix (every recognition path × every
  shape archetype) on its first run.

## 0.8.0 — 2026-08-29

- **A field is never DROPPED — it widens to `json`.** Every field-map loop in
  the JSON Schema → KindSchema direction now goes through
  `convertFieldOrWiden`. A construct the vocabulary cannot name precisely — a
  2-D array (`rows: [[...]]`), an array of untyped objects, a mixed `anyOf`, an
  unresolvable `$ref` — becomes `json` with a warning instead of disappearing
  from the kind.

  WHY THIS IS NOT A DETAIL: a dropped field does not merely lose type
  information, it inverts the outcome. The field is absent from the field map,
  so a real payload's value for it becomes unknown-key residue; if the schema
  marked it required, validation FAILS; the node goes `raw`; and the render
  route correctly refuses it a component. Dropping one field could cost a kind
  its entire component. `json` is always a true statement about such a field.

  A recovery also downgrades whatever the failed attempt logged at `error`
  severity to `warning`, so a host that screams on errors does not scream
  about kinds that render perfectly.

- Live-corpus result: all **502** active kinds now derive a complete field map
  from `emitted_json_schema` alone (was 62 with a stored field list, and 466
  before this change). 28 carry at least one widened field, and each one names
  itself in the problems list so the schema can be tightened later.

## 0.7.0 — 2026-08-29

- **`kindSchemaFromJsonSchema` — the registry-facing door for deriving a
  kind's fields from its schema.** One schema in; the kind's `KindSchema` out,
  plus every child kind the schema declared inline or through `$defs`, plus
  the conversion problems. Pure and synchronous, so a host can call it from a
  warm load, a cold fetch, a reducer, or a test. Accepts a bare JSON Schema
  and the provider `{name, schema, strict}` envelope alike.

  This is what makes a stored second copy of a kind's fields unnecessary. It
  never returns a partial guess: an inexpressible field widens to `json`, and
  only a structurally unusable root yields null.

## 0.6.0 — 2026-08-29

- **`$ref` / `$defs` resolution — a kind's child kinds are no longer invisible
  to JSON Schema → KindSchema.** `convertAiSchemaToBlockFields` resolves `#`,
  `#/$defs/<name>` and `#/definitions/<name>` in field position, in array
  `items`, and inside `anyOf`/`oneOf` (including `items: {anyOf: [$ref, …]}`,
  the multi-kind array). Referenced defs are registered as their own drafts,
  with a cycle brake so a self-referencing kind terminates. An unresolvable
  ref is a loud error, never a guess.

  WHY: `kindSchemaToJsonSchema` externalizes every child kind as a `$def`, and
  the reverse direction rejected `$ref` outright — so every
  `{type:"object", kind}` and `{type:"array", itemKinds}` field came back
  EMPTY. Measured over the 62 live kinds carrying both a stored field list and
  a schema, 36 lost fields this way (sections, slides, nodes, edges, criteria,
  questions, segments, …).

- **A `__kind`-carrying record reads back as a record.** An object whose only
  declared property is `__kind`, with typed `additionalProperties`, is the
  forward converter's emission for `{type:"record", values}` — it was widening
  to an open `inline_object` and losing the value type.

- Live-corpus result: schema-derived KindSchemas now match the stored field
  list byte-for-byte for 48 of 62 kinds, with 1 strictly richer. The remaining
  13 are DIVERGENCE BETWEEN THE TWO STORED COPIES, not conversion gaps — 4
  stored lists carry `__kind` as a data field (it is the discriminator, never
  a field), and 9 disagree on enums, bounds, required flags or descriptions
  the schema states and the copy does not. That is the evidence for retiring
  the stored copy.

## 0.5.0 — 2026-08-29

- **A multi-type union is no longer silently narrowed to its first member.**
  `resolvePrimaryType` now reports every non-null member, and
  `convertAiSchemaToBlockFields` branches on them: a union spanning
  objects/arrays becomes `json`, a scalar-only union becomes
  `{type:"union", scalars}`, and anything unnameable stays `json` with a
  warning. Single-typed fields are untouched.

  WHY: `type: ["string","number","boolean","object","array","null"]` —
  pydantic's `Any`, and the commonest construct in user-authored kinds —
  converted to plain `string`. The loss was invisible and it inverted the
  field's meaning: an `estimated_count` of `1` then FAILED validation, so a
  correct payload read as a broken instance. Widening is safe; silently
  narrowing is not.

- New `__tests__/json-schema-to-kind.test.ts` pins JSON Schema → KindSchema
  over the constructs a stored field list historically could not hold —
  pydantic-Any unions, `items: {}` arrays, arrays of `__kind` children,
  nested `__kind` objects, and markerless inline objects. All convert with
  zero errors, which is the evidence that `emitted_json_schema` is sufficient
  on its own and a second stored copy of a kind's fields is unnecessary.

## 0.4.0 — 2026-08-29

- **`unverified` — "we never checked it" is no longer "we checked it and it
  failed."** `IrKindState` gains `unverified`, and `raw_object` events carry a
  `cause` (`"unverified" | "invalid"`, absent reads as `"invalid"`). The four
  no-schema-registered degrades — the two pending-schema resolutions, plus the
  typed and speculated finalizers — now report `unverified`; every validation
  failure, array-item mismatch, duplicate key, bad placement, and contradicted
  speculation still reports `invalid`. `IrTree` carries the cause onto
  `root.kindState` and into `nodeIndex`.

  WHY THIS EXISTS: `raw` meant both things, and on 2026-08-28 the render route
  began reading `raw` as "broken instance" and diverting it away from the
  kind's component. Every kind whose schema cannot be reconstructed — nested
  objects, arrays of child kinds, loose fields — degrades on the no-schema
  path, so ~221 live kinds with purpose-built components silently started
  rendering as key/value dumps. Consumers MUST NOT treat `unverified` as a
  failure: the value is intact and was never examined.

## 0.3.0 — 2026-08-29

- **KIND PRESERVATION now covers STRUCTURAL raws.** A node that degrades to
  `raw_object` because its value failed schema validation, carried a duplicate
  key, or violated the parent's `itemKinds` keeps its identified kind on the
  raw event, the envelope root (`root.kind` with `kindState: "raw"`), and the
  `nodeIndex` entry for nested children. Previously only schema-availability
  raws preserved the kind, so a payload that was off by ONE field rendered as
  anonymous raw JSON with no repaint subscription and no route — the single
  most damaging class of the 2026-08-29 "kind slips through the cracks" audit
  (crack #9). The render seam's registration boundary is unchanged: an
  unregistered slug is still never claimed. Consumers should route a
  registered kind-preserved raw to their generic floor and surface
  `residue.notices` (the exact `raw_fallback` reason is recorded there).
  Pinned by two new kind-parser tests.

## 0.2.2 — 2026-08-24

- `rehydrateRunResult` now rejects nested entries whose `__kind` is not
  `node_outcome`, even when the object carries plausible run/node identity.
  This keeps a malformed sibling from masquerading as a valid terminal result.
- Workflow Studio now delegates runtime-wrapper field reading and output-ref
  rehydration to the package APIs; its host adapter retains only deferred,
  persistent diagnostics for present-but-broken wrappers.

## 0.2.1 — 2026-08-24

- **FIX — `validateStructuralLeg` no longer rejects every current kind
  instance.** The leg validated ONLY the `__kind`-stripped copy of the sample,
  a leftover from the dead "marker is envelope framing" doctrine. Since the
  2026-08-23 ruling every `emitted_json_schema` DECLARES `__kind` (const +
  required), so stripping turned a valid instance into
  `(root) must have required property '__kind'` — 503 of the 842 live kind
  definitions, i.e. every marker-declaring kind, failed the Shape Studio Test
  tab, the gate tab, the example manager, and instance save. The leg now
  validates the caller's value VERBATIM and only retries the marker-free
  reduction if that fails (backward compatibility for a row still pinned to a
  pre-2026-08-23 marker-free schema), and reports the errors from whichever
  attempt matches the schema in hand.
- `JsonTokenizer` and `KindStreamParser` declare explicit fields instead of
  constructor parameter properties. Consumers compile this SOURCE directly, and
  a strict host (`apps/dashboard`) sets `erasableSyntaxOnly`, under which
  parameter properties are a hard error — the flag had to be turned off in
  Workflow Studio for exactly this reason, and is now back on. Behaviour is
  unchanged; the published 0.2.0 tarball does not carry this edit.


## 0.2.0 — 2026-08-23

- Added the `wire/` layer — the server→UI contracts every UI reads the same way:
  - `wire/partial-kind` — the streaming partial-kinds channel (`__ir_partial`): `readPartialKindEvent`,
    `sanitizeInboundPartialKindMetadata`, `advancePartialKind`, `makePartialKindStalenessGate`,
    `isProvisionalKind`, `isTerminalKindEvent`, `IR_PARTIAL_KEY` + the event types.
  - `wire/runtime-wrapper` — the `node_outcome` / `run_result` runtime-wrapper reader and THE `output_ref`
    elision gate: `readOutputRef`, `rehydrateNodeOutcome`, `rehydrateRunResult`, `readNodeOutcomeValue`,
    `readRunResultValue`, `kindVerdictOf`, `NODE_OUTCOME_KIND`, `RUN_RESULT_KIND` + the wrapper types.
  - `wire/emit-payload` — the emit/render payload composer: `withRootKind`, `emitPayloadJson`, `emitPayloadFence`.
- Brought the partial-kind suite pinned to the Python producer's generated fixture
  (`__tests__/partial-kind-events.generated.json`, regenerated by aidream `scripts/generate_partial_kind_fixture.py`
  and drift-gated by `packages/matrx-ai/tests/parity/test_partial_kind_fixture.py`) plus new pure suites for the
  runtime-wrapper reader and the emit composer (196 tests).
- Existing 0.1.x API unchanged.

## 0.1.2 — 2026-08-22

- Added a CommonJS export condition for Jest and other require-based consumers.
- Expanded the isolated tarball canary to prove both ESM import and CommonJS require.

## 0.1.1 — 2026-08-22

- Made this package the canonical authoring point for the portable Content IR kernel.
- Retired the temporary cross-repository source-twin ownership contract.
- Kept the public runtime API unchanged while consumers move to the registry artifact.

## 0.1.0 — 2026-08-22

- Promoted the existing verified Content IR twin into an independently installable public package.
- Added ESM and declaration builds, package linting, packed-artifact analysis, and an isolated consumer canary.
- Preserved all parser, session, normalization, schema conversion, and structural-validation behavior with the existing shared fixtures and fingerprint vectors.


