@openpresentation/opf 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/docs.js CHANGED
@@ -40,19 +40,19 @@ var docsData = Object.freeze([
40
40
  "slug": "design-resolution",
41
41
  "file": "docs/design-resolution.md",
42
42
  "title": "Design Resolution",
43
- "markdown": '# Design Resolution\n\nHow an engine decides the effective design for any given slide. The schema spreads these rules across field descriptions; this page states them once, as an algorithm.\n\n## Precedence\n\nFor every design field independently, the most specific source wins:\n\n```\n wins +--------------------------------------------------------+\n ^ | 1. slide design slides[i].design.* |\n | +--------------------------------------------------------+\n | | 2. deck design design.* on the presentation root |\n | +--------------------------------------------------------+\n | | 3. resolved theme colorScheme, fontScheme, |\n | | background, dimensions from the |\n | | theme record |\n | +--------------------------------------------------------+\n loses | 4. engine defaults e.g. spec/reference/ |\n v | engine-defaults.json |\n +--------------------------------------------------------+\n```\n\n1. **Slide design** \u2014 `slides[].design.*`\n2. **Deck design** \u2014 `design.*` on the presentation root\n3. **Resolved theme** \u2014 defaults carried by the theme record (`colorScheme`, `fontScheme`, `background`, `dimensions`)\n4. **Engine defaults** \u2014 engine configuration such as [`spec/reference/engine-defaults.json`](../spec/reference/engine-defaults.json)\n\nResolution is **per field**, not per object. A slide that sets only `design.contentAlignment` inherits everything else from the deck design; a deck that sets only `design.colorScheme` keeps the theme\'s font scheme and background.\n\nTwo field-level rules complete the picture:\n\n- **Base-plus-overrides within one object.** Wherever a reference object carries an `id` (`Theme`, `ColorScheme`, `FontScheme`), the `id` resolves a catalog record as the base and sibling fields override the resolved record per key. The string shorthand (`"colorScheme": "cool-horizon"`) is equivalent to setting only `id`.\n\n ```\n "colorScheme": { "id": "cool-horizon", "accent1": "#0F4C81" }\n\n catalog record "cool-horizon" sibling fields on the object\n accent1: "#2874A6" <-- replaced -- accent1: "#0F4C81"\n accent2: "#1B4F72" <-- kept\n light1: "#FFFFFF" <-- kept\n |\n v\n effective scheme: accent1 from the override, everything else\n from the record\n ```\n- **Explicit suppression.** `watermark`, `header`, and `footer` accept `false` to switch off an inherited value \u2014 distinct from omitting the field, which inherits.\n\nCatalog lookups inside this chain follow the standard resolution order (inline `catalogs.<kind>.records[]` \u2192 `catalogs.<kind>.source` \u2192 default catalog); see [`how-opf-works.md`](./how-opf-works.md).\n\n## Worked example 1: color scheme through every level\n\n```json\n{\n "design": {\n "theme": "classic",\n "colorScheme": "forest-green"\n },\n "slides": [\n { "title": "Inherits the deck" },\n {\n "title": "Slide override",\n "design": {\n "colorScheme": { "id": "cool-horizon", "accent1": "#0F4C81" }\n }\n }\n ]\n}\n```\n\n- Slide 1: the `classic` theme record supplies its own default color scheme, but the deck design sets `colorScheme` explicitly, so `forest-green` wins (level 2 beats level 3). Fonts, background, and dimensions still come from `classic`.\n- Slide 2: slide design beats deck design (level 1 beats level 2). The `cool-horizon` record resolves as the base, then `accent1` is replaced by `#0F4C81`. All other `cool-horizon` slots survive.\n\nThere is no ambiguity between "override" and "reference": every scheme value *is* a reference, and any sibling fields on the same object are overrides applied after the reference resolves.\n\n## Worked example 2: backgrounds and suppression\n\n```json\n{\n "design": {\n "theme": "dark",\n "background": "light1",\n "footer": {\n "left": { "text": "Acme Corp" },\n "right": { "slideNumber": true }\n }\n },\n "slides": [\n { "title": "Light slide in a dark theme" },\n {\n "title": "Section divider",\n "design": {\n "background": {\n "type": "gradient",\n "gradient": {\n "angle": 90,\n "stops": [\n { "color": "#0B1B2B", "position": 0 },\n { "color": "#123A5F", "position": 1 }\n ]\n }\n },\n "footer": false\n }\n }\n ]\n}\n```\n\n- Slide 1: the deck-level `background: "light1"` overrides the `dark` theme\'s default background. `light1` is a theme slot \u2014 it resolves through the effective color scheme, which itself resolved through the chain above.\n- Slide 2: the gradient replaces the deck background for this slide only, and `footer: false` suppresses the inherited footer rather than inheriting or replacing it.\n\n## Worked example 3: font scheme models\n\n```json\n{\n "design": {\n "fontScheme": {\n "id": "aptos",\n "code": { "family": "JetBrains Mono" }\n }\n }\n}\n```\n\nThe `aptos` record supplies the OOXML pair (`major`/`minor`). The `code` role is an OPF-specific addition with no OOXML slot, so it layers on top without disturbing the pair. When serializing to PowerPoint, engines write `major`/`minor` to `majorFont`/`minorFont` and map abstract roles (`heading`, `body`) onto those slots. `accent` has no slot; the `code` family is written directly on code runs. The same slot-versus-role split applies to color schemes: OOXML slots (`accent1`\u2013`accent6`, `dark1/2`, `light1/2`) round-trip directly, abstract roles (`primary`, `text`, `surface`, \u2026) are mapped onto slots by the engine.\n\n### Code font\n\nThe `code` role resolves per key like every other override:\n\n1. `code` on the effective `design.fontScheme` object;\n2. `code` on the resolved font-scheme record (the `consolas` and `courier-new` records carry `{ "family": "Consolas" }` and `{ "family": "Courier New" }`);\n3. otherwise **Roboto Mono**, the documented fallback that `@openpresentation/opf-render` bundles.\n\nThe heading and body families are never reused as the code fallback, so choosing `aptos` still gives Roboto Mono code unless the deck sets `code`. `resolveFontFamilies()` in `@openpresentation/opf` applies these rules for all engines.\n\n### Engine default font scheme\n\nThe last-resort font scheme applies only when neither the slide, the deck nor the resolved theme names one. Every bundled theme names a font scheme (`minimal` uses `aptos`), and engines default the theme to `minimal`, so a document with no `design` gets `aptos` in every engine.\n\nEvery engine shares one last resort, `aptos`, so a custom theme without `fontScheme` is measured, paginated, previewed and exported in the same fonts. `@openpresentation/opf` exports it as `DEFAULT_FONT_SCHEME` (`resolveScriptFonts()` uses it too), and [`engine-defaults.json`](../spec/reference/engine-defaults.json) records it as `fontScheme.pptx.latin`:\n\n| Engine | Last resort | Where |\n| --- | --- | --- |\n| Core pagination | `DEFAULT_FONT_SCHEME` (`aptos`) | `packages/javascript/src/pagination.ts` |\n| opf-render preview | `aptos` (`engineDefaults.fontScheme.pptx.latin`) | `src/svg.js` |\n| opf-editor composition and slide transfer | `aptos` | `src/font-defaults.js` |\n| opf-pptx export | `aptos` (`DEFAULTS.fontScheme`) | `src/index.js` |\n\n\n`fontScheme.google` (`roboto`, `noto-sans-sc`, `noto-sans`) is not read by any current engine. It is kept as the intended default for a future Google Slides exporter, whose output renders in Google-hosted fonts.\n\nAptos is not openly licensed, so no OPF package bundles it. Previews take the same path for the last resort as for any `aptos` deck:\n\n- **Estimated layout** (no `textMeasurement`): the SVG names `Aptos` and `Aptos Display`, and the default raster engine draws them with its bundled sans-serif fallback (Roboto).\n- **Measured layout** with the opf-render office font pack and `substitutionPolicy: "visual"`: `Aptos` and `Aptos Display` resolve to their visual replacements from the OPF font policy (FF-31, provisional: Roboto, 2.15% mean width difference, and Carlito, 1.8%), and the substitution report lists both. Before FF-31 both resolved to Carlito. This is a visual-only look-alike: a documented fallback and a known layout-fidelity gap until a metric-compatible replacement exists. The PPTX always names the selected font, Aptos, never the replacement. See [font-fidelity.md](font-fidelity.md#font-policy-ff-31).\n- **Measured layout under the default metric policy**, or with only the base pack: `font-unavailable` for Aptos, as for a document with no `design`. Supply licensed Aptos faces, allow visual substitution, or set a `fallbackFamily`.\n\nUntil FF-35 (font-fidelity-everywhere), core pagination, opf-render and opf-editor fell back to `roboto` while opf-pptx used `aptos`, so such a deck was measured in Roboto but exported with Aptos. None of the 126 bundled examples reaches the last resort: all 805 renderer golden rasters and all 126 exported PPTX files are byte-identical before and after the change. `packages/javascript/test/font-scheme-defaults.test.mjs` checks the shared default in core pagination, and each sibling repository has a parity test.\n\n### Unknown font scheme\n\nA font-scheme id that matches no inline or bundled record (`"fontScheme": "no-such-scheme"`, `{ "id": "no-such-scheme", ... }`, or a theme record that names one) is handled the same way in every engine. The document still validates, because an id may name a record from a catalog the engine has not loaded:\n\n1. The `DEFAULT_FONT_SCHEME` record (`aptos`) is the base. Sibling fields on an object reference still override it per key, so `{ "id": "no-such-scheme", "major": "Inter", "minor": "Inter" }` uses Inter, and a `code` role still applies.\n2. The engine reports one `unresolved-font-scheme` diagnostic: `{ code, path, id, fallback: "aptos", message }`. `path` is where the id is written: `slides.N.design.fontScheme`, `design.fontScheme`, or the `slides.N.design.theme` / `design.theme` reference whose record names it.\n3. An object without `id` is an inline scheme on the same base and reports nothing.\n\n`resolveFontSchemeReference(reference, lookup, path)` in `@openpresentation/opf` implements this rule. `resolveFontFamilies()` also falls back to the default scheme\'s families (Aptos Display, Aptos) when a scheme names no heading or body family, instead of Roboto. Authoring-time `lintPresentation()` already warns about the unknown id (`opf/catalog-reference`).\n\n| Engine | Diagnostic channel | Reported |\n| --- | --- | --- |\n| Core pagination | `paginatePresentation(..., { onDiagnostic })` | once per path per call |\n| opf-render preview | `renderSvg` / `renderSvgDeck` `onDiagnostic` | once per path per rendered slide |\n| opf-editor | `session.composeSlide` / `paginateSlide` `onDiagnostic` option | once per call |\n| opf-pptx export | `toPptx(..., { onDiagnostic })` | once per path per export |\n\nBefore FF-35b, core pagination and opf-editor measured such decks in Roboto and opf-render threw `catalog-resolution-failed`. opf-pptx already exported Aptos, but reported nothing. None of the 126 bundled examples names an unknown font scheme. The 805 example SVGs, the 805 golden rasters and the 126 exported PPTX files are byte-identical before and after the change.\n\n### Sibling agreement checks\n\nopf-render, opf-editor and opf-pptx run the same unknown-scheme cases as core (`test/default-font-scheme.mjs`). Each package keeps a local copy of the default, and in opf-editor of the resolver. Their checks against core\'s `DEFAULT_FONT_SCHEME`, `resolveFontSchemeReference` and `paginatePresentation` run only when the installed core exports them. Those checks are skipped today: the siblings install the published `@openpresentation/opf` 0.11.0, which predates FF-35. They activate in either of two ways:\n\n- **Sibling CI:** after a core release that includes FF-35 and FF-35b is published, and each sibling\'s `@openpresentation/opf` dependency and lockfile move to it. After that release, the local copies can import core directly.\n- **Core ecosystem CI** (`.github/workflows/ecosystem-ci.yml`), which links this checkout\'s core into pinned sibling commits and runs their `npm test`: after those pins move to sibling commits that contain the FF-35 and FF-35b tests (the program\'s sibling pin bump).\n\nUntil then, the equality with core is established by running the sibling tests against a locally linked core.\n\n## Color references in content\n\nContent color fields (`TextRun.color`, styled table cell `style.fill` / `style.color`, table cell border `color`) accept references as well as literal hex, and those references resolve through the same chain above:\n\n```\n "color": "accent2" slot name -> effective color scheme slot\n "color": "text" role name -> role-to-slot mapping, then the slot\n "color": "var:risk" variable -> top-level variables map\n "color": "#B42318" literal -> used as-is (frozen at authoring time)\n```\n\n- **Slot names** (`accent1`\u2013`accent6`, `dark1`, `dark2`, `light1`, `light2`, `hyperlink`, `followedHyperlink`) read the named slot from the *effective* color scheme \u2014 the one produced by the slide \u2192 deck \u2192 theme \u2192 engine-default precedence at the top of this page. A slide-level `design.colorScheme` override therefore recolors that slide\'s named runs too.\n- **Role names** (`primary`, `secondary`, `accent`, `background`, `surface`, `text`, `textSecondary`) resolve through the same role handling engines already apply to color schemes: a role defined on the effective scheme is used directly; otherwise the engine maps the role onto a slot exactly as it does when serializing schemes.\n- **Variable references** (`var:<id>`) resolve against the document\'s top-level `variables` map, independent of the scheme. Variables are deck-scoped named colors \u2014 use them for values that have meaning (`var:risk`) or repeat across slides. An unknown id is a validation warning, never an error, and engines fall back to their default text color.\n\nThe styled table cell and border color fields enforce the reference forms at the schema level (a typo like `"acent2"` is a schema error there \u2014 neither hex, a known name, nor a `var:` reference). Run colors stay open strings so imported decks keep validating: an unrecognized run color is a validation warning, and renderers fall back to the theme text color \u2014 the same warn-don\'t-error posture unknown catalog ids get. Unknown `var:` ids are warnings everywhere.\n\n`@openpresentation/opf` exports `resolveColorRef()` with the shared slot, role, variable, and hex rules above so renderers and exporters do not drift. Pass the effective color scheme, optional resolved role colors, the deck `variables` map, and a theme-text `fallback` for unrecognized references.\n\n## Script fonts and language\n\nOOXML gives each theme font (major and minor) three script slots: `latin`, East Asian (`ea`) and complex script (`cs`). The presentation `language` and the effective font scheme resolve to all three:\n\n```\n latin design font scheme heading/body (the chain above)\n eastAsian 1. design.fontScheme.eastAsian explicit slot\n complexScript 2. the scheme\'s own major/minor when languageFamily is ea / cs and its\n languages list is empty or names the language\n 3. the language\'s font scheme when the language\'s script uses the slot\n 4. the latin family otherwise\n```\n\n- A language record\'s `script` (ISO 15924) picks its slot. East Asian scripts (`Jpan`, `Hans`, `Hant`, `Kore`, ...) use `eastAsian`. Complex scripts (`Arab`, `Hebr`, `Deva`, `Thai`, ...) use `complexScript`. Latin, Cyrillic, Greek and other scripts use `latin`. `direction` defaults from the script (Arabic and Hebrew are right-to-left).\n- The language\'s `fontScheme` applies to PowerPoint output and `googleFontScheme` to Google Slides output. For Latin-script languages, the design font scheme always supplies the latin slot.\n- A Latin deck therefore repeats its heading/body family in `ea`/`cs`. A Japanese deck with `design.fontScheme: { "major": "Carlito", "minor": "Carlito" }` keeps the Latin family in `latin` and uses Meiryo (PowerPoint) or Noto Sans JP (Google Slides) in `ea`. `design.fontScheme.eastAsian` / `.complexScript` (`{ "major": ..., "minor": ... }`) name a script font explicitly, for example for CJK text inside a Latin deck.\n\n`@openpresentation/opf` exports `resolveScriptFonts(document, { app, slideIndex })`, which returns the heading and body slots, the OOXML `lang` (a curated `ooxmlLang` culture tag such as `ja-JP` or `ms-MY`, or an authored region tag), the canonical `bcp47` tag, `script`, `direction`/`rtl`, and the per-script supplemental theme font. Renderers and exporters should use it rather than re-deriving slots. The model, the OOXML mapping and the open questions are in [`programs/font-fidelity-everywhere/script-font-model.md`](./programs/font-fidelity-everywhere/script-font-model.md). The renderer and exporter adopt it in separate changes, so their output is unchanged by this model alone.\n\n## What is *not* part of this chain\n\nBeyond the color references above, content payloads carry no design controls in v1 \u2014 `position`, `fontSize` overrides at payload level, and the like were deliberately kept out while the content model stabilizes (see [`content-item-design-overrides.md`](./content-item-design-overrides.md); styled table cells and rich-text runs carry the only per-content styling, and their color fields take the reference forms above). The design system, plus layout hints (`titleAlignment`, `contentBox`, `chartPrimary`, ...) and dynamic composition, is the styling surface of an OPF document.\n'
43
+ "markdown": '# Design Resolution\n\nHow an engine decides the effective design for any given slide. The schema spreads these rules across field descriptions; this page states them once, as an algorithm.\n\n## Precedence\n\nFor every design field independently, the most specific source wins:\n\n```\n wins +--------------------------------------------------------+\n ^ | 1. slide design slides[i].design.* |\n | +--------------------------------------------------------+\n | | 2. deck design design.* on the presentation root |\n | +--------------------------------------------------------+\n | | 3. resolved theme colorScheme, fontScheme, |\n | | background, dimensions from the |\n | | theme record |\n | +--------------------------------------------------------+\n loses | 4. engine defaults e.g. spec/reference/ |\n v | engine-defaults.json |\n +--------------------------------------------------------+\n```\n\n1. **Slide design** \u2014 `slides[].design.*`\n2. **Deck design** \u2014 `design.*` on the presentation root\n3. **Resolved theme** \u2014 defaults carried by the theme record (`colorScheme`, `fontScheme`, `background`, `dimensions`)\n4. **Engine defaults** \u2014 engine configuration such as [`spec/reference/engine-defaults.json`](../spec/reference/engine-defaults.json)\n\nResolution is **per field**, not per object. A slide that sets only `design.contentAlignment` inherits everything else from the deck design; a deck that sets only `design.colorScheme` keeps the theme\'s font scheme and background.\n\nTwo field-level rules complete the picture:\n\n- **Base-plus-overrides within one object.** Wherever a reference object carries an `id` (`Theme`, `ColorScheme`, `FontScheme`), the `id` resolves a catalog record as the base and sibling fields override the resolved record per key. The string shorthand (`"colorScheme": "cool-horizon"`) is equivalent to setting only `id`.\n\n ```\n "colorScheme": { "id": "cool-horizon", "accent1": "#0F4C81" }\n\n catalog record "cool-horizon" sibling fields on the object\n accent1: "#2874A6" <-- replaced -- accent1: "#0F4C81"\n accent2: "#1B4F72" <-- kept\n light1: "#FFFFFF" <-- kept\n |\n v\n effective scheme: accent1 from the override, everything else\n from the record\n ```\n- **Explicit suppression.** `watermark`, `header`, and `footer` accept `false` to switch off an inherited value \u2014 distinct from omitting the field, which inherits.\n\nCatalog lookups inside this chain follow the standard resolution order (inline `catalogs.<kind>.records[]` \u2192 `catalogs.<kind>.source` \u2192 default catalog); see [`how-opf-works.md`](./how-opf-works.md).\n\n## Worked example 1: color scheme through every level\n\n```json\n{\n "design": {\n "theme": "classic",\n "colorScheme": "forest-green"\n },\n "slides": [\n { "title": "Inherits the deck" },\n {\n "title": "Slide override",\n "design": {\n "colorScheme": { "id": "cool-horizon", "accent1": "#0F4C81" }\n }\n }\n ]\n}\n```\n\n- Slide 1: the `classic` theme record supplies its own default color scheme, but the deck design sets `colorScheme` explicitly, so `forest-green` wins (level 2 beats level 3). Fonts, background, and dimensions still come from `classic`.\n- Slide 2: slide design beats deck design (level 1 beats level 2). The `cool-horizon` record resolves as the base, then `accent1` is replaced by `#0F4C81`. All other `cool-horizon` slots survive.\n\nThere is no ambiguity between "override" and "reference": every scheme value *is* a reference, and any sibling fields on the same object are overrides applied after the reference resolves.\n\n## Worked example 2: backgrounds and suppression\n\n```json\n{\n "design": {\n "theme": "dark",\n "background": "light1",\n "footer": {\n "left": { "text": "Acme Corp" },\n "right": { "slideNumber": true }\n }\n },\n "slides": [\n { "title": "Light slide in a dark theme" },\n {\n "title": "Section divider",\n "design": {\n "background": {\n "type": "gradient",\n "gradient": {\n "angle": 90,\n "stops": [\n { "color": "#0B1B2B", "position": 0 },\n { "color": "#123A5F", "position": 1 }\n ]\n }\n },\n "footer": false\n }\n }\n ]\n}\n```\n\n- Slide 1: the deck-level `background: "light1"` overrides the `dark` theme\'s default background. `light1` is a theme slot \u2014 it resolves through the effective color scheme, which itself resolved through the chain above.\n- Slide 2: the gradient replaces the deck background for this slide only, and `footer: false` suppresses the inherited footer rather than inheriting or replacing it.\n\n## Worked example 3: font scheme models\n\n```json\n{\n "design": {\n "fontScheme": {\n "id": "aptos",\n "code": { "family": "JetBrains Mono" }\n }\n }\n}\n```\n\nThe `aptos` record supplies the OOXML pair (`major`/`minor`). The `code` role is an OPF-specific addition with no OOXML slot, so it layers on top without disturbing the pair. When serializing to PowerPoint, engines write `major`/`minor` to `majorFont`/`minorFont` and map abstract roles (`heading`, `body`) onto those slots. `accent` has no slot; the `code` family is written directly on code runs. The same slot-versus-role split applies to color schemes: OOXML slots (`accent1`\u2013`accent6`, `dark1/2`, `light1/2`) round-trip directly, abstract roles (`primary`, `text`, `surface`, \u2026) are mapped onto slots by the engine.\n\n### Code font\n\nThe `code` role resolves per key like every other override:\n\n1. `code` on the effective `design.fontScheme` object;\n2. `code` on the resolved font-scheme record (the `consolas` and `courier-new` records carry `{ "family": "Consolas" }` and `{ "family": "Courier New" }`);\n3. otherwise **Roboto Mono**, the documented fallback that `@openpresentation/opf-render` bundles.\n\nThe heading and body families are never reused as the code fallback, so choosing `aptos` still gives Roboto Mono code unless the deck sets `code`. `resolveFontFamilies()` in `@openpresentation/opf` applies these rules for all engines.\n\n### Engine default font scheme\n\nThe last-resort font scheme applies only when neither the slide, the deck nor the resolved theme names one. Every bundled theme names a font scheme (`minimal` uses `aptos`), and engines default the theme to `minimal`, so a document with no `design` gets `aptos` in every engine.\n\nEvery engine shares one last resort, `aptos`, so a custom theme without `fontScheme` is measured, paginated, previewed and exported in the same fonts. `@openpresentation/opf` exports it as `DEFAULT_FONT_SCHEME` (`resolveScriptFonts()` uses it too), and [`engine-defaults.json`](../spec/reference/engine-defaults.json) records it as `fontScheme.pptx.latin`:\n\n| Engine | Last resort | Where |\n| --- | --- | --- |\n| Core pagination | `DEFAULT_FONT_SCHEME` (`aptos`) | `packages/javascript/src/pagination.ts` |\n| opf-render preview | `aptos` (`engineDefaults.fontScheme.pptx.latin`) | `src/svg.js` |\n| opf-editor composition and slide transfer | `aptos` | `src/font-defaults.js` |\n| opf-pptx export | `aptos` (`DEFAULTS.fontScheme`) | `src/index.js` |\n\n\n`fontScheme.google` (`roboto`, `noto-sans-sc`, `noto-sans`) is not read by any current engine. It is kept as the intended default for a future Google Slides exporter, whose output renders in Google-hosted fonts.\n\nAptos is not openly licensed, so no OPF package bundles it. Previews take the same path for the last resort as for any `aptos` deck:\n\n- **Estimated layout** (no `textMeasurement`): the SVG names `Aptos` and `Aptos Display`, and the default raster engine draws them with its bundled sans-serif fallback (Roboto).\n- **Measured layout** with the opf-render office font pack (the default for `prepareNodeFonts({pack: \'office\'})`): `Aptos` and `Aptos Display` resolve to Intos and Intos Display, metric-compatible replacements from the OPF font policy (0.000% mean width difference against Aptos 2.01), and the substitution report lists both. With only the base pack and `substitutionPolicy: "visual"` they fall back to the visual alternates Roboto and Carlito. The PPTX always names Aptos. See [font-fidelity.md](font-fidelity.md#font-policy-ff-31).\n- **Measured layout with only the base pack under the metric policy**: `font-unavailable` for Aptos, as for a document with no `design`. Supply licensed Aptos faces, allow visual substitution, or set a `fallbackFamily`.\n\nUntil FF-35 (font-fidelity-everywhere), core pagination, opf-render and opf-editor fell back to `roboto` while opf-pptx used `aptos`, so such a deck was measured in Roboto but exported with Aptos. None of the 126 bundled examples reaches the last resort: all 805 renderer golden rasters and all 126 exported PPTX files are byte-identical before and after the change. `packages/javascript/test/font-scheme-defaults.test.mjs` checks the shared default in core pagination, and each sibling repository has a parity test.\n\n### Unknown font scheme\n\nA font-scheme id that matches no inline or bundled record (`"fontScheme": "no-such-scheme"`, `{ "id": "no-such-scheme", ... }`, or a theme record that names one) is handled the same way in every engine. The document still validates, because an id may name a record from a catalog the engine has not loaded:\n\n1. The `DEFAULT_FONT_SCHEME` record (`aptos`) is the base. Sibling fields on an object reference still override it per key, so `{ "id": "no-such-scheme", "major": "Inter", "minor": "Inter" }` uses Inter, and a `code` role still applies.\n2. The engine reports one `unresolved-font-scheme` diagnostic: `{ code, path, id, fallback: "aptos", message }`. `path` is where the id is written: `slides.N.design.fontScheme`, `design.fontScheme`, or the `slides.N.design.theme` / `design.theme` reference whose record names it.\n3. An object without `id` is an inline scheme on the same base and reports nothing.\n\n`resolveFontSchemeReference(reference, lookup, path)` in `@openpresentation/opf` implements this rule. `resolveFontFamilies()` also falls back to the default scheme\'s families (Aptos Display, Aptos) when a scheme names no heading or body family, instead of Roboto. Authoring-time `lintPresentation()` already warns about the unknown id (`opf/catalog-reference`).\n\n| Engine | Diagnostic channel | Reported |\n| --- | --- | --- |\n| Core pagination | `paginatePresentation(..., { onDiagnostic })` | once per path per call |\n| opf-render preview | `renderSvg` / `renderSvgDeck` `onDiagnostic` | once per path per rendered slide |\n| opf-editor | `session.composeSlide` / `paginateSlide` `onDiagnostic` option | once per call |\n| opf-pptx export | `toPptx(..., { onDiagnostic })` | once per path per export |\n\nBefore FF-35b, core pagination and opf-editor measured such decks in Roboto and opf-render threw `catalog-resolution-failed`. opf-pptx already exported Aptos, but reported nothing. None of the 126 bundled examples names an unknown font scheme. The 805 example SVGs, the 805 golden rasters and the 126 exported PPTX files are byte-identical before and after the change.\n\n### Sibling agreement checks\n\nopf-render, opf-editor and opf-pptx run the same unknown-scheme cases as core (`test/default-font-scheme.mjs`). Each package keeps a local copy of the default, and in opf-editor of the resolver. Their checks against core\'s `DEFAULT_FONT_SCHEME`, `resolveFontSchemeReference` and `paginatePresentation` run only when the installed core exports them. Those checks are skipped today: the siblings install the published `@openpresentation/opf` 0.11.0, which predates FF-35. They activate in either of two ways:\n\n- **Sibling CI:** after a core release that includes FF-35 and FF-35b is published, and each sibling\'s `@openpresentation/opf` dependency and lockfile move to it. After that release, the local copies can import core directly.\n- **Core ecosystem CI** (`.github/workflows/ecosystem-ci.yml`), which links this checkout\'s core into pinned sibling commits and runs their `npm test`: after those pins move to sibling commits that contain the FF-35 and FF-35b tests (the program\'s sibling pin bump).\n\nUntil then, the equality with core is established by running the sibling tests against a locally linked core.\n\n## Color references in content\n\nContent color fields (`TextRun.color`, styled table cell `style.fill` / `style.color`, table cell border `color`) accept references as well as literal hex, and those references resolve through the same chain above:\n\n```\n "color": "accent2" slot name -> effective color scheme slot\n "color": "text" role name -> role-to-slot mapping, then the slot\n "color": "var:risk" variable -> top-level variables map\n "color": "#B42318" literal -> used as-is (frozen at authoring time)\n```\n\n- **Slot names** (`accent1`\u2013`accent6`, `dark1`, `dark2`, `light1`, `light2`, `hyperlink`, `followedHyperlink`) read the named slot from the *effective* color scheme \u2014 the one produced by the slide \u2192 deck \u2192 theme \u2192 engine-default precedence at the top of this page. A slide-level `design.colorScheme` override therefore recolors that slide\'s named runs too.\n- **Role names** (`primary`, `secondary`, `accent`, `background`, `surface`, `text`, `textSecondary`) resolve through the same role handling engines already apply to color schemes: a role defined on the effective scheme is used directly; otherwise the engine maps the role onto a slot exactly as it does when serializing schemes.\n- **Variable references** (`var:<id>`) resolve against the document\'s top-level `variables` map, independent of the scheme. Variables are deck-scoped named colors \u2014 use them for values that have meaning (`var:risk`) or repeat across slides. An unknown id is a validation warning, never an error, and engines fall back to their default text color.\n\nThe styled table cell and border color fields enforce the reference forms at the schema level (a typo like `"acent2"` is a schema error there \u2014 neither hex, a known name, nor a `var:` reference). Run colors stay open strings so imported decks keep validating: an unrecognized run color is a validation warning, and renderers fall back to the theme text color \u2014 the same warn-don\'t-error posture unknown catalog ids get. Unknown `var:` ids are warnings everywhere.\n\n`@openpresentation/opf` exports `resolveColorRef()` with the shared slot, role, variable, and hex rules above so renderers and exporters do not drift. Pass the effective color scheme, optional resolved role colors, the deck `variables` map, and a theme-text `fallback` for unrecognized references.\n\n## Script fonts and language\n\nOOXML gives each theme font (major and minor) three script slots: `latin`, East Asian (`ea`) and complex script (`cs`). The presentation `language` and the effective font scheme resolve to all three:\n\n```\n latin design font scheme heading/body (the chain above)\n eastAsian 1. design.fontScheme.eastAsian explicit slot\n complexScript 2. the scheme\'s own major/minor when languageFamily is ea / cs and its\n languages list is empty or names the language\n 3. the language\'s font scheme when the language\'s script uses the slot\n 4. the latin family otherwise\n```\n\n- A language record\'s `script` (ISO 15924) picks its slot. East Asian scripts (`Jpan`, `Hans`, `Hant`, `Kore`, ...) use `eastAsian`. Complex scripts (`Arab`, `Hebr`, `Deva`, `Thai`, ...) use `complexScript`. Latin, Cyrillic, Greek and other scripts use `latin`. `direction` defaults from the script (Arabic and Hebrew are right-to-left).\n- The language\'s `fontScheme` applies to PowerPoint output and `googleFontScheme` to Google Slides output. For Latin-script languages, the design font scheme always supplies the latin slot.\n- A Latin deck therefore repeats its heading/body family in `ea`/`cs`. A Japanese deck with `design.fontScheme: { "major": "Carlito", "minor": "Carlito" }` keeps the Latin family in `latin` and uses Meiryo (PowerPoint) or Noto Sans JP (Google Slides) in `ea`. `design.fontScheme.eastAsian` / `.complexScript` (`{ "major": ..., "minor": ... }`) name a script font explicitly, for example for CJK text inside a Latin deck.\n\n`@openpresentation/opf` exports `resolveScriptFonts(document, { app, slideIndex })`, which returns the heading and body slots, the OOXML `lang` (a curated `ooxmlLang` culture tag such as `ja-JP` or `ms-MY`, or an authored region tag), the canonical `bcp47` tag, `script`, `direction`/`rtl`, and the per-script supplemental theme font. Renderers and exporters should use it rather than re-deriving slots. The model, the OOXML mapping and the open questions are in [`programs/font-fidelity-everywhere/script-font-model.md`](./programs/font-fidelity-everywhere/script-font-model.md). The renderer and exporter adopt it in separate changes, so their output is unchanged by this model alone.\n\n## What is *not* part of this chain\n\nBeyond the color references above, content payloads carry no design controls in v1 \u2014 `position`, `fontSize` overrides at payload level, and the like were deliberately kept out while the content model stabilizes (see [`content-item-design-overrides.md`](./content-item-design-overrides.md); styled table cells and rich-text runs carry the only per-content styling, and their color fields take the reference forms above). The design system, plus layout hints (`titleAlignment`, `contentBox`, `chartPrimary`, ...) and dynamic composition, is the styling surface of an OPF document.\n'
44
44
  },
45
45
  {
46
46
  "slug": "dynamic-composition",
47
47
  "file": "docs/dynamic-composition.md",
48
48
  "title": "Dynamic composition",
49
- "markdown": '# Dynamic composition\n\nOPF keeps authoring intent in JSON. Use `blocks` when content can reflow; use promoted regions when relative placement is meaningful. `composition` on a slide overrides fields in the resolved layout\'s `composition`. Existing documents remain valid.\n\nThe current published Node 24 train is core 0.11.0, renderer 0.9.0, PPTX 0.9.1, editor 0.8.0 and CLI 0.9.0. Use the exact pins in [release-plan.json](../release-plan.json); the [compatibility matrix](compatibility-matrix.md) separates package support from native Office and font gates. Older version references below identify when individual contracts were introduced.\n\n```json\n{\n "name": "Decision brief",\n "slides": [{\n "title": "Make the main idea clear",\n "composition": { "mode": "row", "weights": [2, 1], "overflow": "error" },\n "blocks": [\n { "text": "The evidence and recommendation receive twice the width." },\n { "text": "The supporting detail receives the remaining width." }\n ]\n }]\n}\n```\n\n`auto` evaluates candidate grids using text fit and cell proportions. `columns` limits its candidates. `grid` uses `columns` if given, otherwise a grid based on the canvas shape. `row` uses one row; `column` uses one column. Items retain source order. Weights size columns except in column mode, where they size rows. Missing weights are 1; unused weights have no effect. A partially filled final row retains its grid tracks.\n\n`gap` defaults to 1/30 and `padding` to 0.08, both fractions of the canvas\'s shorter edge. Large gaps are reduced when necessary to keep cells positive. `minFontSize` defaults to 16 reference pixels at a 720-pixel short edge. The reference coordinate system uses 96 pixels per inch. Explicit inch dimensions override presets independently for each axis.\n\nHeadings reserve space according to their wrapped text. Content that exceeds the number of preset placeholders reflows together; it is not drawn over already-bound content. Promoted regions keep the 3\xD73 vocabulary, including standalone `top`, `middle`, and `bottom`. They ignore flow direction and track weights.\n\n## Shared headers and footers\n\nPublished core 0.11.0 exposes `layoutFurniture(slide, options)` and `geometry.furniture`, separate from body `items`. Composition identifies the available-space policy as `grid-score-v9`. Raw callers pass the presentation as `options.presentation`, resolved dimensions/fonts and the same measurement provider used by preview. `slideIndex` identifies source paths; optional `slideNumber` is the one-based displayed number.\n\nThe core resolver honors whole local header/footer overrides, including `false` and empty objects. Each zone retains its image and all configured text fields in source-aware parts. Literal text and dates preserve whitespace and empty strings. Organization and section values point to their metadata source; page numbers use the actual output sequence. A missing organization/section, or `date: true` without a host-supplied current date, produces `unresolved-content`; the implementation never consults a clock or invents source text.\n\nSlide numbers and dates carry formats. `slideNumberFormat` is a template such as `"A-{current}"` or `"{current} / {total}"`: `{current}` is the displayed number and `{total}` is the displayed slide count (`options.slideCount`, else `presentation.slides.length`; whole-deck pagination iterates to the final page count). `dateFormat` is an LDML-style pattern (`yyyy`, `yy`, `MMMM`, `MMM`, `MM`, `M`, `dd`, `d`, `EEEE`, `EEE`, quoted literals) with fixed English month and weekday names. A string `date` with `dateFormat` must be an ISO `YYYY-MM-DD` date and renders as fixed, generated text tied to that source value; without `dateFormat` a date string stays literal and editable. `date: true` is the current date: hosts pass today\'s ISO date as `options.date` (composition, pagination, renderer and exporter), and the default pattern is `M/d/yyyy`. Text parts expose `fields` (half-open UTF-16 ranges of each `{current}` number and of a whole current date), so exporters can write native live fields while `{total}` and fixed dates stay fixed text. `formatFurnitureDate()` and `formatSlideNumber()` are exported for hosts. All configured fields in one zone stack, in the order image, text, organization, section, slide number, date; put a date and a slide number in different zones to keep one line each. Hiding furniture on a title slide is the slide-level `design.header: false` / `design.footer: false` override.\n\n`socials: true` (core 0.11.1 and later) generates one `socials` part from the primary organization\'s `organization.socials`. The part has one source line per platform, in key order, and a parallel `links` array (`platform`, `text`, `href`, `resolved`, `sourcePath`). `resolveSocialProfile(platform, value, records, owner)` formats each value without network access. A handle loses its `handlePrefix` and is substituted into `companyUrlPattern`, then `profileUrlPattern`, then `baseUrl/{handle}`; the result is shown as that URL without `https://`. A URL value passes through unchanged except that `https://` is dropped from the display. With no matching record, the value is shown raw with no link, which is the Socials engine fallback. Records come from inline `catalogs.socialPlatforms.records` first and then from host-supplied `options.socialPlatforms`. Composition never loads the bundled catalog itself; `paginatePresentation`, opf-render and opf-pptx pass it. A missing organization, or one with no non-empty socials, produces `unresolved-content`. Speaker socials, platform icons, brand colors and slide-size presets are not rendered.\n\n`furniture-flow-v2` gives each left/center/right zone 26% of the canvas width. Parts stack within a zone; the tallest zone sets the natural band height. Text uses at least the selected readability floor, with complete accepted source lines and optional measured outline placement. Header and footer bands reserve room before heading and body allocation. Irreducible text, conflicting bands or a heading displaced beyond the remaining space produce diagnostics; strict composition rejects them. No-furniture body geometry remains unchanged.\n\nPagination repeats these fields without putting them among body slices. An optional `page.repeatedMappings` records repeated heading/furniture and metadata paths while the existing `page.mappings` retains its body-fragment contract. Whole-deck pagination evaluates final output numbers, including preceding continuation pages, and rejects unresolved repeated content atomically. Renderer and editor reuse the accepted parts; literal text/date fields, including empty values, support direct canvas editing and undo. Generated labels remain tied to metadata.\n\nPublished PPTX 0.9.1 draws the accepted editable text boxes and fitted images and records furniture provenance in tagged slide shapes. Reimport uses current native text and images; damaged or ambiguous provenance retains visible content with diagnostics. The [fresh installed-package evidence](evidence/shipped-train-20260921/installed/acceptance-summary.json) includes deterministic export, current-content reimport controls and offline canvas editing/undo. Native PowerPoint acceptance, font compatibility and full visual review remain separate gates; this is not native `p:hf` Header/Footer support. Bounds/readability checks do not certify whole-slide design quality: long labels can wrap heavily in portrait zones, and outline agreement does not establish native font identity.\n\n## Slide-level images\n\nCore 0.11.1 and later resolve `design.slideImage` into `geometry.slideImage`, beside body `items`. It applies to a slide in three cases:\n\n- The slide sets its own `design.slideImage`.\n- The deck sets `design.slideImage` and the slide\'s layout record declares `slideImage: true`.\n- The deck sets `design.slideImage` and the slide\'s root `image` is the same source, as in the pptx.gallery image-treatment snippets.\n\nOther slides ignore a deck-level value, so existing decks keep their geometry: 81 bundled example decks set a deck-level slide image and none of them changes. When the value is the asset shorthand rather than a `{ position }` object, the layout\'s `slideImageAlignment` supplies the position, and `background` is the fallback.\n\n`background` gives the image the whole slide, and headings and content compose unchanged over it. `left`, `right`, `top` and `bottom` give the image half the slide, edge to edge, and headings and content compose in the other half with the usual padding. Header and footer bands keep their full-width placement. The frame uses `design.imageFill`, with `crop` as the default: `crop` covers the frame from the center and `fit` shows the whole image centered inside it. Without `design.imageFill`, the content-image default stays `fit`.\n\nThe slide\'s root `image` becomes the slide image, not a second content item, in two cases: the treatment object omits `src`, or `src` is the same source as the root image. A root image with a different source stays content. The result reports `path` (the configuring design value), `sourcePath` (where the drawn asset lives), `region`, `box` and `replacesContent`. Coordinated opf-render draws the frame beneath content. Coordinated opf-pptx exports one native `p:pic` at the same frame, with crop and fit written as `a:srcRect`. A tagged picture that has not been edited imports back as the slide\'s `design.slideImage`. The treatment vocabulary is covered in [image treatments](image-treatments.md): size, inset, aspect ratio, preset masks, line, opacity, grayscale or duotone, and overlay. That page also gives the support status of each pptx.gallery treatment.\n\n## Nested groups\n\n### Shared content cards\n\nShared content cards are published in core 0.10.0 and later, including current core 0.11.0. For `design.contentBox: true`, each body leaf carries a `frameBox` at its outer allocation and a `box` padded inward by 12 reference pixels at a 720-pixel short edge, capped at one quarter of the frame\'s width or height. Scoring, accepted payload measurement, strict overflow and pagination all use that rounded interior. Headings remain unframed, nested groups keep their original padding, and explicit outer regions/track weights remain authoritative. Automatic candidates may change because their available content space changes.\n\nEvery composed item carries its resolved horizontal text `alignment` (`left`, `center` or `right`). The title uses `titleAlignment`; every other item, including subtitle, tag, body text, lists, tables and metrics, uses `contentAlignment`. A slide\'s explicit design value wins over the host option, and the default is `left`. The title never inherits `contentAlignment`. Accepted outline placement and metric internals use the same value. The renderer and the PPTX exporter anchor preview and native text to `item.alignment`, so both engines place a layout\'s text the same way.\n\nRaw composition callers pass their resolved deck flag as `composeSlide(slide, {contentBox: effectiveDesign.contentBox, ...options})`; a slide\'s explicit `design.contentBox: false` overrides it. Coordinated renderer, editor and whole-presentation pagination resolve this option for their callers. Consumers draw at `frameBox` and use the accepted `box` and payload internals without another inset. Core 0.9.0 predates this behavior. Content cards do not make the incomplete chart/timeline density models complete or certify native raster fidelity.\n\nA block or promoted region can contain its own `blocks` and `composition`. The optional discriminator is `"type": "group"`. A group has at least one child and cannot mix children with leaf fields such as `text` or `image`.\n\n```json\n{\n "composition": { "mode": "row", "weights": [2, 1] },\n "blocks": [\n {\n "composition": { "mode": "column", "padding": 0.02 },\n "blocks": [{ "text": "Recommendation" }, { "text": "Supporting evidence" }]\n },\n { "text": "Context" }\n ]\n}\n```\n\nThe parent allocates a box to each group, then the group arranges its children inside that box. Group padding defaults to zero; padding and gap use the group\'s shorter edge. Only `minFontSize` and `overflow` inherit. A strict ancestor cannot be weakened by a child\'s `overflow: "warn"`. Font sizes remain relative to the canvas, not the group. Groups can nest up to 32 levels; cycles and deeper nesting fail with an explicit error.\n\nAutomatic grid scoring inspects descendant text using each descendant\'s explicit arrangement or geometric automatic seed. After selecting the parent\'s grid, it optimizes each child\'s automatic grid. This deterministic, bounded search avoids exponential combinations; it does not claim a globally optimal packing.\n\n`result.items` contains every leaf with its full source path and effective composition. `result.groups` contains group paths, outer bounds, and content bounds. The editor\'s `setGroupComposition(path, value)` validates and records undo/redo just like slide composition edits.\n\n## Inspecting and repairing layout\n\n```js\nimport { composeSlide } from \'@openpresentation/opf/composition\';\nconst result = composeSlide(deck.slides[0], { width: 1280, height: 720, layout: resolvedLayout });\nconsole.log(result.items); // Source paths, content, geometry, and text estimates\nconsole.log(result.diagnostics); // Path-specific text-overflow and small-cell messages\n```\n\nThis pure function expects a validated slide. The caller resolves catalog records and passes the canvas size. The rendering and export packages perform those steps at their boundaries. No network, DOM, system font, or AI dependency is required.\n\n### Explain automatic selection (core 0.8.0 and later)\n\nPass `explain: true` to return `result.explanation`. This opt-in API requires core 0.8.0; it is absent from core 0.7.0. Enabling explanations adds no measurement calls and does not change geometry, source content, reading order, weights or selected arrangements within the same engine version.\n\n```js\nconst result = composeSlide(slide, {...resolvedOptions, explain: true});\nfor (const decision of result.explanation.decisions) {\n console.log(decision.path, decision.reason, decision.selectedColumns);\n console.table(decision.candidates);\n}\nconsole.log(result.explanation.textMeasurement);\nconsole.log(result.explanation.unmeasuredPayloads);\n```\n\n`resolvedOptions` supplies the same dimensions, layout, fonts and optional width provider as the preview. Core 0.8.0 identifies its explanation as `grid-score-v2`; core 0.9.0 advances to `grid-score-v3` to include complete code metadata/body measurements. Current core 0.11.0 reports `grid-score-v9`. These versions record containers in parent-before-child order. `lowest-score` reports the candidates actually tried; `configured-mode` respects resolved row/column/grid intent and returns no invented candidates. `promoted-regions` leaves region placement fixed and has no selected column count. Empty slides have no decisions. Automatic search tries one through `min(slotCount, columns ?? 6)` columns, in ascending order; ties retain the first candidate. Reserved placeholders count as slots. The schema caps an explicit candidate limit at twelve columns.\n\nEach candidate has `columns`, `rows`, `score` and additive `penalties`:\n\n| Penalty | Rule |\n| --- | --- |\n| `cellProportions` | Sum of `abs(log(cellAspect / 1.6))` for descendant leaves |\n| `fontReduction` | Reduction from 25 reference pixels for text-like leaves; current quote/code/metric/timeline layouts sum requested-minus-fitted sizes across their parts, divided by canvas scale |\n| `textOverflow` | 1,000 per overflowing text-like leaf or complete quote/code/metric/timeline payload, regardless of the number of internal failure reasons |\n| `tableOverflow` | 1,000 per table whose shared cell layout overflows |\n| `smallCells` | 100 per leaf narrower than 100 or shorter than 60 reference pixels |\n| `emptySlots` | 2 per unused position in the candidate grid\'s final row |\n\nScores are preference costs, not quality percentages or guarantees. Floating-point summation can make the component total differ slightly from `score`. Parent scoring uses descendant explicit arrangements or geometric automatic seeds; child automatic grids are optimized only after selecting the parent. Candidate scores therefore describe the bounded search, not a full assessment of the final optimized subtree. Heading fit remains in ordinary diagnostics, outside body-grid scoring. A strict-fit rejection exposes the explanation on `OPFCompositionError` when requested.\n\n`textMeasurement` is `estimated` without a provider and `provided` with one. A provided width function does not establish font provenance, glyph coverage, shaping or native raster fidelity. Text, rich text, lists, quotes, table cells and code in core 0.9 participate in the fit model. Current core 0.11.0 also measures metric and timeline parts; its `unmeasuredPayloads` identifies images, video and charts whose complete internal layout is not assessed. Core 0.8 reports code as incomplete; core 0.9 measures its filename/language/body and insets. Media aspect ratios, chart labels and complete timeline visual density still require inspection. A zero score or empty diagnostics is not proof that those payloads fit.\n\nExplanations expose the search for inspection and do not silently paginate or rewrite a document. Published editor 0.8.0 provides guarded track resizing, block moves, creation/removal and explicit pagination with preview/undo. A general automatic repair loop, automatic weight allocation, complete payload-internal measurement, CLI explanations and a canvas **Auto arrange** preview/undo operation remain open work; the **Arrange** controls below are explicit human adjustments.\n\nWith `overflow: "warn"` (default), the result retains all text and returns diagnostics. SVG emits all lines and marks overflowing groups with `data-opf-overflow="true"`; text may extend beyond its box or canvas. Consumers can collect diagnostics using `onDiagnostic`. With `overflow: "error"`, the layout rejects content that does not fit. Shorten the affected content, give it more space, or explicitly split it into another slide. Use the explicit pagination transform below to produce additional editable slides.\n\nThe editor exposes `editor.composeSlide(index)` and `editor.setComposition(index, value)`. The latter validates the change, records JSON Patch history, and supports undo/redo.\n\n## Pagination\n\n```js\nimport { paginateSlide, paginatePresentation } from \'@openpresentation/opf/pagination\';\nconst { presentation, pages } = paginatePresentation(deck);\n// Review, save, render, or export `presentation`; pages maps output fragments to source paths.\nconst single = paginateSlide(deck.slides[0], { width: 1280, height: 720, minFontSize: 24 });\n```\n\nPagination is an authoring operation. It produces ordinary OPF slides; previews and PPTX export consume those exact pages. It preserves the input, body order, nested groups, promoted regions, rich-text formatting, and source text characters. Plain text and rich runs split at grapheme boundaries, preferring sentence/paragraph breaks and then word breaks. Lists split between items, tables between rows with column labels repeated, and code splits without rewriting its source. Indivisible payloads remain intact. Existing track weights continue to apply to positions on each resulting page.\n\nThe default readability target is 24 reference pixels. Core 0.8.0 returns slides that persist that floor in `composition.minFontSize`, including an already-fitting one-page result; existing higher minima and strict overflow policies remain intact. Quotes can raise their nominal body/footer sizes to the floor. Current core 0.11.0 enforces the selected floor in shared plain/rich/list/table fitting, including painted rich fragments, while retaining authored style metadata. Unsupported fits report overflow rather than silently capping output below the floor. The [readability-floor checkpoint](plans/readability-floor.md) records the earlier candidate and its bounded-search tradeoffs. Pagination relies on the shared engine\'s estimates; it is not a guarantee that every host font renders identically. Headings repeat unchanged, speaker notes remain on the first page, and continuation IDs avoid existing deck IDs. `pages[].mappings` records full source/output paths and half-open text or item ranges. Text offsets use UTF-16, so source strings can be reconstructed exactly. Quote bodies split at grapheme boundaries and repeat complete attribution/source fields on each page. An irreducible footer rejects the whole operation, including a quote with an empty body after earlier content.\n\nIf a heading, individual list item, table row, or other atomic payload cannot fit on an otherwise empty page, `OPFPaginationError` returns actionable diagnostics. There is no partial output. `maxSlides` defaults to 100, and a layout-evaluation limit bounds work on pathological input. Specialized chart and timeline internals still require visual inspection; their complete density models remain outstanding.\n\nThe editor\'s `editor.paginateSlide(index)` is one validated transaction with undo/redo. It returns `{change, pagination}`. Editor 0.5.0 commits a one-page readability-policy change too; repeating the operation after the policy is recorded returns `change: null`. The playground includes an overflowing draft and **Split overflow** action. The CLI writes a new file and refuses to overwrite an existing one:\n\n```sh\nopf paginate input.opf.json output.opf.json\n```\n\n## Fidelity boundary\n\nThe shared engine provides identical body and heading geometry to SVG and editable PPTX export. Text measurements default to deterministic estimates. For actual font advances, use the shared provider described in [measured fonts](font-fidelity.md). Complex scripts, fallback fonts, PowerPoint text rendering, rich text, charts, tables, and images still need visual verification. Dynamic composition is not a guarantee of pixel-identical PowerPoint output. List density includes rich runs, descriptions and nesting via `fitList`, with the same hanging indents used in preview and export. Only text-like payloads currently receive content-density estimates; small-cell diagnostics also cover non-text content.\n\nSVG embeds raster data URI images locally. Remote and file images require a host resolver that supplies a raster data URI; otherwise they appear as placeholders. `strictAssets` rejects unresolved images. The runtime never fetches them.\n\nSee [the complete example](../examples/technical/dynamic-composition.opf.json) and [local ecosystem verification](ecosystem-development.md).\n\n\n## Metric internals (published coordinated packages)\n\nPublished core 0.11.0 exports `layoutMetric(value, box, options)` from the root and `@openpresentation/opf/composition`. It accepts a finite number, string, or `{value, unit?, label?, description?, delta?, trend?}`. Renderer 0.9.0, editor 0.8.0 and PPTX 0.9.1 consume its shared geometry for composition, atomic pagination, preview, editing and export. Core 0.9.0 predates this API. The [primitive checkpoint](plans/shared-metric-layout.md) and [source integration checkpoint](plans/shared-metric-integration.md) record its development; current pins and remaining native/font gates are in the [compatibility matrix](compatibility-matrix.md).\n\nPass the allocated reference-pixel `box`, resolved heading/body `fonts`, `textMeasurement`, canvas `scale`, effective `minFontSize`, source `path` and optional `overflow: \'error\'`. `metric-flow-v1` returns separate value/unit/label/description/delta/trend parts in that order. Numeric zero is visible; scalar values keep the scalar path. Every provided field retains its original string or number in `sources[].value`. Source ranges address `String(value)` using UTF-16 offsets; the original spelling of a numeric JSON token is not available. No locale formatting, trend icon, case conversion or separator is invented. Empty optional strings retain source mappings with `visible: false`; the required empty value keeps a targetable blank line.\n\nThe allocator tries an adjacent value/unit baseline when both fit one line and the unit uses at most 35% of the cell width; otherwise it stacks the fields. Metadata has an eight-reference-pixel gap, with twelve pixels after the primary row. Related fields stay together rather than being separated by a percentage of the cell height. The value starts at up to 76 reference pixels (28% of cell height); label/unit/delta start at 23, description at 20 and trend at 18. Every requested size is raised to the chosen floor, scaled once. Natural metadata height gets space before reducing type. At most 48 arrangements are evaluated, each with at most 77 value-size trials; identical inputs and a deterministic measurement provider select the fitting candidate with least summed font reduction, preferring the first candidate on ties.\n\nEach part exposes requested/resolved styles and the same source-preserving line/segment representation used by code (`CodeTextFit`), measured with proportional heading/body fonts. CR/LF/CRLF, tabs, whitespace and grapheme boundaries remain exact. Consumers must reuse accepted line and segment positions, font sizes and styles rather than independently re-fit or normalize text. This API reports `provided` measurement when a provider is passed, without claiming that its glyph coverage or shaping is complete. Unsupported glyphs propagate the provider\'s error with the field path.\n\nPass `align: \'left\' | \'center\' | \'right\'` (default left) to the primitive. Its returned `alignment` and per-part `linePositions` give an absolute x origin and baseline for each `fit.sourceLines` entry, including blank lines. An inline value/unit pair moves together, with the gap following the actual value advance. Composition accepts host-resolved `contentAlignment`; an explicit slide `design.contentAlignment` overrides it. Renderer/export/pagination pass the effective design into the same operation. Alignment does not trigger a second font fit.\n\nCheck `overflow` before consuming parts. Irreducible text, invalid available space, parts outside the cell and overlapping occupied line boxes return field-specific diagnostics; strict mode throws `OPFCompositionError`. Invalid available boxes retain their dimensions and have no fit. These are advance-based line rectangles, not glyph outlines: the controlled browser evidence separately records small glyph overhangs. The API is a bounded internal allocator, not the complete layout-repair/Auto arrange operation or a native export fidelity guarantee.\n\nCurrent `grid-score-v9` retains the metric scoring introduced by `grid-score-v4`: every metric part contributes font reduction, with one overflow penalty per failing metric leaf. `item.metricLayout` is measured against the rounded accepted cell; `item.text`/`item.textStyle` alias the value fit/style. Field diagnostics obey strict ancestor policies, while explicit modes/weights/regions remain authoritative. Metrics are excluded from the advance-model `unmeasuredPayloads` list. Explicit pagination retains metrics atomically with complete source types/metadata and rejects irreducible fields without returning partial output. The published train has [installed-package acceptance](evidence/shipped-train-20260921/installed/acceptance-summary.json), including metric provenance controls. That evidence does not establish native raster or font equivalence.\n\n## Code internals (core 0.9.0 and coordinated packages)\n\nCore 0.9.0 introduced `layoutCode(value, box, options)` for the schema\'s string shorthand or `{source, language?, filename?}` object. It returns measured filename/language/body parts with exact original text, requested/resolved styles, readability floors, available boxes and diagnostics. Its original integration targeted renderer/PPTX 0.7.0 and editor 0.6.0; the [release checkpoint](plans/shared-code-release.md) records that rollout. These contracts are published in the current core 0.11.0, renderer 0.9.0, PPTX 0.9.1 and editor 0.8.0 train. Core 0.8.0 predates this API and retains the [recorded code-label, filename and whitespace defects](plans/layout-repair.md).\n\n`grid-score-v3` charges code font reductions across all metadata/body parts and one overflow penalty per failing leaf. It preserves explicit modes, weights, regions and source order. Accepted `item.codeLayout` is fitted to the same rounded cell exposed as `item.box`; `item.text` and `item.textStyle` alias the body, not the first metadata part. Strict ancestor settings apply to internal `.source`, `.filename` and `.language` diagnostics. Code no longer appears in `explanation.unmeasuredPayloads`, which concerns the core advance-based model only. It does not mean browser/native fidelity is verified.\n\nPagination slices the code body at grapheme boundaries, repeats filename/language and returns contiguous UTF-16 body ranges while preserving all source bytes and the evaluated readability floor. Irreducible metadata rejects all output, including when the body is empty or earlier content could have fitted. Consumers must preview/export the returned document. The [integration checkpoint](plans/shared-code-integration.md) separates source, installed browser, Windows PowerPoint and remaining release gates.\n\nEach fitted part retains every space and explicit CR/LF/CRLF break. `fit.lines` contains exact source slices, and `fit.sourceLines` records half-open UTF-16 `start`, `end` and `nextStart` offsets, the measured width and a `soft`, `hard` or `end` boundary. A hard break occupies `[end, nextStart)`; soft wrapping consumes no source character. Joining `part.text.slice(line.start, line.nextStart)` reconstructs the original part. Blank lines and a final empty line are retained, and long tokens split only at grapheme boundaries. Filename and language text are not case-converted. An absent/empty metadata pair creates a generated `code` label with no source range.\n\n`code-flow-v1` uses 18-reference-pixel outer insets, an eight-pixel gap between filename and language, and a twelve-pixel gap before the body. Nominal metadata/body sizes are 14/18 reference pixels, raised when necessary to respect the selected minimum, then scaled once. At most four metadata nominal/floor combinations are tried; each body fit tries at most 19 sizes regardless of canvas scale. The fitting combination with least font reduction wins. Irreducible metadata/body failures retain all text and diagnostic paths; invalid available boxes have no fit. `overflow: \'error\'` rejects rather than returning partial output.\n\nTabs remain literal characters in part text and displayed-line slices. Measurement advances to the next multiple of four measured spaces from that line\'s origin; `fit.tabSize` and `fit.tabWidth` expose the rule. Each source line\'s `segments` contains exact text/tab source ranges plus measured `x`/`width` values relative to its origin. Consumers must reuse those positions: an Edge probe showed that SVG treats a tab as one space despite CSS `tab-size: 4`. The published SVG renderer uses positioned spans and geometric precision; native export uses accepted tab stops. Width measurements and source preservation alone do not establish glyph-outline containment, shaping/bidi support or native fidelity. [Installed workflow evidence](evidence/shared-code-installed/summary.json) records the separate actual browser and native checks with their exact font/runtime scope.\n\nPublished native export stores source boundaries in standard PowerPoint shape tags. Complete unique groups recover exact code/source metadata, with current native text taking precedence. Missing, damaged or ambiguous groups retain visible native shapes and report diagnostics. Reimport does not reconstruct native formatting, positioning, font theme or readability policy. Eight installed-export wide/portrait slides pass native edit/save/reopen and all 24 original/saved/edited imports on the recorded Windows PowerPoint build; this is not arbitrary PowerPoint round-trip or pixel equivalence. The editor preserves untouched CRLF/CR source around edits and keeps committed preview geometry separate from its active native textarea caret.\n\nThe JSON schema can accept strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets). Published SVG/PPTX code output rejects forbidden controls, unpaired UTF-16 surrogates, U+FFFE and U+FFFF with `invalid-code-text`, the source field path and UTF-16 offset in the message. The input stays unchanged; the caller can correct that character explicitly. Tabs, CR/LF/CRLF and valid supplementary characters remain accepted for serialization. Schema support, format representability and glyph coverage are separate properties.\n\nThe controlled SVG harness requests `text-rendering="geometricPrecision"` as well as explicit segment placement. Initial Linux Chromium CI rounded glyph advances under default hinting, unlike Windows Edge with the same font bytes. The [SVG specification](https://www.w3.org/TR/SVG/painting.html#TextRenderingProperty) defines geometric precision as a rendering hint, so consumers still need actual browser checks with their exact fonts and supported environments; the hint alone does not certify agreement. The harness retains a 0.1-reference-pixel tolerance and records observations before assertions.\n\n## Quote internals (core 0.8.0 and coordinated packages)\n\nCore 0.8.0 exports `layoutQuote(value, box, options)` from the root or composition entrypoint. Pass validated quote content (object or string shorthand), its allocated reference-pixel box, resolved `fonts`, `textMeasurement`, `scale` (canvas short edge / 720), effective `minFontSize`, `overflow` policy and its source `path`.\n\nThe result contains `parts` for the body and any nonempty footer, exact display `text`, source mappings, requested and resolved text styles, and the available boxes/fits. Source ranges use half-open UTF-16 offsets in both the source field and display string; generated quotation marks and the footer separator have no source range. The original content is never modified. A supplied width provider is reported as `provided`; it does not certify shaping or font fidelity.\n\nCheck `overflow` and `diagnostics` before accepting the parts. Invalid available dimensions remain visible with `fit` absent, and `overflow: \'error\'` throws `OPFCompositionError`. Diagnostics distinguish invalid part space, parts outside their cell, text that exceeds its reserved space, and overlapping line rectangles. Those rectangles are conservative text-layout bounds, not measured glyph outlines. The readability floor is scaled once and can raise the nominal body (28) or footer (17) size; it is never silently capped below the selected floor.\n\n`quote-flow-v1` keeps 18-reference-pixel outer insets and an 18-pixel body/footer gap while fonts scale with the canvas. A 40-pixel footer is a whitespace preference. The allocator expands it for long sources or compacts it for dense bodies, trying at most the nominal and minimum footer sizes and selecting the fitting pair with least total font reduction. If neither fits, it returns floor-size failure diagnostics. This is a bounded internal allocation step, not a complete layout-repair engine.\n\n`composeSlide` scores both parts and accepts geometry against the final rounded item box. Each quote item carries `quoteLayout`; its compatibility `text` field is the same fit object as the quote body, including generated quotation marks. Consumers needing original offsets must use the explicit `sources` mappings. The coordinated renderer and PPTX consume these parts without another measurement/style-resolution pass. Missing geometry or invalid part boxes reject rendering/export rather than omitting content. This requires core 0.8.0 with renderer/PPTX 0.6.0; older core 0.7.0/renderer 0.5.1/PPTX 0.5.2 lack these changes. The complete published set, immutable verification refs and fresh registry evidence are recorded in `release-plan.json` and [the release plan](plans/shared-quote-release.md).\n\nBrowser glyph bounds can extend slightly beyond advance-based part boxes into the reserved inset. Current loaded-font tests record those overhangs, verify glyph containment inside the full quote cell and check body/footer separation. Native PowerPoint fixtures separately verify text, sizes, cell containment, save/reopen and reimport. Neither test establishes universal pixel equivalence. Original requested-font provenance through host substitutions and non-quote payload internals remain open requirements.\n\n## Resizing in the preview\n\nChoose **Arrange** in the editor to reveal track dividers. Drag a divider to redistribute the space between adjacent columns (row/grid) or rows (column), including nested groups. Arrow keys make small changes; Shift makes larger changes. Escape discards a pointer draft. One drag creates one undo step, and no content is removed. Strict overflow rejects a resize that violates its fit constraints.\n\nResizing an automatic layout makes its chosen columns explicit as `mode: grid` with `columns`. This prevents the number of columns from changing under the pointer. The adjacent share clamps to 5\u201395%, with positive schema-valid weights. Other track proportions and unrelated document fields remain intact. Promoted regions retain their positions; their nested groups can still be resized. Layouts with reserved placeholder slots need an explicit arrangement first. Flows with more than twelve tracks need grouping before the current resize controls can express all weights.\n\n`createCanvasEditor(container, {layoutEditing: true, ...options})` enables dividers initially. `canvas.setLayoutEditing(boolean)` toggles them, and `canvas.commit()` / `canvas.cancel()` also handle an active resize. `onDraft` receives the proposed document; the session stays unchanged until commit. Changes to the resized container cancel a stale draft; unrelated updates are retained.\n\nThe shared engine exposes `geometry.flows`: each flow has its container path, content box, resolved column/row tracks (offset and size), clamped gap, effective composition, item count, and reserved slot count. This is renderer geometry, not new OPF document fields.\n\nAgents can prepare the same guarded change without a DOM:\n\n```js\nimport {prepareTrackResize} from \'@openpresentation/opf-editor/layout\';\nimport {resolvePresentation} from \'@openpresentation/opf-render/svg\';\nconst geometry = resolvePresentation(editor.document, renderOptions).slides[0].geometry;\nconst flow = geometry.flows.find(flow => flow.path === \'slides.0\');\nconst prepared = prepareTrackResize(editor.document, flow, 0, 0.65);\n// Boundary 0: give the first track 65% of the adjacent pair\'s combined space.\n// Preview prepared.document with the same renderer and font provider before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\nThe patch contains a `test` guard for the container before changing its composition. Failed tests do not mutate the document or its history. A test-only patch is read-only. Rendering is preflighted by the canvas; headless callers should likewise render a candidate to enforce font and overflow constraints.\n\nVerification: editor layout model tests, `/layout-tests.html` browser keyboard checks and trusted-pointer specimens, and `pnpm test:layout` for measured SVG/native PPTX coordinate parity. Shape-coordinate checks do not establish PowerPoint raster pixel parity.\n\n\n## Reordering and moving blocks\n\nIn **Arrange**, drag a numbered block handle to reorder siblings. The insertion marker shows the destination; the shared renderer reflows the slide after drop. Arrow keys on a handle move the whole block earlier or later. Click a handle for **Earlier**, **Later**, or an explicit destination and insertion position. The destination menu supports existing groups and block-based slides, including moving a child out of a group or moving a whole group to another slide. `canvas.openBlockMenu(path)` opens the same controls programmatically.\n\nA move preserves the entire block and its nested content, formatting, data, and references. Parent composition weights describe positions, so they stay in place. Moving to another container can change the block\'s inherited design and readability constraints; the canvas renders the candidate before committing it. Strict overflow or an unavailable required font rejects the move. A move cannot leave an empty block container or put a group inside its own descendants. Move the group or add another block first when the source has only one child.\n\n```js\nimport {prepareBlockMove, listBlockContainers} from \'@openpresentation/opf-editor/layout\';\nconst containers = listBlockContainers(editor.document);\nconst prepared = prepareBlockMove(editor.document,\n \'/slides/0/blocks/0\', \'/slides/0/blocks/1\', 1);\n// Insert the first block before child 1 of the second block\'s group.\n// Destination indexes refer to the document before removal.\n// prepared.path reports the moved block\'s address after any index shifts.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\n`prepareBlockMove` returns `{document, patches, path, changed}`. It validates the complete result and emits guarded remove/add patches, so the editor or CLI can apply it atomically. No-op moves return `changed: false` and no patches. `listBlockContainers(document, {slideIndex})` optionally limits discovery to a single slide and excludes arbitrary extension data. Headless callers should render the candidate with their intended font provider before applying. The browser and installed-package block harnesses exercise nested moves, undo, stale menus, keyboard access, strict-fit rejection, and native drag reordering.\n\nCreation and deletion use the same layout engine: insertions can normalize implicit payloads into explicit blocks; deletions prune empty groups while retaining the slide. Existing track weights stay positional. See the [editor creation guide](live-editor.md#create-duplicate-and-delete-content) for the guarded APIs and canvas controls.\n'
49
+ "markdown": '# Dynamic composition\n\nOPF keeps authoring intent in JSON. Use `blocks` when content can reflow; use promoted regions when relative placement is meaningful. `composition` on a slide overrides fields in the resolved layout\'s `composition`. Existing documents remain valid.\n\nThe current published Node 24 train is core 0.11.0, renderer 0.9.0, PPTX 0.9.1, editor 0.8.0 and CLI 0.9.0. Use the exact pins in [release-plan.json](../release-plan.json); the [compatibility matrix](compatibility-matrix.md) separates package support from native Office and font gates. Older version references below identify when individual contracts were introduced.\n\n```json\n{\n "name": "Decision brief",\n "slides": [{\n "title": "Make the main idea clear",\n "composition": { "mode": "row", "weights": [2, 1], "overflow": "error" },\n "blocks": [\n { "text": "The evidence and recommendation receive twice the width." },\n { "text": "The supporting detail receives the remaining width." }\n ]\n }]\n}\n```\n\n`auto` evaluates candidate grids using text fit and cell proportions. `columns` limits its candidates. `grid` uses `columns` if given, otherwise a grid based on the canvas shape. `row` uses one row; `column` uses one column. Items retain source order. Weights size columns except in column mode, where they size rows. Missing weights are 1; unused weights have no effect. A partially filled final row retains its grid tracks.\n\n`gap` defaults to 1/30 and `padding` to 0.08, both fractions of the canvas\'s shorter edge. Large gaps are reduced when necessary to keep cells positive. `minFontSize` defaults to 16 reference pixels at a 720-pixel short edge. The reference coordinate system uses 96 pixels per inch. Explicit inch dimensions override presets independently for each axis.\n\nHeadings reserve space according to their wrapped text. Title and subtitle share the padded width of the free area, and a missing tag or subtitle leaves no gap.\n\nCover slides vertically center the combined tag/title/subtitle group in the free heading area. A cover is a slide with no body payload (no root content field including `image`, no `blocks`, no promoted regions; empty payloads such as `blocks: []`, `text: ""`, empty lists and regions with nothing in them count as no body; whitespace-only text is still body) on a heading-only layout: layout id `title` or `title-subtitle`, or a layout whose placeholders are all headings, or a slide with no layout at all. The free area is the slide minus the image-safe band reserved by a `left`, `right`, `top` or `bottom` slide image, header and footer furniture, and the usual padding; a `background` image reserves nothing. A wrapped heading makes the group taller and the group recenters. A group that already fills the free area is not moved. Accepted line and outline origins move with the boxes. Explicit heading `alignment` positions ink inside the box and never changes the vertical position. A root `image` that is drawn as the slide image still counts as body, so image slides keep the top-aligned content origin. Content slides are not affected: headings stay at the top and the body follows them. This is a reference-engine default, not a schema field.\n\nContent that exceeds the number of preset placeholders reflows together; it is not drawn over already-bound content. Promoted regions keep the 3\xD73 vocabulary, including standalone `top`, `middle`, and `bottom`. They ignore flow direction and track weights.\n\n## Shared headers and footers\n\nPublished core 0.11.0 exposes `layoutFurniture(slide, options)` and `geometry.furniture`, separate from body `items`. Composition identifies the available-space policy as `grid-score-v9`. Raw callers pass the presentation as `options.presentation`, resolved dimensions/fonts and the same measurement provider used by preview. `slideIndex` identifies source paths; optional `slideNumber` is the one-based displayed number.\n\nThe core resolver honors whole local header/footer overrides, including `false` and empty objects. Each zone retains its image and all configured text fields in source-aware parts. Literal text and dates preserve whitespace and empty strings. Organization and section values point to their metadata source; page numbers use the actual output sequence. A missing organization/section, or `date: true` without a host-supplied current date, produces `unresolved-content`; the implementation never consults a clock or invents source text.\n\nSlide numbers and dates carry formats. `slideNumberFormat` is a template such as `"A-{current}"` or `"{current} / {total}"`: `{current}` is the displayed number and `{total}` is the displayed slide count (`options.slideCount`, else `presentation.slides.length`; whole-deck pagination iterates to the final page count). `dateFormat` is an LDML-style pattern (`yyyy`, `yy`, `MMMM`, `MMM`, `MM`, `M`, `dd`, `d`, `EEEE`, `EEE`, quoted literals) with fixed English month and weekday names. A string `date` with `dateFormat` must be an ISO `YYYY-MM-DD` date and renders as fixed, generated text tied to that source value; without `dateFormat` a date string stays literal and editable. `date: true` is the current date: hosts pass today\'s ISO date as `options.date` (composition, pagination, renderer and exporter), and the default pattern is `M/d/yyyy`. Text parts expose `fields` (half-open UTF-16 ranges of each `{current}` number and of a whole current date), so exporters can write native live fields while `{total}` and fixed dates stay fixed text. `formatFurnitureDate()` and `formatSlideNumber()` are exported for hosts. All configured fields in one zone stack, in the order image, text, organization, section, slide number, date; put a date and a slide number in different zones to keep one line each. Hiding furniture on a title slide is the slide-level `design.header: false` / `design.footer: false` override.\n\n`socials: true` (core 0.11.1 and later) generates one `socials` part from the primary organization\'s `organization.socials`. The part has one source line per platform, in key order, and a parallel `links` array (`platform`, `text`, `href`, `resolved`, `sourcePath`). `resolveSocialProfile(platform, value, records, owner)` formats each value without network access. A handle loses its `handlePrefix` and is substituted into `companyUrlPattern`, then `profileUrlPattern`, then `baseUrl/{handle}`; the result is shown as that URL without `https://`. A URL value passes through unchanged except that `https://` is dropped from the display. With no matching record, the value is shown raw with no link, which is the Socials engine fallback. Records come from inline `catalogs.socialPlatforms.records` first and then from host-supplied `options.socialPlatforms`. Composition never loads the bundled catalog itself; `paginatePresentation`, opf-render and opf-pptx pass it. A missing organization, or one with no non-empty socials, produces `unresolved-content`. Speaker socials, platform icons, brand colors and slide-size presets are not rendered.\n\n`furniture-flow-v2` gives each left/center/right zone 26% of the canvas width. Parts stack within a zone; the tallest zone sets the natural band height. Text uses at least the selected readability floor, with complete accepted source lines and optional measured outline placement. Header and footer bands reserve room before heading and body allocation. Irreducible text, conflicting bands or a heading displaced beyond the remaining space produce diagnostics; strict composition rejects them. No-furniture body geometry remains unchanged.\n\nPagination repeats these fields without putting them among body slices. An optional `page.repeatedMappings` records repeated heading/furniture and metadata paths while the existing `page.mappings` retains its body-fragment contract. Whole-deck pagination evaluates final output numbers, including preceding continuation pages, and rejects unresolved repeated content atomically. Renderer and editor reuse the accepted parts; literal text/date fields, including empty values, support direct canvas editing and undo. Generated labels remain tied to metadata.\n\nPublished PPTX 0.9.1 draws the accepted editable text boxes and fitted images and records furniture provenance in tagged slide shapes. Reimport uses current native text and images; damaged or ambiguous provenance retains visible content with diagnostics. The [fresh installed-package evidence](evidence/shipped-train-20260921/installed/acceptance-summary.json) includes deterministic export, current-content reimport controls and offline canvas editing/undo. Native PowerPoint acceptance, font compatibility and full visual review remain separate gates; this is not native `p:hf` Header/Footer support. Bounds/readability checks do not certify whole-slide design quality: long labels can wrap heavily in portrait zones, and outline agreement does not establish native font identity.\n\n## Slide-level images\n\nCore 0.11.1 and later resolve `design.slideImage` into `geometry.slideImage`, beside body `items`. It applies to a slide in three cases:\n\n- The slide sets its own `design.slideImage`.\n- The deck sets `design.slideImage` and the slide\'s layout record declares `slideImage: true`.\n- The deck sets `design.slideImage` and the slide\'s root `image` is the same source, as in the pptx.gallery image-treatment snippets.\n\nOther slides ignore a deck-level value, so existing decks keep their geometry: 81 bundled example decks set a deck-level slide image and none of them changes. When the value is the asset shorthand rather than a `{ position }` object, the layout\'s `slideImageAlignment` supplies the position, and `background` is the fallback.\n\n`background` gives the image the whole slide, and headings and content compose unchanged over it. `left`, `right`, `top` and `bottom` give the image half the slide, edge to edge, and headings and content compose in the other half with the usual padding. Header and footer bands keep their full-width placement. The frame uses `design.imageFill`, with `crop` as the default: `crop` covers the frame from the center and `fit` shows the whole image centered inside it. Without `design.imageFill`, the content-image default stays `fit`.\n\nThe slide\'s root `image` becomes the slide image, not a second content item, in two cases: the treatment object omits `src`, or `src` is the same source as the root image. A root image with a different source stays content. The result reports `path` (the configuring design value), `sourcePath` (where the drawn asset lives), `region`, `box` and `replacesContent`. Coordinated opf-render draws the frame beneath content. Coordinated opf-pptx exports one native `p:pic` at the same frame, with crop and fit written as `a:srcRect`. A tagged picture that has not been edited imports back as the slide\'s `design.slideImage`. The treatment vocabulary is covered in [image treatments](image-treatments.md): size, inset, aspect ratio, preset masks, line, opacity, grayscale or duotone, and overlay. That page also gives the support status of each pptx.gallery treatment.\n\n## Nested groups\n\n### Shared content cards\n\nShared content cards are published in core 0.10.0 and later, including current core 0.11.0. For `design.contentBox: true`, each body leaf carries a `frameBox` at its outer allocation and a `box` padded inward by 12 reference pixels at a 720-pixel short edge, capped at one quarter of the frame\'s width or height. Scoring, accepted payload measurement, strict overflow and pagination all use that rounded interior. Headings remain unframed, nested groups keep their original padding, and explicit outer regions/track weights remain authoritative. Automatic candidates may change because their available content space changes.\n\nEvery composed item carries its resolved horizontal text `alignment` (`left`, `center` or `right`). The title uses `titleAlignment`; every other item, including subtitle, tag, body text, lists, tables and metrics, uses `contentAlignment`. A slide\'s explicit design value wins over the host option, and the default is `left`. The title never inherits `contentAlignment`. Accepted outline placement and metric internals use the same value. The renderer and the PPTX exporter anchor preview and native text to `item.alignment`, so both engines place a layout\'s text the same way.\n\nRaw composition callers pass their resolved deck flag as `composeSlide(slide, {contentBox: effectiveDesign.contentBox, ...options})`; a slide\'s explicit `design.contentBox: false` overrides it. Coordinated renderer, editor and whole-presentation pagination resolve this option for their callers. Consumers draw at `frameBox` and use the accepted `box` and payload internals without another inset. Core 0.9.0 predates this behavior. Content cards do not make the incomplete chart/timeline density models complete or certify native raster fidelity.\n\nA block or promoted region can contain its own `blocks` and `composition`. The optional discriminator is `"type": "group"`. A group has at least one child and cannot mix children with leaf fields such as `text` or `image`.\n\n```json\n{\n "composition": { "mode": "row", "weights": [2, 1] },\n "blocks": [\n {\n "composition": { "mode": "column", "padding": 0.02 },\n "blocks": [{ "text": "Recommendation" }, { "text": "Supporting evidence" }]\n },\n { "text": "Context" }\n ]\n}\n```\n\nThe parent allocates a box to each group, then the group arranges its children inside that box. Group padding defaults to zero; padding and gap use the group\'s shorter edge. Only `minFontSize` and `overflow` inherit. A strict ancestor cannot be weakened by a child\'s `overflow: "warn"`. Font sizes remain relative to the canvas, not the group. Groups can nest up to 32 levels; cycles and deeper nesting fail with an explicit error.\n\nAutomatic grid scoring inspects descendant text using each descendant\'s explicit arrangement or geometric automatic seed. After selecting the parent\'s grid, it optimizes each child\'s automatic grid. This deterministic, bounded search avoids exponential combinations; it does not claim a globally optimal packing.\n\n`result.items` contains every leaf with its full source path and effective composition. `result.groups` contains group paths, outer bounds, and content bounds. The editor\'s `setGroupComposition(path, value)` validates and records undo/redo just like slide composition edits.\n\n## Inspecting and repairing layout\n\n```js\nimport { composeSlide } from \'@openpresentation/opf/composition\';\nconst result = composeSlide(deck.slides[0], { width: 1280, height: 720, layout: resolvedLayout });\nconsole.log(result.items); // Source paths, content, geometry, and text estimates\nconsole.log(result.diagnostics); // Path-specific text-overflow and small-cell messages\n```\n\nThis pure function expects a validated slide. The caller resolves catalog records and passes the canvas size. The rendering and export packages perform those steps at their boundaries. No network, DOM, system font, or AI dependency is required.\n\n### Explain automatic selection (core 0.8.0 and later)\n\nPass `explain: true` to return `result.explanation`. This opt-in API requires core 0.8.0; it is absent from core 0.7.0. Enabling explanations adds no measurement calls and does not change geometry, source content, reading order, weights or selected arrangements within the same engine version.\n\n```js\nconst result = composeSlide(slide, {...resolvedOptions, explain: true});\nfor (const decision of result.explanation.decisions) {\n console.log(decision.path, decision.reason, decision.selectedColumns);\n console.table(decision.candidates);\n}\nconsole.log(result.explanation.textMeasurement);\nconsole.log(result.explanation.unmeasuredPayloads);\n```\n\n`resolvedOptions` supplies the same dimensions, layout, fonts and optional width provider as the preview. Core 0.8.0 identifies its explanation as `grid-score-v2`; core 0.9.0 advances to `grid-score-v3` to include complete code metadata/body measurements. Current core 0.11.0 reports `grid-score-v9`. These versions record containers in parent-before-child order. `lowest-score` reports the candidates actually tried; `configured-mode` respects resolved row/column/grid intent and returns no invented candidates. `promoted-regions` leaves region placement fixed and has no selected column count. Empty slides have no decisions. Automatic search tries one through `min(slotCount, columns ?? 6)` columns, in ascending order; ties retain the first candidate. Reserved placeholders count as slots. The schema caps an explicit candidate limit at twelve columns.\n\nEach candidate has `columns`, `rows`, `score` and additive `penalties`:\n\n| Penalty | Rule |\n| --- | --- |\n| `cellProportions` | Sum of `abs(log(cellAspect / 1.6))` for descendant leaves |\n| `fontReduction` | Reduction from 25 reference pixels for text-like leaves; current quote/code/metric/timeline layouts sum requested-minus-fitted sizes across their parts, divided by canvas scale |\n| `textOverflow` | 1,000 per overflowing text-like leaf or complete quote/code/metric/timeline payload, regardless of the number of internal failure reasons |\n| `tableOverflow` | 1,000 per table whose shared cell layout overflows |\n| `smallCells` | 100 per leaf narrower than 100 or shorter than 60 reference pixels |\n| `emptySlots` | 2 per unused position in the candidate grid\'s final row |\n\nScores are preference costs, not quality percentages or guarantees. Floating-point summation can make the component total differ slightly from `score`. Parent scoring uses descendant explicit arrangements or geometric automatic seeds; child automatic grids are optimized only after selecting the parent. Candidate scores therefore describe the bounded search, not a full assessment of the final optimized subtree. Heading fit remains in ordinary diagnostics, outside body-grid scoring. A strict-fit rejection exposes the explanation on `OPFCompositionError` when requested.\n\n`textMeasurement` is `estimated` without a provider and `provided` with one. A provided width function does not establish font provenance, glyph coverage, shaping or native raster fidelity. Text, rich text, lists, quotes, table cells and code in core 0.9 participate in the fit model. Current core 0.11.0 also measures metric and timeline parts; its `unmeasuredPayloads` identifies images, video and charts whose complete internal layout is not assessed. Core 0.8 reports code as incomplete; core 0.9 measures its filename/language/body and insets. Media aspect ratios, chart labels and complete timeline visual density still require inspection. A zero score or empty diagnostics is not proof that those payloads fit.\n\nExplanations expose the search for inspection and do not silently paginate or rewrite a document. Published editor 0.8.0 provides guarded track resizing, block moves, creation/removal and explicit pagination with preview/undo. A general automatic repair loop, automatic weight allocation, complete payload-internal measurement, CLI explanations and a canvas **Auto arrange** preview/undo operation remain open work; the **Arrange** controls below are explicit human adjustments.\n\nWith `overflow: "warn"` (default), the result retains all text and returns diagnostics. SVG emits all lines and marks overflowing groups with `data-opf-overflow="true"`; text may extend beyond its box or canvas. Consumers can collect diagnostics using `onDiagnostic`. With `overflow: "error"`, the layout rejects content that does not fit. Shorten the affected content, give it more space, or explicitly split it into another slide. Use the explicit pagination transform below to produce additional editable slides.\n\nThe editor exposes `editor.composeSlide(index)` and `editor.setComposition(index, value)`. The latter validates the change, records JSON Patch history, and supports undo/redo.\n\n## Pagination\n\n```js\nimport { paginateSlide, paginatePresentation } from \'@openpresentation/opf/pagination\';\nconst { presentation, pages } = paginatePresentation(deck);\n// Review, save, render, or export `presentation`; pages maps output fragments to source paths.\nconst single = paginateSlide(deck.slides[0], { width: 1280, height: 720, minFontSize: 24 });\n```\n\nPagination is an authoring operation. It produces ordinary OPF slides; previews and PPTX export consume those exact pages. It preserves the input, body order, nested groups, promoted regions, rich-text formatting, and source text characters. Plain text and rich runs split at grapheme boundaries, preferring sentence/paragraph breaks and then word breaks. Lists split between items, tables between rows with column labels repeated, and code splits without rewriting its source. Indivisible payloads remain intact. Existing track weights continue to apply to positions on each resulting page.\n\nThe default readability target is 24 reference pixels. Core 0.8.0 returns slides that persist that floor in `composition.minFontSize`, including an already-fitting one-page result; existing higher minima and strict overflow policies remain intact. Quotes can raise their nominal body/footer sizes to the floor. Current core 0.11.0 enforces the selected floor in shared plain/rich/list/table fitting, including painted rich fragments, while retaining authored style metadata. Unsupported fits report overflow rather than silently capping output below the floor. The [readability-floor checkpoint](plans/readability-floor.md) records the earlier candidate and its bounded-search tradeoffs. Pagination relies on the shared engine\'s estimates; it is not a guarantee that every host font renders identically. Headings repeat unchanged, speaker notes remain on the first page, and continuation IDs avoid existing deck IDs. `pages[].mappings` records full source/output paths and half-open text or item ranges. Text offsets use UTF-16, so source strings can be reconstructed exactly. Quote bodies split at grapheme boundaries and repeat complete attribution/source fields on each page. An irreducible footer rejects the whole operation, including a quote with an empty body after earlier content.\n\nIf a heading, individual list item, table row, or other atomic payload cannot fit on an otherwise empty page, `OPFPaginationError` returns actionable diagnostics. There is no partial output. `maxSlides` defaults to 100, and a layout-evaluation limit bounds work on pathological input. Specialized chart and timeline internals still require visual inspection; their complete density models remain outstanding.\n\nThe editor\'s `editor.paginateSlide(index)` is one validated transaction with undo/redo. It returns `{change, pagination}`. Editor 0.5.0 commits a one-page readability-policy change too; repeating the operation after the policy is recorded returns `change: null`. The playground includes an overflowing draft and **Split overflow** action. The CLI writes a new file and refuses to overwrite an existing one:\n\n```sh\nopf paginate input.opf.json output.opf.json\n```\n\n## Fidelity boundary\n\nThe shared engine provides identical body and heading geometry to SVG and editable PPTX export. Text measurements default to deterministic estimates. For actual font advances, use the shared provider described in [measured fonts](font-fidelity.md). Complex scripts, fallback fonts, PowerPoint text rendering, rich text, charts, tables, and images still need visual verification. Dynamic composition is not a guarantee of pixel-identical PowerPoint output. List density includes rich runs, descriptions and nesting via `fitList`, with the same hanging indents used in preview and export. Only text-like payloads currently receive content-density estimates; small-cell diagnostics also cover non-text content.\n\nSVG embeds raster data URI images locally. Remote and file images require a host resolver that supplies a raster data URI; otherwise they appear as placeholders. `strictAssets` rejects unresolved images. The runtime never fetches them.\n\nSee [the complete example](../examples/technical/dynamic-composition.opf.json) and [local ecosystem verification](ecosystem-development.md).\n\n\n## Metric internals (published coordinated packages)\n\nPublished core 0.11.0 exports `layoutMetric(value, box, options)` from the root and `@openpresentation/opf/composition`. It accepts a finite number, string, or `{value, unit?, label?, description?, delta?, trend?}`. Renderer 0.9.0, editor 0.8.0 and PPTX 0.9.1 consume its shared geometry for composition, atomic pagination, preview, editing and export. Core 0.9.0 predates this API. The [primitive checkpoint](plans/shared-metric-layout.md) and [source integration checkpoint](plans/shared-metric-integration.md) record its development; current pins and remaining native/font gates are in the [compatibility matrix](compatibility-matrix.md).\n\nPass the allocated reference-pixel `box`, resolved heading/body `fonts`, `textMeasurement`, canvas `scale`, effective `minFontSize`, source `path` and optional `overflow: \'error\'`. `metric-flow-v1` returns separate value/unit/label/description/delta/trend parts in that order. Numeric zero is visible; scalar values keep the scalar path. Every provided field retains its original string or number in `sources[].value`. Source ranges address `String(value)` using UTF-16 offsets; the original spelling of a numeric JSON token is not available. No locale formatting, trend icon, case conversion or separator is invented. Empty optional strings retain source mappings with `visible: false`; the required empty value keeps a targetable blank line.\n\nThe allocator tries an adjacent value/unit baseline when both fit one line and the unit uses at most 35% of the cell width; otherwise it stacks the fields. Metadata has an eight-reference-pixel gap, with twelve pixels after the primary row. Related fields stay together rather than being separated by a percentage of the cell height. The value starts at up to 76 reference pixels (28% of cell height); label/unit/delta start at 23, description at 20 and trend at 18. Every requested size is raised to the chosen floor, scaled once. Natural metadata height gets space before reducing type. At most 48 arrangements are evaluated, each with at most 77 value-size trials; identical inputs and a deterministic measurement provider select the fitting candidate with least summed font reduction, preferring the first candidate on ties.\n\nEach part exposes requested/resolved styles and the same source-preserving line/segment representation used by code (`CodeTextFit`), measured with proportional heading/body fonts. CR/LF/CRLF, tabs, whitespace and grapheme boundaries remain exact. Consumers must reuse accepted line and segment positions, font sizes and styles rather than independently re-fit or normalize text. This API reports `provided` measurement when a provider is passed, without claiming that its glyph coverage or shaping is complete. Unsupported glyphs propagate the provider\'s error with the field path.\n\nPass `align: \'left\' | \'center\' | \'right\'` (default left) to the primitive. Its returned `alignment` and per-part `linePositions` give an absolute x origin and baseline for each `fit.sourceLines` entry, including blank lines. An inline value/unit pair moves together, with the gap following the actual value advance. Composition accepts host-resolved `contentAlignment`; an explicit slide `design.contentAlignment` overrides it. Renderer/export/pagination pass the effective design into the same operation. Alignment does not trigger a second font fit.\n\nCheck `overflow` before consuming parts. Irreducible text, invalid available space, parts outside the cell and overlapping occupied line boxes return field-specific diagnostics; strict mode throws `OPFCompositionError`. Invalid available boxes retain their dimensions and have no fit. These are advance-based line rectangles, not glyph outlines: the controlled browser evidence separately records small glyph overhangs. The API is a bounded internal allocator, not the complete layout-repair/Auto arrange operation or a native export fidelity guarantee.\n\nCurrent `grid-score-v9` retains the metric scoring introduced by `grid-score-v4`: every metric part contributes font reduction, with one overflow penalty per failing metric leaf. `item.metricLayout` is measured against the rounded accepted cell; `item.text`/`item.textStyle` alias the value fit/style. Field diagnostics obey strict ancestor policies, while explicit modes/weights/regions remain authoritative. Metrics are excluded from the advance-model `unmeasuredPayloads` list. Explicit pagination retains metrics atomically with complete source types/metadata and rejects irreducible fields without returning partial output. The published train has [installed-package acceptance](evidence/shipped-train-20260921/installed/acceptance-summary.json), including metric provenance controls. That evidence does not establish native raster or font equivalence.\n\n## Code internals (core 0.9.0 and coordinated packages)\n\nCore 0.9.0 introduced `layoutCode(value, box, options)` for the schema\'s string shorthand or `{source, language?, filename?}` object. It returns measured filename/language/body parts with exact original text, requested/resolved styles, readability floors, available boxes and diagnostics. Its original integration targeted renderer/PPTX 0.7.0 and editor 0.6.0; the [release checkpoint](plans/shared-code-release.md) records that rollout. These contracts are published in the current core 0.11.0, renderer 0.9.0, PPTX 0.9.1 and editor 0.8.0 train. Core 0.8.0 predates this API and retains the [recorded code-label, filename and whitespace defects](plans/layout-repair.md).\n\n`grid-score-v3` charges code font reductions across all metadata/body parts and one overflow penalty per failing leaf. It preserves explicit modes, weights, regions and source order. Accepted `item.codeLayout` is fitted to the same rounded cell exposed as `item.box`; `item.text` and `item.textStyle` alias the body, not the first metadata part. Strict ancestor settings apply to internal `.source`, `.filename` and `.language` diagnostics. Code no longer appears in `explanation.unmeasuredPayloads`, which concerns the core advance-based model only. It does not mean browser/native fidelity is verified.\n\nPagination slices the code body at grapheme boundaries, repeats filename/language and returns contiguous UTF-16 body ranges while preserving all source bytes and the evaluated readability floor. Irreducible metadata rejects all output, including when the body is empty or earlier content could have fitted. Consumers must preview/export the returned document. The [integration checkpoint](plans/shared-code-integration.md) separates source, installed browser, Windows PowerPoint and remaining release gates.\n\nEach fitted part retains every space and explicit CR/LF/CRLF break. `fit.lines` contains exact source slices, and `fit.sourceLines` records half-open UTF-16 `start`, `end` and `nextStart` offsets, the measured width and a `soft`, `hard` or `end` boundary. A hard break occupies `[end, nextStart)`; soft wrapping consumes no source character. Joining `part.text.slice(line.start, line.nextStart)` reconstructs the original part. Blank lines and a final empty line are retained, and long tokens split only at grapheme boundaries. Filename and language text are not case-converted. An absent/empty metadata pair creates a generated `code` label with no source range.\n\n`code-flow-v1` uses 18-reference-pixel outer insets, an eight-pixel gap between filename and language, and a twelve-pixel gap before the body. Nominal metadata/body sizes are 14/18 reference pixels, raised when necessary to respect the selected minimum, then scaled once. At most four metadata nominal/floor combinations are tried; each body fit tries at most 19 sizes regardless of canvas scale. The fitting combination with least font reduction wins. Irreducible metadata/body failures retain all text and diagnostic paths; invalid available boxes have no fit. `overflow: \'error\'` rejects rather than returning partial output.\n\nTabs remain literal characters in part text and displayed-line slices. Measurement advances to the next multiple of four measured spaces from that line\'s origin; `fit.tabSize` and `fit.tabWidth` expose the rule. Each source line\'s `segments` contains exact text/tab source ranges plus measured `x`/`width` values relative to its origin. Consumers must reuse those positions: an Edge probe showed that SVG treats a tab as one space despite CSS `tab-size: 4`. The published SVG renderer uses positioned spans and geometric precision; native export uses accepted tab stops. Width measurements and source preservation alone do not establish glyph-outline containment, shaping/bidi support or native fidelity. [Installed workflow evidence](evidence/shared-code-installed/summary.json) records the separate actual browser and native checks with their exact font/runtime scope.\n\nPublished native export stores source boundaries in standard PowerPoint shape tags. Complete unique groups recover exact code/source metadata, with current native text taking precedence. Missing, damaged or ambiguous groups retain visible native shapes and report diagnostics. Reimport does not reconstruct native formatting, positioning, font theme or readability policy. Eight installed-export wide/portrait slides pass native edit/save/reopen and all 24 original/saved/edited imports on the recorded Windows PowerPoint build; this is not arbitrary PowerPoint round-trip or pixel equivalence. The editor preserves untouched CRLF/CR source around edits and keeps committed preview geometry separate from its active native textarea caret.\n\nThe JSON schema can accept strings that [XML 1.0 cannot represent](https://www.w3.org/TR/xml/#charsets). Published SVG/PPTX code output rejects forbidden controls, unpaired UTF-16 surrogates, U+FFFE and U+FFFF with `invalid-code-text`, the source field path and UTF-16 offset in the message. The input stays unchanged; the caller can correct that character explicitly. Tabs, CR/LF/CRLF and valid supplementary characters remain accepted for serialization. Schema support, format representability and glyph coverage are separate properties.\n\nThe controlled SVG harness requests `text-rendering="geometricPrecision"` as well as explicit segment placement. Initial Linux Chromium CI rounded glyph advances under default hinting, unlike Windows Edge with the same font bytes. The [SVG specification](https://www.w3.org/TR/SVG/painting.html#TextRenderingProperty) defines geometric precision as a rendering hint, so consumers still need actual browser checks with their exact fonts and supported environments; the hint alone does not certify agreement. The harness retains a 0.1-reference-pixel tolerance and records observations before assertions.\n\n## Quote internals (core 0.8.0 and coordinated packages)\n\nCore 0.8.0 exports `layoutQuote(value, box, options)` from the root or composition entrypoint. Pass validated quote content (object or string shorthand), its allocated reference-pixel box, resolved `fonts`, `textMeasurement`, `scale` (canvas short edge / 720), effective `minFontSize`, `overflow` policy and its source `path`.\n\nThe result contains `parts` for the body and any nonempty footer, exact display `text`, source mappings, requested and resolved text styles, and the available boxes/fits. Source ranges use half-open UTF-16 offsets in both the source field and display string; generated quotation marks and the footer separator have no source range. The original content is never modified. A supplied width provider is reported as `provided`; it does not certify shaping or font fidelity.\n\nCheck `overflow` and `diagnostics` before accepting the parts. Invalid available dimensions remain visible with `fit` absent, and `overflow: \'error\'` throws `OPFCompositionError`. Diagnostics distinguish invalid part space, parts outside their cell, text that exceeds its reserved space, and overlapping line rectangles. Those rectangles are conservative text-layout bounds, not measured glyph outlines. The readability floor is scaled once and can raise the nominal body (28) or footer (17) size; it is never silently capped below the selected floor.\n\n`quote-flow-v1` keeps 18-reference-pixel outer insets and an 18-pixel body/footer gap while fonts scale with the canvas. A 40-pixel footer is a whitespace preference. The allocator expands it for long sources or compacts it for dense bodies, trying at most the nominal and minimum footer sizes and selecting the fitting pair with least total font reduction. If neither fits, it returns floor-size failure diagnostics. This is a bounded internal allocation step, not a complete layout-repair engine.\n\n`composeSlide` scores both parts and accepts geometry against the final rounded item box. Each quote item carries `quoteLayout`; its compatibility `text` field is the same fit object as the quote body, including generated quotation marks. Consumers needing original offsets must use the explicit `sources` mappings. The coordinated renderer and PPTX consume these parts without another measurement/style-resolution pass. Missing geometry or invalid part boxes reject rendering/export rather than omitting content. This requires core 0.8.0 with renderer/PPTX 0.6.0; older core 0.7.0/renderer 0.5.1/PPTX 0.5.2 lack these changes. The complete published set, immutable verification refs and fresh registry evidence are recorded in `release-plan.json` and [the release plan](plans/shared-quote-release.md).\n\nBrowser glyph bounds can extend slightly beyond advance-based part boxes into the reserved inset. Current loaded-font tests record those overhangs, verify glyph containment inside the full quote cell and check body/footer separation. Native PowerPoint fixtures separately verify text, sizes, cell containment, save/reopen and reimport. Neither test establishes universal pixel equivalence. Original requested-font provenance through host substitutions and non-quote payload internals remain open requirements.\n\n## Resizing in the preview\n\nChoose **Arrange** in the editor to reveal track dividers. Drag a divider to redistribute the space between adjacent columns (row/grid) or rows (column), including nested groups. Arrow keys make small changes; Shift makes larger changes. Escape discards a pointer draft. One drag creates one undo step, and no content is removed. Strict overflow rejects a resize that violates its fit constraints.\n\nResizing an automatic layout makes its chosen columns explicit as `mode: grid` with `columns`. This prevents the number of columns from changing under the pointer. The adjacent share clamps to 5\u201395%, with positive schema-valid weights. Other track proportions and unrelated document fields remain intact. Promoted regions retain their positions; their nested groups can still be resized. Layouts with reserved placeholder slots need an explicit arrangement first. Flows with more than twelve tracks need grouping before the current resize controls can express all weights.\n\n`createCanvasEditor(container, {layoutEditing: true, ...options})` enables dividers initially. `canvas.setLayoutEditing(boolean)` toggles them, and `canvas.commit()` / `canvas.cancel()` also handle an active resize. `onDraft` receives the proposed document; the session stays unchanged until commit. Changes to the resized container cancel a stale draft; unrelated updates are retained.\n\nThe shared engine exposes `geometry.flows`: each flow has its container path, content box, resolved column/row tracks (offset and size), clamped gap, effective composition, item count, and reserved slot count. This is renderer geometry, not new OPF document fields.\n\nAgents can prepare the same guarded change without a DOM:\n\n```js\nimport {prepareTrackResize} from \'@openpresentation/opf-editor/layout\';\nimport {resolvePresentation} from \'@openpresentation/opf-render/svg\';\nconst geometry = resolvePresentation(editor.document, renderOptions).slides[0].geometry;\nconst flow = geometry.flows.find(flow => flow.path === \'slides.0\');\nconst prepared = prepareTrackResize(editor.document, flow, 0, 0.65);\n// Boundary 0: give the first track 65% of the adjacent pair\'s combined space.\n// Preview prepared.document with the same renderer and font provider before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\nThe patch contains a `test` guard for the container before changing its composition. Failed tests do not mutate the document or its history. A test-only patch is read-only. Rendering is preflighted by the canvas; headless callers should likewise render a candidate to enforce font and overflow constraints.\n\nVerification: editor layout model tests, `/layout-tests.html` browser keyboard checks and trusted-pointer specimens, and `pnpm test:layout` for measured SVG/native PPTX coordinate parity. Shape-coordinate checks do not establish PowerPoint raster pixel parity.\n\n\n## Reordering and moving blocks\n\nIn **Arrange**, drag a numbered block handle to reorder siblings. The insertion marker shows the destination; the shared renderer reflows the slide after drop. Arrow keys on a handle move the whole block earlier or later. Click a handle for **Earlier**, **Later**, or an explicit destination and insertion position. The destination menu supports existing groups and block-based slides, including moving a child out of a group or moving a whole group to another slide. `canvas.openBlockMenu(path)` opens the same controls programmatically.\n\nA move preserves the entire block and its nested content, formatting, data, and references. Parent composition weights describe positions, so they stay in place. Moving to another container can change the block\'s inherited design and readability constraints; the canvas renders the candidate before committing it. Strict overflow or an unavailable required font rejects the move. A move cannot leave an empty block container or put a group inside its own descendants. Move the group or add another block first when the source has only one child.\n\n```js\nimport {prepareBlockMove, listBlockContainers} from \'@openpresentation/opf-editor/layout\';\nconst containers = listBlockContainers(editor.document);\nconst prepared = prepareBlockMove(editor.document,\n \'/slides/0/blocks/0\', \'/slides/0/blocks/1\', 1);\n// Insert the first block before child 1 of the second block\'s group.\n// Destination indexes refer to the document before removal.\n// prepared.path reports the moved block\'s address after any index shifts.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n```\n\n`prepareBlockMove` returns `{document, patches, path, changed}`. It validates the complete result and emits guarded remove/add patches, so the editor or CLI can apply it atomically. No-op moves return `changed: false` and no patches. `listBlockContainers(document, {slideIndex})` optionally limits discovery to a single slide and excludes arbitrary extension data. Headless callers should render the candidate with their intended font provider before applying. The browser and installed-package block harnesses exercise nested moves, undo, stale menus, keyboard access, strict-fit rejection, and native drag reordering.\n\nCreation and deletion use the same layout engine: insertions can normalize implicit payloads into explicit blocks; deletions prune empty groups while retaining the slide. Existing track weights stay positional. See the [editor creation guide](live-editor.md#create-duplicate-and-delete-content) for the guarded APIs and canvas controls.\n'
50
50
  },
51
51
  {
52
52
  "slug": "ecosystem-development",
53
53
  "file": "docs/ecosystem-development.md",
54
54
  "title": "Local ecosystem development",
55
- "markdown": "# Local ecosystem development\n\nUse Node 24 for the current source and published packages. Keep `opf`, `opf-render`, `opf-pptx`, `opf-editor`, and `pptx-gallery` in the same parent directory. Install each repository's dependencies normally, then run these commands from `opf`:\n\n```sh\npnpm build\nnode scripts/link-ecosystem.mjs\npnpm test:ecosystem\npnpm test:gallery\n```\n\nThe link command replaces the installed `@openpresentation/opf` package in sibling `node_modules` with a link to this checkout and builds the toolkit packages. It also links the renderer into editor/converter consumers and the converter into the editor. It does not save machine-specific paths in package manifests or lockfiles. Reinstalling dependencies can replace the links; rerun the command afterwards. Use `--packages-only` to omit the gallery checkout.\n\nOn Windows, directory junctions work without granting file-symlink privileges. The linker refuses a package parent that resolves outside the sibling checkout's `node_modules`, and replaces existing links without following them into source. npm/pnpm orchestration invokes the package manager's JavaScript entrypoint with the selected Node runtime instead of running a batch shim through a shell. Paths with spaces and shell metacharacters remain literal arguments. The supported npm-installed and npm-exec package-manager layouts are discovered from `PATH` or the matching `npm_execpath`; a missing manager returns an explicit installation error.\n\nThe core packed-install smoke check also uses this Windows invocation. The following portability results record the historical September 9 integration, before the current Node 24 requirement; current acceptance is linked from the [compatibility matrix](compatibility-matrix.md). Node 20/24 local evidence on the `codex/windows-test-harness-20260909` branch: all 414 core tests plus composition/pagination/data/rich-text/list suites pass, and actual local tarballs install into fresh temporary projects and pass 519 packed-entry checks. New isolated tests execute real npm builds, replace existing junctions, retain literal arguments, and reject an external `node_modules` parent without modifying its package. The then-current Windows/macOS CI repeated the core packed installation on both runtimes. These are local unpublished tarballs, not republished core 0.7.0 or proof of native rendering fidelity.\n\nAfter integrating reviewed layout PR #43, the combined source passes all 420 core tests on local Windows Node 24. Exact combined-source CI and review are recorded on PR #44.\n\nCoordinated CI `34384776504` and `34385059710` caught an older isolated-link fixture copying the linker without its new helper, causing `ERR_MODULE_NOT_FOUND` before package tests ran. The fixture now copies both files, passes directly on Windows Node 20/24, and runs in the Windows/macOS matrix as well as coordinated CI. This failure was fixed rather than waived; renewed combined-source CI was required at that checkpoint.\n\nThe current published compatible set is core 0.11.0, CLI 0.9.0, renderer 0.9.0, PPTX 0.9.1 and editor 0.8.0 on Node 24. Clean registry installs include shared composition and styled table rows without sibling links. `release-plan.json` records exact versions and immutable verification sources; `pnpm test:registry-ecosystem` and `pnpm test:registry-fidelity` exercise those installed packages. Source links are for coordinated development.\n\nExecute the installed-package browser harnesses after their corresponding build:\n\n```sh\npnpm test:packages\npnpm test:packed-browser\npnpm test:registry-ecosystem\npnpm test:packed-browser registry\n```\n\nThe renderer checkout supplies its locked Playwright test dependency. Install Chromium with `npm exec --prefix ../opf-render -- playwright install --with-deps chromium` on Linux. Local Windows runs use Edge; `OPF_BROWSER_CHANNEL` can explicitly select another installed Playwright channel. CI uses the matching official Playwright container pinned by digest, without installing OS packages during each run.\n\nEach build writes `artifacts/editor/packed-browser-manifest.json` with the mode, installed versions, consumer build ID, dependency-lock hash and exact font/HTML/JavaScript hashes. The runner rejects a different mode, stale consumer or changed asset. It serves only the verified bytes on loopback and rejects external requests and network writes. Rebuild before switching between candidate and registry modes. Reports include the browser and Node versions and are saved by mode/runtime; failures retain a screenshot.\n\n`node scripts/test-packed-browser-guards.mjs` verifies those four rejection cases against the current disposable harness and restores each changed fixture byte-for-byte. CI runs it after the registry browser checks.\n\nSeven suites exercise canvas, rich text, lists, creation, layout, block moves and styled tables. Real browser input covers divider resizing/cancellation/concurrent changes, block dragging, merged-cell typing/redo/undo, plain-to-rich conversion and bold formatting, and empty-cell typing/undo. Conversion and formatting currently create separate undo transactions. Harness DOM assertions also cover renderer agreement and preservation. These checks do not replace full application export/reimport, public deployment checks or native PowerPoint raster evidence.\n\nTo browse the gallery with the linked package:\n\n```sh\ncd ../pptx-gallery\nOPF_LOCAL_WORKSPACE=1 pnpm dev\n```\n\nLayout detail pages have an interactive composition example. The flag expands Turbopack's local root to include the sibling package; production builds use the gallery root.\n\n`pnpm test:ecosystem` validates the dynamic composition fixture, edits and undoes a composition, renders SVG/PNG/PDF, exports editable PPTX, checks OOXML text-box coordinates against the shared geometry, and imports the result back into schema-valid OPF. Artifacts are written to a temporary directory and its location is printed.\n\nFor tests that should read current source without modifying installed packages, use Node's local loader after building OPF:\n\n```sh\nnode --import ./scripts/register-local-opf.mjs ../opf-render/test/smoke.mjs\n```\n\nThe loader redirects only `@openpresentation/opf` imports to this checkout. Ordinary dependencies still resolve from the consuming repository.\n\nFor full gallery render coverage, run `pnpm test:gallery -- --render` (or invoke the script with `--render`). The test validates all 854 generated documents and can render them with the local SVG engine.\n\nBuild OPF before starting a linked gallery. Stop and restart the gallery around clean OPF rebuilds; removing the linked `dist` directory during compilation can leave Turbopack with stale missing-module errors.\n\n`pnpm test:pagination` verifies long-text and table pagination through SVG and editable PPTX, including exact source reconstruction, table row counts, and absence of extra exporter-created pages. It writes review artifacts under `artifacts/pagination/`.\n\n`pnpm test:fonts` verifies actual-font measurement across editor, SVG, pagination, and PPTX. See [font fidelity](font-fidelity.md) for loading and embedding local fonts and for current native PowerPoint limits.\n"
55
+ "markdown": "# Local ecosystem development\n\nUse Node 24 for the current source and published packages. Keep `opf`, `opf-render`, `opf-pptx`, `opf-editor`, and `pptx-gallery` in the same parent directory. Install each repository's dependencies normally, then run these commands from `opf`:\n\n```sh\npnpm build\nnode scripts/link-ecosystem.mjs\npnpm test:ecosystem\npnpm test:gallery\n```\n\nThe link command replaces the installed `@openpresentation/opf` package in sibling `node_modules` with a link to this checkout and builds the toolkit packages. It also links the renderer into editor/converter consumers and the converter into the editor. It does not save machine-specific paths in package manifests or lockfiles. Reinstalling dependencies can replace the links; rerun the command afterwards. Use `--packages-only` to omit the gallery checkout.\n\nOn Windows, directory junctions work without granting file-symlink privileges. The linker refuses a package parent that resolves outside the sibling checkout's `node_modules`, and replaces existing links without following them into source. npm/pnpm orchestration invokes the package manager's JavaScript entrypoint with the selected Node runtime instead of running a batch shim through a shell. Paths with spaces and shell metacharacters remain literal arguments. The supported npm-installed and npm-exec package-manager layouts are discovered from `PATH` or the matching `npm_execpath`; a missing manager returns an explicit installation error.\n\nThe core packed-install smoke check also uses this Windows invocation. The following portability results record the historical September 9 integration, before the current Node 24 requirement; current acceptance is linked from the [compatibility matrix](compatibility-matrix.md). Node 20/24 local evidence on the `codex/windows-test-harness-20260909` branch: all 414 core tests plus composition/pagination/data/rich-text/list suites pass, and actual local tarballs install into fresh temporary projects and pass 519 packed-entry checks. New isolated tests execute real npm builds, replace existing junctions, retain literal arguments, and reject an external `node_modules` parent without modifying its package. The then-current Windows/macOS CI repeated the core packed installation on both runtimes. These are local unpublished tarballs, not republished core 0.7.0 or proof of native rendering fidelity.\n\nAfter integrating reviewed layout PR #43, the combined source passes all 420 core tests on local Windows Node 24. Exact combined-source CI and review are recorded on PR #44.\n\nCoordinated CI `34384776504` and `34385059710` caught an older isolated-link fixture copying the linker without its new helper, causing `ERR_MODULE_NOT_FOUND` before package tests ran. The fixture now copies both files, passes directly on Windows Node 20/24, and runs in the Windows/macOS matrix as well as coordinated CI. This failure was fixed rather than waived; renewed combined-source CI was required at that checkpoint.\n\nThe current published compatible set is core 0.11.0, CLI 0.9.0, renderer 0.9.0, PPTX 0.9.1 and editor 0.8.0 on Node 24. Clean registry installs include shared composition and styled table rows without sibling links. `release-plan.json` records exact versions and immutable verification sources; `pnpm test:registry-ecosystem` and `pnpm test:registry-fidelity` exercise those installed packages. Source links are for coordinated development.\n\nExecute the installed-package browser harnesses after their corresponding build:\n\n```sh\npnpm test:packages\npnpm test:packed-browser\npnpm test:registry-ecosystem\npnpm test:packed-browser registry\n```\n\nThe renderer checkout supplies its locked Playwright test dependency. Install Chromium with `npm exec --prefix ../opf-render -- playwright install --with-deps chromium` on Linux. Local Windows runs use Edge; `OPF_BROWSER_CHANNEL` can explicitly select another installed Playwright channel. CI uses the matching official Playwright container pinned by digest, without installing OS packages during each run.\n\nEach build writes `artifacts/editor/packed-browser-manifest.json` with the mode, installed versions, consumer build ID, dependency-lock hash and exact font/HTML/JavaScript hashes. The runner rejects a different mode, stale consumer or changed asset. It serves only the verified bytes on loopback and rejects external requests and network writes. Rebuild before switching between candidate and registry modes. Reports include the browser and Node versions and are saved by mode/runtime; failures retain a screenshot.\n\n`node scripts/test-packed-browser-guards.mjs` verifies those four rejection cases against the current disposable harness and restores each changed fixture byte-for-byte. CI runs it after the registry browser checks.\n\nSeven suites exercise canvas, rich text, lists, creation, layout, block moves and styled tables. Real browser input covers divider resizing/cancellation/concurrent changes, block dragging, merged-cell typing/redo/undo, plain-to-rich conversion and bold formatting, and empty-cell typing/undo. Conversion and formatting currently create separate undo transactions. Harness DOM assertions also cover renderer agreement and preservation. These checks do not replace full application export/reimport, public deployment checks or native PowerPoint raster evidence.\n\nTo browse the gallery with the linked package:\n\n```sh\ncd ../pptx-gallery\nOPF_LOCAL_WORKSPACE=1 pnpm dev\n```\n\nLayout detail pages have an interactive composition example. The flag expands Turbopack's local root to include the sibling package; production builds use the gallery root.\n\n`pnpm test:ecosystem` validates the dynamic composition fixture, edits and undoes a composition, renders SVG/PNG/PDF, exports editable PPTX, checks OOXML text-box coordinates against the shared geometry, and imports the result back into schema-valid OPF. Artifacts are written to a temporary directory and its location is printed.\n\nFor tests that should read current source without modifying installed packages, use Node's local loader after building OPF:\n\n```sh\nnode --import ./scripts/register-local-opf.mjs ../opf-render/test/smoke.mjs\n```\n\nThe loader redirects only `@openpresentation/opf` imports to this checkout. Ordinary dependencies still resolve from the consuming repository.\n\nFor full gallery render coverage, run `pnpm test:gallery -- --render` (or invoke the script with `--render`). The test validates all 854 generated documents and can render them with the local SVG engine.\n\nBuild OPF before starting a linked gallery. Stop and restart the gallery around clean OPF rebuilds; removing the linked `dist` directory during compilation can leave Turbopack with stale missing-module errors.\n\n`pnpm test:pagination` verifies long-text and table pagination through SVG and editable PPTX, including exact source reconstruction, table row counts, and absence of extra exporter-created pages. It writes review artifacts under `artifacts/pagination/`.\n\n`pnpm test:fonts` verifies actual-font measurement across editor, SVG, pagination, and PPTX. It also runs the offline font-switch matrix (`scripts/test-font-switch-ecosystem.mjs`, FF-09): a seeded pairwise covering array of 58 decks over the 14 gallery dimensions, plus fixed content-type, block-replacement, per-slide override, CJK-in-Latin, theme and language switches, each switched A to B and back to A. A value class is a group of catalog values that take the same path through the engines, derived from the catalogs in the script: font schemes by language family, then by licensing and preview policy (Office metric, Office visual-only, monospace, open Google); one language per script family in the array and every other catalog script in a language chain; every layout family; the eight content blocks; every distinct chart export path of the non-deprecated chart types; header/footer, background (theme, solid, gradient, pattern, image) and slide-image treatments by kind; and the first and last record of the metadata dimensions. Dimensions that a deck can carry several times (font scheme states, layouts, blocks, charts, backgrounds, images) take several values per deck. Every state is exported and checked with the FF-08 typeface inventory, the catalog's literal theme fonts, a package structure check, a preview re-render and a re-import. It pins the office font pack with visual substitution and asserts every substitution; known engine limitations, including chart types the preview approximates or the exporter writes as bar charts, are named expected failures in the script that fail with a \"limitation resolved\" message when they go away. It runs no browser and no Office. Its report is written to `artifacts/font-switch-matrix/report.json`. See [font fidelity](font-fidelity.md) for loading and embedding local fonts and for current native PowerPoint limits.\n"
56
56
  },
57
57
  {
58
58
  "slug": "evidence-2026-09-08-windows",
@@ -70,7 +70,7 @@ var docsData = Object.freeze([
70
70
  "slug": "font-fidelity",
71
71
  "file": "docs/font-fidelity.md",
72
72
  "title": "Measured fonts and reproducible previews",
73
- "markdown": "# Measured fonts and reproducible previews\n\nFor the starter set and delivery priorities, see the [font roadmap](plans/font-roadmap.md).\n\n## Font policy (FF-31)\n\nOPF keeps one machine-readable font policy table, [`spec/reference/font-policy.json`](../spec/reference/font-policy.json). Core exports it as `FONT_POLICY`, `fontPolicyFor()` and `applyFontPolicyDecisions()` from `@openpresentation/opf` or `@openpresentation/opf/font-policy`. Each row gives a family's license class, where viewers get it, whether OPF may ever embed it, and its preview replacement with a measured width difference. It also lists alternates, ending where possible with a face that already ships with opf-render. The [licensing table](programs/font-fidelity-everywhere/font-licensing.md) lists all 153 rows. [`font-policy.schema.json`](../spec/reference/font-policy.schema.json) is its JSON Schema. The [measurement evidence](evidence/font-replacements-20260923/README.md) explains how each replacement was chosen.\n\n**The policy in brief (owner decisions, 2026-09-29).** This is the canonical statement; the sibling repositories link here.\n\n1. **The user's font selection is the source of truth.** The user picks a font, for example Aptos or Calibri through a font scheme. That name is what the document, the theme and the PPTX carry.\n2. **License-restricted (proprietary) fonts are never bundled or embedded,** so previews cannot draw them. Openly licensed fonts such as Carlito and Roboto are bundled, and \"licensed\" below always means license-restricted. Live previews, SVG, the editor and gallery thumbnails use an open look-alike instead. The goal is a replacement that looks similar and is metric-compatible (same advance widths and line metrics), so text occupies the same size on screen and wraps as it does in PowerPoint. Calibri\u2192Carlito is the model.\n3. **Where no metric-compatible open replacement exists yet (Aptos today),** a visual-only look-alike is a documented fallback. It is reported as visual and is a known layout-fidelity gap to close, not the intended end state. The parity scoreboard counts it as near, not perfect.\n4. **PPTX export always writes the selected font name,** for example `typeface=\"Aptos\"` in the theme and in runs, never the replacement. PowerPoint then opens the file and shows the actual font, installed or as a Microsoft 365 cloud font.\n5. **Only open fonts may be embedded,** through the explicit embed path (FF-13).\n\n**Release status.** Point 4 is true on opf-pptx `main`. The published opf-pptx 0.9.1 still writes the substitute into the PPTX, and so do the editor and pptx.gallery builds that depend on it; selected-name export reaches them with the next opf-pptx release.\n\n**Provisional owner decisions (provisional, owner may revise).** Three choices about which open face stands in for a family are pending with the owner. The policy above is settled; only these replacement picks are provisional. Root resolved them provisionally with the recommended defaults:\n\n| Decision | Families | Replacement |\n| --- | --- | --- |\n| `aptos-preview` | Aptos (the default `aptos` scheme) | Roboto (visual) |\n| `segoe-ui-preview` | Segoe UI, Semibold, Light and Semilight | Red Hat Display (visual) |\n| `cambria-tier` | Cambria | Caladea, reclassified from metric to visual. Its `metricModeFallback` keeps metric-mode registries previewing Cambria with Caladea, reported as visual, as they did before FF-31. |\n\nAll three live in one block, `provisionalDecisions`, at the top of the JSON. The rows that follow a decision carry no replacement family of their own. A change of decision is therefore a one-line edit. When a decision changes, a stored measurement of the old family is dropped as unmeasured until `scripts/measure-font-replacements.mjs` is run again.\n\n1. **Licensed, non-free fonts are never bundled or embedded.** This covers Aptos, Calibri, Cambria, Segoe UI, Georgia, Tahoma, Grandview, Seaford, Tenorite, Consolas, Times New Roman, Arial, Courier New and the Windows script fonts. Rendering uses the family's designated open replacement instead:\n - **Metric-compatible** is the goal, used where a replacement exists and measures identical: Calibri\u2192Carlito, Arial\u2192Arimo, Times New Roman\u2192Tinos and Courier New\u2192Cousine. A metric row needs an upstream statement and a measurement in all four styles, with a mean width difference below 0.1% and no corpus string more than 0.3% off. Georgia\u2192Gelasio fails that test: ligature runs differ by up to 1.02% as opf-render shapes them. It is therefore visual, even though every basic-Latin advance matches.\n - **Alternates** are tried, in order, when the declared replacement's pack is not loaded. An alternate is always reported as visual, including on a metric row.\n - **Otherwise the closest measured open face**, marked visual: a documented fallback and a known layout-fidelity gap until a metric-compatible replacement exists. For example, Aptos\u2192Roboto measures a 2.15% mean width difference.\n2. **The PPTX always names the chosen family, and no font file is included.** The exporter writes `Aptos` when the document chose Aptos. PowerPoint resolves standard fonts on the viewer's machine: those shipped with Office, Windows or macOS, and Microsoft 365 cloud fonts. The replacement never reaches the package (opf-pptx `test/export-chosen-fonts.mjs`).\n3. **Openly licensed fonts render as themselves.** Examples are Roboto, Carlito and the Noto script families. They can reach a PPTX only through an explicit embed path (FF-13), never by default.\n4. **Families that are proprietary and non-standard, or missing from the table,** keep their name in the PPTX. `fontAvailabilityDiagnostics()` reports that viewers may lack them. It also flags Microsoft 365 cloud-only fonts (such as Aptos) and families that ship only in an optional Windows language feature.\n\n### Which faces are available\n\nopf-render ships only fonts that it already pins and hash-verifies:\n- **Base pack:** Roboto and Roboto Mono.\n- **Office pack:** Carlito, Caladea, Arimo, Tinos, Cousine and Gelasio.\n- **Optional `scripts` pack (FF-19):** Noto for non-Latin scripts and CJK.\n\nSome replacements are open families that no renderer pack ships yet, such as Red Hat Display for Segoe UI and Red Hat Text for Tahoma. For these, the renderer tries the declared replacement first, then the alternates. The last alternate is the best measured bundled face, so previews stay deterministic without any extra download. `registry.substitutions` records which face was used and its tier. The proposed `catalog` pack (21 OFL `@expo-google-fonts` packages, listed in the opf-render PR) would make the declared replacements and the open catalog families available. Downloading it needs approval, and it is not part of this change.\n\n| Environment | Open catalog families | Proprietary families | When the real font is required |\n| --- | --- | --- | --- |\n| Local Node, cloud or serverless (`prepareNodeFonts({pack: 'office', substitutionPolicy: 'visual'})`) | Exact when a pack ships them (Roboto, Carlito, \u2026, Noto with `scripts`) | Metric replacement: identical widths. Visual replacement: approximate, reported in `registry.substitutions` with the measured delta | Supply licensed files with `prepareNodeFonts({faces: [{path, family, weight, italic}]})`; they resolve as exact faces |\n| Strict mode (`substitutionPolicy: 'metric'`) | Exact | Metric replacements only | `font-unavailable` names the license class, the declared replacement and tier, the pack, and the caller hook. There is never a silent wrong-metric fallback |\n| Browser (`loadBrowserFontRegistry`) | Exact when the host serves the pack files | Same replacement rules | Same hook: pass the caller's own faces |\n| PowerPoint (exported PPTX) | Named; the viewer needs the font or substitutes | The selected name, never the replacement: the real font on Office, Windows or macOS, or through Microsoft 365 cloud fonts | Named; the viewer substitutes |\n\n**Aptos, the default scheme (replacement pick provisional, owner may revise).** Aptos is a Microsoft 365 cloud font and is not redistributable. The recommended cloud default previews Aptos with Roboto, the closest measured open face with all four styles, already in the base pack: mean width difference 2.15%, signed +0.1%, maximum 7.4% on a single string. Aptos Display previews with Carlito (1.8%). The exported PPTX still names Aptos and Aptos Display. Deployments that hold an Aptos license can pass the real files through `faces` for exact previews.\n\nThe pptx.gallery parity scoreboard's `fontResolution` check counts a family as perfect only when it is the real face or a metric-compatible replacement. The owner decided on 2026-09-29 that a declared, measured visual replacement such as Aptos\u2192Roboto is acceptable as a fallback when the PPTX keeps the selected name. It is classified near, not perfect, so the default `aptos` scheme stays short of perfect there until a metric-compatible Aptos replacement exists or licensed Aptos faces are supplied. The harness change merged in [opf#159](https://github.com/OpenPresentation/opf/pull/159). See the [program decision](programs/font-fidelity-everywhere/README.md#decisions).\n\nThe [Windows reference-font advance study](evidence/shared-metric-native-anchor/font-study-comparison.json) is exploratory source-checkpoint evidence, not an additional compatibility certification. It measures 1,024 cases across regular/bold Calibri, Arial, Times New Roman and Courier New, recording local reference-file versions/hashes and native font-slot names. Disabling optional ligatures and rounding base glyph advances to eighth-point steps predicts 949 observations within 0.02pt; 75 outliers remain, including combining marks, Arabic and Calibri kerning. Office theme tokens and per-glyph fallback are not resolved to exact native files by these name properties. No runtime provider or open-font mapping changes from this hypothesis, and no reference font is redistributed.\n\nThe composition API accepts a `textMeasurement` provider. A provider resolves font faces and returns actual text widths; callers pass the same provider to pagination, editor geometry, SVG rendering, and PPTX export. Without one, the existing deterministic character-width estimate remains available.\n\nRenderer 0.8.0 publishes `prepareNodeFonts` from `/fonts-node`. Its returned `options` combine the same registry measurement, embedded SVG fonts and explicit raster files, with system/bundled fallback disabled for raster calls. Pass these options to pagination, editor geometry, SVG, PPTX and PNG/PDF export. `pack: 'base'` is the default for authored Roboto decks; `pack: 'office'` adds the six Office substitute families and retains metric policy unless visual substitution is explicitly requested. `registry.substitutions` records actual substitutions; the helper does not rewrite the authored document or add native embedding.\n\nRenderer 0.8.0 groups static files by their OpenType preferred family while retaining legacy family names and explicit custom namespaces. `Roboto` requests at 500/600/800 now select the actual Medium/SemiBold/ExtraBold files instead of nearby 400/700 faces. Optional `TextStyle.fontFace` carries the physical legacy family and native bold/italic flags independently of CSS numeric weight: SemiBold/ExtraBold are regular within their legacy families. The converter consumes this metadata; providers without it retain their prior behavior. Nine actual base faces pass metadata/measurement/outline checks and offline Chromium advances on Node 20/24. Seven payload slides cover serialized native selectors, deterministic output and source/reimport. These checks do not establish native Office paint, embedding or broader script coverage; see the [Mac candidate evidence](evidence/mac-font-variants/README.md).\n\nRenderer 0.8.0 uses adjacent SVG spans when no measurement provider is supplied. This closes the visible gaps caused by estimated fragment widths while retaining estimated line breaks and all run text/style/source offsets. Supplied providers and accepted placements still use exact fragment origins. Measured SVG requests geometric precision; accepted outline placements also constrain horizontal advances with `textLength`/`spacingAndGlyphs` to avoid browser quantization drift. This can scale glyphs horizontally while retaining nominal font size and baseline. Width-only providers do not receive that constraint, and constrained widths do not certify raw font-metric equivalence. The compatible editor supports both forms. This is a spacing improvement, not evidence that unmeasured wrapping or glyph coverage is accurate; see [candidate evidence](evidence/mac-rich-flow/README.md).\n\nBoth Node loaders verify exact package versions, 33 font-file hashes and eight license-notice hashes against the immutable `BUNDLED_FONT_MANIFEST` exported from `/fonts-node`. Missing/modified resources reject with actionable errors. Default raster loading now includes all nine base faces instead of omitting Roboto semibold, italic and bold italic. Font files must remain available and unchanged between preparation and raster export. Browser loading, actual glyph coverage, variant naming, rich spacing, and native compatibility remain separate requirements. These APIs are available in [renderer 0.8.0](https://github.com/OpenPresentation/opf-render/releases/tag/opf-render-v0.8.0), published against core 0.10.0 with Node 24. Prepared HarfBuzz shaping and variable-instance work remain separate drafts.\n\nThe renderer's optional font registry uses [Fontkit](https://github.com/foliojs/fontkit) to shape text and measure glyph advances from local font bytes. It does not discover system fonts or fetch fonts. The Node helper loads the renderer's bundled Roboto and Roboto Mono faces:\n\n```js\nimport { loadBundledFontRegistry } from '@openpresentation/opf-render/fonts-node';\nimport { renderSvg, svgToPng } from '@openpresentation/opf-render';\nimport { paginatePresentation } from '@openpresentation/opf/pagination';\nimport { toPptx } from '@openpresentation/opf-pptx';\n\nconst registry = await loadBundledFontRegistry();\nconst options = { textMeasurement: registry.textMeasurement };\n// Use design.fontScheme: 'roboto', or supply the document's actual font files.\nconst { presentation } = paginatePresentation(deck, options);\nconst svg = renderSvg(presentation, {\n ...options,\n embeddedFonts: registry.embeddedFonts,\n});\nconst png = await svgToPng(svg, {\n fontFiles: registry.fontFiles,\n useBundledFonts: false,\n loadSystemFonts: false,\n});\nconst pptx = await toPptx(presentation, options);\n```\n\n`createFontRegistry` from `@openpresentation/opf-render/fonts` accepts `{data: Uint8Array, weight, italic?, family?, postscriptName?, license?}` entries in Node or the browser. Weights are explicit, with 400 as the default. Supply each style that the document uses. Missing font families and unsupported glyphs fail with `OPFFontError`, including the source path where available. Collection fonts require a `postscriptName` selecting one face.\n\nAliases and fallback families are explicit choices:\n\n```js\nconst registry = createFontRegistry(faces, {\n aliases: { Aptos: 'Roboto', 'Aptos Display': 'Roboto' },\n fallbackFamily: 'Roboto',\n});\nconsole.log(registry.substitutions);\nregistry.clearSubstitutions(); // Start a fresh render's diagnostic collection.\n```\n\nAn available exact family takes precedence over aliases. The registry resolves a requested weight to the closest supplied weight, reports the substitution, and makes the resolved style available to rendering. Missing italic/upright styles fail instead of synthesizing an unmeasured style. `strictGlyphs: false` is an explicit escape hatch for hosts with their own glyph-fallback policy; it is unsuitable for fidelity verification.\n\nSVG embeds supplied fonts using data URIs and includes supplied license notices as metadata. The bundled loader carries the fonts' SIL Open Font License notices. For PNG/PDF, pass the same font files to the rasterizer; its native font loader does not depend on browser CSS font loading. In a browser, wait for `document.fonts.ready` before measuring or taking a screenshot. The editor playground loads and embeds bundled fonts and displays substitutions.\n\nCurrent `svgToPdf` output is image-only: each slide is rasterized and embedded as a PNG on a PDF page. Text is not selectable or searchable through PDF text objects, and shapes are not preserved as vectors. The accepted [selectable/vector PDF roadmap](plans/pdf-export.md) adds a separate backend and verification requirement, including permitted font embedding, Unicode extraction and shared placement. Raster PDF will remain an explicit compatibility mode when the verified vector mode becomes the default; no vector mode has shipped yet.\n\n## Office compatibility pack\n\n`loadOfficeFontRegistry` from `@openpresentation/opf-render/fonts-node` supplies regular, bold, italic, and bold italic faces of Carlito, Caladea, Arimo, Tinos, Cousine, and Gelasio, plus the base Roboto pack. Package versions are pinned and each face carries its distribution's license notice. `includeBaseFonts: false` omits Roboto. Loading never installs fonts into the operating system or downloads fonts at render time.\n\n```js\nconst registry = await loadOfficeFontRegistry({\n substitutionPolicy: 'metric', // Default for this loader; no visual fallback.\n});\nregistry.resolveFont({fontFamily: 'Calibri', fontWeight: 400});\n// requestedFamily: Calibri, resolvedFamily: Carlito, compatibility: metric\n```\n\n`createFontRegistry` defaults to `substitutionPolicy: 'none'`. Policies are `none`, `metric`, and `visual`; visual permits both curated tiers. An explicit `fallbackFamily` is a separate, reported `generic` fallback. Aliases are explicit visual substitutions and never establish metric compatibility. `resolveFont` reports exact resolutions as well; `substitutions` only collects changes. Resolution records include requested/resolved weights, italic, source path, and supporting upstream information where available.\n\n| Requested family | Bundled substitute | Current automatic tier |\n| --- | --- | --- |\n| Calibri | Carlito | Metric intent, standard 400/700 styles |\n| Cambria | Caladea | Visual: advances differ from Cambria 6.99 by a mean of 2.7% (FF-31 measurement). Metric-mode registries still use it, reported as visual (`metricModeFallback`) |\n| Arial | Arimo | Metric, standard 400/700 styles |\n| Times New Roman | Tinos | Metric, standard 400/700 styles |\n| Courier New | Cousine | Metric, standard 400/700 styles |\n| Georgia | Gelasio | Visual: basic-Latin advances identical, but ligature runs differ by up to 1.02% as opf-render shapes them |\n| Calibri Light | Carlito | Visual: the bundle has no Carlito Light face |\n| Aptos / Aptos Display | Roboto / Carlito (FF-31 policy) | Visual; no Aptos metric claim |\n\nUpstream evidence: [Carlito](https://github.com/googlefonts/carlito), [Fontconfig mappings](https://chromium.googlesource.com/external/fontconfig/+/refs/heads/main/conf.d/30-metric-aliases.conf), [Arimo](https://github.com/google/fonts/blob/main/ofl/arimo/DESCRIPTION.en_us.html), [Tinos](https://github.com/google/fonts/blob/main/ofl/tinos/DESCRIPTION.en_us.html), [Cousine](https://github.com/google/fonts/blob/main/apache/cousine/DESCRIPTION.en_us.html), and [Gelasio](https://github.com/SorkinType/Gelasio). Metric classification describes compatibility intent within the stated style scope, not universal identical output. Missing matching weights cannot silently qualify for the metric tier.\n\nThe exported `FONT_COMPATIBILITY` list also contains optional visual candidates and CJK families. Listing a candidate does not bundle it or imply complete character coverage. Liberation Sans Narrow is a separate legacy distribution with a different license history; it is not part of this bundle. Wingdings, Webdings, and Symbol require character mapping before substitution; an ordinary fallback fails with `font-encoding-required`. Missing math fonts require an explicit math-aware choice and fail with `math-font-required` instead of falling through to body text.\n\nDrawingML tokens such as `+mn-lt` resolve through the registry's explicit `themeFonts` option before substitution. Supply concrete `majorLatin`, `minorLatin`, and, where used, `majorEastAsia`, `minorEastAsia`, `majorComplexScript`, or `minorComplexScript` families. Missing theme mappings fail. This helper does not yet extract theme font records or embedded fonts from imported PPTX files.\n\n### Measured results and experimental fonts\n\n`node --import ./scripts/register-local-opf.mjs scripts/test-office-fonts.mjs --system` compares the bundle to reference fonts already installed in macOS's Supplemental directory. It does not redistribute reference fonts. The report records source-file hashes and individual shaped widths. Across four samples and four styles, Arimo/Arial, Tinos/Times New Roman, and Cousine/Courier New matched exactly on 48 runs. Gelasio/Georgia differed on ligature-containing runs, with a maximum difference of 2.0125%. Individual basic-Latin advances matched; disabling optional ligatures removed the tested difference. Until feature handling is consistent across outputs, the policy conservatively labels Gelasio approximate. Calibri and Cambria reference fonts were not available for this comparison.\n\n[Akasia](https://codeberg.org/bloudraad/akasia) remains experimental after the [v0.0.2 open-file assessment](evidence/akasia-assessment/README.md). All twelve styles match public upstream advance/kerning/ligature data, but 14 reference codepoints are missing and Black Italic decomposed accents expose a 0.421875px Fontkit/Chromium advance difference at size 32. Both Node runtimes retain the failure. This is not independent Aptos-binary or native Office verification; no mapping or bundled pack changes. `EXPERIMENTAL_FONT_CANDIDATES` records it separately. Aptos Narrow and Display remain outside that evidence.\n\nAn original OPF font project is technically feasible: independently designed or suitably open-licensed glyph outlines can be fitted to target advance widths, placement, vertical metrics, and shaping behavior. A successful font needs a reproducible source build, provenance, style/coverage tests, visual review, and cross-renderer conformance. Matching bounding boxes alone is insufficient: [OpenType horizontal metrics](https://learn.microsoft.com/en-us/typography/opentype/spec/hmtx) and [glyph positioning](https://learn.microsoft.com/en-us/typography/opentype/spec/gpos) jointly control text placement. Universal pixel identity across rasterizers is not the acceptance criterion; measured layout preservation over an explicit test matrix is.\n\n## Verification and remaining work\n\n`pnpm test:fonts` checks that editor and SVG geometry match and that every native PPTX text box has the same coordinates and measured line breaks. With opf-pptx FF-31 (opf-pptx#63), export names the chosen family, not the preview substitute. It writes artifacts to `artifacts/fonts/`. A real-browser check of the same Roboto run measured 324.032 pixels versus the font engine's 324.170 pixels at 25 pixels, a difference of 0.138 pixels. These are measured tolerances, not a promise of pixel identity.\n\nPPTX records the chosen font family (FF-31); it never records a preview replacement and never embeds a proprietary font binary. PowerPoint still needs those fonts installed, through Office, the OS or Microsoft 365 cloud fonts, or it substitutes them. Line height remains the shared 1.22 multiplier, rather than a complete ascent/descent model. Rich-text font overrides, mixed-script fallback and bidi layout, specialized payload internals, and native font embedding remain active fidelity work. Passing a width provider does not remove those limits.\n"
73
+ "markdown": "# Measured fonts and reproducible previews\n\nFor the starter set and delivery priorities, see the [font roadmap](plans/font-roadmap.md).\n\n## Font policy (FF-31)\n\nOPF keeps one machine-readable font policy table, [`spec/reference/font-policy.json`](../spec/reference/font-policy.json). Core exports it as `FONT_POLICY`, `fontPolicyFor()` and `applyFontPolicyDecisions()` from `@openpresentation/opf` or `@openpresentation/opf/font-policy`. Each row gives a family's license class, where viewers get it, whether OPF may ever embed it, and its preview replacement with a measured width difference. It also lists alternates, ending where possible with a face that already ships with opf-render. The [licensing table](programs/font-fidelity-everywhere/font-licensing.md) lists all 153 rows. [`font-policy.schema.json`](../spec/reference/font-policy.schema.json) is its JSON Schema. The [measurement evidence](evidence/font-replacements-20260923/README.md) explains how each replacement was chosen.\n\n**The policy in brief (owner decisions, 2026-09-29).** This is the canonical statement; the sibling repositories link here.\n\n1. **The user's font selection is the source of truth.** The user picks a font, for example Aptos or Calibri through a font scheme. That name is what the document, the theme and the PPTX carry.\n2. **License-restricted (proprietary) fonts are never bundled or embedded,** so previews cannot draw them. Openly licensed fonts such as Carlito and Roboto are bundled, and \"licensed\" below always means license-restricted. Live previews, SVG, the editor and gallery thumbnails use an open look-alike instead. The goal is a replacement that looks similar and is metric-compatible (same advance widths and line metrics), so text occupies the same size on screen and wraps as it does in PowerPoint. Calibri\u2192Carlito is the model.\n3. **Where no metric-compatible open replacement exists yet,** a visual-only look-alike is a documented fallback. It is reported as visual and is a known layout-fidelity gap to close, not the intended end state. The parity scoreboard counts it as near, not perfect. The Aptos family is no longer such a case: Intos previews it as metric.\n4. **PPTX export always writes the selected font name,** for example `typeface=\"Aptos\"` in the theme and in runs, never the replacement. PowerPoint then opens the file and shows the actual font, installed or as a Microsoft 365 cloud font.\n5. **Only open fonts may be embedded,** through the explicit embed path (FF-13).\n6. **Font files are bundled, never hotlinked.** Fonts, Google Fonts included, ship as pinned files (an exact npm version, or a vendored file with a recorded sha256) and are never loaded at runtime from a font CDN. That protects visitor privacy (a German court held the Google Fonts CDN a GDPR violation in 2022) and keeps previews offline-capable and audits reproducible. Build-time self-hosting such as `next/font/google` is not a hotlink but is not pinned, so use `next/font/local` with pinned files. Guards fail on CDN references in every repository.\n7. **Every bundled face records a verified permissive license.** Allowed: exactly OFL-1.1, Apache-2.0, MIT and UFL-1.0; not GPL, LGPL, AGPL, proprietary or unclear public-domain fonts. The record has the SPDX id, any Reserved Font Name the copyright block declares, the source URL, package@version and sha256, all checked against the LICENSE file the font ships with. A modified version (subset, instance, conversion) may not use a Reserved Font Name in its name: a family whose served name contains its reserved name (Carlito, Raleway) is bundled only as the unmodified upstream file, while one that reserves another name (Noto Sans JP reserves \"Source\") may be modified. Details and enforcement: [Font files: bundling and licenses](programs/font-fidelity-everywhere/font-licensing.md#font-files-bundling-and-licenses).\n\n**Release status.** Point 4 is true on opf-pptx `main`. The published opf-pptx 0.9.1 still writes the substitute into the PPTX, and so do the editor and pptx.gallery builds that depend on it; selected-name export reaches them with the next opf-pptx release.\n\n**Provisional owner decisions (provisional, owner may revise).** Three choices about which open face stands in for a family are pending with the owner. The policy above is settled; only these replacement picks are provisional. Root resolved them provisionally with the recommended defaults:\n\n| Decision | Families | Replacement |\n| --- | --- | --- |\n| `aptos-preview` | Aptos (the default `aptos` scheme) | Intos (metric, owner policy 2026-09-29; Roboto and Carlito are alternates) |\n| `segoe-ui-preview` | Segoe UI, Semibold, Light and Semilight | Red Hat Display (visual) |\n| `cambria-tier` | Cambria | Caladea, reclassified from metric to visual. Its `metricModeFallback` keeps metric-mode registries previewing Cambria with Caladea, reported as visual, as they did before FF-31. |\n\nAll three live in one block, `provisionalDecisions`, at the top of the JSON. The rows that follow a decision carry no replacement family of their own. A change of decision is therefore a one-line edit. When a decision changes, a stored measurement of the old family is dropped as unmeasured until `scripts/measure-font-replacements.mjs` is run again.\n\n1. **Licensed, non-free fonts are never bundled or embedded.** This covers Aptos, Calibri, Cambria, Segoe UI, Georgia, Tahoma, Grandview, Seaford, Tenorite, Consolas, Times New Roman, Arial, Courier New and the Windows script fonts. Rendering uses the family's designated open replacement instead:\n - **Metric-compatible** is the goal, used where a replacement exists and measures identical: Calibri\u2192Carlito, Arial\u2192Arimo, Times New Roman\u2192Tinos, Courier New\u2192Cousine and Georgia\u2192Gelasio. A metric row needs an upstream statement and a measurement in all four styles, with a mean width difference below 0.1% and no corpus string more than 0.3% off. Georgia\u2192Gelasio passes only with Gelasio shaped with its `liga` and `clig` features off (the row lists them as `disabledFeatures`): with default features Gelasio ligates fi, fl, ffi and ffl, which Georgia does not, and runs differ by up to 1.02%. With them off every one of the 300 corpus strings matches in all four styles (mean and maximum below 0.01%). opf-render turns those features off in measurement and in SVG, so a renderer that does not is visual against Georgia.\n - **Alternates** are tried, in order, when the declared replacement's pack is not loaded. An alternate is always reported as visual, including on a metric row.\n - **Aptos family:** Aptos\u2192Intos, Aptos Display\u2192Intos Display, Aptos Narrow\u2192Intos Narrow and Aptos Serif\u2192Intos Serif are metric: 0.000% mean and maximum against Aptos 2.01 in all four styles, with equal vertical metrics.\n - **Otherwise the closest measured open face**, marked visual: a documented fallback and a known layout-fidelity gap until a metric-compatible replacement exists. For example, Segoe UI\u2192Red Hat Display measures a 1.72% mean width difference.\n2. **The PPTX always names the chosen family, and no font file is included.** The exporter writes `Aptos` when the document chose Aptos. PowerPoint resolves standard fonts on the viewer's machine: those shipped with Office, Windows or macOS, and Microsoft 365 cloud fonts. The replacement never reaches the package (opf-pptx `test/export-chosen-fonts.mjs`).\n3. **Openly licensed fonts render as themselves.** Examples are Roboto, Carlito and the Noto script families. They can reach a PPTX only through an explicit embed path (FF-13), never by default.\n4. **Families that are proprietary and non-standard, or missing from the table,** keep their name in the PPTX. `fontAvailabilityDiagnostics()` reports that viewers may lack them. It also flags Microsoft 365 cloud-only fonts (such as Aptos) and families that ship only in an optional Windows language feature.\n\n### Which faces are available\n\nopf-render ships only fonts that it already pins and hash-verifies:\n- **Base pack:** Roboto and Roboto Mono.\n- **Office pack:** Carlito, Caladea, Arimo, Tinos, Cousine and Gelasio.\n- **Office pack, Aptos family:** Intos, Intos Display, Intos Narrow and Intos Serif (OFL-1.1, vendored in opf-render at a pinned commit, unmodified from the upstream files). A registry built without them (for example the base pack alone) falls back to the alternates Roboto and Carlito, reported as visual.\n- **Optional `scripts` pack (FF-19):** Noto for non-Latin scripts and CJK.\n\nSome replacements are open families that no renderer pack ships yet, such as Red Hat Display for Segoe UI and Red Hat Text for Tahoma. For these, the renderer tries the declared replacement first, then the alternates. The last alternate is the best measured bundled face, so previews stay deterministic without any extra download. `registry.substitutions` records which face was used and its tier. The proposed `catalog` pack (21 OFL `@expo-google-fonts` packages, listed in the opf-render PR) would make the declared replacements and the open catalog families available. Downloading it needs approval, and it is not part of this change.\n\n| Environment | Open catalog families | Proprietary families | When the real font is required |\n| --- | --- | --- | --- |\n| Local Node, cloud or serverless (`prepareNodeFonts({pack: 'office', substitutionPolicy: 'visual'})`) | Exact when a pack ships them (Roboto, Carlito, \u2026, Noto with `scripts`) | Metric replacement: identical widths. Visual replacement: approximate, reported in `registry.substitutions` with the measured delta | Supply licensed files with `prepareNodeFonts({faces: [{path, family, weight, italic}]})`; they resolve as exact faces |\n| Strict mode (`substitutionPolicy: 'metric'`) | Exact | Metric replacements only | `font-unavailable` names the license class, the declared replacement and tier, the pack, and the caller hook. There is never a silent wrong-metric fallback |\n| Browser (`loadBrowserFontRegistry`) | Exact when the host serves the pack files | Same replacement rules | Same hook: pass the caller's own faces |\n| PowerPoint (exported PPTX) | Named; the viewer needs the font or substitutes | The selected name, never the replacement: the real font on Office, Windows or macOS, or through Microsoft 365 cloud fonts | Named; the viewer substitutes |\n\n**Aptos, the default scheme.** Aptos is a Microsoft 365 cloud font and is not redistributable. Under the owner policy of 2026-09-29 it previews with Intos, an OFL font whose advance widths, kerning and vertical metrics equal Aptos 2.01: 0.000% mean and maximum width difference over the 300-string corpus in regular, bold, italic and bold italic, for Aptos, Aptos Display, Aptos Narrow and Aptos Serif (Aptos Serif measured from Microsoft's standalone Aptos Fonts download, the others from the Microsoft 365 cloud fonts). Line breaks, line heights and text sizes therefore agree with Aptos. The letter shapes are Intos's own (Inter-derived, Gelasio-derived for the serif), not Aptos's. The exported PPTX still names Aptos, Aptos Display, Aptos Narrow or Aptos Serif, and no Aptos file is bundled or embedded. Deployments that hold an Aptos license can pass the real files through `faces` for exact previews.\n\nIntos ships in opf-render's default office pack (`prepareNodeFonts({pack: 'office'})`, `loadOfficeFontRegistry()`), about 12 MB of font files, so the default metric policy previews the Aptos family without asking for visual mode. Without those faces, previews fall back to the alternates Roboto and Carlito, marked visual. Like the open families, Intos is an `embed: \"used\"` face: `registry.embeddedFonts` stays the 33 eager npm faces, `prepareNodeFonts().options.embeddedFonts` supplies it, and a standalone SVG embeds only the Intos faces its text draws (an Aptos slide: Intos regular and Intos Display bold, 14.7 MB with the eager faces, against 20.7 MB with all eight styles). Intos is a single-maintainer project started in September 2026, so it is pinned by commit and SHA-256 and the previous replacements stay as alternates. The pptx.gallery parity scoreboard's `fontResolution` check counts the Aptos family as perfect because the replacement is metric-compatible.\n\n**Browser hosts load the vendored faces on demand.** The eager list is what a host puts in one `fonts.json` (12.6 MB); the vendored faces (Intos and the open families, `registry.lazyFonts`, 51 faces) would add 19.4 MB, so they ship as separate hash-pinned files at their package-relative paths (`fonts/intos/...`, `fonts/<family>/...`) and load through `loadBrowserFontRegistry(faces, {lazyFontsBaseUrl})`. `await registry.ensureLazyFonts(presentation)` fetches and verifies only the files the document's font families resolve to (the default Aptos scheme: Intos and Intos Display, 8 files, 5.9 MB), then adds them to the document and the registry together, so the editor never measures with a face it paints as a fallback. The gallery commits only a pinned manifest, `lazy-fonts.json` (`scripts/gallery-lazy-fonts.mjs`, written by `build-registry-gallery-editor` from the published renderer's manifest when the pinned editor example calls `ensureLazyFonts`, and copied by `prepare-gallery-editor`): exact renderer version, SPDX license, license-file hash and every face's SHA-256, no bytes. The gallery's own build copies the faces from the pinned renderer package's `fonts/` directory into an untracked path, verifying each hash, the way it does for script fonts. The local editor demo (`build-editor-demo`, through `scripts/emit-lazy-fonts.mjs`) copies the files beside the page instead. The editor playground calls `ensureLazyFonts` when a document needs them. `node scripts/test-editor-lazy-fonts.mjs` drives the built playground in Chromium.\n\nThe [Windows reference-font advance study](evidence/shared-metric-native-anchor/font-study-comparison.json) is exploratory source-checkpoint evidence, not an additional compatibility certification. It measures 1,024 cases across regular/bold Calibri, Arial, Times New Roman and Courier New, recording local reference-file versions/hashes and native font-slot names. Disabling optional ligatures and rounding base glyph advances to eighth-point steps predicts 949 observations within 0.02pt; 75 outliers remain, including combining marks, Arabic and Calibri kerning. Office theme tokens and per-glyph fallback are not resolved to exact native files by these name properties. No runtime provider or open-font mapping changes from this hypothesis, and no reference font is redistributed.\n\nThe composition API accepts a `textMeasurement` provider. A provider resolves font faces and returns actual text widths; callers pass the same provider to pagination, editor geometry, SVG rendering, and PPTX export. Without one, the existing deterministic character-width estimate remains available.\n\nRenderer 0.8.0 publishes `prepareNodeFonts` from `/fonts-node`. Its returned `options` combine the same registry measurement, embedded SVG fonts and explicit raster files, with system/bundled fallback disabled for raster calls. Pass these options to pagination, editor geometry, SVG, PPTX and PNG/PDF export. `pack: 'base'` is the default for authored Roboto decks; `pack: 'office'` adds the six Office substitute families and retains metric policy unless visual substitution is explicitly requested. `registry.substitutions` records actual substitutions; the helper does not rewrite the authored document or add native embedding.\n\nRenderer 0.8.0 groups static files by their OpenType preferred family while retaining legacy family names and explicit custom namespaces. `Roboto` requests at 500/600/800 now select the actual Medium/SemiBold/ExtraBold files instead of nearby 400/700 faces. Optional `TextStyle.fontFace` carries the physical legacy family and native bold/italic flags independently of CSS numeric weight: SemiBold/ExtraBold are regular within their legacy families. The converter consumes this metadata; providers without it retain their prior behavior. Nine actual base faces pass metadata/measurement/outline checks and offline Chromium advances on Node 20/24. Seven payload slides cover serialized native selectors, deterministic output and source/reimport. These checks do not establish native Office paint, embedding or broader script coverage; see the [Mac candidate evidence](evidence/mac-font-variants/README.md).\n\nRenderer 0.8.0 uses adjacent SVG spans when no measurement provider is supplied. This closes the visible gaps caused by estimated fragment widths while retaining estimated line breaks and all run text/style/source offsets. Supplied providers and accepted placements still use exact fragment origins. Measured SVG requests geometric precision; accepted outline placements also constrain horizontal advances with `textLength`/`spacingAndGlyphs` to avoid browser quantization drift. This can scale glyphs horizontally while retaining nominal font size and baseline. Width-only providers do not receive that constraint, and constrained widths do not certify raw font-metric equivalence. The compatible editor supports both forms. This is a spacing improvement, not evidence that unmeasured wrapping or glyph coverage is accurate; see [candidate evidence](evidence/mac-rich-flow/README.md).\n\nBoth Node loaders verify exact package versions, 33 font-file hashes and eight license-notice hashes against the immutable `BUNDLED_FONT_MANIFEST` exported from `/fonts-node`. The office loader also verifies the vendored faces: the Carlito files, the 35 open-family files and the 16 Intos files, with the hashes of their licenses and, for Intos, its provenance notice. Missing/modified resources reject with actionable errors. Default raster loading now includes all nine base faces instead of omitting Roboto semibold, italic and bold italic. Font files must remain available and unchanged between preparation and raster export. Browser loading, actual glyph coverage, variant naming, rich spacing, and native compatibility remain separate requirements. These APIs are available in [renderer 0.8.0](https://github.com/OpenPresentation/opf-render/releases/tag/opf-render-v0.8.0), published against core 0.10.0 with Node 24. Prepared HarfBuzz shaping and variable-instance work remain separate drafts.\n\nThe renderer's optional font registry uses [Fontkit](https://github.com/foliojs/fontkit) to shape text and measure glyph advances from local font bytes. It does not discover system fonts or fetch fonts. The Node helper loads the renderer's bundled Roboto and Roboto Mono faces:\n\n```js\nimport { loadBundledFontRegistry } from '@openpresentation/opf-render/fonts-node';\nimport { renderSvg, svgToPng } from '@openpresentation/opf-render';\nimport { paginatePresentation } from '@openpresentation/opf/pagination';\nimport { toPptx } from '@openpresentation/opf-pptx';\n\nconst registry = await loadBundledFontRegistry();\nconst options = { textMeasurement: registry.textMeasurement };\n// Use design.fontScheme: 'roboto', or supply the document's actual font files.\nconst { presentation } = paginatePresentation(deck, options);\nconst svg = renderSvg(presentation, {\n ...options,\n embeddedFonts: registry.embeddedFonts,\n});\nconst png = await svgToPng(svg, {\n fontFiles: registry.fontFiles,\n useBundledFonts: false,\n loadSystemFonts: false,\n});\nconst pptx = await toPptx(presentation, options);\n```\n\n`createFontRegistry` from `@openpresentation/opf-render/fonts` accepts `{data: Uint8Array, weight, italic?, family?, postscriptName?, license?}` entries in Node or the browser. Weights are explicit, with 400 as the default. Supply each style that the document uses. Missing font families and unsupported glyphs fail with `OPFFontError`, including the source path where available. Collection fonts require a `postscriptName` selecting one face.\n\nAliases and fallback families are explicit choices:\n\n```js\nconst registry = createFontRegistry(faces, {\n aliases: { Aptos: 'Roboto', 'Aptos Display': 'Roboto' },\n fallbackFamily: 'Roboto',\n});\nconsole.log(registry.substitutions);\nregistry.clearSubstitutions(); // Start a fresh render's diagnostic collection.\n```\n\nAn available exact family takes precedence over aliases. The registry resolves a requested weight to the closest supplied weight, reports the substitution, and makes the resolved style available to rendering. Missing italic/upright styles fail instead of synthesizing an unmeasured style. `strictGlyphs: false` is an explicit escape hatch for hosts with their own glyph-fallback policy; it is unsuitable for fidelity verification.\n\nSVG embeds supplied fonts using data URIs and includes supplied license notices as metadata. The bundled loader carries the fonts' SIL Open Font License notices. For PNG/PDF, pass the same font files to the rasterizer; its native font loader does not depend on browser CSS font loading. In a browser, wait for `document.fonts.ready` before measuring or taking a screenshot. The editor playground loads and embeds bundled fonts and displays substitutions.\n\nCurrent `svgToPdf` output is image-only: each slide is rasterized and embedded as a PNG on a PDF page. Text is not selectable or searchable through PDF text objects, and shapes are not preserved as vectors. The accepted [selectable/vector PDF roadmap](plans/pdf-export.md) adds a separate backend and verification requirement, including permitted font embedding, Unicode extraction and shared placement. Raster PDF will remain an explicit compatibility mode when the verified vector mode becomes the default; no vector mode has shipped yet.\n\n## Office compatibility pack\n\n`loadOfficeFontRegistry` from `@openpresentation/opf-render/fonts-node` supplies regular, bold, italic, and bold italic faces of Carlito, Caladea, Arimo, Tinos, Cousine, and Gelasio, plus the base Roboto pack. Package versions are pinned and each face carries its distribution's license notice. `includeBaseFonts: false` omits Roboto. Loading never installs fonts into the operating system or downloads fonts at render time.\n\n```js\nconst registry = await loadOfficeFontRegistry({\n substitutionPolicy: 'metric', // Default for this loader; no visual fallback.\n});\nregistry.resolveFont({fontFamily: 'Calibri', fontWeight: 400});\n// requestedFamily: Calibri, resolvedFamily: Carlito, compatibility: metric\n```\n\n`createFontRegistry` defaults to `substitutionPolicy: 'none'`. Policies are `none`, `metric`, and `visual`; visual permits both curated tiers. An explicit `fallbackFamily` is a separate, reported `generic` fallback. Aliases are explicit visual substitutions and never establish metric compatibility. `resolveFont` reports exact resolutions as well; `substitutions` only collects changes. Resolution records include requested/resolved weights, italic, source path, and supporting upstream information where available.\n\n| Requested family | Bundled substitute | Current automatic tier |\n| --- | --- | --- |\n| Calibri | Carlito | Metric intent, standard 400/700 styles |\n| Cambria | Caladea | Visual: advances differ from Cambria 6.99 by a mean of 2.7% (FF-31 measurement). Metric-mode registries still use it, reported as visual (`metricModeFallback`) |\n| Arial | Arimo | Metric, standard 400/700 styles |\n| Times New Roman | Tinos | Metric, standard 400/700 styles |\n| Courier New | Cousine | Metric, standard 400/700 styles |\n| Georgia | Gelasio | Metric with `liga` and `clig` off (`disabledFeatures`, applied by opf-render): advances identical on all 300 corpus strings in four styles. With default features, ligature runs differ by up to 1.02% |\n| Calibri Light | Carlito | Visual: the bundle has no Carlito Light face |\n| Aptos, Aptos Display, Aptos Narrow, Aptos Serif | Intos, Intos Display, Intos Narrow, Intos Serif (office pack) | Metric: 0.000% mean and maximum against Aptos 2.01, all four styles; Roboto and Carlito are visual alternates |\n\nUpstream evidence: [Carlito](https://github.com/googlefonts/carlito), [Fontconfig mappings](https://chromium.googlesource.com/external/fontconfig/+/refs/heads/main/conf.d/30-metric-aliases.conf), [Arimo](https://github.com/google/fonts/blob/main/ofl/arimo/DESCRIPTION.en_us.html), [Tinos](https://github.com/google/fonts/blob/main/ofl/tinos/DESCRIPTION.en_us.html), [Cousine](https://github.com/google/fonts/blob/main/apache/cousine/DESCRIPTION.en_us.html), and [Gelasio](https://github.com/SorkinType/Gelasio). Metric classification describes compatibility intent within the stated style scope, not universal identical output. Missing matching weights cannot silently qualify for the metric tier.\n\nThe exported `FONT_COMPATIBILITY` list also contains optional visual candidates and CJK families. Listing a candidate does not bundle it or imply complete character coverage. Liberation Sans Narrow is a separate legacy distribution with a different license history; it is not part of this bundle. Wingdings, Webdings, and Symbol require character mapping before substitution; an ordinary fallback fails with `font-encoding-required`. Missing math fonts require an explicit math-aware choice and fail with `math-font-required` instead of falling through to body text.\n\nDrawingML tokens such as `+mn-lt` resolve through the registry's explicit `themeFonts` option before substitution. Supply concrete `majorLatin`, `minorLatin`, and, where used, `majorEastAsia`, `minorEastAsia`, `majorComplexScript`, or `minorComplexScript` families. Missing theme mappings fail. This helper does not yet extract theme font records or embedded fonts from imported PPTX files.\n\n### Measured results and experimental fonts\n\n`node --import ./scripts/register-local-opf.mjs scripts/test-office-fonts.mjs --system` compares the bundle to reference fonts already installed in macOS's Supplemental directory. It does not redistribute reference fonts. The report records source-file hashes and individual shaped widths. Across four samples and four styles, Arimo/Arial, Tinos/Times New Roman, and Cousine/Courier New matched exactly on 48 runs. Gelasio/Georgia differed on ligature-containing runs, with a maximum difference of 2.0125%. Individual basic-Latin advances matched; disabling optional ligatures removed the tested difference. Until feature handling is consistent across outputs, the policy conservatively labels Gelasio approximate. Calibri and Cambria reference fonts were not available for this comparison.\n\n[Akasia](https://codeberg.org/bloudraad/akasia), assessed earlier ([v0.0.2 open-file assessment](evidence/akasia-assessment/README.md)), is dropped: its repository is no longer available, and Intos replaces it. `EXPERIMENTAL_FONT_CANDIDATES` now records Microsoft's Selawik, measured for Segoe UI on 2026-09-29 and rejected: 0.16% mean and 2.5% maximum in regular, no italic faces, 349 code points, lowercase 4.8% shorter. The acceptance rules for replacement fonts are in the [licensing table](programs/font-fidelity-everywhere/font-licensing.md#replacement-font-acceptance-rules).\n\nAn original OPF font project is technically feasible: independently designed or suitably open-licensed glyph outlines can be fitted to target advance widths, placement, vertical metrics, and shaping behavior. A successful font needs a reproducible source build, provenance, style/coverage tests, visual review, and cross-renderer conformance. Matching bounding boxes alone is insufficient: [OpenType horizontal metrics](https://learn.microsoft.com/en-us/typography/opentype/spec/hmtx) and [glyph positioning](https://learn.microsoft.com/en-us/typography/opentype/spec/gpos) jointly control text placement. Universal pixel identity across rasterizers is not the acceptance criterion; measured layout preservation over an explicit test matrix is.\n\n## Verification and remaining work\n\n`pnpm test:fonts` checks that editor and SVG geometry match and that every native PPTX text box has the same coordinates and measured line breaks. With opf-pptx FF-31 (opf-pptx#63), export names the chosen family, not the preview substitute. It writes artifacts to `artifacts/fonts/`. A real-browser check of the same Roboto run measured 324.032 pixels versus the font engine's 324.170 pixels at 25 pixels, a difference of 0.138 pixels. These are measured tolerances, not a promise of pixel identity.\n\nPPTX records the chosen font family (FF-31); it never records a preview replacement and never embeds a proprietary font binary. PowerPoint still needs those fonts installed, through Office, the OS or Microsoft 365 cloud fonts, or it substitutes them. Line height remains the shared 1.22 multiplier, rather than a complete ascent/descent model. Rich-text font overrides, mixed-script fallback and bidi layout, specialized payload internals, and native font embedding remain active fidelity work. Passing a width provider does not remove those limits.\n"
74
74
  },
75
75
  {
76
76
  "slug": "format-card",
@@ -166,13 +166,13 @@ var docsData = Object.freeze([
166
166
  "slug": "live-editor",
167
167
  "file": "docs/live-editor.md",
168
168
  "title": "Browser preview and live editing",
169
- "markdown": "# Browser preview and live editing\n\nPublished editor 0.8.0 provides an embeddable SVG canvas in `@openpresentation/opf-editor/canvas`. OPF JSON remains the document; the canvas writes validated JSON Patch operations through an `EditorSession`. Draft edits render with the same SVG engine used for standalone previews. Completed edits produce one undoable change.\n\nThe published canvas covers the interactions below; complete PowerPoint feature coverage remains separate work. \u201CPixel perfect\u201D is a fidelity target with specific prerequisites and remaining gaps described below.\n\n## Install the published packages\n\nUse Node 24 with core 0.11.0, renderer 0.9.0, editor 0.8.0 and PPTX 0.9.1:\n\n```sh\nnpm install --save-exact @openpresentation/opf@0.11.0 @openpresentation/opf-render@0.9.0 @openpresentation/opf-editor@0.8.0 @openpresentation/opf-pptx@0.9.1\n```\n\nNo paid service or provider account is required. The six agent skills install with `npx @openpresentation/cli@0.9.0 skills install`. See the [quickstart](quickstart.md) for an installed-package workflow and the [compatibility matrix](compatibility-matrix.md) for separately scoped browser and native evidence.\n\nFor library development, separately regenerate unpublished local preview tarballs from sibling checkouts:\n\n```sh\npnpm build\nnode scripts/link-ecosystem.mjs\npnpm pack:ecosystem\npnpm test:packed-ecosystem\n```\n\nThe packed consumer installs actual tarballs without workspace aliases, exercises editing/SVG/PPTX, checks TypeScript declarations, and bundles a browser entry without Node shims. For a public release, advance source versions and downstream minimums/lockfiles together and follow the release process.\n\nThe gallery host example also offers local PPTX file import with preview/diagnostics and editable PowerPoint download. It commits active canvas text before export, shares preview text measurements and applies imports as a single undoable change. Save OPF to preserve the original source; native PowerPoint positions, fonts and unsupported features can change during conversion. The browser E2E checks run offline after loading and inspect the downloaded native merged table, then reimport and undo/redo. Native edit/save/reopen is a separate targeted check, not a pixel-equivalence claim.\n\n`pnpm prepare:gallery:registry` builds host controls from the immutable `exampleRefs.opf-editor` in `release-plan.json` while resolving libraries only from the fresh npm consumer. Package `verificationRefs` continue to point at actual published releases. The gallery manifest records both the example source hashes and registry package integrities. Updating example controls does not imply a new editor library release.\n\nThe browser bundle links `playground.js.LEGAL.txt`, included in the hashed resources. It contains bundled license notices and package license files, including the vendored PptxGenJS MIT license. For dependencies that publish only an explicit MIT declaration in their README, the build retains that declaration/attribution and the standard terms; omitted upstream notices use a version-specific source URL and verified supplement hash. License collection runs offline from the verified installation and committed supplement. Runtime JavaScript is not rewritten to normalize comment whitespace.\n\n## Embed in any browser application\n\nMount after the host DOM exists. The container controls width; the slide retains its aspect ratio. React and Svelte applications can mount this framework-independent API in their normal client lifecycle and destroy it on unmount.\n\n```js\nimport { createCanvasEditor } from '@openpresentation/opf-editor/canvas';\nimport { loadBrowserFontRegistry } from '@openpresentation/opf-render/fonts-browser';\n\n// Copy these licensed font files into your application's static assets first.\n// Use pinned, static faces; include every weight/style required by your deck.\nconst fonts = await loadBrowserFontRegistry([\n { url: '/fonts/Roboto-Regular.ttf', family: 'Roboto', weight: 400 },\n { url: '/fonts/Roboto-Bold.ttf', family: 'Roboto', weight: 700 },\n { url: '/fonts/RobotoMono-Regular.ttf', family: 'Roboto Mono', weight: 400 },\n]);\n\nconst canvas = createCanvasEditor(document.querySelector('#slide'), {\n document: {\n design: { theme: 'classic', fontScheme: 'roboto' },\n slides: [{ title: 'An editable presentation', text: 'Double-click to edit.' }],\n },\n renderOptions: { textMeasurement: fonts.textMeasurement },\n onCommit: ({ editor }) => {\n const updatedOPF = editor.document; // Host owns saving and collaboration.\n console.log(updatedOPF);\n },\n onError: error => console.error(error.message),\n});\nawait canvas.ready;\n\n// JSON or LLM patches also update the slide automatically.\ncanvas.editor.set('slides.0.title', 'Changes from another control');\ncanvas.editor.undo();\n\n// On unmount:\n// canvas.destroy();\n// fonts.dispose();\n```\n\n`loadBrowserFontRegistry` accepts explicit font-file URLs or `Uint8Array` data. It uses the same bytes for Fontkit measurement and browser `FontFace` registration, awaits loading, reports failures, and exposes `dispose()` for its owned font faces. Cross-origin font URLs need CORS access. Load fonts once and share the registry between canvases. The canvas does not fetch fonts or catalog sources itself.\n\nFor standalone SVG export, pass `fonts.embeddedFonts` to `renderSvg`; the export carries the font bytes and supplied license metadata. In a running browser canvas the registered fonts are already available, so embedding those bytes into every draft is unnecessary.\n\n```js\nimport { renderSvg } from '@openpresentation/opf-render/svg';\nconst svg = renderSvg(canvas.editor.document, {\n textMeasurement: fonts.textMeasurement,\n embeddedFonts: fonts.embeddedFonts,\n});\n```\n\nThe explicit `/svg` entry is browser safe. Browser-aware bundlers also select it for the renderer's root import. The Node root entry additionally supplies `svgToPng` and `svgToPdf`; those functions are not browser APIs.\n\n## Editing behavior\n\n| Content or action | Current behavior |\n| --- | --- |\n| Titles, subtitles, plain text, simple numeric values | Double-click or focus and press Enter/Space to edit on the slide. |\n| Table headers and string/number cells | Inline editing; numeric cells keep their numeric type. |\n| Lists, charts, metrics, quotes, code, timelines, rich text payloads | Select the object and edit its existing scalar fields in a floating form; valid drafts render immediately. |\n| Images | Edit source/alt fields; replace with a local PNG/JPEG/GIF/WebP file up to 20 MB. External sources still require a host image resolver. |\n| Collections | Add or remove the last item, subject to OPF schema validation. Empty structured collections may need authoring through source. |\n| Dynamic layout | Text edits recompose the slide through shared geometry; row/column/grid controls remain in the demo inspector. |\n| Undo and cancellation | Blur or Ctrl/Cmd+Enter commits plain text; Escape cancels; property forms have Apply/Cancel. |\n| Changes elsewhere | Unrelated edits are preserved; a changed selected payload cancels the stale local draft instead of overwriting it. This is conflict protection, not a distributed collaboration protocol. |\n| JSON editing | The demo Source view previews valid JSON beside the source; Apply records the document replacement. Invalid drafts retain the last valid preview. |\n\n`createCanvasEditor` accepts an existing `editor` session or a `document`, plus `slideIndex`, `renderOptions`, an optional empty `propertiesContainer` to dock forms outside the slide, and callbacks `onSelect`, `onDraft`, `onCommit`, `onCancel`, `onRender`, and `onError`. The returned object exposes `editor`, `ready`, `select`, `beginEdit`, `editProperties`, `commit`, `cancel`, `setSlide`, `setRenderOptions`, `setLayoutEditing`, `render`, and `destroy`. `commit()` and setters return false if a draft cannot be committed. Avoid using public `render(document)` as a second source of truth; normal document changes should flow through the session.\n\n## Fidelity contract and remaining work\n\nThe same document, renderer version, dimensions, font bytes, and measurement provider produce the same SVG geometry in read and edit modes. Inline editing retains the actual SVG glyphs beneath a transparent native input; the input supplies the caret and selection. Browser regression checks compare draft text positions to standalone SVG rendering.\n\nThat is not a promise of identical raster pixels across browser engines, operating systems, or PowerPoint. Native caret/selection wrapping can differ from shaped SVG text, especially for mixed scripts, rich text, or unusual font features. Browser anti-aliasing and native PowerPoint typography also differ. Without a measurement provider the renderer uses deterministic estimates, which are not sufficient for a high-fidelity claim.\n\nStill needed for the requested complete editor:\n\n1. Continuous mixed-style typing and calibrated caret positioning, bidi/IME/vertical-script coverage. Rich text selection, formatting, links, and selected-text replacement are available through the [SVG formatting toolbar and range API](rich-text.md).\n2. More placement constraints and specialized interactions for fixed promoted regions and individual object geometry. **Add content** and **Arrange** already support the insertion, duplication and deletion described below, track resizing, sibling block dragging, and moving complete blocks between existing groups or slides.\n3. Full visual implementations for specialized charts, media playback, image crops/effects, theme chrome, and every catalog preset. Generic property editing does not imply complete renderer support.\n4. Approved screenshot baselines across representative fonts/layouts/browsers, vertical metric tests, and native PPTX comparison/embedding work.\n5. Broader font-family/script coverage and independently loadable font packs; the current base and Office substitute packs do not cover every requested font. Published packages, documentation examples and installed-package browser CI already exist.\n\nGoogle Fonts supports browser loading through its CSS API, and its repository permits self-hosting subject to each font's license. The OPF fidelity path uses pinned files for reproducibility instead of depending on whichever variant a hosted stylesheet returns. Keep the font's accompanying license. Sources: [Google Fonts CSS API](https://developers.google.com/fonts/docs/css2), [Google Fonts files and licenses](https://github.com/google/fonts/blob/main/README.md).\n\nThe [font roadmap](plans/font-roadmap.md) covers the starter Office substitutes and remaining families.\n\n## Verification\n\n`pnpm demo:editor` builds the playground and `/canvas-tests.html`. The browser harness exercises real font registration, live drafts, text-position parity, one-step undo, cancellation, external edit conflicts, number validation, table cells, structured payloads, collection changes, and cleanup. Node tests cover escaped field paths, typed values, immutable drafts, font loader failures and aborts. The renderer's 126-deck corpus and the coordinated CI's pinned furniture PNG baseline are separate checks. Current installed-package and browser results are recorded in the [compatibility matrix](compatibility-matrix.md); neither those checks nor historical rasters establish general native Office parity.\n\n## Copy, paste, files, and galleries\n\nThe demo's **Copy OPF** dialog exports the whole presentation, the current slide with its design/catalogs/assets, or the selected JSON value. Choose readable JSON, compact JSON, or a Markdown code block for an LLM. The slide toolbar and selection inspector offer direct shortcuts. If clipboard permission is unavailable, **Select all** provides a manual copy fallback.\n\n**Add OPF** accepts a document, one slide, a slide array, a JSON value, or a single fenced JSON/OPF block. Paste into its text box, choose a `.opf`/`.json` file, drop a file on the editor, or load a public JSON URL. Preview first, then insert after the current slide, open a presentation, or replace selected content. Imports are validated and create one undo step. Normal copy/paste inside text fields remains native. Outside text fields, Cmd/Ctrl+V opens import review; Cmd/Ctrl+Shift+C opens Copy OPF; Cmd/Ctrl+O opens file import.\n\n**Browse galleries** includes 854 examples generated from the sibling PPTX.gallery checkout and a separate live PPTX.gallery registry. Search by name, category, or description. Select an entry to preview, copy its OPF, or insert it. **Manage galleries** adds/removes custom registry URLs; custom sources persist in this browser's local storage. Host defaults are defined in `opf-editor/examples/galleries.json`. The bundled snapshot is regenerated by `pnpm demo:editor`; it does not update in the background. Some presets are minimal definition examples rather than completed presentation slides.\n\nPublic PPTX.gallery detail links for layouts, colors, typography, themes, charts, backgrounds, narratives, blocks, and image treatments can be entered in the URL tab. Other sites should expose a direct OPF document or a registry JSON endpoint. Cross-origin servers must enable CORS. Requests omit credentials and referrers, are cancelable, and cap responses at 20 MB. A registry item's URL must stay on the configured origin; explicitly load another origin's URL when intended. The editor does not scrape arbitrary HTML pages or automatically load external fonts/catalog sources.\n\nA custom registry can mix inline OPF and relative document URLs:\n\n```json\n{\n \"name\": \"Team slides\",\n \"items\": [\n { \"id\": \"intro\", \"name\": \"Introduction\", \"category\": \"Team\", \"opf\": { \"slides\": [{ \"title\": \"Hello\" }] } },\n { \"id\": \"metrics\", \"name\": \"Metrics\", \"opfUrl\": \"./metrics.opf.json\" }\n ]\n}\n```\n\nImported documents should contain their required inline catalog records and assets. Inserting namespaces catalog IDs and conflicting asset/slide IDs, preserves the source slides' main design defaults, and leaves existing slides intact. It does not merge presentation-level speakers, organizations, or narrative metadata into the current deck. Open as a presentation to retain the complete source document. Conflicting or unresolved external catalog sources require a self-contained document before insertion.\n\nThe reusable npm APIs are browser-safe and independent of the demo UI:\n\n```js\nimport { parseOpfTransfer, serializeOpfTransfer, prepareOpfImport } from '@openpresentation/opf-editor/transfer';\nimport { loadOpfGallery, loadOpfGalleryItem } from '@openpresentation/opf-editor/galleries';\n\nconst markdown = serializeOpfTransfer(editor.document, {\n scope: 'slide', slideIndex: 0, format: 'markdown',\n});\nconst parsed = parseOpfTransfer(markdown);\nconst result = prepareOpfImport(editor.document, parsed, {\n mode: 'insert', slideIndex: 0,\n});\n// Host previews result.document before applying this single undoable change.\neditor.applyPatch([{ op: 'replace', path: '', value: result.document }], {\n source: 'import', rejectInvalid: true,\n});\n\nconst gallery = await loadOpfGallery('https://example.com/registry.json');\nconst document = await loadOpfGalleryItem(gallery.items[0], { gallery: gallery.url });\n```\n\nBoth gallery functions accept an `AbortSignal` and an injected `fetch` for host integrations and tests. Import/copy tests cover format round trips, invalid inputs, conflicting IDs and references, source isolation, and one-step undo; the generated 854-example snapshot is checked through insertion and SVG rendering.\n\n## All OPF properties\n\n**All properties** opens the schema-driven workspace beside a live SVG preview. Use Presentation, Current slide, Selection, or Design to navigate; add optional fields, select structured value forms, edit arrays/maps, and Apply a validated change with one undo step. Click content in the preview to locate its field. Nonvisual metadata remains part of the OPF document. A dirty draft must be applied or discarded before closing.\n\nThe `/schema` and `/schema-inspector` npm exports provide the reusable model and DOM inspector. `createSchemaInspector(container, {editor, path, onDraft})` exposes `navigate`, `commit`, `reset`, `destroy`, and read-only `document`/`dirty` getters. Use `onDraft` to render valid previews. The companion gallery `/spec` reference indexes the same 604 property definitions, and `/editor` embeds the shared browser build.\n\nSee [spec coverage](plans/spec-editor-coverage.md) for the distinction between complete field discovery and the remaining WYSIWYG rendering work.\n\n## Create, duplicate and delete content\n\nUse **Add content** in the editor toolbar, or the canvas button in Arrange mode. Choose a content kind, destination and insertion position. Starter content covers text, lists, charts, tables, metrics, quotes, code, timelines and groups. Image insertion accepts a local PNG/JPEG/WebP file or a source; local files are embedded as data URLs. Video insertion stores a source, but playback and native video export remain separate work. Source URLs and asset references still need the host's supported asset-resolution behavior.\n\nArrange handles also offer **Duplicate**, **Delete**, **Add after** and, for groups, **Add inside**. Duplication copies the complete content subtree while retaining asset references. Deletion prunes empty ancestor groups or their named region, preserving the slide and its metadata. Removing the last root block leaves a valid empty slide. Every operation preflights the complete candidate with the shared renderer and commits one undo step; stale forms are dismissed and strict overflow fails before mutation.\n\nAdding to implicit root content or a named-region leaf converts existing payloads into explicit blocks in the renderer's canonical field order. Headings, notes, design, metadata and neighboring regions stay intact. Named regions are kept in their existing positions; choose one as the destination. Existing composition weights remain attached to positions, so insertion/deletion can change which content occupies a weighted slot.\n\n```js\nimport {\n prepareBlockInsert, prepareBlockDuplicate, prepareBlockRemove, createContentBlock,\n listBlockContainers,\n} from '@openpresentation/opf-editor/layout';\nconst containers = listBlockContainers(editor.document, {includeImplicit: true});\nconst prepared = prepareBlockInsert(editor.document, containers[0].path,\n createContentBlock('table')); // omit index to append\n// Render prepared.document with your intended fonts before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n// Duplicate/remove take complete paths such as /slides/0/blocks/1.\n// canvas.openInsertMenu(containerPath?, index?) opens the browser palette.\n```\n\nThe headless helpers return `{document, patches, path, changed}` and include expected-value guards. Preserve those guards when applying patches. They need no browser, AI provider, account or hosted service. These helpers are published in editor 0.8.0; use the coordinated versions above and check installed exports when working with older packages. `/create-tests.html` and its installed-package equivalent exercise creation, image bytes, regions, duplication, deletion, strict-fit rejection, keyboard focus and undo.\n"
169
+ "markdown": "# Browser preview and live editing\n\nPublished editor 0.8.0 provides an embeddable SVG canvas in `@openpresentation/opf-editor/canvas`. OPF JSON remains the document; the canvas writes validated JSON Patch operations through an `EditorSession`. Draft edits render with the same SVG engine used for standalone previews. Completed edits produce one undoable change.\n\nThe published canvas covers the interactions below; complete PowerPoint feature coverage remains separate work. \u201CPixel perfect\u201D is a fidelity target with specific prerequisites and remaining gaps described below.\n\n## Install the published packages\n\nUse Node 24 with core 0.11.0, renderer 0.9.0, editor 0.8.0 and PPTX 0.9.1:\n\n```sh\nnpm install --save-exact @openpresentation/opf@0.11.0 @openpresentation/opf-render@0.9.0 @openpresentation/opf-editor@0.8.0 @openpresentation/opf-pptx@0.9.1\n```\n\nNo paid service or provider account is required. The six agent skills install with `npx @openpresentation/cli@0.9.0 skills install`. See the [quickstart](quickstart.md) for an installed-package workflow and the [compatibility matrix](compatibility-matrix.md) for separately scoped browser and native evidence.\n\nFor library development, separately regenerate unpublished local preview tarballs from sibling checkouts:\n\n```sh\npnpm build\nnode scripts/link-ecosystem.mjs\npnpm pack:ecosystem\npnpm test:packed-ecosystem\n```\n\nThe packed consumer installs actual tarballs without workspace aliases, exercises editing/SVG/PPTX, checks TypeScript declarations, and bundles a browser entry without Node shims. For a public release, advance source versions and downstream minimums/lockfiles together and follow the release process.\n\nThe gallery host example also offers local PPTX file import with preview/diagnostics and editable PowerPoint download. It commits active canvas text before export, shares preview text measurements and applies imports as a single undoable change. Save OPF to preserve the original source; native PowerPoint positions, fonts and unsupported features can change during conversion. The browser E2E checks run offline after loading and inspect the downloaded native merged table, then reimport and undo/redo. Native edit/save/reopen is a separate targeted check, not a pixel-equivalence claim.\n\n`pnpm prepare:gallery:registry` builds host controls from the immutable `exampleRefs.opf-editor` in `release-plan.json` while resolving libraries only from the fresh npm consumer. Package `verificationRefs` continue to point at actual published releases. The gallery manifest records both the example source hashes and registry package integrities. Updating example controls does not imply a new editor library release.\n\nScript fonts: when the pinned editor example loads faces from `./script-fonts/` and the pinned renderer has the script pack (0.10.0 and later), the registry build also writes `script-fonts.json` and lists its hash in `manifest.json`. The manifest is the reviewable half: every `@expo-google-fonts/noto-*` package, exact version, SPDX license, license-file hash and each face's SHA-256, taken from the published renderer. The faces are binaries (63 files, 66.9 MiB), so they are never committed to the gallery repository. The gallery build copies them from its own pinned npm dependencies into the untracked `public/opf-editor/script-fonts/` directory, verifying every hash, and writes the license notices beside them; nothing is fetched from a font CDN. See `scripts/gallery-script-fonts.mjs` and the gallery's `scripts/prepare-editor-script-fonts.mjs`.\n\nThe browser bundle links `playground.js.LEGAL.txt`, included in the hashed resources. It contains bundled license notices and package license files, including the vendored PptxGenJS MIT license. For dependencies that publish only an explicit MIT declaration in their README, the build retains that declaration/attribution and the standard terms; omitted upstream notices use a version-specific source URL and verified supplement hash. License collection runs offline from the verified installation and committed supplement. Runtime JavaScript is not rewritten to normalize comment whitespace.\n\n## Embed in any browser application\n\nMount after the host DOM exists. The container controls width; the slide retains its aspect ratio. React and Svelte applications can mount this framework-independent API in their normal client lifecycle and destroy it on unmount.\n\n```js\nimport { createCanvasEditor } from '@openpresentation/opf-editor/canvas';\nimport { loadBrowserFontRegistry } from '@openpresentation/opf-render/fonts-browser';\n\n// Copy these licensed font files into your application's static assets first.\n// Use pinned, static faces; include every weight/style required by your deck.\nconst fonts = await loadBrowserFontRegistry([\n { url: '/fonts/Roboto-Regular.ttf', family: 'Roboto', weight: 400 },\n { url: '/fonts/Roboto-Bold.ttf', family: 'Roboto', weight: 700 },\n { url: '/fonts/RobotoMono-Regular.ttf', family: 'Roboto Mono', weight: 400 },\n]);\n\nconst canvas = createCanvasEditor(document.querySelector('#slide'), {\n document: {\n design: { theme: 'classic', fontScheme: 'roboto' },\n slides: [{ title: 'An editable presentation', text: 'Double-click to edit.' }],\n },\n renderOptions: { textMeasurement: fonts.textMeasurement },\n onCommit: ({ editor }) => {\n const updatedOPF = editor.document; // Host owns saving and collaboration.\n console.log(updatedOPF);\n },\n onError: error => console.error(error.message),\n});\nawait canvas.ready;\n\n// JSON or LLM patches also update the slide automatically.\ncanvas.editor.set('slides.0.title', 'Changes from another control');\ncanvas.editor.undo();\n\n// On unmount:\n// canvas.destroy();\n// fonts.dispose();\n```\n\n`loadBrowserFontRegistry` accepts explicit font-file URLs or `Uint8Array` data. It uses the same bytes for Fontkit measurement and browser `FontFace` registration, awaits loading, reports failures, and exposes `dispose()` for its owned font faces. Cross-origin font URLs need CORS access. Load fonts once and share the registry between canvases. The canvas does not fetch fonts or catalog sources itself.\n\nFor standalone SVG export, pass `fonts.embeddedFonts` to `renderSvg`; the export carries the font bytes and supplied license metadata. In a running browser canvas the registered fonts are already available, so embedding those bytes into every draft is unnecessary.\n\n```js\nimport { renderSvg } from '@openpresentation/opf-render/svg';\nconst svg = renderSvg(canvas.editor.document, {\n textMeasurement: fonts.textMeasurement,\n embeddedFonts: fonts.embeddedFonts,\n});\n```\n\nThe explicit `/svg` entry is browser safe. Browser-aware bundlers also select it for the renderer's root import. The Node root entry additionally supplies `svgToPng` and `svgToPdf`; those functions are not browser APIs.\n\n## Editing behavior\n\n| Content or action | Current behavior |\n| --- | --- |\n| Titles, subtitles, plain text, simple numeric values | Double-click or focus and press Enter/Space to edit on the slide. |\n| Table headers and string/number cells | Inline editing; numeric cells keep their numeric type. |\n| Lists, charts, metrics, quotes, code, timelines, rich text payloads | Select the object and edit its existing scalar fields in a floating form; valid drafts render immediately. |\n| Images | Edit source/alt fields; replace with a local PNG/JPEG/GIF/WebP file up to 20 MB. External sources still require a host image resolver. |\n| Collections | Add or remove the last item, subject to OPF schema validation. Empty structured collections may need authoring through source. |\n| Dynamic layout | Text edits recompose the slide through shared geometry; row/column/grid controls remain in the demo inspector. |\n| Undo and cancellation | Blur or Ctrl/Cmd+Enter commits plain text; Escape cancels; property forms have Apply/Cancel. |\n| Changes elsewhere | Unrelated edits are preserved; a changed selected payload cancels the stale local draft instead of overwriting it. This is conflict protection, not a distributed collaboration protocol. |\n| JSON editing | The demo Source view previews valid JSON beside the source; Apply records the document replacement. Invalid drafts retain the last valid preview. |\n\n`createCanvasEditor` accepts an existing `editor` session or a `document`, plus `slideIndex`, `renderOptions`, an optional empty `propertiesContainer` to dock forms outside the slide, and callbacks `onSelect`, `onDraft`, `onCommit`, `onCancel`, `onRender`, and `onError`. The returned object exposes `editor`, `ready`, `select`, `beginEdit`, `editProperties`, `commit`, `cancel`, `setSlide`, `setRenderOptions`, `setLayoutEditing`, `render`, and `destroy`. `commit()` and setters return false if a draft cannot be committed. Avoid using public `render(document)` as a second source of truth; normal document changes should flow through the session.\n\n## Fidelity contract and remaining work\n\nThe same document, renderer version, dimensions, font bytes, and measurement provider produce the same SVG geometry in read and edit modes. Inline editing retains the actual SVG glyphs beneath a transparent native input; the input supplies the caret and selection. Browser regression checks compare draft text positions to standalone SVG rendering.\n\nThat is not a promise of identical raster pixels across browser engines, operating systems, or PowerPoint. Native caret/selection wrapping can differ from shaped SVG text, especially for mixed scripts, rich text, or unusual font features. Browser anti-aliasing and native PowerPoint typography also differ. Without a measurement provider the renderer uses deterministic estimates, which are not sufficient for a high-fidelity claim.\n\nStill needed for the requested complete editor:\n\n1. Continuous mixed-style typing and calibrated caret positioning, bidi/IME/vertical-script coverage. Rich text selection, formatting, links, and selected-text replacement are available through the [SVG formatting toolbar and range API](rich-text.md).\n2. More placement constraints and specialized interactions for fixed promoted regions and individual object geometry. **Add content** and **Arrange** already support the insertion, duplication and deletion described below, track resizing, sibling block dragging, and moving complete blocks between existing groups or slides.\n3. Full visual implementations for specialized charts, media playback, image crops/effects, theme chrome, and every catalog preset. Generic property editing does not imply complete renderer support.\n4. Approved screenshot baselines across representative fonts/layouts/browsers, vertical metric tests, and native PPTX comparison/embedding work.\n5. Broader font-family/script coverage and independently loadable font packs; the current base and Office substitute packs do not cover every requested font. Published packages, documentation examples and installed-package browser CI already exist.\n\nGoogle Fonts supports browser loading through its CSS API, and its repository permits self-hosting subject to each font's license. The OPF fidelity path uses pinned files for reproducibility instead of depending on whichever variant a hosted stylesheet returns. Keep the font's accompanying license. Sources: [Google Fonts CSS API](https://developers.google.com/fonts/docs/css2), [Google Fonts files and licenses](https://github.com/google/fonts/blob/main/README.md).\n\nThe [font roadmap](plans/font-roadmap.md) covers the starter Office substitutes and remaining families.\n\n## Verification\n\n`pnpm demo:editor` builds the playground and `/canvas-tests.html`. The browser harness exercises real font registration, live drafts, text-position parity, one-step undo, cancellation, external edit conflicts, number validation, table cells, structured payloads, collection changes, and cleanup. Node tests cover escaped field paths, typed values, immutable drafts, font loader failures and aborts. The renderer's 126-deck corpus and the coordinated CI's pinned furniture PNG baseline are separate checks. Current installed-package and browser results are recorded in the [compatibility matrix](compatibility-matrix.md); neither those checks nor historical rasters establish general native Office parity.\n\n## Copy, paste, files, and galleries\n\nThe demo's **Copy OPF** dialog exports the whole presentation, the current slide with its design/catalogs/assets, or the selected JSON value. Choose readable JSON, compact JSON, or a Markdown code block for an LLM. The slide toolbar and selection inspector offer direct shortcuts. If clipboard permission is unavailable, **Select all** provides a manual copy fallback.\n\n**Add OPF** accepts a document, one slide, a slide array, a JSON value, or a single fenced JSON/OPF block. Paste into its text box, choose a `.opf`/`.json` file, drop a file on the editor, or load a public JSON URL. Preview first, then insert after the current slide, open a presentation, or replace selected content. Imports are validated and create one undo step. Normal copy/paste inside text fields remains native. Outside text fields, Cmd/Ctrl+V opens import review; Cmd/Ctrl+Shift+C opens Copy OPF; Cmd/Ctrl+O opens file import.\n\n**Browse galleries** includes 854 examples generated from the sibling PPTX.gallery checkout and a separate live PPTX.gallery registry. Search by name, category, or description. Select an entry to preview, copy its OPF, or insert it. **Manage galleries** adds/removes custom registry URLs; custom sources persist in this browser's local storage. Host defaults are defined in `opf-editor/examples/galleries.json`. The bundled snapshot is regenerated by `pnpm demo:editor`; it does not update in the background. Some presets are minimal definition examples rather than completed presentation slides.\n\nPublic PPTX.gallery detail links for layouts, colors, typography, themes, charts, backgrounds, narratives, blocks, and image treatments can be entered in the URL tab. Other sites should expose a direct OPF document or a registry JSON endpoint. Cross-origin servers must enable CORS. Requests omit credentials and referrers, are cancelable, and cap responses at 20 MB. A registry item's URL must stay on the configured origin; explicitly load another origin's URL when intended. The editor does not scrape arbitrary HTML pages or automatically load external fonts/catalog sources.\n\nA custom registry can mix inline OPF and relative document URLs:\n\n```json\n{\n \"name\": \"Team slides\",\n \"items\": [\n { \"id\": \"intro\", \"name\": \"Introduction\", \"category\": \"Team\", \"opf\": { \"slides\": [{ \"title\": \"Hello\" }] } },\n { \"id\": \"metrics\", \"name\": \"Metrics\", \"opfUrl\": \"./metrics.opf.json\" }\n ]\n}\n```\n\nImported documents should contain their required inline catalog records and assets. Inserting namespaces catalog IDs and conflicting asset/slide IDs, preserves the source slides' main design defaults, and leaves existing slides intact. It does not merge presentation-level speakers, organizations, or narrative metadata into the current deck. Open as a presentation to retain the complete source document. Conflicting or unresolved external catalog sources require a self-contained document before insertion.\n\nThe reusable npm APIs are browser-safe and independent of the demo UI:\n\n```js\nimport { parseOpfTransfer, serializeOpfTransfer, prepareOpfImport } from '@openpresentation/opf-editor/transfer';\nimport { loadOpfGallery, loadOpfGalleryItem } from '@openpresentation/opf-editor/galleries';\n\nconst markdown = serializeOpfTransfer(editor.document, {\n scope: 'slide', slideIndex: 0, format: 'markdown',\n});\nconst parsed = parseOpfTransfer(markdown);\nconst result = prepareOpfImport(editor.document, parsed, {\n mode: 'insert', slideIndex: 0,\n});\n// Host previews result.document before applying this single undoable change.\neditor.applyPatch([{ op: 'replace', path: '', value: result.document }], {\n source: 'import', rejectInvalid: true,\n});\n\nconst gallery = await loadOpfGallery('https://example.com/registry.json');\nconst document = await loadOpfGalleryItem(gallery.items[0], { gallery: gallery.url });\n```\n\nBoth gallery functions accept an `AbortSignal` and an injected `fetch` for host integrations and tests. Import/copy tests cover format round trips, invalid inputs, conflicting IDs and references, source isolation, and one-step undo; the generated 854-example snapshot is checked through insertion and SVG rendering.\n\n## All OPF properties\n\n**All properties** opens the schema-driven workspace beside a live SVG preview. Use Presentation, Current slide, Selection, or Design to navigate; add optional fields, select structured value forms, edit arrays/maps, and Apply a validated change with one undo step. Click content in the preview to locate its field. Nonvisual metadata remains part of the OPF document. A dirty draft must be applied or discarded before closing.\n\nThe `/schema` and `/schema-inspector` npm exports provide the reusable model and DOM inspector. `createSchemaInspector(container, {editor, path, onDraft})` exposes `navigate`, `commit`, `reset`, `destroy`, and read-only `document`/`dirty` getters. Use `onDraft` to render valid previews. The companion gallery `/spec` reference indexes the same 604 property definitions, and `/editor` embeds the shared browser build.\n\nSee [spec coverage](plans/spec-editor-coverage.md) for the distinction between complete field discovery and the remaining WYSIWYG rendering work.\n\n## Create, duplicate and delete content\n\nUse **Add content** in the editor toolbar, or the canvas button in Arrange mode. Choose a content kind, destination and insertion position. Starter content covers text, lists, charts, tables, metrics, quotes, code, timelines and groups. Image insertion accepts a local PNG/JPEG/WebP file or a source; local files are embedded as data URLs. Video insertion stores a source, but playback and native video export remain separate work. Source URLs and asset references still need the host's supported asset-resolution behavior.\n\nArrange handles also offer **Duplicate**, **Delete**, **Add after** and, for groups, **Add inside**. Duplication copies the complete content subtree while retaining asset references. Deletion prunes empty ancestor groups or their named region, preserving the slide and its metadata. Removing the last root block leaves a valid empty slide. Every operation preflights the complete candidate with the shared renderer and commits one undo step; stale forms are dismissed and strict overflow fails before mutation.\n\nAdding to implicit root content or a named-region leaf converts existing payloads into explicit blocks in the renderer's canonical field order. Headings, notes, design, metadata and neighboring regions stay intact. Named regions are kept in their existing positions; choose one as the destination. Existing composition weights remain attached to positions, so insertion/deletion can change which content occupies a weighted slot.\n\n```js\nimport {\n prepareBlockInsert, prepareBlockDuplicate, prepareBlockRemove, createContentBlock,\n listBlockContainers,\n} from '@openpresentation/opf-editor/layout';\nconst containers = listBlockContainers(editor.document, {includeImplicit: true});\nconst prepared = prepareBlockInsert(editor.document, containers[0].path,\n createContentBlock('table')); // omit index to append\n// Render prepared.document with your intended fonts before applying.\neditor.applyPatch(prepared.patches, {rejectInvalid: true});\n// Duplicate/remove take complete paths such as /slides/0/blocks/1.\n// canvas.openInsertMenu(containerPath?, index?) opens the browser palette.\n```\n\nThe headless helpers return `{document, patches, path, changed}` and include expected-value guards. Preserve those guards when applying patches. They need no browser, AI provider, account or hosted service. These helpers are published in editor 0.8.0; use the coordinated versions above and check installed exports when working with older packages. `/create-tests.html` and its installed-package equivalent exercise creation, image bytes, regions, duplication, deletion, strict-fit rejection, keyboard focus and undo.\n"
170
170
  },
171
171
  {
172
172
  "slug": "llm-authoring",
173
173
  "file": "docs/llm-authoring.md",
174
174
  "title": "Authoring OPF with an LLM",
175
- "markdown": '# Authoring OPF with an LLM\n\nWrite a complete JSON document with `name` and `slides`. Put visible words in slide fields, not presentation metadata. Use `*.opf.json` filenames and stable, unique slide `id` values when a deck will be revised repeatedly.\n\n```json\n{\n "name": "Launch decision",\n "slides": [{\n "id": "recommendation",\n "title": "Launch to the pilot group first",\n "composition": { "mode": "row", "weights": [2, 1] },\n "blocks": [\n { "items": ["Validate onboarding", "Measure activation", "Fix the largest drop-off"] },\n { "metric": { "value": 200, "label": "Pilot customers" } }\n ],\n "notes": "Confirm the rollout owner and checkpoint date."\n }]\n}\n```\n\nChoose one content structure per slide:\n\n- Root payloads for a simple slide: `text`, `items`, `image`, `chart`, `table`, `code`, `metric`, `quote`, or `timeline`.\n- `blocks` for a sequence that should reflow. Set `composition` only when an arrangement matters. Omit it to let the engine choose.\n- Promoted regions such as `left`, `center+right`, `top`, and `bottom` for spatially meaningful content. Regions must not overlap. Do not mix regions with root payloads or blocks.\n\nUse catalog IDs from the installed package or supply inline records in `catalogs`. A gallery route is a stable identifier, but an extended gallery layout may need the inline record included in the copied document. Do not invent an unresolvable layout or assume a network lookup will happen.\n\nTables use `{ "columns": ["Category", "Value"], "rows": [["A", 10]] }`. Charts put a `type` and the same tabular structure inside `chart.data`. Images use a source string or `{ "src": "...", "alt": "..." }`; use the top-level `assets` registry and `asset:<id>` references for reuse. The local renderer does not fetch remote sources.\n\n## Revision loop\n\n1. Validate with `validatePresentation`. Fix errors at their returned JSON paths. Check warnings for unknown catalog IDs.\n2. Render with `onDiagnostic` and inspect `text-overflow` / `small-cell` paths. Shorten text, reduce the number of blocks, change composition, or explicitly split the slide. Revalidate after edits.\n3. Use `composition.overflow: "error"` for a strict text-layout gate. It does not certify chart readability, font availability, or exact PowerPoint rendering.\n4. Inspect the actual preview and exported PPTX. Geometry is shared; font substitution and specialized objects can still differ. Previews draw an open look-alike where the license-restricted font cannot be bundled (metric-compatible where one exists, for example Carlito for Calibri; visual-only for Aptos today), but the exported PPTX keeps the font name the user selected.\n5. Apply focused JSON Patch edits through the editor session and retain undo history. Resolve stable slide IDs to current array indices before constructing patches; indices can change when slides are inserted or moved.\n\nPreserve factual content, sources, notes, and asset descriptions during layout repair. A fit diagnostic is a request to revise the slide; it is not permission to silently drop the end of a paragraph.\n\nSee [dynamic composition](dynamic-composition.md), [content payloads](content-payloads.md), and [design precedence](design-resolution.md).\n\nUse nested `blocks` to keep related content together. Put `composition` on the group to arrange its children, for example a column of evidence inside a row of sections. Read `composeSlide().groups` for group bounds and `items[].path` for precise leaf edits. Groups inherit readability constraints; splitting content into more levels does not make text smaller.\n\nUse `paginatePresentation(deck)` when a draft exceeds readable space. Review the returned ordinary OPF slides and source mappings before export. Pagination preserves source text exactly; it does not summarize or rewrite it. An atomic item that cannot fit produces a diagnostic for a targeted edit.\n'
175
+ "markdown": '# Authoring OPF with an LLM\n\nWrite a complete JSON document with `name` and `slides`. Put visible words in slide fields, not presentation metadata. Use `*.opf.json` filenames and stable, unique slide `id` values when a deck will be revised repeatedly.\n\n```json\n{\n "name": "Launch decision",\n "slides": [{\n "id": "recommendation",\n "title": "Launch to the pilot group first",\n "composition": { "mode": "row", "weights": [2, 1] },\n "blocks": [\n { "items": ["Validate onboarding", "Measure activation", "Fix the largest drop-off"] },\n { "metric": { "value": 200, "label": "Pilot customers" } }\n ],\n "notes": "Confirm the rollout owner and checkpoint date."\n }]\n}\n```\n\nChoose one content structure per slide:\n\n- Root payloads for a simple slide: `text`, `items`, `image`, `chart`, `table`, `code`, `metric`, `quote`, or `timeline`.\n- `blocks` for a sequence that should reflow. Set `composition` only when an arrangement matters. Omit it to let the engine choose.\n- Promoted regions such as `left`, `center+right`, `top`, and `bottom` for spatially meaningful content. Regions must not overlap. Do not mix regions with root payloads or blocks.\n\nUse catalog IDs from the installed package or supply inline records in `catalogs`. A gallery route is a stable identifier, but an extended gallery layout may need the inline record included in the copied document. Do not invent an unresolvable layout or assume a network lookup will happen.\n\nTables use `{ "columns": ["Category", "Value"], "rows": [["A", 10]] }`. Charts put a `type` and the same tabular structure inside `chart.data`. Images use a source string or `{ "src": "...", "alt": "..." }`; use the top-level `assets` registry and `asset:<id>` references for reuse. The local renderer does not fetch remote sources.\n\n## Revision loop\n\n1. Validate with `validatePresentation`. Fix errors at their returned JSON paths. Check warnings for unknown catalog IDs.\n2. Render with `onDiagnostic` and inspect `text-overflow` / `small-cell` paths. Shorten text, reduce the number of blocks, change composition, or explicitly split the slide. Revalidate after edits.\n3. Use `composition.overflow: "error"` for a strict text-layout gate. It does not certify chart readability, font availability, or exact PowerPoint rendering.\n4. Inspect the actual preview and exported PPTX. Geometry is shared; font substitution and specialized objects can still differ. Previews draw an open look-alike where the license-restricted font cannot be bundled (metric-compatible where one exists, for example Carlito for Calibri; Intos for Aptos), but the exported PPTX keeps the font name the user selected.\n5. Apply focused JSON Patch edits through the editor session and retain undo history. Resolve stable slide IDs to current array indices before constructing patches; indices can change when slides are inserted or moved.\n\nPreserve factual content, sources, notes, and asset descriptions during layout repair. A fit diagnostic is a request to revise the slide; it is not permission to silently drop the end of a paragraph.\n\nSee [dynamic composition](dynamic-composition.md), [content payloads](content-payloads.md), and [design precedence](design-resolution.md).\n\nUse nested `blocks` to keep related content together. Put `composition` on the group to arrange its children, for example a column of evidence inside a row of sections. Read `composeSlide().groups` for group bounds and `items[].path` for precise leaf edits. Groups inherit readability constraints; splitting content into more levels does not make text smaller.\n\nUse `paginatePresentation(deck)` when a draft exceeds readable space. Review the returned ordinary OPF slides and source mappings before export. Pagination preserves source text exactly; it does not summarize or rewrite it. An atomic item that cannot fit produces a diagnostic for a targeted edit.\n'
176
176
  },
177
177
  {
178
178
  "slug": "native-content-release-2026-09-09",
@@ -214,7 +214,7 @@ var docsData = Object.freeze([
214
214
  "slug": "release-process",
215
215
  "file": "docs/release-process.md",
216
216
  "title": "OPF Release Process",
217
- "markdown": "# OPF Release Process\n\nUse Node 24 (`24.x`) for all future source, candidate and registry verification.\nThe next releases must document the [Node 24 migration](migrations/node24.md)\nand use new versions. Historical dual-runtime release records remain unchanged.\n\nThis document is the release runbook for the public JavaScript package,\n[`@openpresentation/opf`](https://www.npmjs.com/package/@openpresentation/opf).\n\nThe canonical release path is:\n\n1. Merge the release commit to `main`.\n2. Push a semver tag whose name matches the package version.\n3. Let GitHub Actions publish to npm through npm trusted publishing.\n4. Verify npm and the automatically generated GitHub release notes.\n\n## Agent authorization and coordinated release order\n\nThe owner authorized agents to prepare and publish npm releases on 2026-09-29\nand for future releases whenever a release is required. This does not waive any\ngate: a release still needs a merged release-prep PR, green required checks and\nthe verification below. Agents keep publishing on the trusted-publishing\nworkflows (GitHub Actions OIDC with `--provenance`); a local `npm publish` is a\nfallback only when a workflow cannot run, and it loses the provenance\nattestation that every previous version carries.\n\nThe engine packages depend on each other, so publish in this order and wait for\neach version to appear on the registry before starting the next:\n\n1. `@openpresentation/opf` (this repository, `opf-vX.Y.Z` tag).\n2. `@openpresentation/opf-render` (`opf-render-vX.Y.Z` tag) and\n `@openpresentation/opf-pptx` (`opf-pptx-vX.Y.Z` tag). Both depend on core; PPTX\n also devDepends on the renderer, so publish the renderer first.\n3. `@openpresentation/opf-editor` (`opf-editor-vX.Y.Z` tag), which depends on core\n and peers/devDepends on the renderer and PPTX.\n\nEach sibling's release-prep PR raises its dependency floors to the just-published\nversions. Its lockfile can only be refreshed after the upstream version exists on\nnpm (`npm install --package-lock-only`), so merge sibling release PRs only after\nthe upstream publish. `@openpresentation/cli` bundles core and is released\nseparately by `cli-publish.yml` (`cli-vX.Y.Z`) when a fresh bundle is needed.\n\nRelease-prep PRs contain only version bumps, changelog entries, dependency ranges\nand lockfile changes (plus current-instruction docs). After the whole set is on\nthe registry, a follow-up docs change updates `release-plan.json`, the\ncompatibility matrix and the quickstart to the published set, and the gallery\nconsumer dependencies are bumped.\n\n## Release Preconditions\n\nBefore tagging, confirm that the release commit on `main` already contains:\n\n- `packages/javascript/package.json` with the intended version.\n- `CHANGELOG.md` with the matching release section.\n- Passing `OPF CI` on the release commit.\n\nThe publish workflow validates the tag name against\n`packages/javascript/package.json`, so the tag must point at the release commit.\n\n## Tag And Publish\n\nUse the `opf-vX.Y.Z` tag form for the package release:\n\n```sh\ngit checkout main\ngit pull origin main\ngrep '\"version\"' packages/javascript/package.json\ngit tag opf-vX.Y.Z\ngit push origin opf-vX.Y.Z\n```\n\nFor example, version `0.3.0` used:\n\n```sh\ngit tag opf-v0.3.0\ngit push origin opf-v0.3.0\n```\n\nPushing the tag triggers `.github/workflows/npm-publish.yml`. The workflow:\n\n- runs on tags matching `opf-v*` or `@openpresentation/opf@v*`\n- installs dependencies with pnpm on Node 24\n- verifies the tag matches `packages/javascript/package.json`\n- runs typecheck and tests\n- runs the npm package dry-run check\n- publishes from `packages/javascript` with `npm publish --access public --provenance`\n\nDo not rerun a successful publish for the same version. npm package versions are\nimmutable; a second publish for an already-published version should fail.\n\n## Trusted Publishing\n\nnpm publishing is configured to use GitHub Actions OIDC trusted publishing, not\na long-lived npm token.\n\nExpected npm package trusted-publisher settings:\n\n| Setting | Value |\n|---|---|\n| Package | `@openpresentation/opf` |\n| Publisher | GitHub Actions |\n| Organization/repository | `OpenPresentation/opf` |\n| Workflow filename | `npm-publish.yml` |\n| Environment | empty, unless the workflow is later moved behind a GitHub Environment |\n| Permission | `npm publish` |\n\nExpected workflow settings:\n\n```yaml\npermissions:\n contents: read\n id-token: write\n```\n\nThe publish step should not set `NODE_AUTH_TOKEN`:\n\n```yaml\n- name: Publish to npm\n working-directory: packages/javascript\n run: npm publish --access public\n```\n\nIf a future release fails with npm authentication errors, check the npm\ntrusted-publisher settings first. Only use an `NPM_TOKEN` repository secret as a\ntemporary fallback, and remove or revoke it once OIDC publishing works again.\n\n## Verify The Release\n\nAfter the workflow completes, verify npm:\n\n```sh\nnpm view @openpresentation/opf version\n```\n\nThe output should equal the package version that was tagged.\n\nSpot-check the validator API from a clean project or temporary directory:\n\n```sh\nnpm install @openpresentation/opf@X.Y.Z\nnode --input-type=module -e \"import {validatePresentation} from '@openpresentation/opf'; console.log(validatePresentation({name:'t', narrative:'not-a-real-id', slides:[{title:'t'}]}).warnings)\"\n```\n\nThe expected result is one warning about an unknown narratives catalog id.\n\n## GitHub Release Notes\n\nThe core tag workflow creates a GitHub Release from the matching changelog section after publishing. Verify that release after npm is verified. If release creation failed, create the missing release for the existing tag:\n\n```sh\ngh release create opf-vX.Y.Z \\\n --repo OpenPresentation/opf \\\n --title '@openpresentation/opf X.Y.Z' \\\n --notes-file /path/to/release-notes.md\n```\n\nUse the matching `## X.Y.Z` section from `CHANGELOG.md` as the release notes.\n\n## Troubleshooting\n\nIf the tag/version check fails, the tag does not point at the release commit or\nthe tag name does not match `packages/javascript/package.json`. Delete the bad\nlocal and remote tag, fetch `main`, and tag the correct commit:\n\n```sh\ngit push origin :refs/tags/opf-vX.Y.Z\ngit tag -d opf-vX.Y.Z\ngit checkout main\ngit pull origin main\ngit tag opf-vX.Y.Z\ngit push origin opf-vX.Y.Z\n```\n\nIf tests fail, fix the code on `main`, create a new release commit, and move the\ntag only if npm has not already published that version.\n\nIf npm publish fails with `ENEEDAUTH`, confirm:\n\n- npm has a trusted publisher for `OpenPresentation/opf`\n- the trusted publisher uses workflow filename `npm-publish.yml`\n- `.github/workflows/npm-publish.yml` has `id-token: write`\n- the publish job is running on a modern Node/npm toolchain\n\nIf npm publish fails after the version is already present on npm, do not retry\nthe same publish. Verify the package and treat the failure as a duplicate\npublish attempt.\n"
217
+ "markdown": "# OPF Release Process\n\nUse Node 24 (`24.x`) for all future source, candidate and registry verification.\nThe next releases must document the [Node 24 migration](migrations/node24.md)\nand use new versions. Historical dual-runtime release records remain unchanged.\n\nThis document is the release runbook for the public JavaScript package,\n[`@openpresentation/opf`](https://www.npmjs.com/package/@openpresentation/opf).\n\nThe canonical release path is:\n\n1. Merge the release commit to `main`.\n2. Push a semver tag whose name matches the package version.\n3. Let GitHub Actions publish to npm through npm trusted publishing.\n4. Verify npm and the automatically generated GitHub release notes.\n\n## Agent authorization and coordinated release order\n\nThe owner authorized agents to prepare and publish npm releases on 2026-09-29\nand for future releases whenever a release is required. This does not waive any\ngate: a release still needs a merged release-prep PR, green required checks and\nthe verification below. Agents keep publishing on the trusted-publishing\nworkflows (GitHub Actions OIDC with `--provenance`); a local `npm publish` is a\nfallback only when a workflow cannot run, and it loses the provenance\nattestation that every previous version carries.\n\nThe engine packages depend on each other, so publish in this order and wait for\neach version to appear on the registry before starting the next:\n\n1. `@openpresentation/opf` (this repository, `opf-vX.Y.Z` tag).\n2. `@openpresentation/opf-render` (`opf-render-vX.Y.Z` tag) and\n `@openpresentation/opf-pptx` (`opf-pptx-vX.Y.Z` tag). Both depend on core; PPTX\n also devDepends on the renderer, so publish the renderer first.\n3. `@openpresentation/opf-editor` (`opf-editor-vX.Y.Z` tag), which depends on core\n and peers/devDepends on the renderer and PPTX.\n\nEach sibling's release-prep PR raises its dependency floors to the just-published\nversions. Its lockfile can only be refreshed after the upstream version exists on\nnpm (`npm install --package-lock-only`), so merge sibling release PRs only after\nthe upstream publish. `@openpresentation/cli` bundles core and is released\nseparately by `cli-publish.yml` (`cli-vX.Y.Z`) when a fresh bundle is needed.\n\nRelease-prep PRs contain only version bumps, changelog entries, dependency ranges\nand lockfile changes (plus current-instruction docs). After the whole set is on\nthe registry, a follow-up docs change updates `release-plan.json`, the\ncompatibility matrix and the quickstart to the published set, and the gallery\nconsumer dependencies are bumped.\n\n## Geometry-moving core releases: lockstep floors\n\nCore composition changes that move geometry (for example opf#169 cover centering) make the preview and the PPTX export drift when `@openpresentation/opf-render` and `@openpresentation/opf-pptx` resolve different core versions (measured: 186-300 pt title offsets on covers).\n\nRule: when a core release contains composition or geometry changes, the same release train must raise BOTH the renderer's and PPTX's core floor (`dependencies` and, where present, `peerDependencies`) to that core version, publish them together, and raise the editor's floor too. Do not release core alone and leave a sibling on the older floor.\n\nThe parity harness must always run with `--import <opf>/scripts/register-local-opf.mjs` (as `run.ps1` does) so every engine shares one core.\n\n## Release Preconditions\n\nBefore tagging, confirm that the release commit on `main` already contains:\n\n- `packages/javascript/package.json` with the intended version.\n- `CHANGELOG.md` with the matching release section.\n- Passing `OPF CI` on the release commit.\n\nThe publish workflow validates the tag name against\n`packages/javascript/package.json`, so the tag must point at the release commit.\n\n## Tag And Publish\n\nUse the `opf-vX.Y.Z` tag form for the package release:\n\n```sh\ngit checkout main\ngit pull origin main\ngrep '\"version\"' packages/javascript/package.json\ngit tag opf-vX.Y.Z\ngit push origin opf-vX.Y.Z\n```\n\nFor example, version `0.3.0` used:\n\n```sh\ngit tag opf-v0.3.0\ngit push origin opf-v0.3.0\n```\n\nPushing the tag triggers `.github/workflows/npm-publish.yml`. The workflow:\n\n- runs on tags matching `opf-v*` or `@openpresentation/opf@v*`\n- installs dependencies with pnpm on Node 24\n- verifies the tag matches `packages/javascript/package.json`\n- runs typecheck and tests\n- runs the npm package dry-run check\n- publishes from `packages/javascript` with `npm publish --access public --provenance`\n\nDo not rerun a successful publish for the same version. npm package versions are\nimmutable; a second publish for an already-published version should fail.\n\n## Trusted Publishing\n\nnpm publishing is configured to use GitHub Actions OIDC trusted publishing, not\na long-lived npm token.\n\nExpected npm package trusted-publisher settings:\n\n| Setting | Value |\n|---|---|\n| Package | `@openpresentation/opf` |\n| Publisher | GitHub Actions |\n| Organization/repository | `OpenPresentation/opf` |\n| Workflow filename | `npm-publish.yml` |\n| Environment | empty, unless the workflow is later moved behind a GitHub Environment |\n| Permission | `npm publish` |\n\nExpected workflow settings:\n\n```yaml\npermissions:\n contents: read\n id-token: write\n```\n\nThe publish step should not set `NODE_AUTH_TOKEN`:\n\n```yaml\n- name: Publish to npm\n working-directory: packages/javascript\n run: npm publish --access public\n```\n\nIf a future release fails with npm authentication errors, check the npm\ntrusted-publisher settings first. Only use an `NPM_TOKEN` repository secret as a\ntemporary fallback, and remove or revoke it once OIDC publishing works again.\n\n## Verify The Release\n\nAfter the workflow completes, verify npm:\n\n```sh\nnpm view @openpresentation/opf version\n```\n\nThe output should equal the package version that was tagged.\n\nSpot-check the validator API from a clean project or temporary directory:\n\n```sh\nnpm install @openpresentation/opf@X.Y.Z\nnode --input-type=module -e \"import {validatePresentation} from '@openpresentation/opf'; console.log(validatePresentation({name:'t', narrative:'not-a-real-id', slides:[{title:'t'}]}).warnings)\"\n```\n\nThe expected result is one warning about an unknown narratives catalog id.\n\n## GitHub Release Notes\n\nThe core tag workflow creates a GitHub Release from the matching changelog section after publishing. Verify that release after npm is verified. If release creation failed, create the missing release for the existing tag:\n\n```sh\ngh release create opf-vX.Y.Z \\\n --repo OpenPresentation/opf \\\n --title '@openpresentation/opf X.Y.Z' \\\n --notes-file /path/to/release-notes.md\n```\n\nUse the matching `## X.Y.Z` section from `CHANGELOG.md` as the release notes.\n\n## Troubleshooting\n\nIf the tag/version check fails, the tag does not point at the release commit or\nthe tag name does not match `packages/javascript/package.json`. Delete the bad\nlocal and remote tag, fetch `main`, and tag the correct commit:\n\n```sh\ngit push origin :refs/tags/opf-vX.Y.Z\ngit tag -d opf-vX.Y.Z\ngit checkout main\ngit pull origin main\ngit tag opf-vX.Y.Z\ngit push origin opf-vX.Y.Z\n```\n\nIf tests fail, fix the code on `main`, create a new release commit, and move the\ntag only if npm has not already published that version.\n\nIf npm publish fails with `ENEEDAUTH`, confirm:\n\n- npm has a trusted publisher for `OpenPresentation/opf`\n- the trusted publisher uses workflow filename `npm-publish.yml`\n- `.github/workflows/npm-publish.yml` has `id-token: write`\n- the publish job is running on a modern Node/npm toolchain\n\nIf npm publish fails after the version is already present on npm, do not retry\nthe same publish. Verify the package and treat the failure as a duplicate\npublish attempt.\n"
218
218
  },
219
219
  {
220
220
  "slug": "rich-text",