@openpresentation/opf 0.26.1 → 0.26.3
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/README.md +4 -0
- package/dist/browser.js +13 -14
- package/dist/{catalog-refs-CSHevLn1.d.ts → catalog-refs-BSfGELpt.d.ts} +1 -1
- package/dist/catalog-schemas-DoxA2BgY.d.ts +19 -0
- package/dist/{chunk-IC6OPLMU.js → chunk-2ZQBMZTZ.js} +2 -2
- package/dist/{chunk-JIHHLV47.js → chunk-34LIUOT7.js} +6 -6
- package/dist/{chunk-FITOGMR3.js → chunk-54OV2WQC.js} +1 -1
- package/dist/{chunk-IWFXFWGD.js → chunk-7RY7QWWJ.js} +1 -1
- package/dist/{chunk-GS37EBPQ.js → chunk-DEI2M2H6.js} +1 -1
- package/dist/{chunk-3OE6PZIC.js → chunk-FXNUM2PN.js} +4 -4
- package/dist/{chunk-WW62PPYV.js → chunk-GLLR3AQ5.js} +1 -2
- package/dist/{chunk-P7S3ORB4.js → chunk-NJWI6MPT.js} +1827 -68
- package/dist/{chunk-F5ABHDUX.js → chunk-PB5X2VSV.js} +4 -4
- package/dist/{chunk-LU6XM5JQ.js → chunk-PXOXTJTN.js} +1 -1
- package/dist/{chunk-MJ57RSYO.js → chunk-WASENX2R.js} +4 -4
- package/dist/{chunk-X7RANZWP.js → chunk-YVNFACTC.js} +1 -1
- package/dist/{composition-Daqvi-GV.d.ts → composition-DIftf6iP.d.ts} +1 -1
- package/dist/composition.d.ts +2 -2
- package/dist/{conversion-BFr5USlh.d.ts → conversion-BjsKgxBZ.d.ts} +5 -5
- package/dist/conversion-F4UCOGR7.js +18 -0
- package/dist/convert.d.ts +3 -3
- package/dist/convert.js +5 -5
- package/dist/{deck-report-DHaboWbM.d.ts → deck-report-5vwMGcQz.d.ts} +2 -2
- package/dist/deck.d.ts +5 -5
- package/dist/deck.js +8 -8
- package/dist/diff.js +2 -3
- package/dist/docs.js +6 -6
- package/dist/index.d.ts +13 -12
- package/dist/index.js +17 -18
- package/dist/markdown.d.ts +3 -3
- package/dist/markdown.js +5 -5
- package/dist/node-engine.d.ts +7 -7
- package/dist/node-engine.js +17 -18
- package/dist/{pagination-R8YFwVj7.d.ts → pagination-D_kJe4jH.d.ts} +2 -2
- package/dist/pagination.d.ts +3 -3
- package/dist/pagination.js +2 -2
- package/dist/patch.js +1 -2
- package/dist/repo-readme.js +1 -1
- package/dist/schema.d.ts +165 -0
- package/dist/schema.js +3 -0
- package/dist/source.d.ts +97 -0
- package/dist/source.js +90 -0
- package/dist/types.d.ts +1 -1
- package/dist/{validation-types-JW8e06ee.d.ts → validation-types-3ARvTsB0.d.ts} +2 -2
- package/dist/validator.d.ts +54 -6
- package/dist/validator.js +1 -1
- package/dist/yaml.d.ts +3 -3
- package/dist/yaml.js +5 -5
- package/package.json +13 -5
- package/dist/chunk-IY5SCRTV.js +0 -261
- package/dist/conversion-ETAHLWAH.js +0 -19
- package/dist/validator-Dym2zYVK.d.ts +0 -67
package/dist/docs.js
CHANGED
|
@@ -28,13 +28,13 @@ var docsData = Object.freeze([
|
|
|
28
28
|
"slug": "cli",
|
|
29
29
|
"file": "docs/cli.md",
|
|
30
30
|
"title": "The `opf` CLI",
|
|
31
|
-
"markdown": '# The `opf` CLI\n\nThe CLI (`@openpresentation/cli`, binary `opf`, Node 22 or later; verified on Node 24) creates, checks, edits, converts and\ninspects decks (see [its README](../packages/cli/README.md)). `opf validate` is the one checker ([validate](validate.md)).\n`opf convert` is the one command that changes formats: a deck form, PDF, PPTX, PNG and SVG. Every command runs core\'s\nfunctions, the ones applications call from [`@openpresentation/opf`](node.md); the commands add the flags, the JSON report and\nthe exit codes.\n\nEverything is deterministic and local: no network, no model, no telemetry, no system fonts. The same document, options and\ninstalled package versions give the same bytes on every operating system.\n\n## Commands\n\n| Command | What it does |\n| --- | --- |\n| `opf create [output] [--title <text> \\| --example <slug>]` | A one-slide starter deck, or one of core\'s bundled examples (`opf catalog examples` lists them). |\n| `opf validate <file\\|dir\\|->...` | The one checker: format, references, policy, accessibility, layout, content. Several files, folders and stdin in one run; `--format json\\|text\\|github`. |\n| `opf convert <input> <output>` | Any change of format: `.json`, `.opf.yaml`, `.opf.md` (and a plain `.md` input), `.pptx`, `.pdf`, `.png`, `.svg`, `.zip`, or stdin and stdout. |\n| `opf edit <file> [output] --patch <patch>` | A JSON Patch, applied whole or not at all, the result checked. |\n| `opf format <file>... [output]` | Canonical key order and layout, in the form the file has. |\n| `opf diff <a> <b>` | What changed between two decks (`--format text\\|json\\|patch`). |\n| `opf merge <base> <ours> <theirs> [output]` | A three-way merge; conflicts are listed. |\n| `opf stats <file>` | Neutral facts about a deck (`--format json\\|text`). |\n| `opf paginate <input> [output]` | Overflowing slides split into continuation slides, measured with the renderer\'s fonts when it is installed. |\n| `opf embed <input> [output]` | The catalog records a deck references, copied into it. |\n| `opf fill <template> <data> [output]` | A template\'s variables filled: one deck per record (the output is a name pattern), or `--combine`. The second argument is always the data (`-` for stdin). |\n| `opf ingest <data> [output] --as table\\|chart` | CSV, TSV or JSON rows as a table or chart slide, alone or added `--into` a deck. |\n| `opf doctor [deck]` | Which formats this install can write, and the one command that installs what is missing. |\n| `opf schemas`, `opf schema [name] [pointer]` | The OPF schemas. |\n| `opf catalogs`, `opf catalog <kind> [id]`, `opf catalog examples [slug]` | The pptx.gallery catalog (`@openpresentation/gallery`) and the bundled example decks. |\n| `opf skills <install\\|update\\|status>` | The six bundled agent skills. |\n\n`opf <command> --help` (or `-h`, or `opf help <command>`) prints a command\'s flags and examples.\n\nRemoved in 0.18 (each is a usage error, exit 2, that names its replacement): `opf render`, `opf export`, `opf import`,\n`opf from-md`, `opf to-md`, `opf from-yaml` and `opf to-yaml` (all `opf convert`), `opf create --from` (`opf convert`), and the\nflags `--output`, `--out`, `--out-dir` and `--name` (a positional output), `--input-format` (`--from`), `--json` (reports are\nJSON), `--dry-run` (stdout), `--data` (fill\'s second argument) and the deck values of `--format` (`--to`).\n\n## One output convention\n\nEvery command that writes a deck takes the same arguments:\n\n| Form | Meaning |\n| --- | --- |\n| `opf embed deck.opf.json` | No output: the deck goes to stdout, the report to stderr. `-` says the same. |\n| `opf embed deck.opf.json out.opf.json` | An output after the inputs: written atomically (a temporary sibling, then a rename); an existing file needs `--force`. |\n| `opf embed deck.opf.json -i` | `-i` (`--in-place`) rewrites the input, refusing if it changed since it was read. Not for stdin, and not with an output. |\n| `opf embed deck.opf.json --check` | Writes nothing and exits 1 when the command would change something: `format --check` (not canonical), `embed --check` (not self-contained) and `paginate --check` (a slide overflows). |\n\nThe form written is the output\'s extension (`.json`, `.opf.yaml`/`.yml`, `.opf.md`), else `--to json|yaml|md`, else the form\nthat was read. An existing output is refused before anything is printed or written.\n\n`opf fill` writes one deck per record through a file name pattern: `{n}` is the record number (padded to the width of the largest, the rule of `opf convert`), `{column}` a slug of that\ncolumn\'s value (`opf fill t.opf.md clients.csv decks/qbr-{client}.opf.md`); `--combine` writes one deck with every record\'s slides.\n\n## What each flag means\n\n| Flag | Meaning |\n| --- | --- |\n| `--format json\\|text\\|github\\|patch` | The report format only: `json` (the default), `text`, `github` (validate\'s workflow annotations) or `patch` (diff\'s JSON Patch). |\n| `--from json\\|yaml\\|md` | The form of a deck input: stdin, and names that say none. Without it, a name ending `.json`, `.opf.yaml`, `.yaml`, `.yml` or `.opf.md` says the form, and text that starts with `{` or `[` is JSON; anything else is a usage error that asks for `--from yaml\\|md`. `opf convert` also takes `--from pptx`. |\n| `--to json\\|yaml\\|md` | The form of a deck output, for stdout and names that say none. `opf convert` also takes `pdf`, `pptx`, `png`, `svg` and `zip`. A `--to` that disagrees with the output\'s extension is refused. |\n| `--data-format csv\\|tsv\\|json` | The form of the data of `opf fill` and `opf ingest` (default: the extension, else JSON when it starts with `{` or `[`, else CSV). |\n| `--fail-on error\\|warning\\|info` | Findings at or above this severity fail the command (exit 1) and nothing is written (default `error`). |\n| `--force` | Replace an existing output. |\n\n## Install\n\n**Published set (2026-10-21):** CLI 0.25.0 (core `^0.25.0`, Gallery `^5.0.0`,\noptional renderer/PPTX `^0.25.0`) with core 0.25.0, renderer 0.25.0, PPTX 0.25.0,\neditor 0.25.3 and Gallery 5.0.0. Use the complete pinned [quickstart installation](quickstart.md#versions) and its\nrequired font/converter peers.\n\nDeck forms (JSON, YAML, Markdown) need nothing beyond the CLI. PDF, PNG and SVG need `@openpresentation/opf-render`; PPTX output\nand `.pptx` input also need `@openpresentation/opf-pptx`. Both are **optional peer dependencies** of the CLI and of core (decision\nRR-27, below), loaded the first time a command needs them, from beside the CLI first and from the working directory second.\n\n`opf doctor` says what this install can write and prints the one command that installs the rest:\n\n```sh\nopf doctor --format text # per format: ready, or what is missing; then one install command\nopf doctor deck.opf.md # JSON; with a deck, a PDF needs sharp only when the deck has pictures\n```\n\nThe command matches the package manager that runs the CLI (`npm_config_user_agent`: npm, pnpm, yarn or bun) and the way the CLI\nis installed: global (`npm install -g ...`), a project (`npm install ...`), or one npx run (`npx -p @openpresentation/cli -p ... opf <command>`).\nA conversion that misses a peer exits 2 with `code: "peer-not-installed"` (or `peer-too-old`), and its `install` field and message\ncarry that one command for the format asked for. What each output needs beside `@openpresentation/opf-render`:\n\n| Output | Also install |\n| --- | --- |\n| every drawn format (the office font pack) | `@expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 @expo-google-fonts/caladea@0.4.2 @expo-google-fonts/arimo@0.4.3 @expo-google-fonts/tinos@0.4.2 @expo-google-fonts/cousine@0.4.3 @expo-google-fonts/gelasio@0.4.1 @expo-google-fonts/noto-sans@0.4.2` |\n| `png` | `@resvg/resvg-js@^2.6.2 sharp@^0.35.5` |\n| `pdf` | nothing for text in the default vector mode; `pdf-lib@^1.17.1` for `--raster`; `sharp@^0.35.5` when the deck has pictures |\n| `svg` | the fonts only |\n| `pptx`, a `.pptx` input | `@openpresentation/opf-pptx` |\n\n## From code\n\nThe engine the commands run is core\'s `@openpresentation/opf` in Node, which an application imports instead of spawning `opf`.\nIn Node, `validate`, `stats`, `paginate`, `embed` and `edit` register the gallery (`@openpresentation/gallery`) when a call names none, as the commands\ndo, so `opf validate deck` and `validate(deck)` agree:\n\n```ts\nimport * as opf from "@openpresentation/opf";\n\nawait opf.convert("deck.opf.md", "deck.pdf"); // opf convert deck.opf.md deck.pdf\nconst deck = await opf.open("deck.pptx"); // opf convert deck.pptx -\nconst { files } = await opf.convert(deck, { format: "png", scale: 2 }); // bytes, nothing written\nconst { presentation } = opf.edit(deck, patch); // opf edit deck --patch patch.json\nconst { decks } = opf.fill(template, csvText); // opf fill template data.csv out-{n}.opf.json\n```\n\nThe options are the flags in camel case (`--raster` is `raster: true`, `--fonts` is `fonts` (a folder or a list of folders),\n`--asset-dir` is `assetDir`, `--include-hidden` is `includeHidden`). The commands refuse an existing output without `--force`;\n`convert` replaces it unless `overwrite: false`. See [OPF files in Node](node.md).\n\n## `opf convert`\n\n```sh\nopf convert deck.opf.md deck.pdf\nopf convert deck.opf.md slides/deck.png --slides 1,3-5 --scale 2 # slides/deck-1.png, -3, -4, -5\nopf convert deck.opf.md "slides/slide-{n}.png" # slides/slide-1.png, slide-2.png, ... (always numbered)\nopf convert deck.opf.md slides.zip --to svg\nopf convert deck.pptx deck.opf.md --signals signals.json\nopf convert outline.md deck.opf.yaml # a plain .md input is OPF Markdown\nopf convert deck.opf.json deck.opf.yaml --schema-comment\nopf convert deck.opf.md - --to pdf > deck.pdf\n```\n\n| Output | Written as |\n| --- | --- |\n| `.pdf`, `.pptx` | one file |\n| `.png`, `.svg` | one file per slide beside the output, named after it: `slides/deck.png` writes `slides/deck-1.png`, `-2`, ... (the deck\'s slide numbers). One selected slide, or a one-slide deck, is written to the output itself. The extension is the format: `deck.png` is always PNG. With `{n}` in the output (`"slides/slide-{n}.png"`) the files are always numbered, even for one slide or one selected slide, and `{n}` is the deck\'s slide number (2 and 5 with `--slides 2,5`). `{n}` is the only placeholder: any other `{key}`, and `{n}` in `.pdf`, `.pptx`, `.zip` or a deck form, is a usage error (exit 2, `invalid-option`). |\n\nNumbers are padded with zeros to the width of the largest number written: `1`...`9` for slides 1 to 9, `01`...`99` up to 99, `001` from 100. With `--slides` the width comes from the largest slide selected (`--slides 2,5` gives `deck-2.png`, `deck-5.png`). The same rule pads `{n}` in `opf fill` (the largest record number) and the zip entries below.\n| `.zip` | one archive of the slides, its entries named as the plain `.png` and `.svg` files above (`slides.zip` holds `slides-1.png`, ...; always numbered, and no `{n}` in the archive\'s own name): PNG, or SVG with `--to svg`. Entries are stored with a fixed timestamp, so the archive is byte-identical everywhere. |\n| `.json`, `.opf.yaml`, `.yml`, `.opf.md` | the deck in that form |\n| `-` | stdout, with `--to`: one file (`pdf`, `pptx`, `zip`, a deck form, or one slide as `png` or `svg`) |\n\nA flag applies only when its format is involved; anywhere else it is a usage error (exit 2, `option-not-applicable`):\n\n| Involved | Flags |\n| --- | --- |\n| a Markdown input | `--split auto\\|rules\\|headings` (default `auto`: slides at `---` lines, and an outline with none at its `#` headings), `--title <text>` (the deck name unless the front matter sets one) |\n| a YAML input | `--aliases` (expand anchors, aliases and merge keys, at most 100) |\n| a `.pptx` input | `--signals <file>` (also write the raw per-shape layout and style signals, `fromPptx` with `signals: true`) |\n| a YAML output | `--schema-comment` (the `# yaml-language-server: $schema=...` line) |\n| a Markdown output | `--drop-unsupported` (leave out what the dialect has no syntax for and list it as loss, instead of embedding it as YAML in a fence) |\n| pdf, png, svg | `--slides 1,3-5` (one-based; `2-` to the end), `--include-hidden` |\n| pdf, png, svg, pptx | `--paginate`, `--date YYYY-MM-DD`, `--fonts <dir>` (repeatable), `--asset-dir <dir>` |\n| png, raster pdf | `--scale 0.1-8` (1 draws the 1280 x 720 reference slide at 1280 x 720 pixels) |\n| pdf | `--raster` (each page a picture instead of selectable vector text) |\n| svg | `--text fonts\\|system\\|paths` (embed the faces the slide uses, none, or draw glyph outlines) |\n| pptx | `--charts auto\\|native\\|picture`, `--images compatible\\|preserve`, `--provenance full\\|references-only\\|none`, `--body-text measured\\|native`, `--slide-numbers static\\|live` |\n\nA `.pptx` input is imported, checked and then written or drawn; `findings` holds both steps (`import/` rules from the importer).\nPer-slide images and the PDF skip slides marked `hidden: true` unless `--include-hidden`; slides named with `--slides` are always\nwritten, and the report lists the slides left out in `skippedHidden`. The PPTX keeps a hidden slide as a hidden slide.\n\n### PPTX text (RR-88, RR-91)\n\nBy default (`--body-text measured`) a wrapped paragraph becomes one text box per line, laid out exactly as the preview draws it, so editing a line in PowerPoint or Keynote does not reflow the rest. `--body-text native` opts in to whole paragraphs: a title that wraps is one Title placeholder, and a text, bullets or numbered-list region is one body that the application re-wraps when edited. What native mode cannot carry (groups, cards, nested or described lists, restarted numbering, titles with a hard break) stays measured and is reported as `native-body-fallback`. `--slide-numbers` decides a footer such as `{{slide.number}} / {{deck.slideCount}}`: `static` (the default) writes the number as fixed text, so Keynote shows the words around it, and `live` keeps the slide-number field, which Keynote draws as the digit alone because it drops the words next to a live number. A footer that is only `{{slide.number}}` stays a live field in both modes (the total is always fixed text). Both flags need an opf-pptx that reads them (0.22.0 or later); an older one is refused (`peer-too-old`) rather than ignoring them, and they are exit 2 for any other output.\n\n**Which to choose.** Pick `measured` (the default) when the deck is final and the PPTX is a delivery format: every line sits where the preview put it, and the deck is edited in OPF. Pick `native` when people will edit the text in PowerPoint or Keynote: a longer bullet wraps, a title keeps its placeholder (so outline view and slide titles work), and numbered lists renumber. Native text is re-wrapped by the application\'s own fonts and engine, so line breaks can differ from the preview; native wrapping is not yet qualified in every application, and anything it cannot carry (see above) stays measured with a `native-body-fallback` finding, so read the findings. `--slide-numbers static` is the default because Keynote replaces any paragraph that holds a live slide-number field with the number alone, so `{{slide.number}} / {{deck.slideCount}}` showed only the digit; the cost is that PowerPoint does not renumber a static number when you reorder slides there (choose `live` for that, and accept the digit-only footer in Keynote).\n\n## The other commands\n\n- **`opf create`** writes a starter deck (`--title`) or a bundled example (`--example <slug>`, from `opf catalog examples`).\n- **`opf validate`** checks files, folders (their `*.opf.json`, `*.opf.yaml`, `*.opf.yml` and `*.opf.md`, sorted, skipping\n `node_modules` and dot folders) and `-`. One input prints its report; several print `{ command, ok, files: [reports], counts }`.\n The exit status is the worst over every file. `--format github` prints `::error`, `::warning` and `::notice` annotations with the\n file, line and column. See [validate](validate.md).\n- **`opf paginate`** measures the page breaks with the fonts `opf convert --paginate` draws with when `@openpresentation/opf-render`\n is installed (`layout: "measured"`); without it they are estimated (`layout: "estimated"`) and `hint` names the install command.\n- **`opf fill`** reads CSV, TSV or JSON data; a blank cell keeps the variable\'s declared value; an unfilled required variable fails\n unless `--partial`; `--examples` fills from the declared examples. The second argument is always the data, whatever its name\n (`-` reads it from stdin), and the third is the output; a template alone fills from its own values, to stdout.\n- **`opf ingest`** gives the same slide id and bytes for the same inputs; `--dataset <id>` stores the rows in the deck\'s datasets.\n\n## Reports and errors\n\nEvery command that reports prints one envelope:\n\n```json\n{ "command": "convert", "ok": true, "input": { "file": "/abs/deck.opf.md", "sha256": "\u2026", "bytes": 1234 },\n "outputs": [{ "file": "/abs/deck.pdf", "sha256": "\u2026", "bytes": 56789, "mediaType": "application/pdf" }],\n "findings": [], "counts": { "error": 0, "warning": 0, "info": 0 }, "format": "pdf", "\u2026": "\u2026" }\n```\n\n`input` is `null` when nothing was read and a list when several were (diff, merge, fill). The report is on stdout, or on stderr when\nstdout carries the document. Findings have the [`opf validate`](validate.md) shape (`ruleId`, `severity`, `category`, JSON Pointer\n`path`, `message`, `help`, and `location` when read from text); engine findings carry the prefix `render/`, `pptx/`, `pdf/`,\n`fonts/`, `import/` or `cli/`. The lookup commands (`schemas`, `schema`, `catalogs`, `catalog`, `--version`) print the data asked for.\n\nErrors are `{ command, ok: false, code, error, ... }` on stderr. The message states the fix: a missing argument names it and\nprints the usage, an unknown command, option or value suggests the nearest one ("Did you mean --check?"), and a removed command\nor flag names its replacement. Codes include `usage`, `unknown-command`, `unknown-option`, `removed-command`, `removed-option`,\n`missing-argument`, `extra-argument`, `missing-value`, `invalid-value`, `option-not-applicable`, `unknown-input-format`,\n`input-not-found`, `invalid-json`, `invalid-yaml`, `invalid-markdown`, `invalid-document`, `findings-at-fail-on`,\n`output-exists`, `merge-conflict`, `fill-failed`, `peer-not-installed` and `peer-too-old`.\n\nExit `0`: success (warnings allowed). Exit `1`: an invalid document, a finding at or above `--fail-on`, an existing output, a\nmerge conflict, or a `--check` that found a change. Exit `2`: usage, a read or I/O problem, text that is not valid JSON, YAML or\nMarkdown, or a missing or too-old peer. (`opf validate` reports text that does not parse as a finding, exit 1.)\n\n## Fonts\n\nThe renderer\'s bundled open font pack (the office pack: Carlito, Intos for Aptos, the open families font schemes select and the\nopen replacements the font policy routes proprietary families to) with the visual substitution policy, plus the files from\n`--fonts`. **System fonts are never loaded**, and nothing is downloaded. Font substitutions are listed under\n`fonts.substitutions` in the report. The PPTX keeps the font names the document chose.\n\nScripts beyond Latin, Greek and Cyrillic need the optional Noto script packages of the renderer (`@expo-google-fonts/noto-sans-jp`\nand so on); install the ones named in the `fonts/script-font-not-installed` diagnostic next to the CLI. A standalone SVG\n(`--text fonts`, the default) embeds the faces its text uses; `--text system` omits all font bytes; `--text paths` draws the text\nas glyph outlines (the real text kept invisible beside it for selection and search), so the SVG needs no font.\n\n## Images and assets\n\nRelative image paths and `file:` paths resolve against the input\'s folder (or `--asset-dir`). Only `.png`, `.jpg`, `.jpeg`,\n`.gif`, `.webp` and `.svg` files whose content matches are read, and only inside that folder (symlinks are resolved first). URLs\nare never fetched. For SVG and PNG an unreadable image draws the renderer\'s placeholder with an `unresolved-asset` or\n`cli/asset-blocked` warning. For PPTX (opf-pptx 0.24.1 and later) an unreadable or blocked local path and an unresolved remote URL\nare one case: the deck exports without that image (an unavailable-image placeholder, or the fallback background for a background\npicture) and the report carries a `pptx/unresolved-asset` warning. Use `--fail-on warning` to reject that output before any file is\nwritten. Before opf-pptx 0.24.1 an unreadable or blocked local path stopped the export (`pptx/asset-unresolved`, an error).\n\n## Markdown and YAML\n\nJSON stays the canonical form of a deck. YAML (`deck.opf.yaml`) and Markdown (`deck.opf.md`) are authoring forms of the same data,\nand every command that reads or writes a deck reads and writes all three:\n\n```sh\nopf convert deck.md deck.opf.json # Markdown in the OPF dialect (or an outline) to a deck\nopf convert deck.opf.json deck.opf.md # a deck as Markdown that reads back unchanged\nopf convert deck.opf.yaml deck.opf.json --aliases # strict YAML (--aliases expands anchors)\nopf validate deck.opf.yaml deck.opf.md # every command reads .yaml, .yml and .opf.md\nopf edit deck.opf.md --patch changes.json -i # the file stays Markdown\nopf format deck.opf.md --check # canonical Markdown\nopf validate - --from yaml < deck.txt # stdin and other names: --from\n```\n\nSee [Markdown and outlines](markdown.md) and [OPF as YAML](yaml.md). A syntax error exits 2 with the line and column (`opf\nvalidate` reports it as a `yaml/<rule>` or `markdown/<rule>` finding and exits 1); a command that rewrites a YAML file does not\nkeep its comments and says so on stderr, and one that has to put content into an `opf-slide`/`opf-block` fence says so too.\n\n## Runtime choices\n\n- **Peers, not bundled.** opf-render and opf-pptx are optional peer dependencies loaded lazily. opf-pptx pulls the native `sharp`\n engine, opf-render `resvg`, `fontkit` and the font packs (about 135 MB installed); bundling them would make the small CLI\n impossible to install offline or on a locked-down agent host, and validating or editing a deck would pay for it.\n- **JSON reports.** `--format text` gives human-readable output where a command has it; `--format github` gives CI annotations.\n- **No clock.** `--date` is explicit so a rerun tomorrow gives the same bytes.\n- **Fonts are the bundled pack plus `--fonts`.** Never system fonts (owner font policy).\n- **Stored zip entries.** Deflate output differs between zlib builds, which would make archive hashes host-dependent.\n\n## Catalog names\n\n`opf catalogs` lists canonical camelCase names such as `colorSchemes` and `fontSchemes`, and `examples`. `opf catalog` also\naccepts the website-style `color-schemes`, `font-schemes` and `chart-types` spellings.\n'
|
|
31
|
+
"markdown": '# The `opf` CLI\n\nThe CLI (`@openpresentation/cli`, binary `opf`, Node 22 or later; verified on Node 24) creates, checks, edits, converts and\ninspects decks (see [its README](../packages/cli/README.md)). `opf validate` is the one checker ([validate](validate.md)).\n`opf convert` is the one command that changes formats: a deck form, PDF, PPTX, PNG and SVG. Every command runs core\'s\nfunctions, the ones applications call from [`@openpresentation/opf`](node.md); the commands add the flags, the JSON report and\nthe exit codes.\n\nEverything is deterministic and local: no network, no model, no telemetry, no system fonts. The same document, options and\ninstalled package versions give the same bytes on every operating system.\n\n## Commands\n\n| Command | What it does |\n| --- | --- |\n| `opf create [output] [--title <text> \\| --example <slug>]` | A one-slide starter deck, or one of core\'s bundled examples (`opf catalog examples` lists them). |\n| `opf validate <file\\|dir\\|->...` | The one checker: format, references, policy, accessibility, layout, content. Several files, folders and stdin in one run; `--format json\\|text\\|github`. |\n| `opf convert <input> <output>` | Any change of format: `.json`, `.opf.yaml`, `.opf.md` (and a plain `.md` input), `.pptx`, `.pdf`, `.png`, `.svg`, `.zip`, or stdin and stdout. |\n| `opf edit <file> [output] --patch <patch>` | A JSON Patch, applied whole or not at all, the result checked. |\n| `opf format <file>... [output]` | Canonical key order and layout, in the form the file has. |\n| `opf diff <a> <b>` | What changed between two decks (`--format text\\|json\\|patch`). |\n| `opf merge <base> <ours> <theirs> [output]` | A three-way merge; conflicts are listed. |\n| `opf stats <file>` | Neutral facts about a deck (`--format json\\|text`). |\n| `opf paginate <input> [output]` | Overflowing slides split into continuation slides, measured with the renderer\'s fonts when it is installed. |\n| `opf embed <input> [output]` | The catalog records a deck references, copied into it. |\n| `opf fill <template> <data> [output]` | A template\'s variables filled: one deck per record (the output is a name pattern), or `--combine`. The second argument is always the data (`-` for stdin). |\n| `opf ingest <data> [output] --as table\\|chart` | CSV, TSV or JSON rows as a table or chart slide, alone or added `--into` a deck. |\n| `opf doctor [deck]` | Which formats this install can write, and the one command that installs what is missing. |\n| `opf schemas`, `opf schema [name] [pointer]` | The OPF schemas. |\n| `opf catalogs`, `opf catalog <kind> [id]`, `opf catalog examples [slug]` | The pptx.gallery catalog (`@openpresentation/gallery`) and the bundled example decks. |\n| `opf skills <install\\|update\\|status>` | The six bundled agent skills. |\n\n`opf <command> --help` (or `-h`, or `opf help <command>`) prints a command\'s flags and examples.\n\nRemoved in 0.18 (each is a usage error, exit 2, that names its replacement): `opf render`, `opf export`, `opf import`,\n`opf from-md`, `opf to-md`, `opf from-yaml` and `opf to-yaml` (all `opf convert`), `opf create --from` (`opf convert`), and the\nflags `--output`, `--out`, `--out-dir` and `--name` (a positional output), `--input-format` (`--from`), `--json` (reports are\nJSON), `--dry-run` (stdout), `--data` (fill\'s second argument) and the deck values of `--format` (`--to`).\n\n## One output convention\n\nEvery command that writes a deck takes the same arguments:\n\n| Form | Meaning |\n| --- | --- |\n| `opf embed deck.opf.json` | No output: the deck goes to stdout, the report to stderr. `-` says the same. |\n| `opf embed deck.opf.json out.opf.json` | An output after the inputs: written atomically (a temporary sibling, then a rename); an existing file needs `--force`. |\n| `opf embed deck.opf.json -i` | `-i` (`--in-place`) rewrites the input, refusing if it changed since it was read. Not for stdin, and not with an output. |\n| `opf embed deck.opf.json --check` | Writes nothing and exits 1 when the command would change something: `format --check` (not canonical), `embed --check` (not self-contained) and `paginate --check` (a slide overflows). |\n\nThe form written is the output\'s extension (`.json`, `.opf.yaml`/`.yml`, `.opf.md`), else `--to json|yaml|md`, else the form\nthat was read. An existing output is refused before anything is printed or written.\n\n`opf fill` writes one deck per record through a file name pattern: `{n}` is the record number (padded to the width of the largest, the rule of `opf convert`), `{column}` a slug of that\ncolumn\'s value (`opf fill t.opf.md clients.csv decks/qbr-{client}.opf.md`); `--combine` writes one deck with every record\'s slides.\n\n## What each flag means\n\n| Flag | Meaning |\n| --- | --- |\n| `--format json\\|text\\|github\\|patch` | The report format only: `json` (the default), `text`, `github` (validate\'s workflow annotations) or `patch` (diff\'s JSON Patch). |\n| `--from json\\|yaml\\|md` | The form of a deck input: stdin, and names that say none. Without it, a name ending `.json`, `.opf.yaml`, `.yaml`, `.yml` or `.opf.md` says the form, and text that starts with `{` or `[` is JSON; anything else is a usage error that asks for `--from yaml\\|md`. `opf convert` also takes `--from pptx`. |\n| `--to json\\|yaml\\|md` | The form of a deck output, for stdout and names that say none. `opf convert` also takes `pdf`, `pptx`, `png`, `svg` and `zip`. A `--to` that disagrees with the output\'s extension is refused. |\n| `--data-format csv\\|tsv\\|json` | The form of the data of `opf fill` and `opf ingest` (default: the extension, else JSON when it starts with `{` or `[`, else CSV). |\n| `--fail-on error\\|warning\\|info` | Findings at or above this severity fail the command (exit 1) and nothing is written (default `error`). |\n| `--force` | Replace an existing output. |\n\n## Install\n\n**Published set (2026-10-24):** CLI 0.26.1 (core `^0.26.1`, Gallery `^0.26.1`,\noptional renderer/PPTX `^0.26.1`) with core, renderer, PPTX, editor and Gallery 0.26.1. Use the complete pinned [quickstart installation](quickstart.md#versions) and its\nrequired font/converter peers.\n\nDeck forms (JSON, YAML, Markdown) need nothing beyond the CLI. PDF, PNG and SVG need `@openpresentation/opf-render`; PPTX output\nand `.pptx` input also need `@openpresentation/opf-pptx`. Both are **optional peer dependencies** of the CLI and of core (decision\nRR-27, below), loaded the first time a command needs them, from beside the CLI first and from the working directory second.\n\n`opf doctor` says what this install can write and prints the one command that installs the rest:\n\n```sh\nopf doctor --format text # per format: ready, or what is missing; then one install command\nopf doctor deck.opf.md # JSON; with a deck, a PDF needs sharp only when the deck has pictures\n```\n\nThe command matches the package manager that runs the CLI (`npm_config_user_agent`: npm, pnpm, yarn or bun) and the way the CLI\nis installed: global (`npm install -g ...`), a project (`npm install ...`), or one npx run (`npx -p @openpresentation/cli -p ... opf <command>`).\nA conversion that misses a peer exits 2 with `code: "peer-not-installed"` (or `peer-too-old`), and its `install` field and message\ncarry that one command for the format asked for. What each output needs beside `@openpresentation/opf-render`:\n\n| Output | Also install |\n| --- | --- |\n| every drawn format (the office font pack) | `@expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 @expo-google-fonts/caladea@0.4.2 @expo-google-fonts/arimo@0.4.3 @expo-google-fonts/tinos@0.4.2 @expo-google-fonts/cousine@0.4.3 @expo-google-fonts/gelasio@0.4.1 @expo-google-fonts/noto-sans@0.4.2` |\n| `png` | `@resvg/resvg-js@^2.6.2 sharp@^0.35.5` |\n| `pdf` | nothing for text in the default vector mode; `pdf-lib@^1.17.1` for `--raster`; `sharp@^0.35.5` when the deck has pictures |\n| `svg` | the fonts only |\n| `pptx`, a `.pptx` input | `@openpresentation/opf-pptx` |\n\n## From code\n\nThe engine the commands run is core\'s `@openpresentation/opf` in Node, which an application imports instead of spawning `opf`.\nIn Node, `validate`, `stats`, `paginate`, `embed` and `edit` register the gallery (`@openpresentation/gallery`) when a call names none, as the commands\ndo, so `opf validate deck` and `validate(deck)` agree:\n\n```ts\nimport * as opf from "@openpresentation/opf";\n\nawait opf.convert("deck.opf.md", "deck.pdf"); // opf convert deck.opf.md deck.pdf\nconst deck = await opf.open("deck.pptx"); // opf convert deck.pptx -\nconst { files } = await opf.convert(deck, { format: "png", scale: 2 }); // bytes, nothing written\nconst { presentation } = opf.edit(deck, patch); // opf edit deck --patch patch.json\nconst { decks } = opf.fill(template, csvText); // opf fill template data.csv out-{n}.opf.json\n```\n\nThe options are the flags in camel case (`--raster` is `raster: true`, `--fonts` is `fonts` (a folder or a list of folders),\n`--asset-dir` is `assetDir`, `--include-hidden` is `includeHidden`). The commands refuse an existing output without `--force`;\n`convert` replaces it unless `overwrite: false`. See [OPF files in Node](node.md).\n\n## `opf convert`\n\n```sh\nopf convert deck.opf.md deck.pdf\nopf convert deck.opf.md slides/deck.png --slides 1,3-5 --scale 2 # slides/deck-1.png, -3, -4, -5\nopf convert deck.opf.md "slides/slide-{n}.png" # slides/slide-1.png, slide-2.png, ... (always numbered)\nopf convert deck.opf.md slides.zip --to svg\nopf convert deck.pptx deck.opf.md --signals signals.json\nopf convert outline.md deck.opf.yaml # a plain .md input is OPF Markdown\nopf convert deck.opf.json deck.opf.yaml --schema-comment\nopf convert deck.opf.md - --to pdf > deck.pdf\n```\n\n| Output | Written as |\n| --- | --- |\n| `.pdf`, `.pptx` | one file |\n| `.png`, `.svg` | one file per slide beside the output, named after it: `slides/deck.png` writes `slides/deck-1.png`, `-2`, ... (the deck\'s slide numbers). One selected slide, or a one-slide deck, is written to the output itself. The extension is the format: `deck.png` is always PNG. With `{n}` in the output (`"slides/slide-{n}.png"`) the files are always numbered, even for one slide or one selected slide, and `{n}` is the deck\'s slide number (2 and 5 with `--slides 2,5`). `{n}` is the only placeholder: any other `{key}`, and `{n}` in `.pdf`, `.pptx`, `.zip` or a deck form, is a usage error (exit 2, `invalid-option`). |\n\nNumbers are padded with zeros to the width of the largest number written: `1`...`9` for slides 1 to 9, `01`...`99` up to 99, `001` from 100. With `--slides` the width comes from the largest slide selected (`--slides 2,5` gives `deck-2.png`, `deck-5.png`). The same rule pads `{n}` in `opf fill` (the largest record number) and the zip entries below.\n| `.zip` | one archive of the slides, its entries named as the plain `.png` and `.svg` files above (`slides.zip` holds `slides-1.png`, ...; always numbered, and no `{n}` in the archive\'s own name): PNG, or SVG with `--to svg`. Entries are stored with a fixed timestamp, so the archive is byte-identical everywhere. |\n| `.json`, `.opf.yaml`, `.yml`, `.opf.md` | the deck in that form |\n| `-` | stdout, with `--to`: one file (`pdf`, `pptx`, `zip`, a deck form, or one slide as `png` or `svg`) |\n\nA flag applies only when its format is involved; anywhere else it is a usage error (exit 2, `option-not-applicable`):\n\n| Involved | Flags |\n| --- | --- |\n| a Markdown input | `--split auto\\|rules\\|headings` (default `auto`: slides at `---` lines, and an outline with none at its `#` headings), `--title <text>` (the deck name unless the front matter sets one) |\n| a YAML input | `--aliases` (expand anchors, aliases and merge keys, at most 100) |\n| a `.pptx` input | `--signals <file>` (also write the raw per-shape layout and style signals, `fromPptx` with `signals: true`) |\n| a YAML output | `--schema-comment` (the `# yaml-language-server: $schema=...` line) |\n| a Markdown output | `--drop-unsupported` (leave out what the dialect has no syntax for and list it as loss, instead of embedding it as YAML in a fence) |\n| pdf, png, svg | `--slides 1,3-5` (one-based; `2-` to the end), `--include-hidden` |\n| pdf, png, svg, pptx | `--paginate`, `--date YYYY-MM-DD`, `--fonts <dir>` (repeatable), `--asset-dir <dir>` |\n| png, raster pdf | `--scale 0.1-8` (1 draws the 1280 x 720 reference slide at 1280 x 720 pixels) |\n| pdf | `--raster` (each page a picture instead of selectable vector text) |\n| svg | `--text fonts\\|system\\|paths` (embed the faces the slide uses, none, or draw glyph outlines) |\n| pptx | `--charts auto\\|native\\|picture`, `--images compatible\\|preserve`, `--provenance full\\|references-only\\|none`, `--body-text measured\\|native`, `--slide-numbers static\\|live` |\n\nA `.pptx` input is imported, checked and then written or drawn; `findings` holds both steps (`import/` rules from the importer).\nPer-slide images and the PDF skip slides marked `hidden: true` unless `--include-hidden`; slides named with `--slides` are always\nwritten, and the report lists the slides left out in `skippedHidden`. The PPTX keeps a hidden slide as a hidden slide.\n\n### PPTX text (RR-88, RR-91)\n\nBy default (`--body-text measured`) a wrapped paragraph becomes one text box per line, laid out exactly as the preview draws it, so editing a line in PowerPoint or Keynote does not reflow the rest. `--body-text native` opts in to whole paragraphs: a title that wraps is one Title placeholder, and a text, bullets or numbered-list region is one body that the application re-wraps when edited. What native mode cannot carry (groups, cards, nested or described lists, restarted numbering, titles with a hard break) stays measured and is reported as `native-body-fallback`. `--slide-numbers` decides a footer such as `{{slide.number}} / {{deck.slideCount}}`: `static` (the default) writes the number as fixed text, so Keynote shows the words around it, and `live` keeps the slide-number field, which Keynote draws as the digit alone because it drops the words next to a live number. A footer that is only `{{slide.number}}` stays a live field in both modes (the total is always fixed text). Both flags need an opf-pptx that reads them (0.22.0 or later); an older one is refused (`peer-too-old`) rather than ignoring them, and they are exit 2 for any other output.\n\n**Which to choose.** Pick `measured` (the default) when the deck is final and the PPTX is a delivery format: every line sits where the preview put it, and the deck is edited in OPF. Pick `native` when people will edit the text in PowerPoint or Keynote: a longer bullet wraps, a title keeps its placeholder (so outline view and slide titles work), and numbered lists renumber. Native text is re-wrapped by the application\'s own fonts and engine, so line breaks can differ from the preview; native wrapping is not yet qualified in every application, and anything it cannot carry (see above) stays measured with a `native-body-fallback` finding, so read the findings. `--slide-numbers static` is the default because Keynote replaces any paragraph that holds a live slide-number field with the number alone, so `{{slide.number}} / {{deck.slideCount}}` showed only the digit; the cost is that PowerPoint does not renumber a static number when you reorder slides there (choose `live` for that, and accept the digit-only footer in Keynote).\n\n## The other commands\n\n- **`opf create`** writes a starter deck (`--title`) or a bundled example (`--example <slug>`, from `opf catalog examples`).\n- **`opf validate`** checks files, folders (their `*.opf.json`, `*.opf.yaml`, `*.opf.yml` and `*.opf.md`, sorted, skipping\n `node_modules` and dot folders) and `-`. One input prints its report; several print `{ command, ok, files: [reports], counts }`.\n The exit status is the worst over every file. `--format github` prints `::error`, `::warning` and `::notice` annotations with the\n file, line and column. See [validate](validate.md).\n- **`opf paginate`** measures the page breaks with the fonts `opf convert --paginate` draws with when `@openpresentation/opf-render`\n is installed (`layout: "measured"`); without it they are estimated (`layout: "estimated"`) and `hint` names the install command.\n- **`opf fill`** reads CSV, TSV or JSON data; a blank cell keeps the variable\'s declared value; an unfilled required variable fails\n unless `--partial`; `--examples` fills from the declared examples. The second argument is always the data, whatever its name\n (`-` reads it from stdin), and the third is the output; a template alone fills from its own values, to stdout.\n- **`opf ingest`** gives the same slide id and bytes for the same inputs; `--dataset <id>` stores the rows in the deck\'s datasets.\n\n## Reports and errors\n\nEvery command that reports prints one envelope:\n\n```json\n{ "command": "convert", "ok": true, "input": { "file": "/abs/deck.opf.md", "sha256": "\u2026", "bytes": 1234 },\n "outputs": [{ "file": "/abs/deck.pdf", "sha256": "\u2026", "bytes": 56789, "mediaType": "application/pdf" }],\n "findings": [], "counts": { "error": 0, "warning": 0, "info": 0 }, "format": "pdf", "\u2026": "\u2026" }\n```\n\n`input` is `null` when nothing was read and a list when several were (diff, merge, fill). The report is on stdout, or on stderr when\nstdout carries the document. Findings have the [`opf validate`](validate.md) shape (`ruleId`, `severity`, `category`, JSON Pointer\n`path`, `message`, `help`, and `location` when read from text); engine findings carry the prefix `render/`, `pptx/`, `pdf/`,\n`fonts/`, `import/` or `cli/`. The lookup commands (`schemas`, `schema`, `catalogs`, `catalog`, `--version`) print the data asked for.\n\nErrors are `{ command, ok: false, code, error, ... }` on stderr. The message states the fix: a missing argument names it and\nprints the usage, an unknown command, option or value suggests the nearest one ("Did you mean --check?"), and a removed command\nor flag names its replacement. Codes include `usage`, `unknown-command`, `unknown-option`, `removed-command`, `removed-option`,\n`missing-argument`, `extra-argument`, `missing-value`, `invalid-value`, `option-not-applicable`, `unknown-input-format`,\n`input-not-found`, `invalid-json`, `invalid-yaml`, `invalid-markdown`, `invalid-document`, `findings-at-fail-on`,\n`output-exists`, `merge-conflict`, `fill-failed`, `peer-not-installed` and `peer-too-old`.\n\nExit `0`: success (warnings allowed). Exit `1`: an invalid document, a finding at or above `--fail-on`, an existing output, a\nmerge conflict, or a `--check` that found a change. Exit `2`: usage, a read or I/O problem, text that is not valid JSON, YAML or\nMarkdown, or a missing or too-old peer. (`opf validate` reports text that does not parse as a finding, exit 1.)\n\n## Fonts\n\nThe renderer\'s bundled open font pack (the office pack: Carlito, Intos for Aptos, the open families font schemes select and the\nopen replacements the font policy routes proprietary families to) with the visual substitution policy, plus the files from\n`--fonts`. **System fonts are never loaded**, and nothing is downloaded. Font substitutions are listed under\n`fonts.substitutions` in the report. The PPTX keeps the font names the document chose.\n\nScripts beyond Latin, Greek and Cyrillic need the optional Noto script packages of the renderer (`@expo-google-fonts/noto-sans-jp`\nand so on); install the ones named in the `fonts/script-font-not-installed` diagnostic next to the CLI. A standalone SVG\n(`--text fonts`, the default) embeds the faces its text uses; `--text system` omits all font bytes; `--text paths` draws the text\nas glyph outlines (the real text kept invisible beside it for selection and search), so the SVG needs no font.\n\n## Images and assets\n\nRelative image paths and `file:` paths resolve against the input\'s folder (or `--asset-dir`). Only `.png`, `.jpg`, `.jpeg`,\n`.gif`, `.webp` and `.svg` files whose content matches are read, and only inside that folder (symlinks are resolved first). URLs\nare never fetched. For SVG and PNG an unreadable image draws the renderer\'s placeholder with an `unresolved-asset` or\n`cli/asset-blocked` warning. For PPTX (opf-pptx 0.24.1 and later) an unreadable or blocked local path and an unresolved remote URL\nare one case: the deck exports without that image (an unavailable-image placeholder, or the fallback background for a background\npicture) and the report carries a `pptx/unresolved-asset` warning. Use `--fail-on warning` to reject that output before any file is\nwritten. Before opf-pptx 0.24.1 an unreadable or blocked local path stopped the export (`pptx/asset-unresolved`, an error).\n\n## Markdown and YAML\n\nJSON stays the canonical form of a deck. YAML (`deck.opf.yaml`) and Markdown (`deck.opf.md`) are authoring forms of the same data,\nand every command that reads or writes a deck reads and writes all three:\n\n```sh\nopf convert deck.md deck.opf.json # Markdown in the OPF dialect (or an outline) to a deck\nopf convert deck.opf.json deck.opf.md # a deck as Markdown that reads back unchanged\nopf convert deck.opf.yaml deck.opf.json --aliases # strict YAML (--aliases expands anchors)\nopf validate deck.opf.yaml deck.opf.md # every command reads .yaml, .yml and .opf.md\nopf edit deck.opf.md --patch changes.json -i # the file stays Markdown\nopf format deck.opf.md --check # canonical Markdown\nopf validate - --from yaml < deck.txt # stdin and other names: --from\n```\n\nSee [Markdown and outlines](markdown.md) and [OPF as YAML](yaml.md). A syntax error exits 2 with the line and column (`opf\nvalidate` reports it as a `yaml/<rule>` or `markdown/<rule>` finding and exits 1); a command that rewrites a YAML file does not\nkeep its comments and says so on stderr, and one that has to put content into an `opf-slide`/`opf-block` fence says so too.\n\n## Runtime choices\n\n- **Peers, not bundled.** opf-render and opf-pptx are optional peer dependencies loaded lazily. opf-pptx pulls the native `sharp`\n engine, opf-render `resvg`, `fontkit` and the font packs (about 135 MB installed); bundling them would make the small CLI\n impossible to install offline or on a locked-down agent host, and validating or editing a deck would pay for it.\n- **JSON reports.** `--format text` gives human-readable output where a command has it; `--format github` gives CI annotations.\n- **No clock.** `--date` is explicit so a rerun tomorrow gives the same bytes.\n- **Fonts are the bundled pack plus `--fonts`.** Never system fonts (owner font policy).\n- **Stored zip entries.** Deflate output differs between zlib builds, which would make archive hashes host-dependent.\n\n## Catalog names\n\n`opf catalogs` lists canonical camelCase names such as `colorSchemes` and `fontSchemes`, and `examples`. `opf catalog` also\naccepts the website-style `color-schemes`, `font-schemes` and `chart-types` spellings.\n'
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
"slug": "compatibility-matrix",
|
|
35
35
|
"file": "docs/compatibility-matrix.md",
|
|
36
36
|
"title": "Compatibility matrix",
|
|
37
|
-
"markdown": "# Compatibility matrix\n\nPublished registry evidence for the coordinated Node 24 toolchain. This matrix\nis the honest supported subset for [the developer quickstart](quickstart.md).\nIt is not universal Office parity and does not describe archived prototypes as\nshipped.\n\n**Published set (2026-10-21):** core 0.25.0, renderer 0.25.0, PPTX 0.25.0,\neditor 0.25.3, CLI 0.25.0 and Gallery 5.0.0. `node scripts/registry-plan.mjs` prints each\nversion with the registry `gitHead` of its publisher commit.\nEarlier matrices and native observations retain their historical identities.\nComplete native compatibility remains a separate gate.\n\n## Published layout contract and the 0.20 engines\n\nA layout record is a `direction` and one ordered `regions` list of regions and\nsplits, nested for two dimensions (FA-33: core 0.25.0 and Gallery 5.0.0, catalog\ncompatibility generation 4). `areas`, `columns`, `rows` and the `regions` map are\ngone, and Gallery 3 and 4 records (generation 3, written as nested `areas`\narrays) no longer validate on core 0.25. Gallery owns every default record;\nper-kind schema versions remain v2. Core 0.19 and later consume the Gallery\npackage and do not export `@openpresentation/opf/catalog` or ship a catalog\nsnapshot.\n\nThe 0.20 engines share measured quote allocation, the 32 reference pixel\nstarting size for lists without an authored size, neutral structural borders\nand default right alignment for numeric table columns. Core and CLI forward\nregistered catalogs during PPTX import. PPTX preserves stretch images with\nzero crop through native save/re-import. These published contracts do not\nimply universal Office or full-catalog visual parity.\n\n## Current published package contracts\n\n| Package | Version | Dependency contract |\n| --- | --- | --- |\n| Core | 0.25.0 | Gallery ^5.0.0; optional renderer/PPTX ^0.25.0 |\n| CLI | 0.25.0 | Core ^0.25.0, Gallery ^5.0.0; optional renderer/PPTX ^0.25.0 |\n| Renderer | 0.25.0 | Core ^0.25.0; optional pinned font/converter peers |\n| PPTX | 0.25.0 | Core ^0.25.0, Gallery ^5.0.0; optional renderer ^0.25.0 |\n| Editor | 0.25.3 | Core ^0.25.0; optional renderer ^0.25.0 |\n| Gallery | 5.0.0 | Independent owner of every default catalog record; 28 intent layouts |\n\n## Runtime\n\n| Requirement | Status |\n| --- | --- |\n| Node.js | `engines.node` is `>=22` on every package below; **Node 24** is the coordinated toolchain these records are verified on (the 0.12.2 set and later are tested on Node 22, 24 and 26) |\n| Package managers | npm for published installs; this repo uses pnpm 10.33.2 for core development |\n| Account / model / hosted API | Not required |\n| Operating systems | macOS, Linux, Windows for Node APIs; browser entrypoints are separate |\n\n## Historical coordinated 0.18 published packages\n\n| Package | Version | Depends on |\n| --- | --- | --- |\n| `@openpresentation/opf` | 0.18.4 | optional peers `@openpresentation/opf-render@^0.18.0` and `@openpresentation/opf-pptx@^0.18.0`, which the Node build loads for `convert`, `open` and `save` |\n| `@openpresentation/cli` | 0.18.1 | `@openpresentation/opf@^0.18.3` (a regular dependency, no bundled copy); optional peers `@openpresentation/opf-render@^0.18.0` and `@openpresentation/opf-pptx@^0.18.0` |\n| `@openpresentation/opf-render` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peers (RR-63): the converters `sharp`, `@resvg/resvg-js` and `pdf-lib` and the `@expo-google-fonts/*` font packages, which a host installs for the formats and fonts it uses |\n| `@openpresentation/opf-editor` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peer `@openpresentation/opf-render@^0.18.2` |\n| `@openpresentation/opf-pptx` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peer `@openpresentation/opf-render@^0.18.2` |\n\nThe 0.18 set is the consistent-API release ([design](programs/release-readiness/0.18-consistent-api.md)), breaking:\n- **One import (RR-70).** `import * as opf from \"@openpresentation/opf\"` works in every runtime through conditional\n exports, and `/node` is removed. `defaultCatalog` lives at `@openpresentation/opf/catalog` only.\n- **Names (RR-73) and options (RR-74).**\n - `ingest` replaces `importData` (and `opf ingest` replaces `opf import-data`).\n - `toSvg`, `toPng`, `toPdf` and `toHtml` work on decks.\n - The editor exports with `convert`.\n - `convert` takes `text`, `raster`, `charts`, `images`, `fonts` and `name`.\n- **Logos (RR-71).** Logos come from `organizations[].logo` (`full`, `stacked`, `icon`, `wordmark`, `onLight`/`onDark`),\n placed with `var:organization.logo.*`. `LogoSet` and zone `logo: true` are removed.\n- **PowerPoint (RR-72).** Shared furniture is written once on the PPTX slide master or layout.\n- **CLI (RR-75).** `opf convert` absorbs `render`, `export` and `import`. There is one output convention, plus\n `opf doctor` and a multi-file `validate`.\n\nSee the 0.18.0 and 0.18.1 entries of the [changelog](../CHANGELOG.md) and the 0.18.0 entry of the [CLI changelog](../packages/cli/CHANGELOG.md).\n\nCore starts at **0.18.1**. `opf-v0.18.0` was tagged, but its publish run failed before `npm publish`: a core test\nspawned the CLI, which the publish workflow had not built. So 0.18.1 is the first published 0.18 core.\n\nThe 0.17 set (core, editor and PPTX 0.17.0, renderer 0.17.1, CLI 0.17.1) was the lockstep minor after 0.16, also breaking:\n- **Furniture variables (FA-31):** header and footer text uses the `{{slide.number}}`, `{{slide.section}}` and\n `{{deck.slideCount}}` variables, and the `HeaderFooterItem` flags are removed.\n- **`@openpresentation/opf/node` (RR-62):** `convert`, `open` and `save` work from files and bytes. `parse` and\n `stringify` replace `readDeck` and `writeDeck`. `opf convert` is new, and `@openpresentation/cli/api` is removed.\n- **Fixes and additions:**\n - deterministic data import;\n - `validate` and `paginate` that plan script faces (RR-59);\n - the Arabic and Latin preview fixes;\n - `renderSvg(deck, { textAsPaths: true })` (RR-64 phase 1);\n - the Windows CLI path fixes (RR-66).\n\nSee the 0.17.0 entries of the [changelog](../CHANGELOG.md) and the 0.17.1 entry of the\n[CLI changelog](../packages/cli/CHANGELOG.md).\n\nRenderer 0.17.1 is a patch on the 0.17 line: SVG never embeds a face whose OS/2 `fsType` forbids it (RR-76), embedded faces are cut to the glyphs a slide draws (RR-65, mean example slide 2.4 MB to 59 KB), and `textAsPaths` shapes with HarfBuzz and keeps an invisible text layer (RR-64 phases 2 and 3).\n\nThe CLI starts at **0.17.1**. `cli-v0.17.0` was tagged, but its publish run failed before `npm publish`, because the\nrelease commit carried a stale generated gallery tracker. So 0.17.1 is the first published 0.17 CLI.\n\nThe 0.16 set (core, CLI, renderer and editor 0.16.0, PPTX 0.16.1) was the lockstep minor after 0.15 (owner decision 2026-10-08: no 0.15.x patch train). It adds nested\nplaceholder groups in layout records (FA-26), `Table.alt` (FA-27), the `opf/language-tag` warning (FA-29), Markdown\ndecks (`.opf.md`) in every command with core `readDeck`/`writeDeck` (RR-60), SVGs that embed only the faces they draw\n(RR-61), the CLI's programmatic `@openpresentation/cli/api` (RR-62), a lighter renderer install with optional\nconverters and fonts and the `/png`, `/pdf` and `/html` subpaths (RR-63), and the developer-contract fixes (RR-59).\n0.15 documents are 0.16 documents. See the 0.16.0 entries of the [changelog](../CHANGELOG.md) and the\n[CLI changelog](../packages/cli/CHANGELOG.md).\n\nThe 0.15 set (core 0.15.1, CLI 0.15.0, renderer, PPTX and editor 0.15.0) was the breaking spec line (format audit wave C): catalogs group the records a document embeds by the catalog they\ncame from (`catalogs.default`, `catalogs.custom`, named groups, `name:id` references), core ships no built-in records\n(the default catalog is the opt-in `@openpresentation/opf/catalog`, which a host registers with `{ catalogs }`), a\nfull-bleed photo is `design.background` and every content picture is an image block, and `language` is a BCP-47 tag.\nSee the 0.15.0 and 0.15.1 entries of the [changelog](../CHANGELOG.md).\nCore 0.15 started at **0.15.1**: `opf-v0.15.0` was tagged, but its publish run failed before `npm publish` (the CLI tests\nthrough the published renderer and PPTX ran in the core publish workflow before those 0.15 siblings existed), so 0.15.1\nwas the first published 0.15 core and the floor of every 0.15 sibling.\n\nOPF 0.15 is a breaking spec line (format audit wave C): catalogs group the records a document embeds by the catalog they\ncame from (`catalogs.default`, `catalogs.custom`, named groups, `name:id` references), core ships no built-in records\n(the default catalog is the opt-in `@openpresentation/opf/catalog`, which a host registers with `{ catalogs }`), a\nfull-bleed photo is `design.background` and every content picture is an image block, and `language` is a BCP-47 tag.\nThe 0.15 set reads and writes only 0.15 documents; see the 0.15.0 and 0.15.1 entries of the [changelog](../CHANGELOG.md).\nCore starts at **0.15.1**: `opf-v0.15.0` was tagged, but its publish run failed before `npm publish` (the CLI tests\nthrough the published renderer and PPTX ran in the core publish workflow before those 0.15 siblings existed), so 0.15.1\nis the first published 0.15 core and the floor of every 0.15 sibling.\n\nFrom CLI 0.16.0 the CLI depends on core (`@openpresentation/opf`, a regular dependency, not a bundled copy). CLI 0.16.0\nexposed `@openpresentation/cli/api`, and 0.17 removes it in favour of core's `@openpresentation/opf/node`. The renderer\nand PPTX stay optional peers. CLI 0.15.0 and earlier bundled their core.\n\nInstall the complete pinned set. A caret range starting at 0.10.1 does not\ninclude 0.11.x; old consumers can install a second core and do not establish\nColorRef preview/export support. The selected renderer 0.25.0 and PPTX 0.25.0 and editor 0.25.3 require core ^0.25.0;\nCLI 0.25.0 accepts core ^0.25.0 (CLI 0.24.1 and earlier accept only core 0.24). Pin core 0.25.0 alongside them and\nverify one installed Core so preview and export use the same composition.\nCore 0.20.2 and later center metrics when alignment is omitted; explicit alignment remains\nauthoritative.\n\nShared header/footer geometry (`furniture-flow-v2`) is published. PPTX exports\neditable slide shapes tagged `OPF_FURNITURE_V1` with provenance for controlled\nreimport. Published PPTX through 0.11.8 draws every part that way, not as native\nOffice Header/Footer objects. PPTX 0.11.9 and later (RR-11) write the footer's\nfirst text, date and slide number as native `ftr`, `dt` and `sldNum` placeholders\n(with master/layout placeholders, `p:hf` flags and a notes-master flag) at the same\ngeometry and reads them back with or without provenance; the rest stays tagged\nshapes. Native PowerPoint acceptance remains [issue 87](https://github.com/OpenPresentation/opf/issues/87).\nPPTX 0.18 (RR-72) writes a tagged part that is the same on two or more slides once,\non the slide master or a shared layout (one per background tone for `onLight`/`onDark`\nlogos; a hidden footer uses a layout that hides the master's), and reads it back from\nthere ([rr-72-master-furniture.md](programs/release-readiness/rr-72-master-furniture.md)).\n\nThe [Windows native-picture checkpoint](evidence/windows-native-picture-20260921/README.md)\nand accepted [native B/C bundle](evidence/windows-native-edits-20260921/README.md)\nrecord finite picture/furniture edits, current-content provenance reimport and\nsafe fallback, production notes packaging and two controlled reordered-file\nrefusals. Core105 publishes that evidence; PPTX47 adds the tested harness, with\nno new package version. UI image replacement changes geometry, longer header\ntext clips, and duplicated tagged headers overlap. Refused workers retain their\nfailed cleanup state separately from later empty-workspace observations.\nThis is not general native layout/reflow fidelity.\n\nAccepted core106's [tab and font checkpoint](evidence/windows-native-tabs-fonts-20260921/README.md)\nrecords plain native tab target error **0.022655487060546875pt** and tab/literal\ndifference **0.022678375244140625pt**, both above the unchanged **0.02pt** gate.\nIts bounded four-face Carlito edit/save/reopen control passes exact text/style\npersistence, zero observed bounds drift, matching rasters and owned font cleanup.\nMixed-size table fidelity, physical glyph-font identity, fallback/synthesis and actual embedding remain\nopen; embedding was disabled for this control. The Windows supervisor retains sole\nOffice control. This documentation task reads evidence and makes no Office calls.\n\nThe later [read-only font inventory](evidence/windows-native-font-inventory-20260921/README.md)\nretains the four Carlito text styles but reports both Carlito and unexpected\nAptos in `Presentation.Fonts`. The native font allowlist fails, and no embedding\nwas attempted. The original parent report incorrectly fails cleanup because of\na Windows PowerShell 5.1 JSON-array parsing defect; raw stages and registration\nrows establish one owned close and four removals in a separately labeled offline\naudit. The raw failure remains intact. Collection names and flags do not identify\nthe physical font used for each glyph.\n\nThe [read-only mixed-table observation](evidence/windows-native-mixed-table-20260921/README.md)\nretains all 245 characters, one literal tab and five authored runs, with outer\ngeometry within 0.02pt and confirmed owned close/font cleanup. Native soft-line\nboundaries are 92/194 versus the estimated preview's 78/172, and native default\ntab spacing is 72pt. These finite content/style results do not pass table\nedit/save/reopen, browser/native raster agreement or physical glyph identity.\nThe accepted [nine-pair offline tab analysis](evidence/windows-native-tab-analysis-20260921/REPORT.md)\nfinds a 0.05pt-compatible pattern in the observed character starts, with finer saved\ntab coordinates. These inputs do not distinguish relative versus absolute placement\nor establish an internal engine cause. The 0.02pt native tab gate remains failed;\nthe separate 0.1px renderer gate is unchanged. Accepted core108 `9b277e1` and\ncore109 `b2711549` publish bounded evidence only. Windows-owned [core110](https://github.com/OpenPresentation/opf/pull/110)\nis merged as `4f7a4bd494f1a873319eff897423d301d1cfc9d6`, from reviewed fc3c36e\nwith four required PR checks passing. [Renderer30](https://github.com/OpenPresentation/opf-render/pull/30) is now merged\nas `c8d7d5ca1f67a7b39f70c7c4bd14577a865b175b`, with exact-head CI 35661051100\npassing. The supervisor reports postmerge 35661504472 also passed. Its companion\nsource preserves rich-tab advances/spans; the [accepted Windows wrap-up](handoff-windows-native-2026-09-21-wrap-up.md)\nrecords bounded source-linked rendering/browser checks and the original missing-test\nCI failure. These checks do not update the frozen registry consumer. Core111\n`3c5048522714365a41d9b5b9ba81620affae718b` publishes the font-inventory evidence\nabove with both PR workflows green; its postmerge workflows were started at the\nfinal notice, not recorded as passed. Package/lock/release/site pins, schema,\ngoldens and tolerances are unchanged. Native allowlist and physical-glyph/embedding\nacceptance remain open; no new package train or broad native pass is inferred.\n\n## Independently released packages\n\n| Package | Version | Depends on |\n| --- | --- | --- |\n| `@openpresentation/gallery` | 5.0.0 (independently published and verified; prior 4.0.0 (2026-10-19), 3.0.0, 2.0.0 and 1.0.0 remain historical) | nothing (its `GalleryCatalog` type has the shape of `@openpresentation/opf`'s `Catalog`) |\n\nGallery 4.0.0 (published and independently verified 2026-10-19) adds theme `variants` and removes the `dark` theme; core 0.24 and its siblings depended on `^4.0.0`. It declares catalog compatibility generation 3. Gallery 5.0.0 (published and independently verified; layouts as one list of regions, catalog generation 4) is the range core 0.25.0 and this checkout depend on (`^5.0.0`); the published set depends on it from core 0.25.0, and so does CLI 0.25.0 (CLI 0.24.1 and earlier, on Gallery `^4.0.0`, cannot read it).\n\nThe gallery (RR-78) is the package `packages/gallery` of this repository. Since 0.26.0 it is released with the other five at one\nshared version (`vX.Y.Z` tags, `npm-publish.yml`); until 0.25.0 it was released from its own repository on its own line, which\nended at 5.0.0 (Gallery 1.0.0 to 5.0.0 stay on npm). New catalog records go in any release; a removed id or a changed drawing needs a\nminor release ([release process](release-process.md)). From OPF 0.19 core and the\nCLI consume the Gallery package (Gallery 2 in OPF 0.19; Gallery 3 in OPF 0.20), and core has no `/catalog` subpath; up to 0.18 core ships the same records as\n`@openpresentation/opf/catalog`.\n\n## Supported in this set\n\n| Capability | How | Notes |\n| --- | --- | --- |\n| JSON authoring | `*.opf.json` plus CLI `opf create` | Local files only |\n| Bundled examples catalog | `@openpresentation/opf/examples` | **127** decks; the quickstart JSON is a docs fixture, not a 128th catalog entry |\n| Validate | `validate` / `opf validate` | One read-only checker: format, references, policy, accessibility, layout and content findings ([guide](validate.md)); no network catalog fetch |\n| Color references | `ColorRef`, `variables`, `resolveColorRef` | Core schema/resolution, renderer preview and PPTX resolved colors are shipped. Supported theme-slot colors write native `schemeClr` references; arbitrary native Office recoloring and full visual fidelity remain separately scoped. |\n| Embed catalog records | `embed` / `opf embed` | Embeds every record a document references, in the group it resolves in, so it renders with no catalog registered; remote media and data remain explicit host concerns. |\n| Offline fonts | `loadFonts` (`/fonts-node`) | Bundled Roboto pack (`pack: 'base'`) or the office pack; hashed files; the handle is passed as `{ fonts }` to every deck-level call |\n| Composition | `composeSlide` | Includes shared headers/footers |\n| Pagination | `paginate` / `opf paginate` | Returns mappings; preserves source |\n| Edit + undo | `@openpresentation/opf-editor` `createEditorSession` | JSON Patch undo/redo |\n| JSON Patch CLI | `opf edit` | No persistent CLI undo history |\n| SVG preview | `toSvg(deck)` (every slide) / `toSvg(deck, n)` (slide n, counted from 1) (opf-render 0.18; `renderSvg` / `renderSlideSvg` before) | Local; the same `{ fonts }` as layout; `text: \"paths\"` draws the text as glyph outlines |\n| Convert, export and import files from code | `convert`, `open` and `save` of `@openpresentation/opf` in Node (core 0.18.0 and later; core 0.17 had them at `@openpresentation/opf/node`; CLI 0.16.0 had `exportDeck` / `importDeck` in `@openpresentation/cli/api`) | PDF, PNG, SVG and PPTX out, PPTX in, deck forms both ways, with the bundled fonts, no network and no clock; calls opf-render and opf-pptx as optional peers and throws `OPFExportError` / `OPFImportError` with the install command when one is missing. `opf convert` runs the same engine (CLI 0.18; `opf render`, `opf export` and `opf import` before it) ([OPF files in Node](node.md)) |\n| Read and write a deck in JSON, YAML or Markdown | `parse` / `stringify` (core 0.17.0 and later; `readDeck` / `writeDeck` in 0.16) | One entry point for the three text formats; `parse` throws `OPFValidationError` with located findings |\n| PNG | `toPng` (opf-render 0.18; `svgToPng` before) | Node raster of a deck or of SVG slides |\n| PDF | `toPdf` (opf-render 0.18; `svgToPdf` before) | opf-render 0.12.0 and later (RR-12, opf-render#90): **vector with selectable text by default** (embedded TrueType subsets of the supplied/bundled fonts, ToUnicode, links, metadata, tagged structure); `raster: true` keeps the image-per-slide output (`mode: \"raster\"` before 0.18). Renderers up to 0.11.9: raster-backed, not selectable text. Not a PDF/UA or PDF/A claim; see the renderer's `docs/evidence/rr-12-vector-pdf.md` for reader limits |\n| Editable PPTX export | `toPptx` | OPF \u2192 PPTX serialization. Text is one editable box per wrapped line, so edited text does not reflow in PowerPoint or Keynote, and a wrapped title is not the Title placeholder; the opt-in `bodyText: \"native\"` mode (CLI `--body-text native`, from opf-pptx and CLI 0.22.0) writes whole paragraphs, a wrapped title as the Title placeholder. Shared furniture is written on the master or layout; supported footer placeholders and tagged parts retain controlled re-import provenance. General native Office behavior remains separately scoped |\n| Agent skills | `opf skills install` | Offline after the CLI is installed |\n| Browser canvas | `@openpresentation/opf-editor/canvas` | Host must supply font bytes |\n\n## Public sites\n\nThe current coordinated adoption is recorded in the [final adoption note](evidence/rr-85-final-adoption-2026-10-16/README.md). The following source, CI and canonical production results are the historical September 21 checkpoint, recorded in the\n[font-readiness checkpoint](evidence/font-readiness-acceptance-20260921/README.md),\n[prior Inspector actions checkpoint](evidence/inspector-current-actions-20260921/README.md),\n[earlier publication checkpoint](evidence/inspector-share-acceptance-20260921/README.md),\n[source-preservation checkpoint](evidence/author-source-acceptance-20260921/README.md),\n[completion checkpoint](evidence/completion-acceptance-20260921/README.md),\n[earlier acceptance ledger](evidence/issue88-final-20260921/README.md) and\n[handoff](handoff-2026-09-21.md). [Issue88](https://github.com/OpenPresentation/opf/issues/88)\nremains open. Package adoption, deployed features and complete workflow\nacceptance are separate claims.\n\n| Surface | Deployed scope and acceptance | Source commit |\n| --- | --- | --- |\n| [openpresentation.org](https://www.openpresentation.org) | Current published guides, agent skills, JSON/preview workflow and downloads. Exact canonical deployment passes 321 checks across 11 pages and 18 raw resources, plus two browser flows for agent installation/navigation and JSON/SVG/PPTX downloads. Reviewed screenshots and output hashes match the accepted build. | `a85bcc77d899ce9ba1df659548be564142c16120` |\n| [pptx.dev](https://www.pptx.dev) `/inspector` and `/author` | App54 merged/live on exact READY production. Premerge Linux/Windows pass 704 units, 13 standalone controls and 39/39 browsers. Full canonical acceptance **fails (34/39 passed)** at five no-POST assertions; the bounded audit does not establish an introduced upload regression. Postmerge Linux 39/39 passes, Windows 38/39 fails initial font readiness. Preset Undo all and broader source writers remain unresolved. | `8f54228a9e38a1dcc0bf8188bcdd519795b3799a` |\n| [pptx.gallery](https://www.pptx.gallery) `/docs`, `/editor` and gallery pages | Published ColorRef/bundle guidance, Playground and Editor actions, and the canonical docs-to-editor flow are verified. | `f17e9ae5869669d5fbac3720f285652d0c37551c` |\n\nThe site uses documentation source `120a770`, whose tree matches accepted core\nPR98 commit `b1ff81db6f8714b0db1a98bde482ed8a64d0ccc9`. Core PR93/97/98/99/100/102/103 passed\npre-merge and post-merge CI. Accepted core102 is\n`578bcc6e0894129b00059258bd4ad1994a414baa`; its reviewed and accepted trees match,\nand all four required pre/post-merge runs passed on their first attempts. The\n[core102 receipts](evidence/inspector-share-acceptance-20260921/README.md#accepted-core102)\npin this documentation checkpoint without changing the site's older accepted\ndocumentation snapshot. The site's complete guides and raw resources match\nthe reviewed source; binary evidence remains linked and downloadable without\nbeing decoded into the AI-facing guide.\nCore104 `3d301f1` preserves exact postmerge OPF success and coordinated cancellation;\nit is not a complete green postmerge gate. Accepted descendant core105\n`84e914710520a7b0e777fce30e5758ee64a64924` preserves all 60 core104 evidence blobs\nand passes both exact-head workflows on their first attempts. [Compact receipts](evidence/inspector-current-actions-20260921/README.md#core-source-and-ci)\nkeep descendant acceptance separate from the canceled predecessor run.\nAccepted core106 `3847f712ccb2379952bcc8ab7c9fdbaedfd0a4ce` also passes both\npre/postmerge workflows on their first attempts. Its [compact receipt](evidence/inspector-current-actions-20260921/core106/acceptance-receipt.json)\nbinds the bounded native evidence above without completing general compatibility.\nAccepted core107 `5bc0d3f89414b382b2ce48452c7e56e5d66aaf74` has reviewed tree\n`ffdc678beadf0808bc717d67e7fc0a9ec4790127`. Both original postmerge push workflows\n**35652360502 / 35652360551** passed on attempt 1 under Node24.20.0.\n[Compact receipts](evidence/font-readiness-acceptance-20260921/README.md#core107-acceptance)\nretain earlier automatic premerge cancellations separately from the later automatic\nsuccessful pair; no rerun or accepted checkpoint relabels them.\n\nApp47 corrects the pre-app47\ncompletion adapter's rejected layout choices while preserving unchanged source\ntokens and undo history. Accepted commit `0f35352a1445f56ad4bb7c9f4c5609e01f2dd9ae`\nhas reviewed tree `203bdab509d05911f04f234d996f9c91f2b5e4f2`, green Linux/Windows\npre/post-merge CI and its exact READY canonical deployment. The historical App47 **23/24** production run passes all five new completion cases but still fails the existing\nLF Author third-popup assertion. This does not establish complete public-surface\nacceptance; the [historical App47 report](evidence/completion-acceptance-20260921/canonical/REPORT.md)\nand [earlier failed app45/app46 results](evidence/issue88-final-20260921/README.md)\nretain their evidence and unresolved causes.\n\nThe [source audit](evidence/completion-acceptance-20260921/source-preservation-audit/REPORT.md)\nidentified Author canvas/Copy/export and Inspector JSON-download normalization.\nMerged App53 at\n`e40c287b64fcbcfb85fb4a8a50641aea8e3e54a8` has the identical reviewed b33dc18\ntree and preserves those bounded raw-source\npaths and corrects order-only reimport history. A public Suggest-action guard\naddresses the observed stale Quick Input context competing with focused-editor\nCtrl+Space. Current local checks pass **627 unit tests and 29/29 browser cases\nin 88.78 seconds**, zero retries. First-attempt Linux/Windows application CI\npassed 627 unit tests and 29 browser cases per platform; artifact CI also passed.\nThe [exact READY canonical run](evidence/author-source-acceptance-20260921/canonical/REPORT.md) passes **29/29**, zero retries, with matching deployment receipts before and after. Postmerge application CI fails Linux **28/29** while Windows passes **29/29**; both pass 627 unit tests and separate artifact CI passes. The [Linux failure](evidence/author-source-acceptance-20260921/app53/postmerge-ci/README.md) stops before security assertions because five default-deck canvases remain after the shared-load toast. No rerun or canonical pass replaces that failed gate. The retained draft preview was READY but not browser-accepted.\n\nThe earlier **24/27** import-undo regression, **26/27** local popup failure and\nold-head Windows **26/27** shared-load failure remain historical evidence.\nThe fresh successful Windows job retained its sanitized timing artifact, but\nonly Author timings survived; the Inspector pagehide snapshot is missing. This\ndoes not explain or fix the old readiness delay. Phase4\ncaptured no post-fix stale-context overlap, so causal stress is inconclusive.\nThe existing suggestion-details pane remains clipped; visibility is not legibility.\nThe [postmerge trace diagnosis](evidence/author-source-acceptance-20260921/inspector-share-diagnosis/REPORT.md)\nproves wrong-document automatic share-hash publication during import.\nApp54 first corrected automatic\npublication at `60c91f6`: authoritative source/format is checked before and after\nencoding, load/navigation guards remain, and obsolete `import=hash:` is removed.\nURL transfer preserves semantics, not raw spelling. Local 659-unit/31-browser\nacceptance does not replace original first-attempt CI **35638158483**, which\npassed Linux **31/31** but failed Windows **30/31** at the unchanged 45-second\nAuthor readiness deadline. Both publication cases passed. The [frozen diagnosis](evidence/inspector-share-acceptance-20260921/app54/windows-timeout/REPORT.md)\nretains bounded slow-delivery observations with unknown cause. Late assertions\nare not an in-budget pass. Browser History tests permit prior accepted content\nuntil first new publication; held-promise controls establish the narrower race guard.\n\nThe product correction was added at `57e5e59fddbc94346f142dc12d86a916228bf2ae`, tree\n`d9c3aab543c6a886a85c6ec07dd55e5ab22590cd`. It guards current snapshots for\nexplicit Copy/JSON/Share/PPTX/PDF/Author/Deckchat actions. Pending public canvas\ndrafts commit before capture, pointer-blur rejection survives session recovery,\nand newer source/load/navigation/unmount/action invalidates late results. Raw\nCopy and accepted same-format JSON bytes are preserved; handoffs remain semantic.\nAlready-started clipboard/download effects cannot be retroactively canceled.\nThe integrated portable standalone startup verifies packaged runtime assets,\nfonts and traced schema inputs without changing dependencies or published pins.\n\nLocal runtime checks pass **692 unit tests**, **7 standalone controls**, focused\n**8/8** and full **39/39** browser cases, zero retries, with visual review. The\nimmutable 93-file app bundle\nretains stale-draft negative evidence and the initial candidate **7/8** result.\nThe latter did not establish accepted pasted source before releasing mocked PDF 401;\nfinal visible-code preconditions change no product bytes or budgets. PDF/Deckchat\nare locally mocked. CRLF ingress normalized 279 to 274 LF bytes; subsequent exact\nactions preserve the accepted buffer, not that ingress boundary.\n\n**The original 57e packaging gate failed.** First-attempt CI **35645493900** failed Linux and\nWindows typecheck on archived `.spec.ts` evidence copies after each platform\npassed 692 units and 7 standalone controls. The exact-head preview failed with\n`module_not_found`; precise Vercel compiler logs were unavailable. [The first-attempt receipt](evidence/inspector-current-actions-20260921/app54/first57-ci/REPORT.md)\nrecords skipped build/browser steps and no uploaded artifacts. The passing runtime build\npredates those copies. A byte-identical `.ts.txt` archive correction produced captured App54 head\n`6dfc2584698c1306505929a2bc3e427996d66563`, tree\n`f3279495f7685945b7541c90d24f7239407639a0`. Its final-tree local typecheck passes\nwith all 305 product/test inputs unchanged. [The corrected receipt](evidence/inspector-current-actions-20260921/app54/corrected6df/commit-receipt.json)\nbinds its 106-file evidence bundle. [Exact-head CI 35646213757](evidence/inspector-current-actions-20260921/app54/corrected6df/ci/REPORT.md)\npasses 692 units, seven standalone controls, typechecks and build on both platforms;\nLinux passes **39/39** browsers. Windows executes **zero browser tests** because\nstandalone startup fails with `EPERM` while statting its packaged React dependency\nlink. No timing or test-result artifacts were uploaded. The exact-head preview\nis READY, which is metadata only; at that 6df checkpoint App54 was unmerged and undeployed to production.\nThe [current ledger](evidence/inspector-current-actions-20260921/README.md)\nretains the separate 60c readiness, 57e packaging and 6df startup failures. Production then remained App53 `e40c287`, with its original\nfailed Linux postmerge gate preserved separately from canonical **29/29**.\nThe startup-link correction at 4338e57\nthen reached all browser cases: Linux **39/39**, Windows **38/39** in first-attempt\nCI **35649707689**, with 692 units and 13 standalone controls passing per platform.\nThe sole Windows failure was initial canvas-title visibility at five seconds,\nbefore any edit/recovery operation; correct incoming source remained at loading\nfonts. Late font acquisition and eleven incomplete responses at teardown do not\nestablish a permanent stall or a dominant cause. The [failed gate](evidence/font-readiness-acceptance-20260921/README.md#preserved-failed-windows-gate)\nis preserved.\n\nApp54 font-preparation revision `a4eb88ab7aa585c9efb91de4c190c1f1c0c7d0eb`, tree\n`031d893855a540fd2a4d2ec2162605817b8dde16`, overlaps font acquisition with converter\nwarmup while readiness still waits for both. The offline converter barrier and all\n**33 faces / 9,317,044 bytes**, manifest, substitution/measurement policy and\n`document.fonts.ready` gate remain unchanged. Fresh local Node24.21.0 checks pass\n**704 units, 13 standalone controls and 39/39 browsers**, zero retries, including\nunchanged offline export/reimport. Immutable app evidence\nretains reviewed images, exact source/output bindings and the original failed run.\n\n**The original a4eb gate failed:** first-attempt application **35654753237** passes\nLinux **39/39** but fails Windows **38/39**; both pass 704 units, 13 standalone\ncontrols, typecheck and build. The Open in Author URL assertion exceeds its existing\nfive-second deadline at `inspector-actions.spec.ts:176`; later source checks are\nnot reached. Original4338 font readiness passes in this run. No navigation cause\nor data-loss finding is established. The [frozen final audit](evidence/font-readiness-acceptance-20260921/app54/final-a4eb-ci/release-audit.json)\nretains exact source/artifact bindings and the original failed trace; that failure remains preserved.\nThe Python artifact workflow is not applicable under its full-PR path filters,\nnot a fresh pass. Exact-head preview is READY metadata only; an\nunauthenticated request redirects to sign-in, with no preview-browser acceptance.\nAt that a4eb capture App54 was unmerged and production remained App53\n`e40c287`. The separate suggestion-details candidate remains unreleased and supplies\nno acceptance here. No local/browser pass broadens native compatibility.\nThe [bounded navigation diagnosis](evidence/font-readiness-acceptance-20260921/README.md#author-navigation-diagnosis-and-prospective-policy)\nrecords Loading Author and a delayed successful script response: 1490 bytes inferred\nfrom ETag, 766 compressed bytes recorded, actual body absent. Later DOM does not\naccept unreached assertions or identify a cause. App54 accepted merge\n`8f54228a9e38a1dcc0bf8188bcdd519795b3799a` retains reviewed 79ba tree\n`3639d3c14daec94d13711fa2b10f2b927df45eca`, preregisters `waitForURL(load)` before\nthe real action, matching `page.goto` within the unchanged 45-second test and default\nfive-second content budgets. It retains all oracles but deliberately removes the\nincidental five-second navigation deadline. Fresh local **39/39**, zero retries,\npasses in 112.780048s with reviewed source/images and prepared-tree build/typecheck.\nThe immutable app bundle\nretains original failures. Units/standalone controls were not repeated locally;\nfresh first-attempt application **35659187971 passes 704 units, 13 standalone\ncontrols and 39/39 browsers on both Linux and Windows**. Exact-head preview was\nREADY/protected, not browser accepted. The identical reviewed tree is merged/live\nat 8f on READY deployment `dpl_H1FtXx1QSuGxn3MwzJwWJpGtRg8b`, but full canonical\nacceptance **fails (34/39 passed)** at five no-POST assertions observing Clerk environment\nPOSTs. The [safe audit](evidence/font-readiness-acceptance-20260921/app54/canonical8f/write-audit/REPORT.md.txt)\nrecords ten such requests, nine with HTTP200/zero-length bodies and one incomplete.\nNo fixture-content needle was detected in captured fields; uncaptured data remains\nunknown. Three final action page-error assertions were not reached; the two share\ncases passed theirs. The prior auth/config comparison is 28/29 identical, with only\npackage scripts changed. Production is kept without a rollback, test change or\nrerun; the strict gate remains failed. Raw authentication-bearing diagnostics\nremain private. Postmerge CI 35660464578\nfinishes failed: Linux 39/39 passes while Windows 38/39 fails, with 704 units/13 controls/typecheck/build\npassing on each. Windows fails initial gallery-rail title visibility after 5,000ms\nwith correct source, clean schema and Loading slide fonts; later editing/export/\nreimport checks were not reached. The [final audit](evidence/font-readiness-acceptance-20260921/app54/merged8f/postmerge-ci/REPORT.md.txt)\npreserves this separate failed gate without cause inference or a retry. No canonical\npass or general native/font acceptance is claimed. That September 21 checkpoint was paused; the user resumed work on September 29. See the [current source checkpoint](handoff-runtime-2026-09-29.md) for ongoing repairs and release holds.\n\nSeparate local negative controls confirmed that preset Undo all discarded New run\nand imported replacement documents. The guarded correction is now preserved in\ndraft app #58: independent\nreview and local Node 24 checks passed (716 units, 13 standalone controls and\n49 browsers without retries). Original Linux/Windows CI could not start because\nof the account payment/spending-limit restriction; no application CI or production\nacceptance is claimed. Unbusy asynchronous account replacements still need a\nsynchronous invalidation guard and held-response control.\nOther local source writers still require their separate preservation checks. These unresolved local findings and raw imported-file/account/agent/metadata boundaries remain outside\nApp53 and App54. See the [App53 ledger](evidence/author-source-acceptance-20260921/README.md)\nand its immutable application evidence links. Issue88 remains OPEN. Native/font\ncompatibility, required repair and release gates remain separate; geometry is\ndeferred as the coordinated set below. The unimplemented worker candidate remains in the [handoff](handoff-2026-09-21.md).\n\nThe five coordinated geometry drafts (core94, renderer27, editor25, PPTX42,\nsite40) remain unmerged. In particular, site40 is not independently shipped.\n\n## Explicitly not shipped\n\n| Topic | Tracker | Do not describe as done |\n| --- | --- | --- |\n| Linux vs Chromium native-width residual at the 0.1px gate | [opf-render#24](https://github.com/OpenPresentation/opf-render/issues/24) | Rounding that fixes Linux but breaks macOS is rejected |\n| General native PowerPoint fidelity beyond the supported footer placeholders | [opf#87](https://github.com/OpenPresentation/opf/issues/87) | Finite B/C and bounded Carlito edit controls above are accepted evidence, as is one finite mixed-size table edit/save/reopen ([evidence](evidence/windows-native-mixed-edit-20260922/README.md)). Tab tolerance, general mixed-size table layout and preview/native wrapping, physical glyph identity/fallback/synthesis, embedding and general layout/reflow fidelity remain open; supported native footer placeholders, self-import and tagged furniture do not certify arbitrary Office behavior |\n| Public-surface acceptance checklist | [opf#88](https://github.com/OpenPresentation/opf/issues/88) | Shipping features does not establish every acceptance item; use the checklist and deployment receipts |\n| HarfBuzz / prepared-glyph shaping | Archive branches `codex/archive-shaping-20260915` | Prototypes are preserved, not in npm |\n| Selectable vector PDF | [pdf plan](plans/pdf-export.md) | Shipped in opf-render 0.12.0 (opf-render#90; the browser download entry `@openpresentation/opf-render/export-browser` ships there too); PDF/UA, PDF/A and viewer coverage beyond pdf.js, PDFium and poppler remain open |\n| General SVG diagrams / Mermaid | [diagrams plan](plans/diagrams-svg.md) | Embedded SVG \u2260 native editable primitives |\n| Full visual editor / IME / bidi / repair loop | [developer adoption](plans/developer-adoption-20260915.md) | Schema support \u2260 WYSIWYG coverage |\n\nCLI 0.9.2 and earlier do not render or export PPTX; CLI 0.10.0 adds `opf render`, `opf export` and `opf import`\nthrough the optional peers opf-render and opf-pptx ([CLI reference](cli.md)). The Node `toPng` / `toPdf`\n(`svgToPng` / `svgToPdf` before 0.18) APIs stay Node-only; renderer 0.12.0 adds the separate `@openpresentation/opf-render/export-browser` entry for browsers.\nCore's file API (`open`, `save`, `convert`) is Node-only too: it reads files, font files and the engines' native packages. From\ncore 0.18 the root resolves to a browser-safe build in a browser or a worker, with the same names, where those three reject\nwith `node-only`; every other core entry is browser-safe. In a browser, use the engines' browser entries.\n\n## Predecessor notes\n\n| Older set | Relationship |\n| --- | --- |\n| core 0.17.0, CLI 0.17.1, renderer 0.17.1, PPTX 0.17.0, editor 0.17.0 | Previous coordinated set (FA-31 furniture variables, `@openpresentation/opf/node`, `parse`/`stringify`, `opf convert`). The 0.18 set is breaking: one root import replaces `/node`, the `toSvg`/`ingest` names and `convert` options, organization logos, and the consolidated CLI. |\n| core 0.16.0, CLI 0.16.0, renderer 0.16.0, PPTX 0.16.1, editor 0.16.0 | Previous coordinated set (Markdown decks, used-face SVG embedding, optional renderer converters, nested placeholder groups, `@openpresentation/cli/api`). The 0.17 set is breaking: header and footer flags become `{{slide.number}}`-style variables (FA-31), `readDeck`/`writeDeck` become `parse`/`stringify`, and `@openpresentation/cli/api` is replaced by `@openpresentation/opf/node`. |\n| core 0.15.1, CLI 0.15.0, renderer 0.15.0, PPTX 0.15.0, editor 0.15.0 | Previous coordinated set (OPF 0.15, format audit wave C; CLI 0.15.0 bundles core 0.15.1). The 0.16 set is additive for documents; the renderer's converters and font packages became optional peers (RR-63), so a 0.16 host installs the ones it uses, and the CLI depends on core instead of bundling it (RR-62). |\n| core 0.14.0, CLI 0.11.0, renderer 0.14.0, PPTX 0.14.0, editor 0.14.2 | Previous coordinated set (format audit waves A and B, RR-55 and RR-56; CLI 0.11.0 bundles core 0.13.0). The 0.15 set is breaking: a 0.14 document's catalog shape (`catalogs.<kind>.records`), catalog-id languages and `design.slideImage` are invalid in 0.15, and the 0.15 engines need the host to register the default catalog. |\n| core 0.13.0, CLI 0.11.0, renderer 0.13.1, PPTX 0.13.2, editor 0.12.1 | Previous coordinated set (chart and table data, RR-54). |\n| core 0.12.2, CLI 0.10.1, renderer 0.12.2, PPTX 0.12.4, editor 0.11.3 | Previous coordinated set (the first set whose `engines.node` is the open-ended `>=22`). The 0.13 set adds chart and table data (RR-54, [contract](chart-table-data.md)): strict chart numbers (a value is a finite number or a strict decimal string; `\"12%\"`, `\"$5\"` and `\"Q1\"` are a gap with a `chart-value-not-numeric` warning, never a guessed number), number formats (`DataColumn` `{ name, format }`), top-level `datasets` with `{ \"dataset\", \"fields\" }` references and `chart.mapping`, in core 0.13.0, previewed by renderer 0.13.0, exported and re-imported by PPTX 0.13.0 (`OPF_DATASETS_V1` and `OPF_DATA_V1` provenance), edited in the editor 0.12.x data grid and imported by CLI 0.11.0 (`opf import-data --dataset`). PPTX 0.13.0 also vendors pptxgenjs-plus 4.3.4 (was PptxGenJS 4.0.1) and PPTX 0.13.1 bundles for browsers again. Renderer 0.13.1 and PPTX 0.13.2 pin Sharp 0.35.5 (GHSA-wq5f-xc86-pv6w; output unchanged); editor 0.12.1 replaces 0.12.0, which was never published. Renderer, PPTX and editor raise their core floor to `^0.13.0` and the renderer peer to `^0.13.1` together. |\n| core 0.12.1, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.2, editor 0.11.2 | Previous coordinated set. PPTX 0.12.3 corrects the table range of a chart's embedded workbook (apostrophe-quoted sheet references and bubble-series references), which made Keynote drop category charts on import (opf-pptx#162, opf-pptx#163; every other part of the package is byte-identical). Core, renderer and editor are unchanged and keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.2, editor 0.11.2 | Previous coordinated set. Core 0.12.1 deprecates the six plural audience ids (`executives`, `investors`, `customers`, `sales-team`, `marketing-team`, `regulators`) in favour of the singular ids: additive catalog data, validation warns and never errors, and no geometry moves, so renderer, PPTX and editor keep the core floor `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.1, editor 0.11.1 | Previous coordinated set (PowerPoint lists only the deck's fonts). PPTX 0.12.2 also names the deck's font in the theme's `Viet` (Vietnamese) and `Uigh` (Uyghur) per-language script entries, which PowerPoint applies to `vi-VN` and `ug` runs and which kept Office's Times New Roman and Arial for those two languages (RR-17; every other language is byte-identical). Editor 0.11.2 adds the `slide-sizes` and `purposes` switch dimensions, `SLIDE_SIZE_PRESETS` and the Slide size and Purpose selects in the Design panel (RR-41, additive API). Both keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.0, editor 0.11.0 | Previous coordinated set (the 0.12.0 train). PPTX 0.12.1 writes the deck's fonts so that PowerPoint's font list shows only the fonts the deck uses (FF-05: schema-order `presentation.xml`, an own notes theme, no east-asian or complex-script font on runs, and a theme east-asian slot that is never empty); editor 0.11.1 stops the restore prompt showing a literal `null`. Both keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.9, editor 0.10.6 | Previous coordinated set (native header/footer placeholders, SVG pictures, `fromPptx` import signals; the published 0.11.4 train measured in the font-fidelity program). Core 0.12.0 moves geometry (composed font sizes on PowerPoint's 0.01 pt grid, hanging wrap whitespace, promoted regions in reading order, right-to-left decks composed mirrored), so renderer 0.12.0, PPTX 0.12.0 and editor 0.11.0 raise their core floor to `^0.12.0` and the renderer peer of PPTX and editor to `^0.12.0` together; the set adds templates and variables, numbered lists, footnotes, citations and captions and chart options (additive schema), vector PDF with selectable text and the browser export entry, the `<opf-deck>` player, the editor's slide management, autosave, data grid, find and replace, image crop, Review panel and fill UI, and the shared JSON Patch module. CLI 0.10.0 bundles core 0.12.0 and adds `opf audit`, `from-md`, `to-md`, `diff`, `merge`, `format`, `render`, `export` and `import`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.8, editor 0.10.6 | Previous coordinated set (PPTX 0.11.8 re-imports wrapped rich text as one authored payload; CLI 0.9.2 bundles core 0.11.4). PPTX 0.11.9 writes a deck footer's first text, date and slide number as native PowerPoint Header & Footer placeholders (every export also carries the footer placeholders on its master, layout and notes master, so Insert > Header & Footer works), exports an SVG image as a native SVG picture over a PNG fallback (rasterized in Node by the optional opf-render peer or `options.svgRasterizer`) and adds the opt-in `fromPptx(bytes, {signals: true})` import signals; core floor `^0.11.4` unchanged. |\n| core 0.11.4, CLI 0.9.1, renderer 0.11.9, PPTX 0.11.7, editor 0.10.6 | Previous coordinated set (the design fields compose and export natively). PPTX 0.11.8 re-imports rich text that wraps over several native lines as one authored payload (the export records the line count; decks exported by 0.11.7 import as before) and keeps the core floor `^0.11.4`; CLI 0.9.2 bundles core 0.11.4 (CLI 0.9.1 bundled core 0.11.3) and still requires Node 24. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.6, editor 0.10.5 | Previous coordinated set (the native chartex export by default). Core 0.11.4 composes the design fields (logos on covers and section slides, `contentDirection`, `chartPrimary`, picture bullets, header and footer logos, the accent font), aligns a cover's tag and subtitle with its title, and sizes picture bullets and furniture images as PowerPoint does, so renderer, PPTX and editor raise their core floor to `^0.11.4` together; renderer 0.11.9 draws those fields and applies the tag contrast rule (FF-61: the tag draws in the text colour when the scheme primary is under 4.5:1); PPTX 0.11.7 exports them natively, writes every chart's text at the preview's size (FF-62: 12 pt, not 9 pt), writes slide sections as PowerPoint's section list and restores the authored form of a fresh export on import (a root payload returns as `slides.N.text`, `.items`, `.chart` ... rather than one typed block); editor 0.10.6 is a floor bump. CLI 0.9.1 still bundles core 0.11.3. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.5, editor 0.10.5 | Previous coordinated set (the slide tag draws in the scheme primary colour and PPTX writes it as `a:schemeClr accent1`; the playground loads its base faces through `extraLazyFonts`). PPTX 0.11.6 exports the treemap, histogram, pareto, box-and-whisker, waterfall and funnel charts as native chartex parts by default (`toPptx({chartex: 'auto'})`, confirmed in desktop PowerPoint; `world` stays a clustered column with `chart-data-adapted` because PowerPoint's map needs online geodata; pass `chartex: 'fallback'` for the previous output) and gives chartex text the deck's label colour and font. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.6, PPTX 0.11.4, editor 0.10.4 | Previous coordinated set (the 100-layout catalog and its geometry, category-axis label rotation, quote and slide image re-import). Renderer 0.11.7 adds the `extraLazyFonts` registry option and `splitStartupFaces` (a browser host can start with Roboto Regular alone and load its other base faces on demand); renderer 0.11.8 draws the slide tag in the scheme primary colour; PPTX 0.11.5 writes the tag run as `a:schemeClr accent1` where the deck theme holds the primary (the colour is unchanged); editor 0.10.5 loads its playground base faces through `extraLazyFonts`. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.5, PPTX 0.11.3, editor 0.10.3 | Previous coordinated set (native classic and chartex chart previews, opt-in chartex export, face-level lazy fonts and the font gate's render options). Core 0.11.3 adds the pinned pptx.gallery default catalog and the 70 legacy gallery layout ids (layouts 30 to 100; 25 carry a `composition` or `contentBox` contract, which moves geometry, so renderer, PPTX and editor raise their core floor to `^0.11.3` together); renderer 0.11.6 rotates and skips dense category-axis labels; PPTX 0.11.4 re-imports quote and slide image payloads and writes theme `a:ea`/`a:cs` only where a script font is selected; editor 0.10.4 is a floor bump; CLI 0.9.1 bundles core 0.11.3. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.1, PPTX 0.11.0, editor 0.10.0 | Previous coordinated set (lockstep floors, Intos and the open families, selected-name export). Renderer 0.11.2 adds script-face loading (`scripts: 'auto'`); PPTX 0.11.1 adds `design.watermark` export; editor 0.10.2 loads the fonts a document needs before every render (FF-41). Renderer 0.11.3 previews every kept classic chart type natively; PPTX 0.11.2 exports the native construct for each kept classic chart type (with `chart-data-adapted` diagnostics where data is adapted) and writes theme colour references for table and text colours. Renderer 0.11.4 previews the seven chartex chart types natively (the world map as a non-geographic tile grid), keeps the Latin Noto Sans replacement for script schemes under `scripts: 'auto'`, shapes Noto Sans Mongolian, and bundles Raleway and Playfair Display (94 lazy faces); PPTX 0.11.3 adds the opt-in `toPptx({chartex: 'native'})` export of the chartex chart types (the default output is unchanged) and always imports chartex charts. |\n| core 0.11.0, CLI 0.9.0, renderer 0.9.0, PPTX 0.9.1, editor 0.8.0 | Previous coordinated Node 24 set (ColorRef, shared furniture). Renderer and PPTX had different core floors from 0.10.x. |\n| core 0.10.0, renderer/PPTX/CLI 0.8.0, editor 0.7.0 | Previous coordinated Node 24 baseline. Lint and furniture landed across 0.10.0/0.8.0 then layout-placeholder fixes in 0.10.1/0.8.1/0.7.1. |\n| Node 20 | Not valid for these packages |\n| Node 22 / 26 | Declared by the current set (`engines.node` `>=22`) and tested, but Node 24 is the toolchain the evidence runs on. Releases up to core 0.12.1, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.3 and editor 0.11.2 declared `24.x`, so npm on Node 22 or 26 silently picked an older release |\n\nDo not install sibling `../opf-render` dist folders when following the\nquickstart. Packed and registry consumers must resolve `@openpresentation/*`\nfrom npm.\n"
|
|
37
|
+
"markdown": "# Compatibility matrix\n\nPublished registry evidence for the coordinated Node 24 toolchain. This matrix\nis the honest supported subset for [the developer quickstart](quickstart.md).\nIt is not universal Office parity and does not describe archived prototypes as\nshipped.\n\n**Published set (2026-10-24):** core, renderer, PPTX, editor, CLI and Gallery,\nall at the shared version 0.26.1. `node scripts/registry-plan.mjs` prints each\nversion with the registry `gitHead` of its publisher commit.\nEarlier matrices and native observations retain their historical identities.\nComplete native compatibility remains a separate gate.\n\n## Published layout contract and the 0.20 engines\n\nA layout record is a `direction` and one ordered `regions` list of regions and\nsplits, nested for two dimensions (FA-33: core 0.25.0 and Gallery 5.0.0, catalog\ncompatibility generation 4). `areas`, `columns`, `rows` and the `regions` map are\ngone, and Gallery 3 and 4 records (generation 3, written as nested `areas`\narrays) no longer validate on core 0.25. Gallery owns every default record;\nper-kind schema versions remain v2. Core 0.19 and later consume the Gallery\npackage and do not export `@openpresentation/opf/catalog` or ship a catalog\nsnapshot.\n\nThe 0.20 engines share measured quote allocation, the 32 reference pixel\nstarting size for lists without an authored size, neutral structural borders\nand default right alignment for numeric table columns. Core and CLI forward\nregistered catalogs during PPTX import. PPTX preserves stretch images with\nzero crop through native save/re-import. These published contracts do not\nimply universal Office or full-catalog visual parity.\n\n## Current published package contracts\n\n| Package | Version | Dependency contract |\n| --- | --- | --- |\n| Core | 0.26.1 | Gallery ^0.26.1; optional renderer/PPTX ^0.26.1 |\n| CLI | 0.26.1 | Core ^0.26.1, Gallery ^0.26.1; optional renderer/PPTX ^0.26.1 |\n| Renderer | 0.26.1 | Core ^0.26.1; optional pinned font/converter peers |\n| PPTX | 0.26.1 | Core ^0.26.1, Gallery ^0.26.1; optional renderer ^0.26.1 |\n| Editor | 0.26.1 | Core ^0.26.1; optional renderer ^0.26.1 |\n| Gallery | 0.26.1 | Owner of every default catalog record; 28 intent layouts; same records as 5.0.0 |\n\n## Runtime\n\n| Requirement | Status |\n| --- | --- |\n| Node.js | `engines.node` is `>=22` on every package below; **Node 24** is the coordinated toolchain these records are verified on (the 0.12.2 set and later are tested on Node 22, 24 and 26) |\n| Package managers | npm for published installs; this repo uses pnpm 10.33.2 for core development |\n| Account / model / hosted API | Not required |\n| Operating systems | macOS, Linux, Windows for Node APIs; browser entrypoints are separate |\n\n## Historical coordinated 0.18 published packages\n\n| Package | Version | Depends on |\n| --- | --- | --- |\n| `@openpresentation/opf` | 0.18.4 | optional peers `@openpresentation/opf-render@^0.18.0` and `@openpresentation/opf-pptx@^0.18.0`, which the Node build loads for `convert`, `open` and `save` |\n| `@openpresentation/cli` | 0.18.1 | `@openpresentation/opf@^0.18.3` (a regular dependency, no bundled copy); optional peers `@openpresentation/opf-render@^0.18.0` and `@openpresentation/opf-pptx@^0.18.0` |\n| `@openpresentation/opf-render` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peers (RR-63): the converters `sharp`, `@resvg/resvg-js` and `pdf-lib` and the `@expo-google-fonts/*` font packages, which a host installs for the formats and fonts it uses |\n| `@openpresentation/opf-editor` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peer `@openpresentation/opf-render@^0.18.2` |\n| `@openpresentation/opf-pptx` | 0.18.2 | `@openpresentation/opf@^0.18.4`; optional peer `@openpresentation/opf-render@^0.18.2` |\n\nThe 0.18 set is the consistent-API release ([design](programs/release-readiness/0.18-consistent-api.md)), breaking:\n- **One import (RR-70).** `import * as opf from \"@openpresentation/opf\"` works in every runtime through conditional\n exports, and `/node` is removed. `defaultCatalog` lives at `@openpresentation/opf/catalog` only.\n- **Names (RR-73) and options (RR-74).**\n - `ingest` replaces `importData` (and `opf ingest` replaces `opf import-data`).\n - `toSvg`, `toPng`, `toPdf` and `toHtml` work on decks.\n - The editor exports with `convert`.\n - `convert` takes `text`, `raster`, `charts`, `images`, `fonts` and `name`.\n- **Logos (RR-71).** Logos come from `organizations[].logo` (`full`, `stacked`, `icon`, `wordmark`, `onLight`/`onDark`),\n placed with `var:organization.logo.*`. `LogoSet` and zone `logo: true` are removed.\n- **PowerPoint (RR-72).** Shared furniture is written once on the PPTX slide master or layout.\n- **CLI (RR-75).** `opf convert` absorbs `render`, `export` and `import`. There is one output convention, plus\n `opf doctor` and a multi-file `validate`.\n\nSee the 0.18.0 and 0.18.1 entries of the [changelog](../CHANGELOG.md) and the 0.18.0 entry of the [CLI changelog](../packages/cli/CHANGELOG.md).\n\nCore starts at **0.18.1**. `opf-v0.18.0` was tagged, but its publish run failed before `npm publish`: a core test\nspawned the CLI, which the publish workflow had not built. So 0.18.1 is the first published 0.18 core.\n\nThe 0.17 set (core, editor and PPTX 0.17.0, renderer 0.17.1, CLI 0.17.1) was the lockstep minor after 0.16, also breaking:\n- **Furniture variables (FA-31):** header and footer text uses the `{{slide.number}}`, `{{slide.section}}` and\n `{{deck.slideCount}}` variables, and the `HeaderFooterItem` flags are removed.\n- **`@openpresentation/opf/node` (RR-62):** `convert`, `open` and `save` work from files and bytes. `parse` and\n `stringify` replace `readDeck` and `writeDeck`. `opf convert` is new, and `@openpresentation/cli/api` is removed.\n- **Fixes and additions:**\n - deterministic data import;\n - `validate` and `paginate` that plan script faces (RR-59);\n - the Arabic and Latin preview fixes;\n - `renderSvg(deck, { textAsPaths: true })` (RR-64 phase 1);\n - the Windows CLI path fixes (RR-66).\n\nSee the 0.17.0 entries of the [changelog](../CHANGELOG.md) and the 0.17.1 entry of the\n[CLI changelog](../packages/cli/CHANGELOG.md).\n\nRenderer 0.17.1 is a patch on the 0.17 line: SVG never embeds a face whose OS/2 `fsType` forbids it (RR-76), embedded faces are cut to the glyphs a slide draws (RR-65, mean example slide 2.4 MB to 59 KB), and `textAsPaths` shapes with HarfBuzz and keeps an invisible text layer (RR-64 phases 2 and 3).\n\nThe CLI starts at **0.17.1**. `cli-v0.17.0` was tagged, but its publish run failed before `npm publish`, because the\nrelease commit carried a stale generated gallery tracker. So 0.17.1 is the first published 0.17 CLI.\n\nThe 0.16 set (core, CLI, renderer and editor 0.16.0, PPTX 0.16.1) was the lockstep minor after 0.15 (owner decision 2026-10-08: no 0.15.x patch train). It adds nested\nplaceholder groups in layout records (FA-26), `Table.alt` (FA-27), the `opf/language-tag` warning (FA-29), Markdown\ndecks (`.opf.md`) in every command with core `readDeck`/`writeDeck` (RR-60), SVGs that embed only the faces they draw\n(RR-61), the CLI's programmatic `@openpresentation/cli/api` (RR-62), a lighter renderer install with optional\nconverters and fonts and the `/png`, `/pdf` and `/html` subpaths (RR-63), and the developer-contract fixes (RR-59).\n0.15 documents are 0.16 documents. See the 0.16.0 entries of the [changelog](../CHANGELOG.md) and the\n[CLI changelog](../packages/cli/CHANGELOG.md).\n\nThe 0.15 set (core 0.15.1, CLI 0.15.0, renderer, PPTX and editor 0.15.0) was the breaking spec line (format audit wave C): catalogs group the records a document embeds by the catalog they\ncame from (`catalogs.default`, `catalogs.custom`, named groups, `name:id` references), core ships no built-in records\n(the default catalog is the opt-in `@openpresentation/opf/catalog`, which a host registers with `{ catalogs }`), a\nfull-bleed photo is `design.background` and every content picture is an image block, and `language` is a BCP-47 tag.\nSee the 0.15.0 and 0.15.1 entries of the [changelog](../CHANGELOG.md).\nCore 0.15 started at **0.15.1**: `opf-v0.15.0` was tagged, but its publish run failed before `npm publish` (the CLI tests\nthrough the published renderer and PPTX ran in the core publish workflow before those 0.15 siblings existed), so 0.15.1\nwas the first published 0.15 core and the floor of every 0.15 sibling.\n\nOPF 0.15 is a breaking spec line (format audit wave C): catalogs group the records a document embeds by the catalog they\ncame from (`catalogs.default`, `catalogs.custom`, named groups, `name:id` references), core ships no built-in records\n(the default catalog is the opt-in `@openpresentation/opf/catalog`, which a host registers with `{ catalogs }`), a\nfull-bleed photo is `design.background` and every content picture is an image block, and `language` is a BCP-47 tag.\nThe 0.15 set reads and writes only 0.15 documents; see the 0.15.0 and 0.15.1 entries of the [changelog](../CHANGELOG.md).\nCore starts at **0.15.1**: `opf-v0.15.0` was tagged, but its publish run failed before `npm publish` (the CLI tests\nthrough the published renderer and PPTX ran in the core publish workflow before those 0.15 siblings existed), so 0.15.1\nis the first published 0.15 core and the floor of every 0.15 sibling.\n\nFrom CLI 0.16.0 the CLI depends on core (`@openpresentation/opf`, a regular dependency, not a bundled copy). CLI 0.16.0\nexposed `@openpresentation/cli/api`, and 0.17 removes it in favour of core's `@openpresentation/opf/node`. The renderer\nand PPTX stay optional peers. CLI 0.15.0 and earlier bundled their core.\n\nInstall the complete pinned set. A caret range starting at 0.10.1 does not\ninclude 0.11.x; old consumers can install a second core and do not establish\nColorRef preview/export support. The selected renderer, PPTX and editor 0.26.1 require core ^0.26.1;\nCLI 0.26.1 accepts core ^0.26.1 (CLI 0.25.0 and earlier accept only their own minor). Pin core 0.26.1 alongside them and\nverify one installed Core so preview and export use the same composition.\nCore 0.20.2 and later center metrics when alignment is omitted; explicit alignment remains\nauthoritative.\n\nShared header/footer geometry (`furniture-flow-v2`) is published. PPTX exports\neditable slide shapes tagged `OPF_FURNITURE_V1` with provenance for controlled\nreimport. Published PPTX through 0.11.8 draws every part that way, not as native\nOffice Header/Footer objects. PPTX 0.11.9 and later (RR-11) write the footer's\nfirst text, date and slide number as native `ftr`, `dt` and `sldNum` placeholders\n(with master/layout placeholders, `p:hf` flags and a notes-master flag) at the same\ngeometry and reads them back with or without provenance; the rest stays tagged\nshapes. Native PowerPoint acceptance remains [issue 87](https://github.com/OpenPresentation/opf/issues/87).\nPPTX 0.18 (RR-72) writes a tagged part that is the same on two or more slides once,\non the slide master or a shared layout (one per background tone for `onLight`/`onDark`\nlogos; a hidden footer uses a layout that hides the master's), and reads it back from\nthere ([rr-72-master-furniture.md](programs/release-readiness/rr-72-master-furniture.md)).\n\nThe [Windows native-picture checkpoint](evidence/windows-native-picture-20260921/README.md)\nand accepted [native B/C bundle](evidence/windows-native-edits-20260921/README.md)\nrecord finite picture/furniture edits, current-content provenance reimport and\nsafe fallback, production notes packaging and two controlled reordered-file\nrefusals. Core105 publishes that evidence; PPTX47 adds the tested harness, with\nno new package version. UI image replacement changes geometry, longer header\ntext clips, and duplicated tagged headers overlap. Refused workers retain their\nfailed cleanup state separately from later empty-workspace observations.\nThis is not general native layout/reflow fidelity.\n\nAccepted core106's [tab and font checkpoint](evidence/windows-native-tabs-fonts-20260921/README.md)\nrecords plain native tab target error **0.022655487060546875pt** and tab/literal\ndifference **0.022678375244140625pt**, both above the unchanged **0.02pt** gate.\nIts bounded four-face Carlito edit/save/reopen control passes exact text/style\npersistence, zero observed bounds drift, matching rasters and owned font cleanup.\nMixed-size table fidelity, physical glyph-font identity, fallback/synthesis and actual embedding remain\nopen; embedding was disabled for this control. The Windows supervisor retains sole\nOffice control. This documentation task reads evidence and makes no Office calls.\n\nThe later [read-only font inventory](evidence/windows-native-font-inventory-20260921/README.md)\nretains the four Carlito text styles but reports both Carlito and unexpected\nAptos in `Presentation.Fonts`. The native font allowlist fails, and no embedding\nwas attempted. The original parent report incorrectly fails cleanup because of\na Windows PowerShell 5.1 JSON-array parsing defect; raw stages and registration\nrows establish one owned close and four removals in a separately labeled offline\naudit. The raw failure remains intact. Collection names and flags do not identify\nthe physical font used for each glyph.\n\nThe [read-only mixed-table observation](evidence/windows-native-mixed-table-20260921/README.md)\nretains all 245 characters, one literal tab and five authored runs, with outer\ngeometry within 0.02pt and confirmed owned close/font cleanup. Native soft-line\nboundaries are 92/194 versus the estimated preview's 78/172, and native default\ntab spacing is 72pt. These finite content/style results do not pass table\nedit/save/reopen, browser/native raster agreement or physical glyph identity.\nThe accepted [nine-pair offline tab analysis](evidence/windows-native-tab-analysis-20260921/REPORT.md)\nfinds a 0.05pt-compatible pattern in the observed character starts, with finer saved\ntab coordinates. These inputs do not distinguish relative versus absolute placement\nor establish an internal engine cause. The 0.02pt native tab gate remains failed;\nthe separate 0.1px renderer gate is unchanged. Accepted core108 `9b277e1` and\ncore109 `b2711549` publish bounded evidence only. Windows-owned [core110](https://github.com/OpenPresentation/opf/pull/110)\nis merged as `4f7a4bd494f1a873319eff897423d301d1cfc9d6`, from reviewed fc3c36e\nwith four required PR checks passing. [Renderer30](https://github.com/OpenPresentation/opf-render/pull/30) is now merged\nas `c8d7d5ca1f67a7b39f70c7c4bd14577a865b175b`, with exact-head CI 35661051100\npassing. The supervisor reports postmerge 35661504472 also passed. Its companion\nsource preserves rich-tab advances/spans; the [accepted Windows wrap-up](handoff-windows-native-2026-09-21-wrap-up.md)\nrecords bounded source-linked rendering/browser checks and the original missing-test\nCI failure. These checks do not update the frozen registry consumer. Core111\n`3c5048522714365a41d9b5b9ba81620affae718b` publishes the font-inventory evidence\nabove with both PR workflows green; its postmerge workflows were started at the\nfinal notice, not recorded as passed. Package/lock/release/site pins, schema,\ngoldens and tolerances are unchanged. Native allowlist and physical-glyph/embedding\nacceptance remain open; no new package train or broad native pass is inferred.\n\n## Independently released packages\n\n| Package | Version | Depends on |\n| --- | --- | --- |\n| `@openpresentation/gallery` | 0.26.1 (the shared version, with records identical to 5.0.0, the last number of its own line; 4.0.0 (2026-10-19), 3.0.0, 2.0.0 and 1.0.0 remain historical) | nothing (its `GalleryCatalog` type has the shape of `@openpresentation/opf`'s `Catalog`) |\n\nGallery 4.0.0 (published and independently verified 2026-10-19) adds theme `variants` and removes the `dark` theme; core 0.24 and its siblings depended on `^4.0.0`. It declares catalog compatibility generation 3. Gallery 5.0.0 (published and independently verified; layouts as one list of regions, catalog generation 4) is the range core 0.25.0 and this checkout depend on (`^5.0.0`); the published set depends on it from core 0.25.0, and so does CLI 0.25.0 (CLI 0.24.1 and earlier, on Gallery `^4.0.0`, cannot read it).\n\nThe gallery (RR-78) is the package `packages/gallery` of this repository. Since 0.26.0 it is released with the other five at one\nshared version (`vX.Y.Z` tags, `npm-publish.yml`); until 0.25.0 it was released from its own repository on its own line, which\nended at 5.0.0 (Gallery 1.0.0 to 5.0.0 stay on npm). New catalog records go in any release; a removed id or a changed drawing needs a\nminor release ([release process](release-process.md)). From OPF 0.19 core and the\nCLI consume the Gallery package (Gallery 2 in OPF 0.19; Gallery 3 in OPF 0.20), and core has no `/catalog` subpath; up to 0.18 core ships the same records as\n`@openpresentation/opf/catalog`.\n\n## Supported in this set\n\n| Capability | How | Notes |\n| --- | --- | --- |\n| JSON authoring | `*.opf.json` plus CLI `opf create` | Local files only |\n| Bundled examples catalog | `@openpresentation/opf/examples` | **127** decks; the quickstart JSON is a docs fixture, not a 128th catalog entry |\n| Validate | `validate` / `opf validate` | One read-only checker: format, references, policy, accessibility, layout and content findings ([guide](validate.md)); no network catalog fetch |\n| Color references | `ColorRef`, `variables`, `resolveColorRef` | Core schema/resolution, renderer preview and PPTX resolved colors are shipped. Supported theme-slot colors write native `schemeClr` references; arbitrary native Office recoloring and full visual fidelity remain separately scoped. |\n| Embed catalog records | `embed` / `opf embed` | Embeds every record a document references, in the group it resolves in, so it renders with no catalog registered; remote media and data remain explicit host concerns. |\n| Offline fonts | `loadFonts` (`/fonts-node`) | Bundled Roboto pack (`pack: 'base'`) or the office pack; hashed files; the handle is passed as `{ fonts }` to every deck-level call |\n| Composition | `composeSlide` | Includes shared headers/footers |\n| Pagination | `paginate` / `opf paginate` | Returns mappings; preserves source |\n| Edit + undo | `@openpresentation/opf-editor` `createEditorSession` | JSON Patch undo/redo |\n| JSON Patch CLI | `opf edit` | No persistent CLI undo history |\n| SVG preview | `toSvg(deck)` (every slide) / `toSvg(deck, n)` (slide n, counted from 1) (opf-render 0.18; `renderSvg` / `renderSlideSvg` before) | Local; the same `{ fonts }` as layout; `text: \"paths\"` draws the text as glyph outlines |\n| Convert, export and import files from code | `convert`, `open` and `save` of `@openpresentation/opf` in Node (core 0.18.0 and later; core 0.17 had them at `@openpresentation/opf/node`; CLI 0.16.0 had `exportDeck` / `importDeck` in `@openpresentation/cli/api`) | PDF, PNG, SVG and PPTX out, PPTX in, deck forms both ways, with the bundled fonts, no network and no clock; calls opf-render and opf-pptx as optional peers and throws `OPFExportError` / `OPFImportError` with the install command when one is missing. `opf convert` runs the same engine (CLI 0.18; `opf render`, `opf export` and `opf import` before it) ([OPF files in Node](node.md)) |\n| Read and write a deck in JSON, YAML or Markdown | `parse` / `stringify` (core 0.17.0 and later; `readDeck` / `writeDeck` in 0.16) | One entry point for the three text formats; `parse` throws `OPFValidationError` with located findings |\n| PNG | `toPng` (opf-render 0.18; `svgToPng` before) | Node raster of a deck or of SVG slides |\n| PDF | `toPdf` (opf-render 0.18; `svgToPdf` before) | opf-render 0.12.0 and later (RR-12, opf-render#90): **vector with selectable text by default** (embedded TrueType subsets of the supplied/bundled fonts, ToUnicode, links, metadata, tagged structure); `raster: true` keeps the image-per-slide output (`mode: \"raster\"` before 0.18). Renderers up to 0.11.9: raster-backed, not selectable text. Not a PDF/UA or PDF/A claim; see the renderer's `docs/evidence/rr-12-vector-pdf.md` for reader limits |\n| Editable PPTX export | `toPptx` | OPF \u2192 PPTX serialization. Text is one editable box per wrapped line, so edited text does not reflow in PowerPoint or Keynote, and a wrapped title is not the Title placeholder; the opt-in `bodyText: \"native\"` mode (CLI `--body-text native`, from opf-pptx and CLI 0.22.0) writes whole paragraphs, a wrapped title as the Title placeholder. Shared furniture is written on the master or layout; supported footer placeholders and tagged parts retain controlled re-import provenance. General native Office behavior remains separately scoped |\n| Agent skills | `opf skills install` | Offline after the CLI is installed |\n| Browser canvas | `@openpresentation/opf-editor/canvas` | Host must supply font bytes |\n\n## Public sites\n\nThe current coordinated adoption is recorded in the [final adoption note](evidence/rr-85-final-adoption-2026-10-16/README.md). The following source, CI and canonical production results are the historical September 21 checkpoint, recorded in the\n[font-readiness checkpoint](evidence/font-readiness-acceptance-20260921/README.md),\n[prior Inspector actions checkpoint](evidence/inspector-current-actions-20260921/README.md),\n[earlier publication checkpoint](evidence/inspector-share-acceptance-20260921/README.md),\n[source-preservation checkpoint](evidence/author-source-acceptance-20260921/README.md),\n[completion checkpoint](evidence/completion-acceptance-20260921/README.md),\n[earlier acceptance ledger](evidence/issue88-final-20260921/README.md) and\n[handoff](handoff-2026-09-21.md). [Issue88](https://github.com/OpenPresentation/opf/issues/88)\nremains open. Package adoption, deployed features and complete workflow\nacceptance are separate claims.\n\n| Surface | Deployed scope and acceptance | Source commit |\n| --- | --- | --- |\n| [openpresentation.org](https://www.openpresentation.org) | Current published guides, agent skills, JSON/preview workflow and downloads. Exact canonical deployment passes 321 checks across 11 pages and 18 raw resources, plus two browser flows for agent installation/navigation and JSON/SVG/PPTX downloads. Reviewed screenshots and output hashes match the accepted build. | `a85bcc77d899ce9ba1df659548be564142c16120` |\n| [pptx.dev](https://www.pptx.dev) `/inspector` and `/author` | App54 merged/live on exact READY production. Premerge Linux/Windows pass 704 units, 13 standalone controls and 39/39 browsers. Full canonical acceptance **fails (34/39 passed)** at five no-POST assertions; the bounded audit does not establish an introduced upload regression. Postmerge Linux 39/39 passes, Windows 38/39 fails initial font readiness. Preset Undo all and broader source writers remain unresolved. | `8f54228a9e38a1dcc0bf8188bcdd519795b3799a` |\n| [pptx.gallery](https://www.pptx.gallery) `/docs`, `/editor` and gallery pages | Published ColorRef/bundle guidance, Playground and Editor actions, and the canonical docs-to-editor flow are verified. | `f17e9ae5869669d5fbac3720f285652d0c37551c` |\n\nThe site uses documentation source `120a770`, whose tree matches accepted core\nPR98 commit `b1ff81db6f8714b0db1a98bde482ed8a64d0ccc9`. Core PR93/97/98/99/100/102/103 passed\npre-merge and post-merge CI. Accepted core102 is\n`578bcc6e0894129b00059258bd4ad1994a414baa`; its reviewed and accepted trees match,\nand all four required pre/post-merge runs passed on their first attempts. The\n[core102 receipts](evidence/inspector-share-acceptance-20260921/README.md#accepted-core102)\npin this documentation checkpoint without changing the site's older accepted\ndocumentation snapshot. The site's complete guides and raw resources match\nthe reviewed source; binary evidence remains linked and downloadable without\nbeing decoded into the AI-facing guide.\nCore104 `3d301f1` preserves exact postmerge OPF success and coordinated cancellation;\nit is not a complete green postmerge gate. Accepted descendant core105\n`84e914710520a7b0e777fce30e5758ee64a64924` preserves all 60 core104 evidence blobs\nand passes both exact-head workflows on their first attempts. [Compact receipts](evidence/inspector-current-actions-20260921/README.md#core-source-and-ci)\nkeep descendant acceptance separate from the canceled predecessor run.\nAccepted core106 `3847f712ccb2379952bcc8ab7c9fdbaedfd0a4ce` also passes both\npre/postmerge workflows on their first attempts. Its [compact receipt](evidence/inspector-current-actions-20260921/core106/acceptance-receipt.json)\nbinds the bounded native evidence above without completing general compatibility.\nAccepted core107 `5bc0d3f89414b382b2ce48452c7e56e5d66aaf74` has reviewed tree\n`ffdc678beadf0808bc717d67e7fc0a9ec4790127`. Both original postmerge push workflows\n**35652360502 / 35652360551** passed on attempt 1 under Node24.20.0.\n[Compact receipts](evidence/font-readiness-acceptance-20260921/README.md#core107-acceptance)\nretain earlier automatic premerge cancellations separately from the later automatic\nsuccessful pair; no rerun or accepted checkpoint relabels them.\n\nApp47 corrects the pre-app47\ncompletion adapter's rejected layout choices while preserving unchanged source\ntokens and undo history. Accepted commit `0f35352a1445f56ad4bb7c9f4c5609e01f2dd9ae`\nhas reviewed tree `203bdab509d05911f04f234d996f9c91f2b5e4f2`, green Linux/Windows\npre/post-merge CI and its exact READY canonical deployment. The historical App47 **23/24** production run passes all five new completion cases but still fails the existing\nLF Author third-popup assertion. This does not establish complete public-surface\nacceptance; the [historical App47 report](evidence/completion-acceptance-20260921/canonical/REPORT.md)\nand [earlier failed app45/app46 results](evidence/issue88-final-20260921/README.md)\nretain their evidence and unresolved causes.\n\nThe [source audit](evidence/completion-acceptance-20260921/source-preservation-audit/REPORT.md)\nidentified Author canvas/Copy/export and Inspector JSON-download normalization.\nMerged App53 at\n`e40c287b64fcbcfb85fb4a8a50641aea8e3e54a8` has the identical reviewed b33dc18\ntree and preserves those bounded raw-source\npaths and corrects order-only reimport history. A public Suggest-action guard\naddresses the observed stale Quick Input context competing with focused-editor\nCtrl+Space. Current local checks pass **627 unit tests and 29/29 browser cases\nin 88.78 seconds**, zero retries. First-attempt Linux/Windows application CI\npassed 627 unit tests and 29 browser cases per platform; artifact CI also passed.\nThe [exact READY canonical run](evidence/author-source-acceptance-20260921/canonical/REPORT.md) passes **29/29**, zero retries, with matching deployment receipts before and after. Postmerge application CI fails Linux **28/29** while Windows passes **29/29**; both pass 627 unit tests and separate artifact CI passes. The [Linux failure](evidence/author-source-acceptance-20260921/app53/postmerge-ci/README.md) stops before security assertions because five default-deck canvases remain after the shared-load toast. No rerun or canonical pass replaces that failed gate. The retained draft preview was READY but not browser-accepted.\n\nThe earlier **24/27** import-undo regression, **26/27** local popup failure and\nold-head Windows **26/27** shared-load failure remain historical evidence.\nThe fresh successful Windows job retained its sanitized timing artifact, but\nonly Author timings survived; the Inspector pagehide snapshot is missing. This\ndoes not explain or fix the old readiness delay. Phase4\ncaptured no post-fix stale-context overlap, so causal stress is inconclusive.\nThe existing suggestion-details pane remains clipped; visibility is not legibility.\nThe [postmerge trace diagnosis](evidence/author-source-acceptance-20260921/inspector-share-diagnosis/REPORT.md)\nproves wrong-document automatic share-hash publication during import.\nApp54 first corrected automatic\npublication at `60c91f6`: authoritative source/format is checked before and after\nencoding, load/navigation guards remain, and obsolete `import=hash:` is removed.\nURL transfer preserves semantics, not raw spelling. Local 659-unit/31-browser\nacceptance does not replace original first-attempt CI **35638158483**, which\npassed Linux **31/31** but failed Windows **30/31** at the unchanged 45-second\nAuthor readiness deadline. Both publication cases passed. The [frozen diagnosis](evidence/inspector-share-acceptance-20260921/app54/windows-timeout/REPORT.md)\nretains bounded slow-delivery observations with unknown cause. Late assertions\nare not an in-budget pass. Browser History tests permit prior accepted content\nuntil first new publication; held-promise controls establish the narrower race guard.\n\nThe product correction was added at `57e5e59fddbc94346f142dc12d86a916228bf2ae`, tree\n`d9c3aab543c6a886a85c6ec07dd55e5ab22590cd`. It guards current snapshots for\nexplicit Copy/JSON/Share/PPTX/PDF/Author/Deckchat actions. Pending public canvas\ndrafts commit before capture, pointer-blur rejection survives session recovery,\nand newer source/load/navigation/unmount/action invalidates late results. Raw\nCopy and accepted same-format JSON bytes are preserved; handoffs remain semantic.\nAlready-started clipboard/download effects cannot be retroactively canceled.\nThe integrated portable standalone startup verifies packaged runtime assets,\nfonts and traced schema inputs without changing dependencies or published pins.\n\nLocal runtime checks pass **692 unit tests**, **7 standalone controls**, focused\n**8/8** and full **39/39** browser cases, zero retries, with visual review. The\nimmutable 93-file app bundle\nretains stale-draft negative evidence and the initial candidate **7/8** result.\nThe latter did not establish accepted pasted source before releasing mocked PDF 401;\nfinal visible-code preconditions change no product bytes or budgets. PDF/Deckchat\nare locally mocked. CRLF ingress normalized 279 to 274 LF bytes; subsequent exact\nactions preserve the accepted buffer, not that ingress boundary.\n\n**The original 57e packaging gate failed.** First-attempt CI **35645493900** failed Linux and\nWindows typecheck on archived `.spec.ts` evidence copies after each platform\npassed 692 units and 7 standalone controls. The exact-head preview failed with\n`module_not_found`; precise Vercel compiler logs were unavailable. [The first-attempt receipt](evidence/inspector-current-actions-20260921/app54/first57-ci/REPORT.md)\nrecords skipped build/browser steps and no uploaded artifacts. The passing runtime build\npredates those copies. A byte-identical `.ts.txt` archive correction produced captured App54 head\n`6dfc2584698c1306505929a2bc3e427996d66563`, tree\n`f3279495f7685945b7541c90d24f7239407639a0`. Its final-tree local typecheck passes\nwith all 305 product/test inputs unchanged. [The corrected receipt](evidence/inspector-current-actions-20260921/app54/corrected6df/commit-receipt.json)\nbinds its 106-file evidence bundle. [Exact-head CI 35646213757](evidence/inspector-current-actions-20260921/app54/corrected6df/ci/REPORT.md)\npasses 692 units, seven standalone controls, typechecks and build on both platforms;\nLinux passes **39/39** browsers. Windows executes **zero browser tests** because\nstandalone startup fails with `EPERM` while statting its packaged React dependency\nlink. No timing or test-result artifacts were uploaded. The exact-head preview\nis READY, which is metadata only; at that 6df checkpoint App54 was unmerged and undeployed to production.\nThe [current ledger](evidence/inspector-current-actions-20260921/README.md)\nretains the separate 60c readiness, 57e packaging and 6df startup failures. Production then remained App53 `e40c287`, with its original\nfailed Linux postmerge gate preserved separately from canonical **29/29**.\nThe startup-link correction at 4338e57\nthen reached all browser cases: Linux **39/39**, Windows **38/39** in first-attempt\nCI **35649707689**, with 692 units and 13 standalone controls passing per platform.\nThe sole Windows failure was initial canvas-title visibility at five seconds,\nbefore any edit/recovery operation; correct incoming source remained at loading\nfonts. Late font acquisition and eleven incomplete responses at teardown do not\nestablish a permanent stall or a dominant cause. The [failed gate](evidence/font-readiness-acceptance-20260921/README.md#preserved-failed-windows-gate)\nis preserved.\n\nApp54 font-preparation revision `a4eb88ab7aa585c9efb91de4c190c1f1c0c7d0eb`, tree\n`031d893855a540fd2a4d2ec2162605817b8dde16`, overlaps font acquisition with converter\nwarmup while readiness still waits for both. The offline converter barrier and all\n**33 faces / 9,317,044 bytes**, manifest, substitution/measurement policy and\n`document.fonts.ready` gate remain unchanged. Fresh local Node24.21.0 checks pass\n**704 units, 13 standalone controls and 39/39 browsers**, zero retries, including\nunchanged offline export/reimport. Immutable app evidence\nretains reviewed images, exact source/output bindings and the original failed run.\n\n**The original a4eb gate failed:** first-attempt application **35654753237** passes\nLinux **39/39** but fails Windows **38/39**; both pass 704 units, 13 standalone\ncontrols, typecheck and build. The Open in Author URL assertion exceeds its existing\nfive-second deadline at `inspector-actions.spec.ts:176`; later source checks are\nnot reached. Original4338 font readiness passes in this run. No navigation cause\nor data-loss finding is established. The [frozen final audit](evidence/font-readiness-acceptance-20260921/app54/final-a4eb-ci/release-audit.json)\nretains exact source/artifact bindings and the original failed trace; that failure remains preserved.\nThe Python artifact workflow is not applicable under its full-PR path filters,\nnot a fresh pass. Exact-head preview is READY metadata only; an\nunauthenticated request redirects to sign-in, with no preview-browser acceptance.\nAt that a4eb capture App54 was unmerged and production remained App53\n`e40c287`. The separate suggestion-details candidate remains unreleased and supplies\nno acceptance here. No local/browser pass broadens native compatibility.\nThe [bounded navigation diagnosis](evidence/font-readiness-acceptance-20260921/README.md#author-navigation-diagnosis-and-prospective-policy)\nrecords Loading Author and a delayed successful script response: 1490 bytes inferred\nfrom ETag, 766 compressed bytes recorded, actual body absent. Later DOM does not\naccept unreached assertions or identify a cause. App54 accepted merge\n`8f54228a9e38a1dcc0bf8188bcdd519795b3799a` retains reviewed 79ba tree\n`3639d3c14daec94d13711fa2b10f2b927df45eca`, preregisters `waitForURL(load)` before\nthe real action, matching `page.goto` within the unchanged 45-second test and default\nfive-second content budgets. It retains all oracles but deliberately removes the\nincidental five-second navigation deadline. Fresh local **39/39**, zero retries,\npasses in 112.780048s with reviewed source/images and prepared-tree build/typecheck.\nThe immutable app bundle\nretains original failures. Units/standalone controls were not repeated locally;\nfresh first-attempt application **35659187971 passes 704 units, 13 standalone\ncontrols and 39/39 browsers on both Linux and Windows**. Exact-head preview was\nREADY/protected, not browser accepted. The identical reviewed tree is merged/live\nat 8f on READY deployment `dpl_H1FtXx1QSuGxn3MwzJwWJpGtRg8b`, but full canonical\nacceptance **fails (34/39 passed)** at five no-POST assertions observing Clerk environment\nPOSTs. The [safe audit](evidence/font-readiness-acceptance-20260921/app54/canonical8f/write-audit/REPORT.md.txt)\nrecords ten such requests, nine with HTTP200/zero-length bodies and one incomplete.\nNo fixture-content needle was detected in captured fields; uncaptured data remains\nunknown. Three final action page-error assertions were not reached; the two share\ncases passed theirs. The prior auth/config comparison is 28/29 identical, with only\npackage scripts changed. Production is kept without a rollback, test change or\nrerun; the strict gate remains failed. Raw authentication-bearing diagnostics\nremain private. Postmerge CI 35660464578\nfinishes failed: Linux 39/39 passes while Windows 38/39 fails, with 704 units/13 controls/typecheck/build\npassing on each. Windows fails initial gallery-rail title visibility after 5,000ms\nwith correct source, clean schema and Loading slide fonts; later editing/export/\nreimport checks were not reached. The [final audit](evidence/font-readiness-acceptance-20260921/app54/merged8f/postmerge-ci/REPORT.md.txt)\npreserves this separate failed gate without cause inference or a retry. No canonical\npass or general native/font acceptance is claimed. That September 21 checkpoint was paused; the user resumed work on September 29. See the [current source checkpoint](handoff-runtime-2026-09-29.md) for ongoing repairs and release holds.\n\nSeparate local negative controls confirmed that preset Undo all discarded New run\nand imported replacement documents. The guarded correction is now preserved in\ndraft app #58: independent\nreview and local Node 24 checks passed (716 units, 13 standalone controls and\n49 browsers without retries). Original Linux/Windows CI could not start because\nof the account payment/spending-limit restriction; no application CI or production\nacceptance is claimed. Unbusy asynchronous account replacements still need a\nsynchronous invalidation guard and held-response control.\nOther local source writers still require their separate preservation checks. These unresolved local findings and raw imported-file/account/agent/metadata boundaries remain outside\nApp53 and App54. See the [App53 ledger](evidence/author-source-acceptance-20260921/README.md)\nand its immutable application evidence links. Issue88 remains OPEN. Native/font\ncompatibility, required repair and release gates remain separate; geometry is\ndeferred as the coordinated set below. The unimplemented worker candidate remains in the [handoff](handoff-2026-09-21.md).\n\nThe five coordinated geometry drafts (core94, renderer27, editor25, PPTX42,\nsite40) remain unmerged. In particular, site40 is not independently shipped.\n\n## Explicitly not shipped\n\n| Topic | Tracker | Do not describe as done |\n| --- | --- | --- |\n| Linux vs Chromium native-width residual at the 0.1px gate | [opf-render#24](https://github.com/OpenPresentation/opf-render/issues/24) | Rounding that fixes Linux but breaks macOS is rejected |\n| General native PowerPoint fidelity beyond the supported footer placeholders | [opf#87](https://github.com/OpenPresentation/opf/issues/87) | Finite B/C and bounded Carlito edit controls above are accepted evidence, as is one finite mixed-size table edit/save/reopen ([evidence](evidence/windows-native-mixed-edit-20260922/README.md)). Tab tolerance, general mixed-size table layout and preview/native wrapping, physical glyph identity/fallback/synthesis, embedding and general layout/reflow fidelity remain open; supported native footer placeholders, self-import and tagged furniture do not certify arbitrary Office behavior |\n| Public-surface acceptance checklist | [opf#88](https://github.com/OpenPresentation/opf/issues/88) | Shipping features does not establish every acceptance item; use the checklist and deployment receipts |\n| HarfBuzz / prepared-glyph shaping | Archive branches `codex/archive-shaping-20260915` | Prototypes are preserved, not in npm |\n| Selectable vector PDF | [pdf plan](plans/pdf-export.md) | Shipped in opf-render 0.12.0 (opf-render#90; the browser download entry `@openpresentation/opf-render/export-browser` ships there too); PDF/UA, PDF/A and viewer coverage beyond pdf.js, PDFium and poppler remain open |\n| General SVG diagrams / Mermaid | [diagrams plan](plans/diagrams-svg.md) | Embedded SVG \u2260 native editable primitives |\n| Full visual editor / IME / bidi / repair loop | [developer adoption](plans/developer-adoption-20260915.md) | Schema support \u2260 WYSIWYG coverage |\n\nCLI 0.9.2 and earlier do not render or export PPTX; CLI 0.10.0 adds `opf render`, `opf export` and `opf import`\nthrough the optional peers opf-render and opf-pptx ([CLI reference](cli.md)). The Node `toPng` / `toPdf`\n(`svgToPng` / `svgToPdf` before 0.18) APIs stay Node-only; renderer 0.12.0 adds the separate `@openpresentation/opf-render/export-browser` entry for browsers.\nCore's file API (`open`, `save`, `convert`) is Node-only too: it reads files, font files and the engines' native packages. From\ncore 0.18 the root resolves to a browser-safe build in a browser or a worker, with the same names, where those three reject\nwith `node-only`; every other core entry is browser-safe. In a browser, use the engines' browser entries.\n\n## Predecessor notes\n\n| Older set | Relationship |\n| --- | --- |\n| core 0.17.0, CLI 0.17.1, renderer 0.17.1, PPTX 0.17.0, editor 0.17.0 | Previous coordinated set (FA-31 furniture variables, `@openpresentation/opf/node`, `parse`/`stringify`, `opf convert`). The 0.18 set is breaking: one root import replaces `/node`, the `toSvg`/`ingest` names and `convert` options, organization logos, and the consolidated CLI. |\n| core 0.16.0, CLI 0.16.0, renderer 0.16.0, PPTX 0.16.1, editor 0.16.0 | Previous coordinated set (Markdown decks, used-face SVG embedding, optional renderer converters, nested placeholder groups, `@openpresentation/cli/api`). The 0.17 set is breaking: header and footer flags become `{{slide.number}}`-style variables (FA-31), `readDeck`/`writeDeck` become `parse`/`stringify`, and `@openpresentation/cli/api` is replaced by `@openpresentation/opf/node`. |\n| core 0.15.1, CLI 0.15.0, renderer 0.15.0, PPTX 0.15.0, editor 0.15.0 | Previous coordinated set (OPF 0.15, format audit wave C; CLI 0.15.0 bundles core 0.15.1). The 0.16 set is additive for documents; the renderer's converters and font packages became optional peers (RR-63), so a 0.16 host installs the ones it uses, and the CLI depends on core instead of bundling it (RR-62). |\n| core 0.14.0, CLI 0.11.0, renderer 0.14.0, PPTX 0.14.0, editor 0.14.2 | Previous coordinated set (format audit waves A and B, RR-55 and RR-56; CLI 0.11.0 bundles core 0.13.0). The 0.15 set is breaking: a 0.14 document's catalog shape (`catalogs.<kind>.records`), catalog-id languages and `design.slideImage` are invalid in 0.15, and the 0.15 engines need the host to register the default catalog. |\n| core 0.13.0, CLI 0.11.0, renderer 0.13.1, PPTX 0.13.2, editor 0.12.1 | Previous coordinated set (chart and table data, RR-54). |\n| core 0.12.2, CLI 0.10.1, renderer 0.12.2, PPTX 0.12.4, editor 0.11.3 | Previous coordinated set (the first set whose `engines.node` is the open-ended `>=22`). The 0.13 set adds chart and table data (RR-54, [contract](chart-table-data.md)): strict chart numbers (a value is a finite number or a strict decimal string; `\"12%\"`, `\"$5\"` and `\"Q1\"` are a gap with a `chart-value-not-numeric` warning, never a guessed number), number formats (`DataColumn` `{ name, format }`), top-level `datasets` with `{ \"dataset\", \"fields\" }` references and `chart.mapping`, in core 0.13.0, previewed by renderer 0.13.0, exported and re-imported by PPTX 0.13.0 (`OPF_DATASETS_V1` and `OPF_DATA_V1` provenance), edited in the editor 0.12.x data grid and imported by CLI 0.11.0 (`opf import-data --dataset`). PPTX 0.13.0 also vendors pptxgenjs-plus 4.3.4 (was PptxGenJS 4.0.1) and PPTX 0.13.1 bundles for browsers again. Renderer 0.13.1 and PPTX 0.13.2 pin Sharp 0.35.5 (GHSA-wq5f-xc86-pv6w; output unchanged); editor 0.12.1 replaces 0.12.0, which was never published. Renderer, PPTX and editor raise their core floor to `^0.13.0` and the renderer peer to `^0.13.1` together. |\n| core 0.12.1, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.2, editor 0.11.2 | Previous coordinated set. PPTX 0.12.3 corrects the table range of a chart's embedded workbook (apostrophe-quoted sheet references and bubble-series references), which made Keynote drop category charts on import (opf-pptx#162, opf-pptx#163; every other part of the package is byte-identical). Core, renderer and editor are unchanged and keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.2, editor 0.11.2 | Previous coordinated set. Core 0.12.1 deprecates the six plural audience ids (`executives`, `investors`, `customers`, `sales-team`, `marketing-team`, `regulators`) in favour of the singular ids: additive catalog data, validation warns and never errors, and no geometry moves, so renderer, PPTX and editor keep the core floor `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.1, editor 0.11.1 | Previous coordinated set (PowerPoint lists only the deck's fonts). PPTX 0.12.2 also names the deck's font in the theme's `Viet` (Vietnamese) and `Uigh` (Uyghur) per-language script entries, which PowerPoint applies to `vi-VN` and `ug` runs and which kept Office's Times New Roman and Arial for those two languages (RR-17; every other language is byte-identical). Editor 0.11.2 adds the `slide-sizes` and `purposes` switch dimensions, `SLIDE_SIZE_PRESETS` and the Slide size and Purpose selects in the Design panel (RR-41, additive API). Both keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.12.0, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.0, editor 0.11.0 | Previous coordinated set (the 0.12.0 train). PPTX 0.12.1 writes the deck's fonts so that PowerPoint's font list shows only the fonts the deck uses (FF-05: schema-order `presentation.xml`, an own notes theme, no east-asian or complex-script font on runs, and a theme east-asian slot that is never empty); editor 0.11.1 stops the restore prompt showing a literal `null`. Both keep the core floor `^0.12.0` and the renderer peer `^0.12.0`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.9, editor 0.10.6 | Previous coordinated set (native header/footer placeholders, SVG pictures, `fromPptx` import signals; the published 0.11.4 train measured in the font-fidelity program). Core 0.12.0 moves geometry (composed font sizes on PowerPoint's 0.01 pt grid, hanging wrap whitespace, promoted regions in reading order, right-to-left decks composed mirrored), so renderer 0.12.0, PPTX 0.12.0 and editor 0.11.0 raise their core floor to `^0.12.0` and the renderer peer of PPTX and editor to `^0.12.0` together; the set adds templates and variables, numbered lists, footnotes, citations and captions and chart options (additive schema), vector PDF with selectable text and the browser export entry, the `<opf-deck>` player, the editor's slide management, autosave, data grid, find and replace, image crop, Review panel and fill UI, and the shared JSON Patch module. CLI 0.10.0 bundles core 0.12.0 and adds `opf audit`, `from-md`, `to-md`, `diff`, `merge`, `format`, `render`, `export` and `import`. |\n| core 0.11.4, CLI 0.9.2, renderer 0.11.9, PPTX 0.11.8, editor 0.10.6 | Previous coordinated set (PPTX 0.11.8 re-imports wrapped rich text as one authored payload; CLI 0.9.2 bundles core 0.11.4). PPTX 0.11.9 writes a deck footer's first text, date and slide number as native PowerPoint Header & Footer placeholders (every export also carries the footer placeholders on its master, layout and notes master, so Insert > Header & Footer works), exports an SVG image as a native SVG picture over a PNG fallback (rasterized in Node by the optional opf-render peer or `options.svgRasterizer`) and adds the opt-in `fromPptx(bytes, {signals: true})` import signals; core floor `^0.11.4` unchanged. |\n| core 0.11.4, CLI 0.9.1, renderer 0.11.9, PPTX 0.11.7, editor 0.10.6 | Previous coordinated set (the design fields compose and export natively). PPTX 0.11.8 re-imports rich text that wraps over several native lines as one authored payload (the export records the line count; decks exported by 0.11.7 import as before) and keeps the core floor `^0.11.4`; CLI 0.9.2 bundles core 0.11.4 (CLI 0.9.1 bundled core 0.11.3) and still requires Node 24. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.6, editor 0.10.5 | Previous coordinated set (the native chartex export by default). Core 0.11.4 composes the design fields (logos on covers and section slides, `contentDirection`, `chartPrimary`, picture bullets, header and footer logos, the accent font), aligns a cover's tag and subtitle with its title, and sizes picture bullets and furniture images as PowerPoint does, so renderer, PPTX and editor raise their core floor to `^0.11.4` together; renderer 0.11.9 draws those fields and applies the tag contrast rule (FF-61: the tag draws in the text colour when the scheme primary is under 4.5:1); PPTX 0.11.7 exports them natively, writes every chart's text at the preview's size (FF-62: 12 pt, not 9 pt), writes slide sections as PowerPoint's section list and restores the authored form of a fresh export on import (a root payload returns as `slides.N.text`, `.items`, `.chart` ... rather than one typed block); editor 0.10.6 is a floor bump. CLI 0.9.1 still bundles core 0.11.3. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.8, PPTX 0.11.5, editor 0.10.5 | Previous coordinated set (the slide tag draws in the scheme primary colour and PPTX writes it as `a:schemeClr accent1`; the playground loads its base faces through `extraLazyFonts`). PPTX 0.11.6 exports the treemap, histogram, pareto, box-and-whisker, waterfall and funnel charts as native chartex parts by default (`toPptx({chartex: 'auto'})`, confirmed in desktop PowerPoint; `world` stays a clustered column with `chart-data-adapted` because PowerPoint's map needs online geodata; pass `chartex: 'fallback'` for the previous output) and gives chartex text the deck's label colour and font. |\n| core 0.11.3, CLI 0.9.1, renderer 0.11.6, PPTX 0.11.4, editor 0.10.4 | Previous coordinated set (the 100-layout catalog and its geometry, category-axis label rotation, quote and slide image re-import). Renderer 0.11.7 adds the `extraLazyFonts` registry option and `splitStartupFaces` (a browser host can start with Roboto Regular alone and load its other base faces on demand); renderer 0.11.8 draws the slide tag in the scheme primary colour; PPTX 0.11.5 writes the tag run as `a:schemeClr accent1` where the deck theme holds the primary (the colour is unchanged); editor 0.10.5 loads its playground base faces through `extraLazyFonts`. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.5, PPTX 0.11.3, editor 0.10.3 | Previous coordinated set (native classic and chartex chart previews, opt-in chartex export, face-level lazy fonts and the font gate's render options). Core 0.11.3 adds the pinned pptx.gallery default catalog and the 70 legacy gallery layout ids (layouts 30 to 100; 25 carry a `composition` or `contentBox` contract, which moves geometry, so renderer, PPTX and editor raise their core floor to `^0.11.3` together); renderer 0.11.6 rotates and skips dense category-axis labels; PPTX 0.11.4 re-imports quote and slide image payloads and writes theme `a:ea`/`a:cs` only where a script font is selected; editor 0.10.4 is a floor bump; CLI 0.9.1 bundles core 0.11.3. |\n| core 0.11.2, CLI 0.9.0, renderer 0.11.1, PPTX 0.11.0, editor 0.10.0 | Previous coordinated set (lockstep floors, Intos and the open families, selected-name export). Renderer 0.11.2 adds script-face loading (`scripts: 'auto'`); PPTX 0.11.1 adds `design.watermark` export; editor 0.10.2 loads the fonts a document needs before every render (FF-41). Renderer 0.11.3 previews every kept classic chart type natively; PPTX 0.11.2 exports the native construct for each kept classic chart type (with `chart-data-adapted` diagnostics where data is adapted) and writes theme colour references for table and text colours. Renderer 0.11.4 previews the seven chartex chart types natively (the world map as a non-geographic tile grid), keeps the Latin Noto Sans replacement for script schemes under `scripts: 'auto'`, shapes Noto Sans Mongolian, and bundles Raleway and Playfair Display (94 lazy faces); PPTX 0.11.3 adds the opt-in `toPptx({chartex: 'native'})` export of the chartex chart types (the default output is unchanged) and always imports chartex charts. |\n| core 0.11.0, CLI 0.9.0, renderer 0.9.0, PPTX 0.9.1, editor 0.8.0 | Previous coordinated Node 24 set (ColorRef, shared furniture). Renderer and PPTX had different core floors from 0.10.x. |\n| core 0.10.0, renderer/PPTX/CLI 0.8.0, editor 0.7.0 | Previous coordinated Node 24 baseline. Lint and furniture landed across 0.10.0/0.8.0 then layout-placeholder fixes in 0.10.1/0.8.1/0.7.1. |\n| Node 20 | Not valid for these packages |\n| Node 22 / 26 | Declared by the current set (`engines.node` `>=22`) and tested, but Node 24 is the toolchain the evidence runs on. Releases up to core 0.12.1, CLI 0.10.0, renderer 0.12.0, PPTX 0.12.3 and editor 0.11.2 declared `24.x`, so npm on Node 22 or 26 silently picked an older release |\n\nDo not install sibling `../opf-render` dist folders when following the\nquickstart. Packed and registry consumers must resolve `@openpresentation/*`\nfrom npm.\n"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"slug": "content-item-design-overrides",
|
|
@@ -214,7 +214,7 @@ var docsData = Object.freeze([
|
|
|
214
214
|
"slug": "live-editor",
|
|
215
215
|
"file": "docs/live-editor.md",
|
|
216
216
|
"title": "Browser preview and live editing",
|
|
217
|
-
"markdown": "# Browser preview and live editing\n\nEditor 0.25.3 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.25.0, editor 0.25.3, PPTX 0.25.0, renderer 0.25.0 and Gallery 5.0.0. This is the current published setup; the [quickstart](quickstart.md) is the shared command-line first run:\n\n```sh\nnpm install --save-exact @openpresentation/opf@0.25.0 @openpresentation/opf-render@0.25.0 @openpresentation/opf-editor@0.25.3 @openpresentation/opf-pptx@0.25.0 @openpresentation/gallery@5.0.0\n```\n\nNo paid service or provider account is required. The six agent skills install with `npx @openpresentation/cli@0.20.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\nThe editor exports in the browser (SVG through the renderer's browser entries, PPTX through opf-pptx). To export a saved presentation on a server, call `convert(\"deck.opf.json\", \"deck.pdf\")` (or `convert(presentation, { format: \"pdf\" | \"png\" | \"svg\" | \"pptx\" })` for the bytes) from `@openpresentation/opf` in Node, which draws with the same engines and the bundled fonts; `open(\"deck.pptx\")` reads a PowerPoint file back into a presentation for the editor ([OPF files in Node](node.md)).\n\nFor source builds and the Gallery host bundle, see [ecosystem development](ecosystem-development.md#editor-host-bundle-and-font-provenance).\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 { saveDocument } from '@openpresentation/opf-editor';\nimport { loadFonts } from '@openpresentation/opf-render/fonts-browser';\nimport { gallery } from '@openpresentation/gallery';\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 loadFonts({ faces: [\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 container = document.querySelector('#slide');\nif (!(container instanceof HTMLElement)) throw new Error('Missing slide container.');\nconst canvas = createCanvasEditor(container, {\n presentation: {\n design: { theme: 'classic', fontScheme: 'roboto' },\n slides: [{ title: 'An editable presentation', text: 'Click to edit.' }],\n },\n catalogs: [gallery], // core ships no records: register the catalog the theme and font scheme above come from\n fonts,\n onCommit: ({ editor }) => {\n const { document: updatedOPF, unresolved } = saveDocument(editor);\n // Host owns storage. Surface unresolved catalog references before saving.\n if (unresolved.length) console.warn(unresolved);\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`loadFonts` accepts explicit font-file URLs or `Uint8Array` data (`faces`). It uses the same bytes for Fontkit measurement and browser `FontFace` registration, awaits loading, reports failures, and exposes `dispose()` for its owned font faces; the handle's `pending` and `ensure` load the script and vendored faces a document needs, and the canvas waits for them before it renders. Cross-origin font URLs need CORS access. Load fonts once and share the handle between canvases. The canvas does not fetch fonts or catalog sources itself.\n\nFor standalone SVG export, pass the handle as `{ fonts }` to `toSvg` (`toSvg(deck, n, ...)` is slide n, `toSvg(deck, ...)` every slide); 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 { toSvg } from '@openpresentation/opf-render/svg';\nconst svg = toSvg(canvas.editor.presentation, 1, { fonts, catalogs: canvas.editor.catalogs }); // slide 1: slides count from 1\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 `toPng` and `toPdf`; those functions are not browser APIs.\n\n## Save, undo and host state\n\nBefore saving, exporting, importing or navigating, call `canvas.commit()`. A `false` result means the active draft could not be committed: keep it visible, show the error and stop that action. `canvas.editor.presentation` is the committed document, not necessarily the text currently being edited. A source editor must preserve invalid raw text separately instead of silently replacing it with the last valid preview.\n\n```js\nfunction documentForDownload() {\n if (!canvas.commit()) throw new Error('Finish or cancel the current edit first.');\n const result = saveDocument(canvas.editor);\n if (result.unresolved.length) throw new Error('Resolve catalog references before sharing.');\n return result.document; // Referenced catalog records are embedded; the session is unchanged.\n}\nconst unsubscribe = canvas.editor.subscribe(({ type, snapshot }) => {\n // Runs for patch, undo, redo, restore and catalog changes, not just canvas commits.\n console.log(type, snapshot.canUndo, snapshot.canRedo, snapshot.validation);\n});\n// Host teardown: unsubscribe(); canvas.destroy(); fonts.dispose();\n```\n\nUndo and redo belong to that same session (`canvas.editor.undo()` / `redo()`). Subscribe when synchronizing another control or a save indicator; `onCommit` alone does not cover external edits, undo or restore. Dispose subscriptions and canvases on unmount, and dispose a shared fonts handle only after its last consumer is gone.\n\nLocal persistence is optional. [`createPersistence` from `/persistence`](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/README.md#autosave-and-restore-rr-22) supports restore offers and explicit `flush()`. Commit the canvas yourself before an explicit save or flush; persistence stores the session, not an invalid raw source draft. Show unavailable-storage errors and retain a download path. Call `markSaved()` only after a real host save or OPF download; a preview or PowerPoint export does not preserve the complete OPF source. Destroy the persistence controller on teardown. It is not a server or collaboration service.\n\n## Export results and diagnostics\n\nIn a browser, `toSvg` returns SVG strings and `toPptx` returns bytes; pass the same `fonts` and `catalogs`, surface `onDiagnostic` notifications and catch rejected conversions. When importing with `fromPptx(bytes, { signals: true })`, inspect both `presentation` and `signals` before replacing the session. Native conversion can change unsupported features. In Node, Core `convert` instead returns `{ files, findings }`; [the Node guide](node.md) explains findings, errors and file publication. Do not treat a successful conversion as a claim of complete native fidelity.\n\n## Exact package references\n\n- [Editor session types](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/index.d.ts), [canvas lifecycle](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/canvas.d.ts), [portable saving](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/catalogs.d.ts).\n- [Renderer entries and optional peers](https://github.com/OpenPresentation/opf-render/blob/opf-render-v0.20.2/README.md#install-what-to-add-for-what-rr-63).\n- [PowerPoint export/import types](https://github.com/OpenPresentation/opf-pptx/blob/opf-pptx-v0.20.3/src/index.d.ts).\n\n## Editing behavior\n\n| Content or action | Current behavior |\n| --- | --- |\n| Titles, subtitles, plain text, simple numeric values | One click enters editing with the caret at the clicked character (editor 0.10.2); press-drag selects a range; while editing, double-click selects a word and triple-click a paragraph. Focus a target and press Enter, Space or F2 to edit with all text selected. See *Text entry gestures* below. |\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. A timeline event's `status` (`done`, `current`, `planned`) is a menu, not free text. |\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 `presentation`, plus `slideIndex`, `fonts` (the renderer's fonts handle), `renderOptions`, `textEntry` (`'click'` by default, or `'dblclick'`), 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(presentation)` as a second source of truth; normal document changes should flow through the session.\n\n## Text entry gestures\n\nEditor 0.10.2 follows the PowerPoint and Google Slides convention. Hover outlines a text target. A single press (mouse, pen, or a touch tap) on editable text selects the box, starts inline editing and puts the caret at the nearest character boundary to the pointer, including in wrapped, multi-line, centered, right-aligned, right-to-left and CJK text. Press and drag selects the range from the press point to the release point and never moves the box. While editing, a native double-click selects a word, a triple-click a line or paragraph, and a click elsewhere moves the caret. Clicking a different text target commits the current edit (an invalid edit still refuses) and enters the new target in the same click. Rich text uses the same gestures through its own pointer mapping.\n\nKeyboard entry keeps the replace convention: focus a target and press Enter, Space or F2 to edit with **all** text selected; `canvas.beginEdit(path)` does the same. Escape leaves editing and keeps the box selected. Images, video, charts and other non-text targets are unchanged: a click selects and a double-click opens their properties. Layout handles and block controls keep their own pointer handling.\n\n`createCanvasEditor(container, { textEntry: 'dblclick' })` keeps the older two-step gesture (a click selects, a double-click enters), but the double-click now places the caret at the pointer instead of selecting everything. Tests and hosts that used `dblclick()` and then relied on all text being selected should enter with the keyboard (focus the target, press Enter) or select explicitly; on the default canvas `dblclick()` now places the caret and selects the word under it. Carets are resolved from the traced SVG glyphs (each rendered line carries its source range) and converted to offsets in the input value, so CRLF sources, tabs and wrapped whitespace map exactly; real operating-system IME and bidi caret behavior are not verified.\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 embed the catalog records and assets they use. Inserting slides copies their records with core `copySlides`: groups match by `source`, an identical record is reused, a differing `custom` record is renamed `<id>-2` and a differing catalog revision moves into `custom` as `<id>-2`, so the inserted slides look the same and the existing slides stay 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.\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.presentation, {\n scope: 'slide', slideIndex: 0, format: 'markdown',\n});\nconst parsed = parseOpfTransfer(markdown);\nconst result = prepareOpfImport(editor.presentation, parsed, {\n mode: 'insert', slideIndex: 0,\n});\n// Host previews result.presentation before applying this single undoable change.\neditor.applyPatch([{ op: 'replace', path: '', value: result.presentation }], {\n source: 'import', rejectInvalid: true,\n});\n\nconst gallery = await loadOpfGallery('https://example.com/registry.json');\nconst presentation = 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 `presentation`/`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.presentation, {includeImplicit: true});\nconst prepared = prepareBlockInsert(editor.presentation, containers[0].path,\n createContentBlock('table')); // omit index to append\n// Render prepared.presentation 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 `{presentation, 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"
|
|
217
|
+
"markdown": "# Browser preview and live editing\n\nEditor 0.26.1 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, editor, PPTX, renderer and Gallery 0.26.1. This is the current published setup; the [quickstart](quickstart.md) is the shared command-line first run:\n\n```sh\nnpm install --save-exact @openpresentation/opf@0.26.1 @openpresentation/opf-render@0.26.1 @openpresentation/opf-editor@0.26.1 @openpresentation/opf-pptx@0.26.1 @openpresentation/gallery@0.26.1\n```\n\nNo paid service or provider account is required. The six agent skills install with `npx @openpresentation/cli@0.20.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\nThe editor exports in the browser (SVG through the renderer's browser entries, PPTX through opf-pptx). To export a saved presentation on a server, call `convert(\"deck.opf.json\", \"deck.pdf\")` (or `convert(presentation, { format: \"pdf\" | \"png\" | \"svg\" | \"pptx\" })` for the bytes) from `@openpresentation/opf` in Node, which draws with the same engines and the bundled fonts; `open(\"deck.pptx\")` reads a PowerPoint file back into a presentation for the editor ([OPF files in Node](node.md)).\n\nFor source builds and the Gallery host bundle, see [ecosystem development](ecosystem-development.md#editor-host-bundle-and-font-provenance).\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 { saveDocument } from '@openpresentation/opf-editor';\nimport { loadFonts } from '@openpresentation/opf-render/fonts-browser';\nimport { gallery } from '@openpresentation/gallery';\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 loadFonts({ faces: [\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 container = document.querySelector('#slide');\nif (!(container instanceof HTMLElement)) throw new Error('Missing slide container.');\nconst canvas = createCanvasEditor(container, {\n presentation: {\n design: { theme: 'classic', fontScheme: 'roboto' },\n slides: [{ title: 'An editable presentation', text: 'Click to edit.' }],\n },\n catalogs: [gallery], // core ships no records: register the catalog the theme and font scheme above come from\n fonts,\n onCommit: ({ editor }) => {\n const { document: updatedOPF, unresolved } = saveDocument(editor);\n // Host owns storage. Surface unresolved catalog references before saving.\n if (unresolved.length) console.warn(unresolved);\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`loadFonts` accepts explicit font-file URLs or `Uint8Array` data (`faces`). It uses the same bytes for Fontkit measurement and browser `FontFace` registration, awaits loading, reports failures, and exposes `dispose()` for its owned font faces; the handle's `pending` and `ensure` load the script and vendored faces a document needs, and the canvas waits for them before it renders. Cross-origin font URLs need CORS access. Load fonts once and share the handle between canvases. The canvas does not fetch fonts or catalog sources itself.\n\nFor standalone SVG export, pass the handle as `{ fonts }` to `toSvg` (`toSvg(deck, n, ...)` is slide n, `toSvg(deck, ...)` every slide); 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 { toSvg } from '@openpresentation/opf-render/svg';\nconst svg = toSvg(canvas.editor.presentation, 1, { fonts, catalogs: canvas.editor.catalogs }); // slide 1: slides count from 1\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 `toPng` and `toPdf`; those functions are not browser APIs.\n\n## Save, undo and host state\n\nBefore saving, exporting, importing or navigating, call `canvas.commit()`. A `false` result means the active draft could not be committed: keep it visible, show the error and stop that action. `canvas.editor.presentation` is the committed document, not necessarily the text currently being edited. A source editor must preserve invalid raw text separately instead of silently replacing it with the last valid preview.\n\n```js\nfunction documentForDownload() {\n if (!canvas.commit()) throw new Error('Finish or cancel the current edit first.');\n const result = saveDocument(canvas.editor);\n if (result.unresolved.length) throw new Error('Resolve catalog references before sharing.');\n return result.document; // Referenced catalog records are embedded; the session is unchanged.\n}\nconst unsubscribe = canvas.editor.subscribe(({ type, snapshot }) => {\n // Runs for patch, undo, redo, restore and catalog changes, not just canvas commits.\n console.log(type, snapshot.canUndo, snapshot.canRedo, snapshot.validation);\n});\n// Host teardown: unsubscribe(); canvas.destroy(); fonts.dispose();\n```\n\nUndo and redo belong to that same session (`canvas.editor.undo()` / `redo()`). Subscribe when synchronizing another control or a save indicator; `onCommit` alone does not cover external edits, undo or restore. Dispose subscriptions and canvases on unmount, and dispose a shared fonts handle only after its last consumer is gone.\n\nLocal persistence is optional. [`createPersistence` from `/persistence`](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/README.md#autosave-and-restore-rr-22) supports restore offers and explicit `flush()`. Commit the canvas yourself before an explicit save or flush; persistence stores the session, not an invalid raw source draft. Show unavailable-storage errors and retain a download path. Call `markSaved()` only after a real host save or OPF download; a preview or PowerPoint export does not preserve the complete OPF source. Destroy the persistence controller on teardown. It is not a server or collaboration service.\n\n## Export results and diagnostics\n\nIn a browser, `toSvg` returns SVG strings and `toPptx` returns bytes; pass the same `fonts` and `catalogs`, surface `onDiagnostic` notifications and catch rejected conversions. When importing with `fromPptx(bytes, { signals: true })`, inspect both `presentation` and `signals` before replacing the session. Native conversion can change unsupported features. In Node, Core `convert` instead returns `{ files, findings }`; [the Node guide](node.md) explains findings, errors and file publication. Do not treat a successful conversion as a claim of complete native fidelity.\n\n## Exact package references\n\n- [Editor session types](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/index.d.ts), [canvas lifecycle](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/canvas.d.ts), [portable saving](https://github.com/OpenPresentation/opf-editor/blob/opf-editor-v0.20.3/src/catalogs.d.ts).\n- [Renderer entries and optional peers](https://github.com/OpenPresentation/opf-render/blob/opf-render-v0.20.2/README.md#install-what-to-add-for-what-rr-63).\n- [PowerPoint export/import types](https://github.com/OpenPresentation/opf-pptx/blob/opf-pptx-v0.20.3/src/index.d.ts).\n\n## Editing behavior\n\n| Content or action | Current behavior |\n| --- | --- |\n| Titles, subtitles, plain text, simple numeric values | One click enters editing with the caret at the clicked character (editor 0.10.2); press-drag selects a range; while editing, double-click selects a word and triple-click a paragraph. Focus a target and press Enter, Space or F2 to edit with all text selected. See *Text entry gestures* below. |\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. A timeline event's `status` (`done`, `current`, `planned`) is a menu, not free text. |\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 `presentation`, plus `slideIndex`, `fonts` (the renderer's fonts handle), `renderOptions`, `textEntry` (`'click'` by default, or `'dblclick'`), 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(presentation)` as a second source of truth; normal document changes should flow through the session.\n\n## Text entry gestures\n\nEditor 0.10.2 follows the PowerPoint and Google Slides convention. Hover outlines a text target. A single press (mouse, pen, or a touch tap) on editable text selects the box, starts inline editing and puts the caret at the nearest character boundary to the pointer, including in wrapped, multi-line, centered, right-aligned, right-to-left and CJK text. Press and drag selects the range from the press point to the release point and never moves the box. While editing, a native double-click selects a word, a triple-click a line or paragraph, and a click elsewhere moves the caret. Clicking a different text target commits the current edit (an invalid edit still refuses) and enters the new target in the same click. Rich text uses the same gestures through its own pointer mapping.\n\nKeyboard entry keeps the replace convention: focus a target and press Enter, Space or F2 to edit with **all** text selected; `canvas.beginEdit(path)` does the same. Escape leaves editing and keeps the box selected. Images, video, charts and other non-text targets are unchanged: a click selects and a double-click opens their properties. Layout handles and block controls keep their own pointer handling.\n\n`createCanvasEditor(container, { textEntry: 'dblclick' })` keeps the older two-step gesture (a click selects, a double-click enters), but the double-click now places the caret at the pointer instead of selecting everything. Tests and hosts that used `dblclick()` and then relied on all text being selected should enter with the keyboard (focus the target, press Enter) or select explicitly; on the default canvas `dblclick()` now places the caret and selects the word under it. Carets are resolved from the traced SVG glyphs (each rendered line carries its source range) and converted to offsets in the input value, so CRLF sources, tabs and wrapped whitespace map exactly; real operating-system IME and bidi caret behavior are not verified.\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 embed the catalog records and assets they use. Inserting slides copies their records with core `copySlides`: groups match by `source`, an identical record is reused, a differing `custom` record is renamed `<id>-2` and a differing catalog revision moves into `custom` as `<id>-2`, so the inserted slides look the same and the existing slides stay 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.\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.presentation, {\n scope: 'slide', slideIndex: 0, format: 'markdown',\n});\nconst parsed = parseOpfTransfer(markdown);\nconst result = prepareOpfImport(editor.presentation, parsed, {\n mode: 'insert', slideIndex: 0,\n});\n// Host previews result.presentation before applying this single undoable change.\neditor.applyPatch([{ op: 'replace', path: '', value: result.presentation }], {\n source: 'import', rejectInvalid: true,\n});\n\nconst gallery = await loadOpfGallery('https://example.com/registry.json');\nconst presentation = 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 `presentation`/`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.presentation, {includeImplicit: true});\nconst prepared = prepareBlockInsert(editor.presentation, containers[0].path,\n createContentBlock('table')); // omit index to append\n// Render prepared.presentation 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 `{presentation, 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"
|
|
218
218
|
},
|
|
219
219
|
{
|
|
220
220
|
"slug": "llm-authoring",
|
|
@@ -280,13 +280,13 @@ var docsData = Object.freeze([
|
|
|
280
280
|
"slug": "quickstart",
|
|
281
281
|
"file": "docs/quickstart.md",
|
|
282
282
|
"title": "Developer quickstart",
|
|
283
|
-
"markdown": "# Developer quickstart\n\nA new developer can install the **published** OPF packages into a fresh Node 24\nproject and author, validate, compose, paginate, edit with undo, preview and export\na representative deck. No account, model call, or sibling repository checkout\nis required.\n\nThis is a documented supported subset, not universal Office or all-feature\nparity. See the [compatibility matrix](compatibility-matrix.md) for what is\nshipped versus deferred.\n\n## Versions\n\n**Published set (2026-10-21):** core **0.25.0**, CLI **0.25.0**, renderer **0.25.0**,\nPPTX **0.25.0**, editor **0.25.1** and Gallery **5.0.0**. `node scripts/registry-plan.mjs` prints these releases with their registry `gitHead`;\nregistry identity is separate from the consumer and native checks below. All declare\n`engines.node: >=22`; Node 24 is the toolchain this quickstart is verified on.\n\n```sh\nnode -v # 24.x (verified); 22 and later are declared\nnpm install @openpresentation/opf@0.25.0 \\\n @openpresentation/opf-render@0.25.0 \\\n @openpresentation/opf-editor@0.25.3 \\\n @openpresentation/opf-pptx@0.25.0 \\\n @openpresentation/cli@0.25.0 \\\n @openpresentation/gallery@5.0.0\n# opf-render 0.25 keeps its converters and font packages as optional peers: install the ones you use\n# (here the base font pack, PNG and PDF; add @expo-google-fonts/* office or script packs as needed)\nnpm install @expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 \\\n @resvg/resvg-js@^2.6.2 pdf-lib@^1.17.1 sharp@^0.35.5\n```\n\nA missing converter or font package is a typed error (`converter-missing`, `font-resource-unavailable`) whose message\nnames the `npm install` command, so a host that only previews SVG in a browser installs none of them.\n\nCopy [`docs/quickstart/developer-quickstart.opf.json`](quickstart/developer-quickstart.opf.json)\ninto that project as `deck.opf.json`. That file is a docs fixture, not one of\nthe 127 decks in `@openpresentation/opf/examples`. Verify the install came from the registry\n(`package-lock.json` `resolved` URLs start with `https://registry.npmjs.org/`)\nand that you did not add `file:` dependencies on this repository.\n\nThe [format card](format-card.md) describes ColorRef, named variables and the\ncurrent source contract. `opf embed` embeds the catalog records a deck uses for\nportable offline authoring; it does not download remote assets. Keep the\nColorRef docs fixture outside the 127-deck example/golden corpus in this update.\n\n## The short path\n\nWith core 0.18.0 and later, three calls of `@openpresentation/opf` cover files in a Node application (core 0.17 had them at `@openpresentation/opf/node`):\n\n```js\nimport * as opf from '@openpresentation/opf';\n\nawait opf.convert('deck.opf.md', 'deck.pdf'); // also .pptx, .png (one per slide), .svg, .zip and the deck forms\nconst deck = await opf.open('deck.opf.md'); // .opf.md, .opf.yaml, .opf.json, or a .pptx to import\ndeck.slides.push({ title: 'Q4' });\nawait opf.save(deck, 'deck.opf.md');\n```\n\nThe CLI does the same from a shell: `opf convert deck.opf.md deck.pdf`. The sections below take the pieces one at a time.\n\n## Author and validate\n\n```sh\nnpx --no-install opf --version\nnpx --no-install opf validate deck.opf.json\nnpx --no-install opf validate deck.opf.json --format text\n```\n\nThe CLI depends on core, which holds the schema, catalogs and the checker. Validation never renders; `opf convert` produces PDF, PPTX, PNG and SVG and reads `.pptx` files through the optional peers `@openpresentation/opf-render` and\n`@openpresentation/opf-pptx` (see [the CLI reference](cli.md); `opf doctor` says what is installed).\n`opf --version` reports the CLI and the installed core. Successful validation is not\nvisual verification.\n\nLibrary equivalents:\n\n```js\nimport { readFile } from 'node:fs/promises';\nimport { validate } from '@openpresentation/opf';\n\nconst source = await readFile('deck.opf.json', 'utf8');\nconst document = JSON.parse(source);\nconsole.log(validate(document)); // a parsed document: findings of every category\nconsole.log(validate(source)); // JSON text: the same findings with line and column\n```\n\n## Offline fonts, composition, pagination\n\n```js\nimport { paginate, resolveSlideContext } from '@openpresentation/opf';\nimport { composeSlide } from '@openpresentation/opf/composition';\nimport { loadFonts } from '@openpresentation/opf-render/fonts-node';\n\nconst fonts = await loadFonts({ pack: 'base' });\nconst { slide, options } = resolveSlideContext(document, 0, { fonts });\nconst geometry = composeSlide(slide, options);\nconst { presentation, pages } = paginate(document, { fonts });\n```\n\n`loadFonts({ pack: 'base' })` loads the bundled Roboto faces for\n`design.fontScheme: 'roboto'`. It returns the fonts handle: every deck-level\nverb (`paginate`, `toSvg`, `toPng`, `toPdf`, `toPptx`) takes it as\n`{ fonts }` and reads what it needs from it (the `textMeasurement`, the faces\nto embed, the font files). `resolveSlideContext(document, index, { fonts, catalogs })` resolves\none slide's layout, canvas, theme and font families (slide design, then deck\ndesign, then theme, then the engine default font scheme) into the options `composeSlide`\ntakes, so you never look up a font scheme yourself; a reference that resolves\nnowhere comes back as an `unresolved-reference` diagnostic. It also returns\n`slide`: the slide with `{{slide.number}}`, `{{slide.section}}` and\n`{{deck.slideCount}}` substituted for `options.slideNumber` and\n`options.slideCount`; compose and draw that slide. `paginate`\ndoes the same for every slide. The helper does not install system fonts or\nchange the authored scheme. Reuse the same `fonts` for SVG preview and PPTX\nexport.\n\nShared headers and footers use `furniture-flow-v2`. Body content stays between\n`geometry.furniture.headerBottom` and `geometry.furniture.footerTop`. A zone\nwrites generated values as variables in its `text`, such as\n`\"{{slide.number}} / {{deck.slideCount}}\"` or `\"{{organization.name}}\"`.\n\nPagination returns a new presentation plus source mappings. It preserves\nauthored text, whitespace and reading order; it does not drop overflowed\ncontent.\n\n```sh\nnpx --no-install opf paginate deck.opf.json paginated.opf.json\n```\n\n## Edit with undo\n\n```js\nimport { createEditorSession } from '@openpresentation/opf-editor';\n\nconst editor = createEditorSession(document, { rejectInvalid: true });\nconst original = editor.presentation.slides[0].title;\neditor.set('slides.0.title', 'Edited title');\neditor.undo();\n// original title, including whitespace, is restored\n```\n\nThe CLI can apply JSON Patch edits (`opf edit`) but has no persistent undo\nhistory. Use the editor session or version control for undo.\n\n## Preview and export\n\n`@openpresentation/opf` in Node (core 0.18.0 and later) reads and writes files with the bundled fonts and loads the render and\nPPTX engines you installed:\n\n```js\nimport * as opf from '@openpresentation/opf';\n\nawait opf.convert('deck.opf.json', 'deck.pdf');\nawait opf.convert('deck.opf.json', 'slides/deck.png', { scale: 2 }); // slides/deck-1.png, -2, ... (or \"slides/slide-{n}.png\")\nawait opf.convert('deck.opf.json', 'deck.pptx');\nconst back = await opf.open('deck.pptx'); // PowerPoint to OPF\nconst { files } = await opf.convert(back, { format: 'svg' }); // bytes, nothing written\n```\n\n`convert` checks the deck (format and references), draws with the renderer's bundled open fonts and no system fonts,\nreads images next to the input file, never reads a clock (pass `date: 'YYYY-MM-DD'` for a date field) and writes each file\natomically. A missing engine throws `OPFExportError` with code `peer-not-installed` and the install command. The options and\nerrors are in [OPF files in Node](node.md).\n\n### Advanced: the engines directly\n\n`convert` calls these. Use them when you need the pieces: your own fonts handle, one SVG per slide in a browser,\nor options the function does not expose.\n\n```js\nimport { toSvg, toPng, toPdf } from '@openpresentation/opf-render';\nimport { toPptx } from '@openpresentation/opf-pptx';\n\nconst svgs = toSvg(presentation, { fonts }); // one SVG per slide\nconst png = await toPng(svgs[0], { fonts });\nconst pdf = await toPdf(svgs, { fonts });\nconst pptx = await toPptx(presentation, { fonts });\n```\n\n`toSvg` is the local preview of every slide (`toSvg(presentation, 3, { fonts })` draws slide 3 as one string). PNG rasterizes that SVG.\nPDF (opf-render 0.12.0 and later) converts the same SVG to **vector paths with\nselectable, searchable text** in embedded font subsets, with no second layout pass;\npass `{ raster: true }` for the image-per-slide output that renderers up to 0.11.9\nalways wrote. Pass the same fonts handle as for PNG; vector PDF never uses system\nfonts.\n`toPptx` is the supported PowerPoint export from OPF. **Its text is editable\nline by line, not paragraph by paragraph:** it writes one text box for each\nwrapped line, so text you add or lengthen in PowerPoint or Keynote does not\nreflow into the next line, and a title that wraps is not the Title placeholder.\nFor heavy text changes, edit the OPF source and export again. From opf-pptx and\nCLI 0.22.0, `opf convert deck.opf.json deck.pptx --body-text native` (`bodyText:\n'native'` in `toPptx`) is an opt-in mode that writes whole paragraphs and binds a\nwrapped title to the Title placeholder; see [when to choose it](cli.md#pptx-text-rr-88-rr-91). Shared\nheaders and footers are written on the slide master or layout. The retained\n[Windows sample](evidence/rr-20-windows-current-2026-10-14/README.md) covers\n17 selected native cases and four actual UI operations; 41 of 58 cases were\nnot executed natively. This quickstart verifies published Node APIs and\nexports. It does not certify every Office operation or global native fidelity.\n\nBrowser preview uses the same SVG core plus\n`@openpresentation/opf-render/fonts-browser` and\n`@openpresentation/opf-editor/canvas`. Load the same font bytes the Node helper\nresolved. Do not fetch fonts from the network at render time.\n\n## Prove it\n\nFrom this repository, after a normal `pnpm install`:\n\n```sh\nnode scripts/test-developer-quickstart.mjs\n```\n\nThat script creates an empty temp project, installs the published versions from\nthe npm registry, copies this example, and asserts validate, offline\nfonts, furniture composition, pagination, undo, SVG, PNG, PDF and PPTX.\nIt fails if any package is a `file:` or workspace link.\n\n## What this does not cover\n\n- Renderer native-width residuals:\n [opf-render#24](https://github.com/OpenPresentation/opf-render/issues/24)\n- Native PowerPoint open/edit/save/reopen and real Office Header/Footer (`p:hf`):\n [opf#87](https://github.com/OpenPresentation/opf/issues/87)\n- Remaining GitHub [issue 88](https://github.com/OpenPresentation/opf/issues/88)\n checklist (the Inspector overlay/json-options, gallery Playground+Editor\n links, and Header & footer playground example are already live on\n production; the issue stays open)\n- Archived font-shaping prototypes (not in the published runtime)\n- PDF/UA or PDF/A conformance, general SVG diagrams, and Mermaid\n"
|
|
283
|
+
"markdown": "# Developer quickstart\n\nA new developer can install the **published** OPF packages into a fresh Node 24\nproject and author, validate, compose, paginate, edit with undo, preview and export\na representative deck. No account, model call, or sibling repository checkout\nis required.\n\nThis is a documented supported subset, not universal Office or all-feature\nparity. See the [compatibility matrix](compatibility-matrix.md) for what is\nshipped versus deferred.\n\n## Versions\n\n**Published set (2026-10-24):** core, CLI, renderer, PPTX, editor and Gallery, all at\nthe shared version **0.26.1**. `node scripts/registry-plan.mjs` prints these releases with their registry `gitHead`;\nregistry identity is separate from the consumer and native checks below. All declare\n`engines.node: >=22`; Node 24 is the toolchain this quickstart is verified on.\n\n```sh\nnode -v # 24.x (verified); 22 and later are declared\nnpm install @openpresentation/opf@0.26.1 \\\n @openpresentation/opf-render@0.26.1 \\\n @openpresentation/opf-editor@0.26.1 \\\n @openpresentation/opf-pptx@0.26.1 \\\n @openpresentation/cli@0.26.1 \\\n @openpresentation/gallery@0.26.1\n# opf-render 0.26 keeps its converters and font packages as optional peers: install the ones you use\n# (here the base font pack, PNG and PDF; add @expo-google-fonts/* office or script packs as needed)\nnpm install @expo-google-fonts/roboto@0.4.3 @expo-google-fonts/roboto-mono@0.4.2 \\\n @resvg/resvg-js@^2.6.2 pdf-lib@^1.17.1 sharp@^0.35.5\n```\n\nA missing converter or font package is a typed error (`converter-missing`, `font-resource-unavailable`) whose message\nnames the `npm install` command, so a host that only previews SVG in a browser installs none of them.\n\nCopy [`docs/quickstart/developer-quickstart.opf.json`](quickstart/developer-quickstart.opf.json)\ninto that project as `deck.opf.json`. That file is a docs fixture, not one of\nthe 127 decks in `@openpresentation/opf/examples`. Verify the install came from the registry\n(`package-lock.json` `resolved` URLs start with `https://registry.npmjs.org/`)\nand that you did not add `file:` dependencies on this repository.\n\nThe [format card](format-card.md) describes ColorRef, named variables and the\ncurrent source contract. `opf embed` embeds the catalog records a deck uses for\nportable offline authoring; it does not download remote assets. Keep the\nColorRef docs fixture outside the 127-deck example/golden corpus in this update.\n\n## The short path\n\nWith core 0.18.0 and later, three calls of `@openpresentation/opf` cover files in a Node application (core 0.17 had them at `@openpresentation/opf/node`):\n\n```js\nimport * as opf from '@openpresentation/opf';\n\nawait opf.convert('deck.opf.md', 'deck.pdf'); // also .pptx, .png (one per slide), .svg, .zip and the deck forms\nconst deck = await opf.open('deck.opf.md'); // .opf.md, .opf.yaml, .opf.json, or a .pptx to import\ndeck.slides.push({ title: 'Q4' });\nawait opf.save(deck, 'deck.opf.md');\n```\n\nThe CLI does the same from a shell: `opf convert deck.opf.md deck.pdf`. The sections below take the pieces one at a time.\n\n## Author and validate\n\n```sh\nnpx --no-install opf --version\nnpx --no-install opf validate deck.opf.json\nnpx --no-install opf validate deck.opf.json --format text\n```\n\nThe CLI depends on core, which holds the schema, catalogs and the checker. Validation never renders; `opf convert` produces PDF, PPTX, PNG and SVG and reads `.pptx` files through the optional peers `@openpresentation/opf-render` and\n`@openpresentation/opf-pptx` (see [the CLI reference](cli.md); `opf doctor` says what is installed).\n`opf --version` reports the CLI and the installed core. Successful validation is not\nvisual verification.\n\nLibrary equivalents:\n\n```js\nimport { readFile } from 'node:fs/promises';\nimport { validate } from '@openpresentation/opf';\n\nconst source = await readFile('deck.opf.json', 'utf8');\nconst document = JSON.parse(source);\nconsole.log(validate(document)); // a parsed document: findings of every category\nconsole.log(validate(source)); // JSON text: the same findings with line and column\n```\n\n## Offline fonts, composition, pagination\n\n```js\nimport { paginate, resolveSlideContext } from '@openpresentation/opf';\nimport { composeSlide } from '@openpresentation/opf/composition';\nimport { loadFonts } from '@openpresentation/opf-render/fonts-node';\n\nconst fonts = await loadFonts({ pack: 'base' });\nconst { slide, options } = resolveSlideContext(document, 0, { fonts });\nconst geometry = composeSlide(slide, options);\nconst { presentation, pages } = paginate(document, { fonts });\n```\n\n`loadFonts({ pack: 'base' })` loads the bundled Roboto faces for\n`design.fontScheme: 'roboto'`. It returns the fonts handle: every deck-level\nverb (`paginate`, `toSvg`, `toPng`, `toPdf`, `toPptx`) takes it as\n`{ fonts }` and reads what it needs from it (the `textMeasurement`, the faces\nto embed, the font files). `resolveSlideContext(document, index, { fonts, catalogs })` resolves\none slide's layout, canvas, theme and font families (slide design, then deck\ndesign, then theme, then the engine default font scheme) into the options `composeSlide`\ntakes, so you never look up a font scheme yourself; a reference that resolves\nnowhere comes back as an `unresolved-reference` diagnostic. It also returns\n`slide`: the slide with `{{slide.number}}`, `{{slide.section}}` and\n`{{deck.slideCount}}` substituted for `options.slideNumber` and\n`options.slideCount`; compose and draw that slide. `paginate`\ndoes the same for every slide. The helper does not install system fonts or\nchange the authored scheme. Reuse the same `fonts` for SVG preview and PPTX\nexport.\n\nShared headers and footers use `furniture-flow-v2`. Body content stays between\n`geometry.furniture.headerBottom` and `geometry.furniture.footerTop`. A zone\nwrites generated values as variables in its `text`, such as\n`\"{{slide.number}} / {{deck.slideCount}}\"` or `\"{{organization.name}}\"`.\n\nPagination returns a new presentation plus source mappings. It preserves\nauthored text, whitespace and reading order; it does not drop overflowed\ncontent.\n\n```sh\nnpx --no-install opf paginate deck.opf.json paginated.opf.json\n```\n\n## Edit with undo\n\n```js\nimport { createEditorSession } from '@openpresentation/opf-editor';\n\nconst editor = createEditorSession(document, { rejectInvalid: true });\nconst original = editor.presentation.slides[0].title;\neditor.set('slides.0.title', 'Edited title');\neditor.undo();\n// original title, including whitespace, is restored\n```\n\nThe CLI can apply JSON Patch edits (`opf edit`) but has no persistent undo\nhistory. Use the editor session or version control for undo.\n\n## Preview and export\n\n`@openpresentation/opf` in Node (core 0.18.0 and later) reads and writes files with the bundled fonts and loads the render and\nPPTX engines you installed:\n\n```js\nimport * as opf from '@openpresentation/opf';\n\nawait opf.convert('deck.opf.json', 'deck.pdf');\nawait opf.convert('deck.opf.json', 'slides/deck.png', { scale: 2 }); // slides/deck-1.png, -2, ... (or \"slides/slide-{n}.png\")\nawait opf.convert('deck.opf.json', 'deck.pptx');\nconst back = await opf.open('deck.pptx'); // PowerPoint to OPF\nconst { files } = await opf.convert(back, { format: 'svg' }); // bytes, nothing written\n```\n\n`convert` checks the deck (format and references), draws with the renderer's bundled open fonts and no system fonts,\nreads images next to the input file, never reads a clock (pass `date: 'YYYY-MM-DD'` for a date field) and writes each file\natomically. A missing engine throws `OPFExportError` with code `peer-not-installed` and the install command. The options and\nerrors are in [OPF files in Node](node.md).\n\n### Advanced: the engines directly\n\n`convert` calls these. Use them when you need the pieces: your own fonts handle, one SVG per slide in a browser,\nor options the function does not expose.\n\n```js\nimport { toSvg, toPng, toPdf } from '@openpresentation/opf-render';\nimport { toPptx } from '@openpresentation/opf-pptx';\n\nconst svgs = toSvg(presentation, { fonts }); // one SVG per slide\nconst png = await toPng(svgs[0], { fonts });\nconst pdf = await toPdf(svgs, { fonts });\nconst pptx = await toPptx(presentation, { fonts });\n```\n\n`toSvg` is the local preview of every slide (`toSvg(presentation, 3, { fonts })` draws slide 3 as one string). PNG rasterizes that SVG.\nPDF (opf-render 0.12.0 and later) converts the same SVG to **vector paths with\nselectable, searchable text** in embedded font subsets, with no second layout pass;\npass `{ raster: true }` for the image-per-slide output that renderers up to 0.11.9\nalways wrote. Pass the same fonts handle as for PNG; vector PDF never uses system\nfonts.\n`toPptx` is the supported PowerPoint export from OPF. **Its text is editable\nline by line, not paragraph by paragraph:** it writes one text box for each\nwrapped line, so text you add or lengthen in PowerPoint or Keynote does not\nreflow into the next line, and a title that wraps is not the Title placeholder.\nFor heavy text changes, edit the OPF source and export again. From opf-pptx and\nCLI 0.22.0, `opf convert deck.opf.json deck.pptx --body-text native` (`bodyText:\n'native'` in `toPptx`) is an opt-in mode that writes whole paragraphs and binds a\nwrapped title to the Title placeholder; see [when to choose it](cli.md#pptx-text-rr-88-rr-91). Shared\nheaders and footers are written on the slide master or layout. The retained\n[Windows sample](evidence/rr-20-windows-current-2026-10-14/README.md) covers\n17 selected native cases and four actual UI operations; 41 of 58 cases were\nnot executed natively. This quickstart verifies published Node APIs and\nexports. It does not certify every Office operation or global native fidelity.\n\nBrowser preview uses the same SVG core plus\n`@openpresentation/opf-render/fonts-browser` and\n`@openpresentation/opf-editor/canvas`. Load the same font bytes the Node helper\nresolved. Do not fetch fonts from the network at render time.\n\n## Prove it\n\nFrom this repository, after a normal `pnpm install`:\n\n```sh\nnode scripts/test-developer-quickstart.mjs\n```\n\nThat script creates an empty temp project, installs the published versions from\nthe npm registry, copies this example, and asserts validate, offline\nfonts, furniture composition, pagination, undo, SVG, PNG, PDF and PPTX.\nIt fails if any package is a `file:` or workspace link.\n\n## What this does not cover\n\n- Renderer native-width residuals:\n [opf-render#24](https://github.com/OpenPresentation/opf-render/issues/24)\n- Native PowerPoint open/edit/save/reopen and real Office Header/Footer (`p:hf`):\n [opf#87](https://github.com/OpenPresentation/opf/issues/87)\n- Remaining GitHub [issue 88](https://github.com/OpenPresentation/opf/issues/88)\n checklist (the Inspector overlay/json-options, gallery Playground+Editor\n links, and Header & footer playground example are already live on\n production; the issue stays open)\n- Archived font-shaping prototypes (not in the published runtime)\n- PDF/UA or PDF/A conformance, general SVG diagrams, and Mermaid\n"
|
|
284
284
|
},
|
|
285
285
|
{
|
|
286
286
|
"slug": "release-process",
|
|
287
287
|
"file": "docs/release-process.md",
|
|
288
288
|
"title": "OPF Release Process",
|
|
289
|
-
"markdown": "# OPF Release Process\n\nUse Node 24 (`24.x`) for all future source, candidate and registry verification.\nThe packages declare the open-ended `engines.node` `>=22` (RR-20, tested on Node 22, 24 and 26 by the `node-range`\nCI legs, which run in the nightly workflow, before every release and on pull requests labelled `full-ci`); a closed range such as `24.x` makes npm on any other Node silently install an older release, and\n`pnpm check:engines-range` fails on one, including in a lockfile that pins a release made after the fix.\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 six published packages of this repository:\n[`@openpresentation/opf`](https://www.npmjs.com/package/@openpresentation/opf) (core, `packages/javascript`),\n`@openpresentation/cli`, `@openpresentation/opf-render`, `@openpresentation/opf-pptx`, `@openpresentation/opf-editor` and\n`@openpresentation/gallery`. They are released together, at one version, with one tag.\n\n## Release: one click\n\n> Status: the workflows and `scripts/release.mjs` are in place and rehearsed with dry runs. The first real release through them\n> is the canary `0.26.0-rc.0` (MR-41), then 0.26.0 (MR-44). Releases up to 0.25.x were made from the four old repositories with\n> a per-package train; that process is retired and not described here.\n\n**One version, one tag, one workflow.** All six packages carry the same version (`pnpm check:versions`); a release is the tag\n`vX.Y.Z` on a commit of `main`, published by `.github/workflows/npm-publish.yml` in dependency order: gallery, core, render,\nPPTX, editor, CLI. In 0.x a breaking change is a minor release and everything else a patch. Prereleases (`0.26.0-rc.0`) go to\nthe dist-tag `next`, releases to `latest`; both are passed explicitly to `npm publish --tag` for every package.\n\n### Run a release\n\nActions, **Release**, **Run workflow** on `main`:\n\n| Input | Meaning |\n| --- | --- |\n| `version` | `X.Y.Z`, `X.Y.Z-rc.N`, or `patch` / `minor` of the shared line (the highest `latest` of core, CLI, render, PPTX and editor) |\n| `dry-run` | Default **true**: rehearse everything up to `npm publish --dry-run`; nothing is committed, tagged or published. Set false for the real release |\n| `skip-gate` | A dry run only: skip the slow gate |\n\nRun it with `dry-run` true first when the change touches the release machinery; a real release of ordinary changes needs no\nrehearsal. The jobs:\n\n1. **plan**: a real release runs on `main`, and the check `verify` is green for its head (one API call, no waiting).\n2. **gate** (parallel with prepare): `nightly.yml` called on that commit: Windows, macOS, Node 22 and 26, the packed-install and\n registry consumers. A red leg stops the release before anything is committed or tagged.\n3. **prepare** (parallel with gate): `node scripts/release.mjs prepare <version>` in the runner: sets the six versions, core's\n peer ranges and `PEER_RANGES` (`packages/javascript/src/node/peers.ts`), assembles every `changes/` fragment into its package\n changelog (a package without a fragment gets \"No package changes; released with the set.\"), applies the gallery stability gate\n and the version rules below, refreshes the lockfile, and uploads the result as a patch. A **prerelease** (`X.Y.Z-rc.N`) only sets the versions: it leaves every changelog and `changes/` alone, and its GitHub release notes are assembled from the fragments (`release.mjs notes`), so the stable release that follows carries every note. A refused version fails here, in two\n minutes.\n4. **commit and tag** (real release, after gate and prepare): with the GitHub App's token (a bypass actor on the `main` ruleset)\n applies the patch as one commit `release vX.Y.Z` on exactly the commit the gate tested, pushes it to `main`, then pushes the\n tag `vX.Y.Z`. If `main` moved during the run, or the tag exists, it stops. A tag pushed with the App's token starts\n `npm-publish.yml` (a tag pushed with `GITHUB_TOKEN` would not).\n5. **npm-publish.yml** on the tag: `plan` (the tag is on `main`), `build` (one install and build, `pnpm pack` of the six,\n `scripts/stamp-githead.mjs` adds `gitHead` to each tarball because `pnpm pack` omits it, a smoke test installs the six\n tarballs together, renders the quickstart deck and runs the CLI), `publish` (the only job with `id-token: write`; the\n pre-publish gate, then `npm publish <tarball> --access public --provenance --tag <tag>` six times, printing npm's own output, then polling until the registry serves each version (it lags the upload by up to several minutes); a version already on npm is\n skipped), `verify` (below), `release` (the GitHub release, notes grouped by package) and `report` (a failure opens the issue\n \"Release vX.Y.Z failed\", labeled `release-failed`).\n6. **follow the publish run** (the Release run waits for it, so it is green only when the whole release is).\n\nTime target: about 20 minutes from the click to six verified packages (measured in MR-44).\n\n### Version rules\n\n- `prepare` refuses a version that is not above the shared line's previous release, and one already on npm for any package\n (a published version is never reused; fix forward).\n- **The gallery stability rule** (`scripts/gallery-stability.mjs`, `pnpm check:stability`; see\n [default-catalog.md](default-catalog.md)): a declared drawing change (a `drawing` entry in a gallery fragment), a removed record\n id or a `CATALOG_SCHEMA` change needs a **minor** release; `prepare` refuses a patch. New records and unchanged drawings go in\n any release. The baseline is the gallery's `latest` dist-tag, never its highest version.\n- **The publish gate** (`release.mjs check-publish`, run by `publish`): each package's version is above its own registry\n `latest`, except the gallery, which is compared with the shared line, because its `latest` was 5.0.0 before it joined the\n shared 0.x line (D16). A stable version needs `latest`, a prerelease must not take it. 5.0.0 and the older gallery versions\n stay published and are never deprecated; because they are above the shared line, every gallery publish names its tag explicitly.\n- The shared line cannot use 1.0.0 to 5.0.0 (taken by the gallery): the first stable major for all six is 6.0.0.\n\n### Trusted publishing\n\nAll six packages have the trusted publisher: GitHub Actions, organization `OpenPresentation`, repository `opf`, workflow filename\n`npm-publish.yml`, no environment (owner steps O1 in `docs/programs/monorepo/owner-steps.md`). npm checks the entry against\nthe **top-level** workflow of the run, not against a reusable workflow that contains `npm publish`, so `npm-publish.yml` is the\nworkflow that starts on the tag and `release.yml` only pushes the tag. `publish` refuses to publish from any other top-level\nworkflow. A publish fails with `E404` or `E403 ... not a trusted publisher` when an entry is wrong: fix the entry and re-run\nthe workflow on the tag. There is no `NPM_TOKEN`, and no local publish: the provenance gate applies to every release.\n\n### Retry and failure\n\nA failed `npm-publish.yml` run is fixed and run again on the same tag: Actions, **Publish OPF npm packages**, **Run workflow**,\nref `vX.Y.Z`, `dry-run` false (or re-run the failed run). Versions already on npm are skipped, so it continues where it stopped.\nA partial publish is safe: the `^X.Y.Z` floors point at packages that exist, because the order is dependency order. A real\ndefect in a published version is fixed forward with the next patch and a new tag; tags are never moved. If `prepare` or the gate\nfails, nothing was committed: fix the cause and run the release again.\n\n### Fallback: a release pull request\n\nIf no App token is available, or a person prefers review: on a branch run `pnpm release:prepare X.Y.Z`\n(`node scripts/release.mjs prepare X.Y.Z`; it needs `pnpm build` first for the stability gate), open the pull request, merge it\non green, then on `main` run `git tag vX.Y.Z && git push origin vX.Y.Z`. The publish workflow is the same.\n\n### Verify\n\nThe `verify` job runs `node scripts/release.mjs verify X.Y.Z --tag <dist-tag> --commit <release commit> --wait 10` and then the\nsmoke test against the registry (`release.mjs smoke X.Y.Z --registry`). For all six packages it checks the registry manifest,\n`gitHead` and the provenance source commit equal to the release commit, the provenance workflow `npm-publish.yml` on\n`refs/tags/vX.Y.Z`, the subject digest, the dist-tag, that no manifest carries a `workspace:` range and every sibling range is\n`^X.Y.Z`, `npm audit signatures`, and that each package changelog has the version's section. By hand:\n\n```sh\nnode scripts/release.mjs verify X.Y.Z --tag latest # exit 0 when all six verify\nnpm view @openpresentation/opf@X.Y.Z gitHead dist.attestations.url\nnpm view @openpresentation/gallery dist-tags --json # latest is X.Y.Z; 5.0.0 stays published\n```\n\n### `scripts/release.mjs`\n\n| Command | Does |\n| --- | --- |\n| `prepare <version> [--dry-run] [--no-stability] [--previous V]` | the prepare step above (`--dry-run` writes nothing) |\n| `pack <version> --commit <sha> --out <dir>` | `pnpm pack` of the six, `gitHead` stamped, `release-manifest.json` with digests |\n| `smoke <version> (--dir <dir> \\| --registry)` | install the six together, render, resolve, run the CLI |\n| `check-publish <version> [--tag t]` | the pre-publish gate |\n| `publish <version> --dir <dir> [--tag t] [--dry-run] [--wait minutes] [--poll seconds] [--expect-staged]` | the gate, then `npm publish` in dependency order: npm's output is printed, the version must be served within `--wait` (default 15 min, after all six uploads), a staged result fails unless `--expect-staged` |\n| `verify <version> [--tag t] [--commit sha] [--wait minutes]` | the registry and provenance check (a prerelease has no changelog section to check) |\n| `notes <version> [--out file]` | the GitHub release body |\n| `order` | the publish order |\n\n### After a release: the sites\n\nAfter a **stable** release, `release.yml` calls `.github/workflows/bump-sites.yml` (a prerelease never does). For each of the\nthree sites (`Data-Advantage/openpresentation-site` and `Data-Advantage/pptx-gallery` on `main`, `Data-Advantage/pptx-dev` on\n`master`) it opens one pull request, \"bump to vX.Y.Z\", from the branch `bot/opf-vX.Y.Z`, that sets every `@openpresentation/*`\npin the site declares to the released version and refreshes the site's lockfile (`scripts/bump-site.mjs`). A patch bump is labeled\n`auto-merge`; a minor or major bump (a breaking change in 0.x) is labeled `needs-review` and stays open. The workflow only opens\nand labels the pull request and never merges it: the site's own CI merges an `auto-merge` pull request after its\n`build-and-smoke` job passes, using the GitHub App's token so that the merge starts the site's deploy workflows (D19; the sites\nare private repositories in a Free-plan organization and have no rulesets or required checks). A bump that cannot be made opens\nan issue in this repository (`bump-sites-failed`) and never fails the release. To run it again, or for a release made by the pull-request\nfallback or from a tag, use Actions, **Bump sites**, **Run workflow** with the version. The App, its secrets and the site-side\nworkflows are owner steps O4 and O8 (`docs/programs/monorepo/owner-steps.md`); the workflow files for the sites are in\n`docs/programs/monorepo/site-workflows/`.\n\n### Who may release\n\nThe owner authorized agents to prepare and publish npm releases on 2026-09-29 (`AGENTS.md`). A release is made only through\n`release.yml` (or the pull-request fallback) and `npm-publish.yml`, only when one is required and only with every gate green;\nagents never publish from a laptop, never skip the gate and never pass a different `--tag` to make a check pass. Dry runs are\nfree at any time.\n\n## Geometry-moving core releases\n\nCore composition changes that move geometry make the preview and the PPTX export drift when the renderer and the exporter resolve\ndifferent core versions. This cannot happen between the packages of this repository: all six are released at one version, and\ntheir runtime dependencies and peers on each other are `workspace:^`, published as `^X.Y.Z`, so the floor of every sibling is the\nrelease itself. Do not hand-edit those ranges, and do not release one package alone.\n\n## Renderer font-count gate (RR-90)\n\nThe installed-consumer check (`scripts/test-packed-ecosystem.mjs`) and `test:gallery-lazy-fonts` compare the number of vendored faces a renderer lists as lazy fonts with `scripts/lazy-font-counts.mjs`. A renderer release whose fonts did not change needs no new entry. The gate hashes the renderer package's font set, which is 143 files: `dist/font-manifest.js` and every file under `fonts/` (faces, licenses, `PROVENANCE.json`, `NOTICE.md`). An unlisted version, stable or a prerelease (`X.Y.Z-rc.N`), passes when that hash is in `REVIEWED_FONT_SETS` and its measured count equals the count recorded there.\n\nIt fails closed, with a message that says what changed, when:\n\n- the font files differ from every reviewed set (the message prints the new hashes): review what changed, then add the set to `REVIEWED_FONT_SETS` and the version's count to `LAZY_FONT_COUNTS`;\n- the font files are a reviewed set but the count differs: find out why the count moved, then add an entry;\n\nA prerelease is held to the same rule as the stable version: its label neither admits nor refuses it, its font files and count do.\n\nTo review a set, pack the release (`npm pack @openpresentation/opf-render@<version>`), run `hashFontSet` from `scripts/lazy-font-counts.mjs` over the unpacked `package/` folder, and record the result with the versions you checked. Every existing `LAZY_FONT_COUNTS` entry stays and keeps its own rule.\n\n## Troubleshooting\n\n- **The tag or version check fails** (`npm-publish.yml`, job `plan`): the tag does not point at a commit of `main`, or the version\n is not well formed. Inspect the exact tag. Never move or re-push a release tag; fix a real error on `main` and release the\n next version. A transient failure is retried on the same unchanged tag.\n- **`E404`, `E403` or `ENEEDAUTH` from `npm publish`**: the trusted publisher of that package is missing or wrong. It must be\n organization `OpenPresentation`, repository `opf`, workflow filename `npm-publish.yml`, no environment, and the `publish` job\n needs `id-token: write`. Fix the entry in npm (owner step O1) and re-run the workflow on the tag.\n- **`publish` fails with \"npm accepted X (exit 0) but the registry still does not serve it\"** or \"npm staged X\": npm returned success\n (or staged the upload) but the version is not installable. Open the package on npmjs.com, Staged Packages tab, or run\n `npm stage list <name>` as a maintainer: a staged version must be approved with 2FA (`npm stage approve <stage-id>`;\n trusted-publishing OIDC tokens cannot approve). Otherwise wait for propagation and re-run the workflow on the same tag; the\n packages already served are skipped.\n- **A version is already on npm**: `publish` skips it; do not try to publish it again. Verify the package and continue.\n- **The gate is red**: the failing leg of `nightly.yml` is in the Release run. Fix it on `main` and release again; nothing was\n committed. A flaky leg is quarantined (`pnpm check:quarantine`), never skipped for a release.\n- **`release.yml` stopped with \"main moved\"**: a commit landed on `main` after the run started, and the gate had tested the\n earlier one. Run the release again.\n- **A nightly failure**: the `nightly-red` issue lists the failing legs and the run; it never blocks a pull request, and the\n release gate refuses to publish on the same failure.\n"
|
|
289
|
+
"markdown": "# OPF Release Process\n\nUse Node 24 (`24.x`) for all future source, candidate and registry verification.\nThe packages declare the open-ended `engines.node` `>=22` (RR-20, tested on Node 22, 24 and 26 by the `node-range`\nCI legs, which run in the nightly workflow, before every release and on pull requests labelled `full-ci`); a closed range such as `24.x` makes npm on any other Node silently install an older release, and\n`pnpm check:engines-range` fails on one, including in a lockfile that pins a release made after the fix.\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 six published packages of this repository:\n[`@openpresentation/opf`](https://www.npmjs.com/package/@openpresentation/opf) (core, `packages/javascript`),\n`@openpresentation/cli`, `@openpresentation/opf-render`, `@openpresentation/opf-pptx`, `@openpresentation/opf-editor` and\n`@openpresentation/gallery`. They are released together, at one version, with one tag.\n\n## Release: one click\n\n> Status: the workflows and `scripts/release.mjs` are in place and rehearsed with dry runs. The first real release through them\n> is the canary `0.26.0-rc.0` (MR-41), then 0.26.0 (MR-44). Releases up to 0.25.x were made from the four old repositories with\n> a per-package train; that process is retired and not described here.\n\n**One version, one tag, one workflow.** All six packages carry the same version (`pnpm check:versions`); a release is the tag\n`vX.Y.Z` on a commit of `main`, published by `.github/workflows/npm-publish.yml` in dependency order: gallery, core, render,\nPPTX, editor, CLI. In 0.x a breaking change is a minor release and everything else a patch. Prereleases (`0.26.0-rc.0`) go to\nthe dist-tag `next`, releases to `latest`; both are passed explicitly to `npm publish --tag` for every package.\n\n### Run a release\n\nActions, **Release**, **Run workflow** on `main`:\n\n| Input | Meaning |\n| --- | --- |\n| `version` | `X.Y.Z`, `X.Y.Z-rc.N`, or `patch` / `minor` of the shared line (the highest `latest` of core, CLI, render, PPTX and editor) |\n| `dry-run` | Default **true**: rehearse everything up to `npm publish --dry-run`; nothing is committed, tagged or published. Set false for the real release |\n| `skip-gate` | A dry run only: skip the slow gate |\n\nRun it with `dry-run` true first when the change touches the release machinery; a real release of ordinary changes needs no\nrehearsal. The jobs:\n\n1. **plan**: a real release runs on `main`, and the check `verify` is green for its head (one API call, no waiting).\n2. **gate** (parallel with prepare): `nightly.yml` called on that commit: Windows, macOS, Node 22 and 26, the packed-install and\n registry consumers. A red leg stops the release before anything is committed or tagged.\n3. **prepare** (parallel with gate): `node scripts/release.mjs prepare <version>` in the runner: sets the six versions, core's\n peer ranges and `PEER_RANGES` (`packages/javascript/src/node/peers.ts`), assembles every `changes/` fragment into its package\n changelog (a package without a fragment gets \"No package changes; released with the set.\"), applies the gallery stability gate\n and the version rules below, refreshes the lockfile, and uploads the result as a patch. A **prerelease** (`X.Y.Z-rc.N`) only sets the versions: it leaves every changelog and `changes/` alone, and its GitHub release notes are assembled from the fragments (`release.mjs notes`), so the stable release that follows carries every note. A refused version fails here, in two\n minutes.\n4. **commit and tag** (real release, after gate and prepare): with the GitHub App's token (a bypass actor on the `main` ruleset)\n applies the patch as one commit `release vX.Y.Z` on exactly the commit the gate tested, pushes it to `main`, then pushes the\n tag `vX.Y.Z`. If `main` moved during the run, or the tag exists, it stops. A tag pushed with the App's token starts\n `npm-publish.yml` (a tag pushed with `GITHUB_TOKEN` would not).\n5. **npm-publish.yml** on the tag: `plan` (the tag is on `main`), `build` (one install and build, `pnpm pack` of the six,\n `scripts/stamp-githead.mjs` adds `gitHead` to each tarball because `pnpm pack` omits it, a smoke test installs the six\n tarballs together, renders the quickstart deck and runs the CLI), `publish` (the only job with `id-token: write`; the\n pre-publish gate, then `npm publish <tarball> --access public --provenance --tag <tag>` six times, printing npm's own output, then polling until the registry serves each version (it lags the upload by up to several minutes); a version already on npm is\n skipped), `verify` (below), `release` (the GitHub release, notes grouped by package) and `report` (a failure opens the issue\n \"Release vX.Y.Z failed\", labeled `release-failed`).\n6. **follow the publish run** (the Release run waits for it, so it is green only when the whole release is).\n\nTime target: about 20 minutes from the click to six verified packages (measured in MR-44).\n\n### Version rules\n\n- `prepare` refuses a version that is not above the shared line's previous release, and one already on npm for any package\n (a published version is never reused; fix forward).\n- **The gallery stability rule** (`scripts/gallery-stability.mjs`, `pnpm check:stability`; see\n [default-catalog.md](default-catalog.md)): a declared drawing change (a `drawing` entry in a gallery fragment), a removed record\n id or a `CATALOG_SCHEMA` change needs a **minor** release; `prepare` refuses a patch. New records and unchanged drawings go in\n any release. The baseline is the gallery's `latest` dist-tag, never its highest version.\n- **The publish gate** (`release.mjs check-publish`, run by `publish`): each package's version is above its own registry\n `latest`, except the gallery, which is compared with the shared line, because its `latest` was 5.0.0 before it joined the\n shared 0.x line (D16). A stable version needs `latest`, a prerelease must not take it. 5.0.0 and the older gallery versions\n stay published and are never deprecated; because they are above the shared line, every gallery publish names its tag explicitly.\n- The shared line cannot use 1.0.0 to 5.0.0 (taken by the gallery): the first stable major for all six is 6.0.0.\n\n### Trusted publishing\n\nAll six packages have the trusted publisher: GitHub Actions, organization `OpenPresentation`, repository `opf`, workflow filename\n`npm-publish.yml`, no environment (owner steps O1 in `docs/programs/monorepo/owner-steps.md`). npm checks the entry against\nthe **top-level** workflow of the run, not against a reusable workflow that contains `npm publish`, so `npm-publish.yml` is the\nworkflow that starts on the tag and `release.yml` only pushes the tag. `publish` refuses to publish from any other top-level\nworkflow. A publish fails with `E404` or `E403 ... not a trusted publisher` when an entry is wrong: fix the entry and re-run\nthe workflow on the tag. There is no `NPM_TOKEN`, and no local publish: the provenance gate applies to every release.\n\n### Retry and failure\n\nA failed `npm-publish.yml` run is fixed and run again on the same tag: Actions, **Publish OPF npm packages**, **Run workflow**,\nref `vX.Y.Z`, `dry-run` false (or re-run the failed run). Versions already on npm are skipped, so it continues where it stopped.\nA partial publish is safe: the `^X.Y.Z` floors point at packages that exist, because the order is dependency order. A real\ndefect in a published version is fixed forward with the next patch and a new tag; tags are never moved. If `prepare` or the gate\nfails, nothing was committed: fix the cause and run the release again.\n\n### Fallback: a release pull request\n\nIf no App token is available, or a person prefers review: on a branch run `pnpm release:prepare X.Y.Z`\n(`node scripts/release.mjs prepare X.Y.Z`; it needs `pnpm build` first for the stability gate), open the pull request, merge it\non green, then on `main` run `git tag vX.Y.Z && git push origin vX.Y.Z`. The publish workflow is the same.\n\n### Verify\n\nThe `verify` job runs `node scripts/release.mjs verify X.Y.Z --tag <dist-tag> --commit <release commit> --wait 10` and then the\nsmoke test against the registry (`release.mjs smoke X.Y.Z --registry`). For all six packages it checks the registry manifest,\n`gitHead` and the provenance source commit equal to the release commit, the provenance workflow `npm-publish.yml` on\n`refs/tags/vX.Y.Z`, the subject digest, the dist-tag, that no manifest carries a `workspace:` range and every sibling range is\n`^X.Y.Z`, `npm audit signatures`, and that each package changelog has the version's section. By hand:\n\n```sh\nnode scripts/release.mjs verify X.Y.Z --tag latest # exit 0 when all six verify\nnpm view @openpresentation/opf@X.Y.Z gitHead dist.attestations.url\nnpm view @openpresentation/gallery dist-tags --json # latest is X.Y.Z; 5.0.0 stays published\n```\n\n### `scripts/release.mjs`\n\n| Command | Does |\n| --- | --- |\n| `prepare <version> [--dry-run] [--no-stability] [--previous V]` | the prepare step above (`--dry-run` writes nothing) |\n| `pack <version> --commit <sha> --out <dir>` | `pnpm pack` of the six, `gitHead` stamped, `release-manifest.json` with digests |\n| `smoke <version> (--dir <dir> \\| --registry)` | install the six together, render, resolve, run the CLI |\n| `check-publish <version> [--tag t]` | the pre-publish gate |\n| `publish <version> --dir <dir> [--tag t] [--dry-run] [--wait minutes] [--poll seconds] [--expect-staged]` | the gate, then `npm publish` in dependency order: npm's output is printed, the version must be served within `--wait` (default 15 min, after all six uploads), a staged result fails unless `--expect-staged` |\n| `verify <version> [--tag t] [--commit sha] [--wait minutes]` | the registry and provenance check (a prerelease has no changelog section to check) |\n| `notes <version> [--out file]` | the GitHub release body |\n| `order` | the publish order |\n\n### After a release: the sites\n\nAfter a **stable** release, `release.yml` calls `.github/workflows/bump-sites.yml` (a prerelease never does). For each of the\nthree sites (`Data-Advantage/openpresentation-site` and `Data-Advantage/pptx-gallery` on `main`, `Data-Advantage/pptx-dev` on\n`master`) it opens one pull request, \"bump to vX.Y.Z\", from the branch `bot/opf-vX.Y.Z`. What a bump touches is the site's own\nbusiness, so each site owns one script, `pnpm opf:bump <X.Y.Z> [--opf <checkout>]` (`scripts/opf-bump.mjs` of the site), that sets its\n`@openpresentation/*` pins, refreshes its lockfile, regenerates every file it derives from the release (a release history from the\nregistry and these release notes, a pin mirror, preview fonts, chart previews, the vendored editor bundle, a homepage manifest) and runs\nthe checks of its own CI that read them; `scripts/bump-site.mjs` decides the label and calls it (a site without one gets only the\ngeneric pins and lockfile). A bump needs no human edit to be green; one that does (a release that changes behavior or removes a\ndocument the site links) fails the site's CI and stays open. A patch bump is labeled\n`auto-merge`; a minor or major bump (a breaking change in 0.x) is labeled `needs-review` and stays open. The workflow only opens\nand labels the pull request and never merges it: the site's own CI merges an `auto-merge` pull request after its\n`build-and-smoke` job passes, using the GitHub App's token so that the merge starts the site's deploy workflows (D19; the sites\nare private repositories in a Free-plan organization and have no rulesets or required checks). A bump that cannot be made opens\nan issue in this repository (`bump-sites-failed`) and never fails the release. To run it again, or for a release made by the pull-request\nfallback or from a tag, use Actions, **Bump sites**, **Run workflow** with the version. The App, its secrets and the site-side\nworkflows are owner steps O4 and O8 (`docs/programs/monorepo/owner-steps.md`); the workflow files for the sites are in\n`docs/programs/monorepo/site-workflows/`.\n\n### Who may release\n\nThe owner authorized agents to prepare and publish npm releases on 2026-09-29 (`AGENTS.md`). A release is made only through\n`release.yml` (or the pull-request fallback) and `npm-publish.yml`, only when one is required and only with every gate green;\nagents never publish from a laptop, never skip the gate and never pass a different `--tag` to make a check pass. Dry runs are\nfree at any time.\n\n## Geometry-moving core releases\n\nCore composition changes that move geometry make the preview and the PPTX export drift when the renderer and the exporter resolve\ndifferent core versions. This cannot happen between the packages of this repository: all six are released at one version, and\ntheir runtime dependencies and peers on each other are `workspace:^`, published as `^X.Y.Z`, so the floor of every sibling is the\nrelease itself. Do not hand-edit those ranges, and do not release one package alone.\n\n## Renderer font-count gate (RR-90)\n\nThe installed-consumer check (`scripts/test-packed-ecosystem.mjs`) and `test:gallery-lazy-fonts` compare the number of vendored faces a renderer lists as lazy fonts with `scripts/lazy-font-counts.mjs`. A renderer release whose fonts did not change needs no new entry. The gate hashes the renderer package's font set, which is 143 files: `dist/font-manifest.js` and every file under `fonts/` (faces, licenses, `PROVENANCE.json`, `NOTICE.md`). An unlisted version, stable or a prerelease (`X.Y.Z-rc.N`), passes when that hash is in `REVIEWED_FONT_SETS` and its measured count equals the count recorded there.\n\nIt fails closed, with a message that says what changed, when:\n\n- the font files differ from every reviewed set (the message prints the new hashes): review what changed, then add the set to `REVIEWED_FONT_SETS` and the version's count to `LAZY_FONT_COUNTS`;\n- the font files are a reviewed set but the count differs: find out why the count moved, then add an entry;\n\nA prerelease is held to the same rule as the stable version: its label neither admits nor refuses it, its font files and count do.\n\nTo review a set, pack the release (`npm pack @openpresentation/opf-render@<version>`), run `hashFontSet` from `scripts/lazy-font-counts.mjs` over the unpacked `package/` folder, and record the result with the versions you checked. Every existing `LAZY_FONT_COUNTS` entry stays and keeps its own rule.\n\n## Troubleshooting\n\n- **The tag or version check fails** (`npm-publish.yml`, job `plan`): the tag does not point at a commit of `main`, or the version\n is not well formed. Inspect the exact tag. Never move or re-push a release tag; fix a real error on `main` and release the\n next version. A transient failure is retried on the same unchanged tag.\n- **`E404`, `E403` or `ENEEDAUTH` from `npm publish`**: the trusted publisher of that package is missing or wrong. It must be\n organization `OpenPresentation`, repository `opf`, workflow filename `npm-publish.yml`, no environment, and the `publish` job\n needs `id-token: write`. Fix the entry in npm (owner step O1) and re-run the workflow on the tag.\n- **`publish` fails with \"npm accepted X (exit 0) but the registry still does not serve it\"** or \"npm staged X\": npm returned success\n (or staged the upload) but the version is not installable. Open the package on npmjs.com, Staged Packages tab, or run\n `npm stage list <name>` as a maintainer: a staged version must be approved with 2FA (`npm stage approve <stage-id>`;\n trusted-publishing OIDC tokens cannot approve). Otherwise wait for propagation and re-run the workflow on the same tag; the\n packages already served are skipped.\n- **A version is already on npm**: `publish` skips it; do not try to publish it again. Verify the package and continue.\n- **The gate is red**: the failing leg of `nightly.yml` is in the Release run. Fix it on `main` and release again; nothing was\n committed. A flaky leg is quarantined (`pnpm check:quarantine`), never skipped for a release.\n- **`release.yml` stopped with \"main moved\"**: a commit landed on `main` after the run started, and the gate had tested the\n earlier one. Run the release again.\n- **A nightly failure**: the `nightly-red` issue lists the failing legs and the run; it never blocks a pull request, and the\n release gate refuses to publish on the same failure.\n"
|
|
290
290
|
},
|
|
291
291
|
{
|
|
292
292
|
"slug": "rich-text",
|
|
@@ -352,7 +352,7 @@ var docsData = Object.freeze([
|
|
|
352
352
|
"slug": "validate",
|
|
353
353
|
"file": "docs/validate.md",
|
|
354
354
|
"title": "OPF validate: one checker for format, references, accessibility, layout and content",
|
|
355
|
-
"markdown": "# OPF validate: one checker for format, references, accessibility, layout and content\n\n`validate` in `@openpresentation/opf` and `opf validate` in the CLI check an OPF presentation for everything a tool can tell without an opinion: whether it is well-formed OPF, whether everything it points at resolves, whether a host's house rules hold, and the problems a schema cannot see, such as text that is hard to read, text that does not fit, pictures without alt text, content read out of order, fonts outside the deck's scheme, placeholder text and slides left empty. It is the only checker. Releases before 0.14 had several (a schema validator, a lint and an audit, in the library and the CLI); 0.14 replaces them all with `validate`.\n\n`validate` is read-only, local and deterministic. It fetches nothing (no images, fonts or catalogs), consults no clock and calls no model. The same document, options and fonts give byte-identical findings.\n\n```sh\nopf validate deck.opf.json # everything, JSON report\nopf validate deck.opf.json --format text # one line per finding\nopf validate deck.opf.json --only format,references --fail-on warning\nopf validate deck.opf.json --ignore layout --config house-rules.json\nopf validate --list-rules\n```\n\n```js\nimport { validate } from '@openpresentation/opf';\n\nconst report = validate(presentation); // a parsed presentation\nconst fromText = validate(fileText); // strict JSON text: also syntax, duplicate keys and line/column\nvalidate(presentation, { only: ['format'] }); // the cheap check: schema and semantic rules only\nvalidate(presentation, { ignore: ['layout'] }); // everything except the checks that build slide layouts\nvalidate(presentation, { contracts, severity: { 'opf/text-contrast': 'error' } }); // house rules\n```\n\n## Categories\n\nEvery finding has one of six categories. They say what kind of question the finding answers, and they select what runs.\n\n| Category | Asks | Errors by default | Examples |\n| --- | --- | --- | --- |\n| `format` | Is it well-formed OPF? | yes | JSON syntax, duplicate keys, schema, a citation of an unknown reference, an unknown dataset, a required variable with no value (a warning in a template), a `name:id` prefix that names no catalog group |\n| `references` | Does everything it points at resolve? | missing or circular asset, invalid catalog record; a reference that resolves nowhere and an unused dataset are warnings | `asset:` references, catalog references, citations and datasets nobody uses |\n| `policy` | Does it follow the host's house rules? | the host sets it (default `error`) | \"only these layouts\", brand fonts: the `contracts` option |\n| `accessibility` | Can everyone read it? | no: warning and info | contrast, alt text, slide titles, reading order, link text, language |\n| `layout` | Will it present as authored? | no: warning | overflow, minimum type size, image resolution, fonts outside the scheme |\n| `content` | Is anything left unfinished? | no: warning and info | placeholder text, an empty slide, a chart value that is text |\n\n`valid` keeps meaning \"correct OPF\": it is true when no finding has severity `error`, and by default only `format`, `references` and `policy` can produce one. A low-contrast deck is still a valid deck. A team promotes the rules it cares about with `severity`.\n\nEvery rule is listed, with its category, default severity, cost and the basis it rests on, in [Rules](#rules) below. A rule stays in core only when it has a measurable basis (a standard such as WCAG 2.2, a print-preflight check, or a number computed from the layout); judgments that need an audience and a purpose, such as how many fonts or words a deck should have, are not rules here.\n\n## Input and report\n\n`validate(input, options?)` takes a parsed presentation (an object) or **strict JSON text**. A string is read as JSON and nothing else: it is never sniffed for YAML or Markdown. When the text begins like YAML (`key: value`, `---`) or Markdown (`#`), the `help` of the syntax finding says to read it with `fromYaml` or `fromMarkdown` first, which return a presentation and call `validate` for you.\n\nWith text, a syntax error or a duplicate key comes back with `location` (original-source UTF-16 offset and length, one-based line and column), and so does every finding about a value in the document; a finding about a missing field is located at the object that lacks it. Text that is not JSON reports its syntax errors (`opf/json-syntax`) whatever `only` says, because nothing else can run.\n\n### Reading a deck in any form\n\n`parse(text, { format, filename, catalogs })` (the root and `@openpresentation/opf/deck`, RR-60, RR-62) reads a deck from JSON, YAML or Markdown text and returns the presentation; a syntax, schema or reference error throws `OPFValidationError`, whose `report` is the report `validate` gives the text. The format is the `format` option (`\"json\"`, `\"yaml\"` or `\"markdown\"`), else the `filename` (`.opf.md` is Markdown, `.yaml` and `.yml` are YAML, anything else is JSON), and never the content: a plain `.md` name is not a deck. Findings are located in the text that was read: `opf/json-syntax` (line and column) for JSON, `yaml/<rule>` for YAML and `markdown/<rule>` for Markdown, and every `opf/` finding at the field it names. `parse` checks format and references; warnings do not throw, so call `validate` for the full report (`validate(text)` for JSON text; `fromYaml` and `fromMarkdown` return it for the other forms). It throws a `TypeError` for a text that is not a string, an unknown format or a bad option. `stringify(presentation, { format | filename })` is the other direction: JSON as two-space JSON, YAML and Markdown in canonical form. Both are browser-safe; `open` and `save` of `@openpresentation/opf` do the same for files in Node. See [OPF as YAML](yaml.md) and [Markdown and outlines](markdown.md).\n\n[Validation results](finding-schema-reference.md) use the `FindingReport` contract (`spec/schemas/finding.schema.json`). Core's `ValidationReport` adds a few more fields:\n\n| Field | Meaning |\n| --- | --- |\n| `valid` | No finding has severity `error` |\n| `findings` | Every finding, ordered by slide (document-level findings first), then category, then rule |\n| `counts` | `{ error, warning, info }` |\n| `schemaValid` | The document passes the schema and the semantic format checks; `null` for text that is not JSON. A required variable with no value does not make a document schema-invalid |\n| `checks` | What ran and what was not measured (below) |\n| `template`, `unfilledVariables` | Documents that declare content variables or are templates: whether they were checked as a template, and which required variables have no value |\n\nA finding has a stable `ruleId` (`opf/text-contrast`), a `severity` (`error`, `warning`, `info`), a `category`, a JSON Pointer `path`, a `message`, a `help` sentence and a `definition` link to the rule below or to the schema constraint. It may carry `slide` and `slideId` (zero-based index and id of the slide it belongs to), `location` (from text), `measured` (the numbers behind it: ratios, sizes, counts), `suggestions` (catalog records that exist, never silently applied), `lookup` (arguments for `opf schema` or `opf catalog`, not a shell string), `validation` (the raw schema issue, with every union alternative) and `fixes`.\n\n`checks` says what each part did: `syntax` (`checked` for text), `schema`, `references`, `policy`, `accessibility` and `content` are `checked` or `not-run`; `layout` is `not-run`, `estimated` (text widths are core's portable estimate) or `measured` (the host passed `fonts.textMeasurement`); background picture pixels are never read (`backgroundPixels`), image resolution reads only embedded `data:` images (`imageBytes`), and native PowerPoint rendering is not checked (`nativeExport`).\n\nA document the schema rejects is not audited: its accessibility, layout and content rules do not run, and its format errors are reported even when `only` did not ask for them, so the report says why nothing else ran (switch them off with `ignore: ['format']`).\n\n## Options\n\n```js\nvalidate(presentation, {\n only: ['format', 'opf/missing-alt-text'], // run just these rules or categories\n ignore: ['layout'], // do not run these\n severity: { 'opf/text-contrast': 'error', content: 'off' }, // per rule or category: error, warning, info or off\n ignorePaths: [{ rule: 'opf/text-contrast', path: '/slides/3' }], // an accepted exception, by JSON Pointer prefix\n catalogs: [gallery, acmeCatalog], // the catalogs the host registered (the first is the default); nothing is fetched\n contracts, // host policy (below)\n thresholds: { contrastNormal: 7 }, // see DEFAULT_VALIDATION_THRESHOLDS\n fonts, // the host's fonts: layout rules read fonts.textMeasurement\n chartPalette: ['#0072B2', '#E69F00'], // the engine's chart colours\n template: true, values: { client: 'Acme' }, // check as a template, or fill variables first\n});\n```\n\n- **Names.** `only`, `ignore`, `severity` and `ignorePaths` take full rule ids (`opf/text-contrast`), bare rule names (`text-contrast`) or category names (`accessibility`). No rule is named like a category. A rule beats its category in `severity`. An unknown rule, category, threshold or option throws a `TypeError`, so a typo cannot silently disable a check.\n- **`fonts`.** An object with a `textMeasurement` (the renderer's `loadFonts()` result has one). Without it the layout rules use core's portable estimate and `checks.layout` says `estimated`; with it, `measured`. `textMeasurement` may also be a function of the zero-based slide index, for a script-aware measurement per slide. When the measurement has a `forScripts(profile)` method (a script planner), the layout rules ask it for each slide's script-aware measurement, built from the slide's `resolveScriptFonts` profile and a run language's own profile, the way the renderer and the exporter measure, so Arabic or Japanese text is measured in its script face rather than the Latin face its style names.\n- **`template` and `values`.** A deck that declares content variables is checked as the deck it resolves to; see [templates and variables](templates-and-variables.md).\n- **Thresholds.** `DEFAULT_VALIDATION_THRESHOLDS` holds `contrastNormal`, `contrastLarge`, `minFontSizePt`, `minImagePpi` and `minSeriesColorDifference`; each rule below lists the ones it reads.\n\n`validationRules` lists every rule with its metadata (`id`, `name`, `category`, `severity`, `cost`, `summary`, `rationale`, `standard`, `approximations`, `thresholds`), and `findValidationRule(name)` finds one by id or bare name.\n\n`assertValid(value, options?)` throws `OPFValidationError`, carrying the `report` and its error `findings`, unless `validate(value, options)` is valid, and tells TypeScript that `value` is a `Presentation`. It takes a parsed value, not text.\n\n`validateCatalogRecord(kind, record)` and `assertValidCatalogRecord(kind, record)` check one catalog record (an audience, theme, layout, font scheme and so on) against its schema. The report has the same shape and checks the record format only (`checks.references` is `not-run`): without an enclosing catalog it does not resolve linked ids. Validate a presentation with its registered catalogs to check those references.\n\n## Pass `fonts`, otherwise the estimate\n\nThe layout rules (`opf/text-overflow`, `opf/min-font-size`, reading order, image resolution) depend on how wide the text is. `validate` measures it with `options.fonts`, the handle that opf-render's `loadFonts()` returns:\n\n```js\nconst fonts = await loadFonts({ pack: 'office' });\nconst report = validate(deck, { fonts }); // report.checks.layout === 'measured'\n```\n\nPass the same `fonts` handle to preview (`toSvg`), pagination (`paginate`) and export (`toPng`, `toPdf`, `toPptx`), so that validation measures with the faces the preview draws and a finding means what the slide shows. The same handle gives identical geometry across engines; opf-pptx's `layout-parity` test checks that the exported PowerPoint paragraphs agree with the SVG preview and with core's composed items, with and without `fonts`. See [Pass `fonts`, otherwise the estimate](font-fidelity.md#pass-fonts-otherwise-the-estimate) for the full list.\n\nWithout `fonts` the rules use core's built-in estimate and `checks.layout` says `estimated`: base widths are 0.54 em per character, 0.62 em for capitals and digits, 0.32 em for a space and 1 em for CJK characters. Spacing combining marks outside Latin/CJK contribute; nonspacing/enclosing marks do not. Robust script factors calibrated against the FF-44 corpus reduce underestimation for other scripts; see [script width evidence](evidence/script-width-estimates/README.md). It needs no font files and is stable from run to run, but an estimate can be wider or narrower than a particular face, so an estimated overflow finding (or its absence) is a first pass. `opf validate` on the command line loads no fonts, so its layout checks are estimated too. Node `convert` and the CLI's `opf convert` and `opf paginate` prepare an office-pack handle for you; a library call to `validate` never loads fonts itself.\n\n## Cost\n\n`validate` is lazy: it builds only what the rules you ask for need. Measured with Node 24 on three bundled example decks (milliseconds per call, warm; your machine will differ, the ratios hold):\n\n| Call | `minimal` (3 slides) | `renewal-save-plan` (6 slides, 34 KB) | `broadband-public-hearing-deck` (10 slides, 41 KB) |\n| --- | --- | --- | --- |\n| `only: ['format']` | 0.06 | 0.23 | 0.64 |\n| `only: ['format', 'references']` | 0.7 | 5.2 | 7.6 |\n| everything (the default) | 1.7 | 24 | 32 |\n\n`only: ['format']` costs what the schema check alone costs; it builds no layout. The references walk reads the schema alongside the document and costs about ten times as much. The rules of `layout` (and `opf/text-contrast`, `opf/text-on-image` and `opf/reading-order`) compose every slide and are most of the cost of a full run. Each rule's `cost` says which it needs: `syntax` reads the JSON text, `structure` reads the document alone, `composition` builds slide layouts.\n\nThe default runs every category, including overflow findings; `valid` means no error findings, so an overflow warning alone does not make it false. Hot paths select what they need: pagination, conversion, patch validation and the editor's per-edit check use `{ only: ['format'] }`; CLI document-writing commands check `format` and `references`, so a contrast warning does not block an export unless its severity or failure policy is explicitly raised.\n\n## Quick fixes\n\nA fix is a suggestion that core never applies. A host such as the editor's Review panel shows it and applies it through its own undoable edit path.\n\n- A `patch` fix (`kind: \"patch\"`, the default) carries RFC 6902 JSON Patch operations in `patch` (`add`, `remove`, `replace`, `move`, `copy`, `test`, with JSON Pointer paths). `safe: true` means the change cannot alter what the content says: for example switching a failing text colour to the colour the theme chose for that background.\n- A `focus` fix (`kind: \"focus\"`) names the field the author must fill in (`focus.path`, `focus.field`: `alt`, `title`, `text`, `link`, `language`, `fontSize`).\n- `safe: false` patches change meaning or appearance and need a deliberate click, for example marking a picture decorative with an empty `alt`.\n\n`title` says what applying the fix does.\n\n## Catalog context and contracts\n\nContent references resolve the way every engine resolves them ([catalogs](default-catalog.md)): `catalogs.custom`, then the records embedded under `catalogs.default`, then the catalog the host registered (the `catalogs` option) for its source; `name:id` in `catalogs.<name>`, then the catalog registered for its source. A reference that resolves nowhere is a warning (`opf/unresolved-reference`), never an error; a prefix that names no group is a format error (`opf/undeclared-catalog`): it can never resolve, so engines reject the document at their format check (`validate(document, { only: [\"format\"] })`) instead of drawing a fallback. A `catalogs` option that is not an array of registered catalogs throws `OPFCatalogsOptionError` (`code: \"invalid-catalogs\"`) at every entry point. Nothing is fetched, and without the `catalogs` option only the records the document embeds resolve. Free-form audience and purpose descriptions (any string that is not a bare id or `name:id`) and arbitrary `extensions` data are never references; the `language` tag, `chart.type` and the `socials` keys are engine vocabularies the schema checks. Embedded records, and registered records the document uses, are validated against their companion schema (`opf/catalog-record`). A record under `catalogs.default` or a named group that the catalog registered for the group's source does not publish is `opf/catalog-record-not-in-source` (move it to `catalogs.custom`); with no catalog registered for that source the check is silent. Suggestions name records actually present in the checked context (`origin` `document` or `registered`).\n\nAn `asset:` reference that names no entry of the document's `assets` registry is an error (`opf/asset-reference`), and so is a cycle of `asset:` sources (`opf/asset-cycle`); resource bytes are never fetched. Chart and table data findings keep their code as the rule id (`opf/dataset-unknown`, `opf/chart-value-not-numeric`, ...), and a `opf/chart-value-not-numeric` cell whose column is written in one display style (`\"12%\"`, `\"$1,234\"`) carries `fixes` ([chart and table data](chart-table-data.md#migration-help)). Citation and caption errors keep theirs too (`opf/cite-unknown-reference`, `opf/reference-id-duplicate`, `opf/cite-unsupported-location`, `opf/caption-unsupported-payload`).\n\nA narrative the document defines is a record in `catalogs.custom.narratives`, and `narrative` is always a reference string. When the narrative resolves, a `slides[].beat` id it does not define is `opf/unknown-beat`, a root `duration` outside the record's `duration` range is `opf/duration-outside-narrative`, an embedded record whose `duration.min` exceeds `duration.max` is `opf/narrative-duration-range`, and a beat no slide references is `opf/unused-beat` (only once some slide names a beat). A slide-level `design.theme` whose resolved dimensions differ from the deck's is `opf/slide-theme-dimensions`: a PPTX has one slide size, and a slide's `design` cannot set `dimensions` at all (a schema error). An undeclared `var:<id>` in a solid, gradient-stop or pattern background colour warns like any other colour reference (`opf/variable-reference-unknown`).\n\nHost policy comes from explicit options, never from the document: fields under `extensions` are data and cannot install policy. `contracts` and `catalogs` may live in a local JSON file for the CLI (`--config`).\n\n```json\n{\n \"catalogs\": [\n {\n \"source\": \"pkg:@partner/opf-catalog\",\n \"layouts\": {\n \"partner-title\": {\"name\":\"Partner title\",\"regions\":[{\"name\":\"title\",\"size\":1}]}\n }\n }\n ],\n \"contracts\": [\n {\n \"path\":\"/slides/*/layout\",\n \"allowedValues\":[\"partner-title\",\"text\"],\n \"message\":\"Use the brand layouts {{allowed}} at {{path}}. See {{file}}.\",\n \"documentation\":\"brand-guide.md#layouts\",\n \"severity\":\"error\"\n }\n ]\n}\n```\n\nContract paths are JSON Pointer patterns: `~0` escapes `~`, `~1` escapes `/`, and a whole `*` segment matches one property or array index. Contracts check existing fields; they do not require an omitted field or insert defaults. Allowed values are JSON primitives. Optional message placeholders are `{{path}}`, `{{value}}`, `{{allowed}}`, and `{{file}}`. A contract's severity is `error` unless it says otherwise. Invalid or misspelled configuration keys fail instead of being ignored. Messages and catalog labels are data, not executable instructions.\n\n## The CLI\n\n`opf validate <file|-> [--config <file>] [--only <list>] [--ignore <list>] [--fail-on <error|warning|info>] [--asset-dir <dir>] [--format <json|text>] [--list-rules]`\n\nThe JSON report (the default) is the `validate` report plus the source's SHA-256 (`sha256`), the installed core version (`opfVersion`) and, with `--config`, the configuration file's path and hash (`context`). `--format text` prints one line per finding (`file:line:column severity rule message`), the JSON Pointer path, a hint and any fix, then a summary. `--list-rules` prints every rule (`--format json` with its full metadata).\n\nA file ending `.yaml` or `.yml` (or stdin with `--from yaml`) is read as YAML through `fromYaml`, so the report is the same and every finding is located in the YAML; a YAML syntax or dialect error is a `yaml/<rule>` finding (`format`, exit 1). A file ending `.opf.md` (or stdin with `--from md`) is read as a [Markdown deck](markdown.md#markdown-decks-in-every-command) the same way, and a Markdown error is a `markdown/<rule>` finding. A plain `.md` file, and any other text that does not start with `{` or `[`, needs `--from`.\n\n**Local images (RR-93).** The CLI, not core `validate` (which reads no file), also checks that each relative image or logo path exists: a `cli/asset-missing` warning when `opf convert` could not find it, `cli/asset-blocked` when it is outside the folder or not an image file. It uses convert's resolver and rules: paths are relative to the deck's folder (the working directory for stdin), or to `--asset-dir`; URLs, `data:` URIs and `asset:` references to them are not read. `--only` skips the check and `--ignore asset-missing` (or `asset-blocked`) drops one kind.\n\n**Schema errors in a union.** Where a value can take several forms (a table cell is a scalar, runs or a styled object), a failing value reports the errors of the form it was written for, and each finding's `validation.alternatives` lists the other forms in one line each (`--format text` prints them as `other forms: ...`). When no form fits the value's type, one error names the types accepted.\n\n`--only` and `--ignore` take comma-separated rule ids, bare names or category names. `--config` takes an explicit local JSON file, never `-`, holding `{ catalogs, contracts, severity, only, ignore, ignorePaths, thresholds, chartPalette }`; `--only` replaces the file's `only` and `--ignore` adds to its `ignore`.\n\n| Exit | Meaning |\n| --- | --- |\n| 0 | No finding at or above `--fail-on` (default `error`) |\n| 1 | At least one finding at or above `--fail-on`, or a document that is not valid JSON (or YAML) |\n| 2 | Usage, configuration or I/O error |\n\n`--fail-on` is the one way to say \"fail on warnings\" on every command that checks a document: `--fail-on warning`. Other commands (`create`, `edit`, `fill`, `paginate`, `embed`, `ingest`, `convert`) check only `format` and `references` of what they write and take `--fail-on` the same way.\n\n## How contrast is computed\n\n`opf/text-contrast` reasons about the colours the preview draws, because a check against a different colour than the one on screen is worse than none.\n\n1. **Background.** Design resolves per field: slide design, deck design, theme, engine default. The background is a theme slot or colour (`solid`), the card surface for content on a `contentBox` card, a table cell's fill, a gradient or a pattern. Solid, gradient-stop and pattern colours are read as literal hex colours, as the preview reads them (anything else falls back as it does there). Translucent backgrounds are composited over white.\n2. **Text colour.** Titles, body, lists and tags use the scheme's `dark1`, or `light1` when the background is dark (luminance below 0.179). Furniture and quote attributions use the muted colour, metric values the primary colour, and explicit run and table colours the author's `ColorRef` resolved the same way the renderer resolves it. As the preview does, a gradient background has no single colour and counts as light, so a dark gradient behind default text is reported.\n3. **Gradients.** The gradient is sampled on a 5 by 5 grid over the area the text covers (the lines' measured ink, aligned as the text is), using the preview's gradient geometry, and the worst colour decides. A pattern contributes both of its colours.\n4. **Pictures.** Pixels are never read. A background picture is bounded by a grey ramp from black to white, composited through the picture opacity and any full-frame overlay of the image background (`design.background.overlay`). Text passes only if every step passes; otherwise `opf/text-on-image` says the result cannot be guaranteed. An overlay limited to an edge band is not counted.\n5. **Ratio and size.** The WCAG 2.x relative-luminance contrast ratio is compared with 4.5:1, or 3:1 for large text: at least 18 pt, or 14 pt and bold. Sizes are the fitted sizes, expressed at the 13.33 by 7.5 in reference slide (96 px per inch). The default body size, 18.75 pt, is large text under WCAG, so default body text passes at 3:1; set `contrastLarge` to 4.5 for a stricter policy.\n\nApproximations: anti-aliasing, text shadows, font weight and the exact glyph coverage are not modelled; chart and code text are not checked (charts pick label colours against their surface; code uses its own panel colours). See each rule below for what it cannot see.\n\n## Layout rules use composition\n\nOverflow, minimum type size, reading order and image resolution use `composeSlide` at the deck's slide size with the resolved font scheme, the same composition the preview and the PPTX export consume. Without `fonts.textMeasurement` the text widths are core's portable estimate and can differ from the real fonts by a few percent; hosts that load fonts (the editor, the renderer) pass their measurement for font-exact results. A composition with `overflow: \"error\"` fails at composition time; `validate` still computes the geometry, relaxes the setting, and reports the overflow as a **warning**, so that `valid` never depends on font metrics. Rendering and export still refuse such a slide when it does not fit, and a host that wants the finding to fail promotes it with `severity: { 'opf/text-overflow': 'error' }`.\n\nPromoted region keys (`left`, `center`, `right`, `top:left` and the rest) are composed in visual reading order (rows from top to bottom, then along the row), whatever order the keys are written in. The PPTX export writes shapes in that order, which is the reading order of assistive technology; `opf/reading-order` checks that the composed order still matches the geometry (the same `visualReadingOrder` that composes the regions). Authoring with `blocks` (an ordered list) keeps the reading order equal to the visual order.\n\n## Templates and variables\n\n`opf/variable-unfilled` is one rule for a required variable that has no value. In a deck that is not a template it is an error (the deck is incomplete), so `valid` is false; in a template (`\"template\": true` in the document, or `template: true` in the options) it is a warning, because a template is incomplete on purpose; `unfilledVariables` lists the ids either way. In a document that declares no content variables, a `{{id}}` token left in a string (outside `variables`, `extensions`, `catalogs` and `assets`) is a warning. A `var:` colour that names no declared variable is `opf/variable-reference-unknown`. It is a `format` rule, so `only: ['format']` (what pagination, patching, conversion and every write command run) rejects a deck with an unfilled required variable. Fill a template with `opf fill` or `resolveVariables`, or pass `values`, to clear the findings.\n\n## Rules\n\nThe reference below is generated from the rule registry (`validationRules`); `node scripts/build-validate-docs.mjs --check` (and a core test) fails when it drifts.\n\n<!-- validate-rules:start (generated by scripts/build-validate-docs.mjs; do not edit by hand) -->\n\n| Rule | Category | Default | Cost | What it reports |\n| --- | --- | --- | --- | --- |\n| [`opf/json-syntax`](#opfjson-syntax) | Format | error | syntax | The text is not strict JSON. |\n| [`opf/duplicate-key`](#opfduplicate-key) | Format | error | syntax | A JSON object repeats a key. |\n| [`opf/schema`](#opfschema) | Format | error | structure | The document breaks the OPF schema or a structural rule the schema cannot express. |\n| [`opf/reference-id-duplicate`](#opfreference-id-duplicate) | Format | error | structure | Two references share an id. |\n| [`opf/cite-unknown-reference`](#opfcite-unknown-reference) | Format | error | structure | A citation names a reference that does not exist. |\n| [`opf/cite-unsupported-location`](#opfcite-unsupported-location) | Format | error | structure | A citation or footnote sits where no engine draws it. |\n| [`opf/caption-unsupported-payload`](#opfcaption-unsupported-payload) | Format | error | structure | A caption is on a payload that cannot carry one. |\n| [`opf/image-option-unsupported-payload`](#opfimage-option-unsupported-payload) | Format | error | structure | An image option is on a payload that is not an image. |\n| [`opf/image-placement-invalid`](#opfimage-placement-invalid) | Format | error | structure | An image placement cannot be honoured. |\n| [`opf/layout-placeholder-group`](#opflayout-placeholder-group) | Format | error | structure | A layout record's placeholder group breaks a nesting rule. |\n| [`opf/dataset-unknown`](#opfdataset-unknown) | Format | error | structure | A chart or table names a dataset that does not exist. |\n| [`opf/dataset-field-unknown`](#opfdataset-field-unknown) | Format | error | structure | A table asks a dataset for a column it does not have. |\n| [`opf/data-column-duplicate`](#opfdata-column-duplicate) | Format | error | structure | A dataset or table has two columns with the same name. |\n| [`opf/chart-mapping-unknown-column`](#opfchart-mapping-unknown-column) | Format | error | structure | A chart mapping names a column the data does not have. |\n| [`opf/number-format-invalid`](#opfnumber-format-invalid) | Format | error | structure | A number format cannot be read. |\n| [`opf/variable-invalid-value`](#opfvariable-invalid-value) | Format | error | structure | A variable value does not match its declared type. |\n| [`opf/variable-unknown-builtin`](#opfvariable-unknown-builtin) | Format | error | structure | A token names a built-in variable that does not exist. |\n| [`opf/chart-highlight-unknown-name`](#opfchart-highlight-unknown-name) | Format | error | structure | A chart highlight names a series or category the chart does not have. |\n| [`opf/variable-format`](#opfvariable-format) | Format | error | structure | A variable cannot be formatted. |\n| [`opf/variable-unfilled`](#opfvariable-unfilled) | Format | error | structure | A required template variable has no value. |\n| [`opf/run-color-unrecognized`](#opfrun-color-unrecognized) | Format | warning | structure | A text run colour is none of the documented forms. |\n| [`opf/numbering-start-ignored`](#opfnumbering-start-ignored) | Format | warning | structure | A list entry sets `start` where nothing is numbered. |\n| [`opf/slide-size-out-of-range`](#opfslide-size-out-of-range) | Format | warning | structure | The slide size is outside PowerPoint's 1 to 56 inch range. |\n| [`opf/undeclared-catalog`](#opfundeclared-catalog) | Format | error | structure | A reference names a catalog group the document does not declare. |\n| [`opf/asset-reference`](#opfasset-reference) | References | error | structure | An `asset:` reference names an asset that is not in the registry. |\n| [`opf/asset-cycle`](#opfasset-cycle) | References | error | structure | Asset references form a cycle. |\n| [`opf/catalog-record`](#opfcatalog-record) | References | error | structure | An embedded or registered catalog record is invalid. |\n| [`opf/layout-template`](#opflayout-template) | References | error | structure | A layout template's region list is malformed. |\n| [`opf/layout-region`](#opflayout-region) | References | error | structure | A layout template's regions are not valid. |\n| [`opf/unresolved-reference`](#opfunresolved-reference) | References | warning | structure | A content reference resolves nowhere. |\n| [`opf/catalog-record-not-in-source`](#opfcatalog-record-not-in-source) | References | warning | structure | A record embedded under a catalog group is not in that catalog. |\n| [`opf/unused-reference`](#opfunused-reference) | References | warning | structure | A reference is never cited. |\n| [`opf/unused-dataset`](#opfunused-dataset) | References | warning | structure | A dataset is never used. |\n| [`opf/unknown-beat`](#opfunknown-beat) | References | warning | structure | A slide names a beat its narrative does not define. |\n| [`opf/slide-theme-dimensions`](#opfslide-theme-dimensions) | References | warning | structure | A slide theme sets a slide size that differs from the deck. |\n| [`opf/variable-unknown`](#opfvariable-unknown) | References | warning | structure | A `{{token}}` names a variable that is not declared. |\n| [`opf/variable-unknown-value`](#opfvariable-unknown-value) | References | warning | structure | A value was supplied for a variable that is not declared. |\n| [`opf/variable-unused`](#opfvariable-unused) | References | warning | structure | A variable is declared but never used. |\n| [`opf/variable-reference-unknown`](#opfvariable-reference-unknown) | References | warning | structure | A `var:` colour names a variable that is not declared. |\n| [`opf/contract`](#opfcontract) | Policy | error | structure | A field is outside the values a host policy allows. |\n| [`opf/text-contrast`](#opftext-contrast) | Accessibility | warning | composition | Text colour has too little contrast against the background it sits on. |\n| [`opf/text-on-image`](#opftext-on-image) | Accessibility | info | composition | Text sits on a background picture whose pixels cannot be measured. |\n| [`opf/missing-alt-text`](#opfmissing-alt-text) | Accessibility | warning | structure | A picture has no alt text and is not marked decorative. |\n| [`opf/poor-alt-text`](#opfpoor-alt-text) | Accessibility | info | structure | Alt text is a file name, a URL, a generic word or very long. |\n| [`opf/missing-slide-title`](#opfmissing-slide-title) | Accessibility | warning | structure | A slide has no title. |\n| [`opf/duplicate-slide-title`](#opfduplicate-slide-title) | Accessibility | info | structure | Two slides have the same title. |\n| [`opf/reading-order`](#opfreading-order) | Accessibility | warning | composition | The order content is read differs from the order it appears on the slide. |\n| [`opf/link-text`](#opflink-text) | Accessibility | warning | structure | Link text does not say where the link goes. |\n| [`opf/chart-color-only`](#opfchart-color-only) | Accessibility | info | structure | Chart series may be indistinguishable without colour vision. |\n| [`opf/chart-colors-adjusted`](#opfchart-colors-adjusted) | Accessibility | warning | structure | A chart colour cannot be used as authored. |\n| [`opf/chart-text-alternative`](#opfchart-text-alternative) | Accessibility | info | structure | A chart has no text alternative, or is marked decorative. |\n| [`opf/missing-language`](#opfmissing-language) | Accessibility | info | structure | The presentation does not declare its language. |\n| [`opf/chart-option-adapted`](#opfchart-option-adapted) | Layout | warning | structure | A chart option cannot be shown by the chart type. |\n| [`opf/text-overflow`](#opftext-overflow) | Layout | warning | composition | Text or a table does not fit its space at the smallest allowed size. |\n| [`opf/unresolved-content`](#opfunresolved-content) | Layout | warning | composition | Content cannot be drawn as authored. |\n| [`opf/layout-failed`](#opflayout-failed) | Layout | warning | composition | The slide layout could not be computed. |\n| [`opf/min-font-size`](#opfmin-font-size) | Layout | warning | composition | Text is drawn smaller than the readable minimum. |\n| [`opf/region-unknown`](#opfregion-unknown) | Layout | warning | composition | A block is pinned to a region the layout does not have. |\n| [`opf/region-kind`](#opfregion-kind) | Layout | warning | composition | A block is pinned to a region that does not accept its kind. |\n| [`opf/region-full`](#opfregion-full) | Layout | warning | composition | A block is pinned to a region that is already full. |\n| [`opf/layout-unplaced`](#opflayout-unplaced) | Layout | warning | composition | No region of the layout has room for a block. |\n| [`opf/furniture-wraps`](#opffurniture-wraps) | Layout | warning | composition | A header or footer zone wraps onto more than 2 lines. |\n| [`opf/font-outside-scheme`](#opffont-outside-scheme) | Layout | warning | structure | Text uses a font family that is not in the deck's font scheme. |\n| [`opf/image-resolution`](#opfimage-resolution) | Layout | warning | composition | An image has too few pixels for the size it is shown at. |\n| [`opf/chart-value-not-numeric`](#opfchart-value-not-numeric) | Content | warning | structure | A chart value is text, not a number. |\n| [`opf/chart-mapping-adapted`](#opfchart-mapping-adapted) | Content | warning | structure | A chart mapping entry is ignored. |\n| [`opf/chart-highlight-adapted`](#opfchart-highlight-adapted) | Content | warning | structure | A chart highlight is adapted by the chart type. |\n| [`opf/code-highlight-out-of-range`](#opfcode-highlight-out-of-range) | Content | warning | structure | A code highlight marks a line the code does not have. |\n| [`opf/code-highlight-range-reversed`](#opfcode-highlight-range-reversed) | Content | warning | structure | A code highlight range ends before it starts. |\n| [`opf/variable-builtin-missing`](#opfvariable-builtin-missing) | Content | warning | structure | A built-in variable has no value in this document. |\n| [`opf/duration-outside-narrative`](#opfduration-outside-narrative) | Content | warning | structure | The target duration is outside the range the narrative suits. |\n| [`opf/narrative-duration-range`](#opfnarrative-duration-range) | Content | warning | structure | An inline narrative duration range has min greater than max. |\n| [`opf/placeholder-text`](#opfplaceholder-text) | Content | warning | structure | Placeholder text was left in the deck. |\n| [`opf/empty-text`](#opfempty-text) | Content | info | structure | A text field is present but empty. |\n| [`opf/unused-beat`](#opfunused-beat) | Content | info | structure | A beat of the narrative has no slide that references it. |\n| [`opf/empty-slide`](#opfempty-slide) | Content | warning | structure | A slide has no content at all. |\n| [`opf/language-tag`](#opflanguage-tag) | Content | warning | structure | A language tag is malformed, uses an unassigned region or is not in canonical case. |\n\n## Format rules\n\n*Is it well-formed OPF?*\n\n### `opf/json-syntax`\n\nDefault severity: **error**. Cost: syntax. The text is not strict JSON.\n\n**Why.** Every OPF tool reads JSON. A misplaced comma, quote, comment or trailing comma makes the file unreadable, and the range says where.\n\n**Basis.** RFC 8259 (strict JSON: no comments, no trailing commas)\n\n**Approximations.** Only text input is checked. A string is always read as JSON; a text that looks like YAML or Markdown gets a hint to use fromYaml or fromMarkdown first.\n\n### `opf/duplicate-key`\n\nDefault severity: **error**. Cost: syntax. A JSON object repeats a key.\n\n**Why.** JSON.parse keeps only the last value of a repeated key, so the earlier one disappears without a trace. The check compares escaped and literal spellings of the same key.\n\n**Basis.** RFC 8259 section 4 (names within an object should be unique)\n\n### `opf/schema`\n\nDefault severity: **error**. Cost: structure. The document breaks the OPF schema or a structural rule the schema cannot express.\n\n**Why.** The schema is the contract every engine, exporter and editor relies on. A field of the wrong type, a missing required field, an unknown property or content fields that cannot be mixed make the document ambiguous or undrawable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** All alternatives of a union are reported, with the failing schema location in `validation`. A required variable with no value is reported by `opf/variable-unfilled`, in the same category.\n\n### `opf/reference-id-duplicate`\n\nDefault severity: **error**. Cost: structure. Two references share an id.\n\n**Why.** A citation names a reference by id; a repeated id makes it ambiguous which entry is cited.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unknown-reference`\n\nDefault severity: **error**. Cost: structure. A citation names a reference that does not exist.\n\n**Why.** A cited id with no entry in `references` has nothing to show in the footnote area.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unsupported-location`\n\nDefault severity: **error**. Cost: structure. A citation or footnote sits where no engine draws it.\n\n**Why.** `cite` and `footnote` apply to runs in text, bullets and list items; table cells, captions, and reference and footnote texts have no marker.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/caption-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. A caption is on a payload that cannot carry one.\n\n**Why.** A caption belongs to exactly one image, chart, table or video; a group, text or code block has no place to draw it.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-option-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. An image option is on a payload that is not an image.\n\n**Why.** `fit`, `focus`, the image treatments (`shape`, `cornerRadius`, `border`, `opacity`, `recolor`, `overlay`, `aspectRatio`) and `placement` describe how a picture is drawn; a group, text or chart block has no picture for them to apply to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-placement-invalid`\n\nDefault severity: **error**. Cost: structure. An image placement cannot be honoured.\n\n**Why.** A placed image bleeds to a slide edge and the rest of the slide composes beside it. Only a top-level block (slides.N.blocks.I) can do that, and a slide edge holds one placed image; a block in a group or a promoted region, or a second block on the same edge, has nowhere to go.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/layout-placeholder-group`\n\nDefault severity: **error**. Cost: structure. A layout record's placeholder group breaks a nesting rule.\n\n**Why.** A placeholder group arranges body regions inside one cell of its layout record. Groups nest at most three levels inside a record, and a group holds body regions and groups only: the headings (title, subtitle, tag) stay at the record's top level, where the engine places them. Composition cannot use a record that breaks either rule.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-unknown`\n\nDefault severity: **error**. Cost: structure. A chart or table names a dataset that does not exist.\n\n**Why.** The data cannot be drawn without the dataset it refers to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-field-unknown`\n\nDefault severity: **error**. Cost: structure. A table asks a dataset for a column it does not have.\n\n**Why.** The column cannot be shown when the dataset has no column of that name.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/data-column-duplicate`\n\nDefault severity: **error**. Cost: structure. A dataset or table has two columns with the same name.\n\n**Why.** Series, mappings and fields address columns by name, so a repeated name makes them ambiguous.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-unknown-column`\n\nDefault severity: **error**. Cost: structure. A chart mapping names a column the data does not have.\n\n**Why.** The series cannot be built from a column that is not there.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/number-format-invalid`\n\nDefault severity: **error**. Cost: structure. A number format cannot be read.\n\n**Why.** A format code that no engine can parse is drawn as the plain number or fails to export.\n\n**Basis.** Excel number format codes, as far as OPF supports them\n\n### `opf/variable-invalid-value`\n\nDefault severity: **error**. Cost: structure. A variable value does not match its declared type.\n\n**Why.** A number variable given text, or a date given a non-date, cannot be filled into the deck.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-builtin`\n\nDefault severity: **error**. Cost: structure. A token names a built-in variable that does not exist.\n\n**Why.** The `deck.`, `speaker.`, `organization.` and `slide.` names (and `speakers`) are reserved for built-in variables; a name under them that is not a built-in can never be filled, so the token would show literally. The slide-scoped `slide.number`, `slide.section` and `deck.slideCount` are inline tokens only, so `var:slide.*` and `var:deck.slideCount` are reported too; organization logos are whole-field references only (`var:organization.logo.icon`), so a `{{organization.logo}}` token, an unknown logo shape or an unknown organization id is reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-unknown-name`\n\nDefault severity: **error**. Cost: structure. A chart highlight names a series or category the chart does not have.\n\n**Why.** `chart.highlight` emphasises marks by name; a name that matches no series or category in the chart data highlights nothing, so the chart is not drawn as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-format`\n\nDefault severity: **error**. Cost: structure. A variable cannot be formatted.\n\n**Why.** A number or date format that does not apply to the value leaves the token unreadable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unfilled`\n\nDefault severity: **error**. Cost: structure. A required template variable has no value.\n\n**Why.** A variable a deck declares as required, with no value, is template scaffolding: `{{name}}` tokens show up literally and colour variables fall back to a default. A normal deck must be filled, so this is an error and the deck is not valid; a template (`template: true`) is incomplete on purpose, so it is a warning.\n\n**Approximations.** Required variables are those the variable machinery reports as unfilled for the document (declared, no value, not `required: false`, not given by the `values` option). For a document that declares no content variables, `{{id}}` / `{{id|format}}` tokens (`{{` escapes) left in any string outside `variables`, `extensions`, `catalogs` and `assets` are reported as warnings. `var:` colour references with no declaration are reported as `opf/variable-reference-unknown`. Fill a template with `opf fill` or `resolveVariables` to clear the findings.\n\n### `opf/run-color-unrecognized`\n\nDefault severity: **warning**. Cost: structure. A text run colour is none of the documented forms.\n\n**Why.** Run colours are open strings so imported decks stay valid, but a value that is not a hex colour, a colour-scheme name or a `var:` reference falls back to the theme colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/numbering-start-ignored`\n\nDefault severity: **warning**. Cost: structure. A list entry sets `start` where nothing is numbered.\n\n**Why.** A start value only restarts an auto-number; without `numbering` on the payload it has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/slide-size-out-of-range`\n\nDefault severity: **warning**. Cost: structure. The slide size is outside PowerPoint's 1 to 56 inch range.\n\n**Why.** The schema accepts any positive custom `widthInches` and `heightInches`, but a PPTX stores the size as `p:sldSz` with each side between 914400 and 51206400 EMU, which is 1 to 56 inches. A custom size outside that range exports a file that does not conform to the standard, which PowerPoint may reject or clamp. It is a warning because the document is still correct OPF and the preview draws it.\n\n**Basis.** ECMA-376 / ISO/IEC 29500-1 section 19.2.1.39 (p:sldSz) and section 19.7.17 (ST_SlideSizeCoordinate): 914400 to 51206400 EMU\n\n**Approximations.** Checks the deck's resolved size: `design.dimensions`, else the resolved theme's `dimensions`. Inches are converted to EMU as the PPTX exporter does (`Math.round(inches * 914400)`), so a side of exactly 1 in or 56 in passes. A slide-level theme with another size is `opf/slide-theme-dimensions`. Presets are always in range. PowerPoint's own behaviour at the limits was not measured.\n\n### `opf/undeclared-catalog`\n\nDefault severity: **error**. Cost: structure. A reference names a catalog group the document does not declare.\n\n**Why.** The prefix of a `name:id` reference names the group of `catalogs` it resolves in. A prefix with no group can never resolve, whatever the host registers, so it is a format error: engines reject the document at their format check instead of drawing a fallback.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## References rules\n\n*Does everything it points at resolve?*\n\n### `opf/asset-reference`\n\nDefault severity: **error**. Cost: structure. An `asset:` reference names an asset that is not in the registry.\n\n**Why.** An image, logo or video that points at a missing asset cannot be drawn or exported. The check reads the document's own registry only; no file or URL is fetched.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/asset-cycle`\n\nDefault severity: **error**. Cost: structure. Asset references form a cycle.\n\n**Why.** An asset whose source points back at itself never resolves to a file.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/catalog-record`\n\nDefault severity: **error**. Cost: structure. An embedded or registered catalog record is invalid.\n\n**Why.** A record that fails its companion schema cannot be resolved. Invalid records are left out of the lookup, so the references to them are reported too.\n\n**Basis.** spec/schemas/<kind>.schema.json\n\n### `opf/layout-template`\n\nDefault severity: **error**. Cost: structure. A layout template's region list is malformed.\n\n**Why.** A layout is one ordered list of regions, nested through splits for two dimensions. Engines cannot compose a list that is not 1 to 12 entries, an entry that is neither a region nor a split, a direction or size that is not valid, an \"auto\" size anywhere but in a column, splits nested more than three levels deep (the record's own list is level 1), or regions that compile to a grid larger than 12 x 12. composeSlide refuses such a record, so a slide that names it composes automatically.\n\n**Basis.** spec/schemas/layout.schema.json and the region list rules (docs/layout-templates.md)\n\n**Approximations.** Checked on every layout record the document embeds and every registered record it uses, once the record passes its schema.\n\n### `opf/layout-region`\n\nDefault severity: **error**. Cost: structure. A layout template's regions are not valid.\n\n**Why.** Region names are unique across the whole record and may not be tag, auto or a promoted-region word (left, center, right, top, middle, bottom), so a Markdown block's region= stays unambiguous; title and subtitle name the heading regions, which take only a size; a subtitle region needs a title region; a region accepts at least one content kind; a region with flow 'none' holds one block; overflowRegion must name a body region. composeSlide refuses such a record.\n\n**Basis.** spec/schemas/layout.schema.json and the region rules (docs/layout-templates.md)\n\n### `opf/unresolved-reference`\n\nDefault severity: **warning**. Cost: structure. A content reference resolves nowhere.\n\n**Why.** A layout, theme, colour scheme, font scheme, narrative, audience, purpose or tone reference that neither the document embeds nor a registered catalog defines falls back: a slide composes automatically, a design uses the engine default. A strict export fails instead. A slide with no layout is automatic composition and is never reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Resolved like every engine resolves it: catalogs.custom, then the records embedded under catalogs.default, then the catalog registered for its source (the first registered catalog when `default` is omitted); `name:id` in catalogs.<name>, then the catalog registered for its source. Nothing is fetched. Free-form audience and purpose text is not a reference.\n\n### `opf/catalog-record-not-in-source`\n\nDefault severity: **warning**. Cost: structure. A record embedded under a catalog group is not in that catalog.\n\n**Why.** A record under catalogs.default or a named group says it came from the catalog its source names. When the catalog the host registered for that source has no record of that kind and id, the record is the document's own and belongs in catalogs.custom, where an update from the catalog never looks for it. Rendering is unchanged: the embedded record still wins.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Checked only against a catalog the host registered for the group's source (the first registered catalog for an omitted default.source); with none registered for it the check is silent. A record the catalog has with different content is an update difference (updateFromCatalog), not this finding. catalogs.custom is never checked.\n\n### `opf/unused-reference`\n\nDefault severity: **warning**. Cost: structure. A reference is never cited.\n\n**Why.** A listed reference that no run cites is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unused-dataset`\n\nDefault severity: **warning**. Cost: structure. A dataset is never used.\n\n**Why.** A dataset no chart or table references is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unknown-beat`\n\nDefault severity: **warning**. Cost: structure. A slide names a beat its narrative does not define.\n\n**Why.** `slides[].beat` links a slide to a step of the narrative plan. A beat id the narrative does not have links to nothing; nothing is drawn from a beat.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** The narrative resolves like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked.\n\n### `opf/slide-theme-dimensions`\n\nDefault severity: **warning**. Cost: structure. A slide theme sets a slide size that differs from the deck.\n\n**Why.** A PPTX has one slide size. The size a slide-level theme carries is never used: design.dimensions of the deck (or of its theme) wins in every engine, and without them the exporter stops with mixed slide sizes.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown`\n\nDefault severity: **warning**. Cost: structure. A `{{token}}` names a variable that is not declared.\n\n**Why.** An undeclared token is shown literally.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-value`\n\nDefault severity: **warning**. Cost: structure. A value was supplied for a variable that is not declared.\n\n**Why.** The value is never used.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unused`\n\nDefault severity: **warning**. Cost: structure. A variable is declared but never used.\n\n**Why.** A declaration nothing refers to has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-reference-unknown`\n\nDefault severity: **warning**. Cost: structure. A `var:` colour names a variable that is not declared.\n\n**Why.** An undeclared colour variable falls back to a default colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## Policy rules\n\n*Does it follow the host's house rules?*\n\n### `opf/contract`\n\nDefault severity: **error**. Cost: structure. A field is outside the values a host policy allows.\n\n**Why.** A host that needs brand layouts, fonts or other fixed choices states them as contracts. The default severity is error; a contract can set its own. Document metadata never installs policy.\n\n**Approximations.** Only the `contracts` option supplies policy, and it checks existing fields: a contract does not require an omitted field or insert a default.\n\n\n## Accessibility rules\n\n*Can everyone read it?*\n\n### `opf/text-contrast`\n\nDefault severity: **warning**. Cost: composition. Text colour has too little contrast against the background it sits on.\n\n**Why.** Low-contrast text is hard or impossible to read for people with low vision, colour-vision differences, glare on a projector or a poor display. WCAG sets 4.5:1 for normal text and 3:1 for large text.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Computed on sRGB colours with the WCAG relative-luminance formula, against the background the preview draws: a solid or theme colour, the card surface, a table cell fill, every colour a gradient takes under the text box (sampled on a 5x5 grid, angle respected), or both colours of a pattern. Anti-aliasing, text shadows and font weight are not modelled. Text colour is the preview's (the scheme's text role or dark1 on a light background and light1 on a dark one, chosen from the background luminance, where a gradient or picture background uses the scheme's default background colour), so a default can fail on a dark gradient. A link with no colour of its own is measured in the scheme's hyperlink colour.\n\n### `opf/text-on-image`\n\nDefault severity: **info**. Cost: composition. Text sits on a background picture whose pixels cannot be measured.\n\n**Why.** Contrast over a photograph depends on the photograph. Without a full-frame overlay that guarantees readability for every possible image, the result cannot be certified from the document.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Core never reads picture pixels. The picture is bounded by a grey ramp from black to white, composited through the image opacity and a full-frame design.background overlay; the text passes only when every step of that ramp passes. An edge-banded overlay is not counted.\n\n### `opf/missing-alt-text`\n\nDefault severity: **warning**. Cost: structure. A picture has no alt text and is not marked decorative.\n\n**Why.** People using a screen reader get nothing for a picture without alternative text. A picture that is purely decorative should say so with an empty alt (alt: \"\"), which is an explicit, reviewed choice instead of an omission.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Checks the alt field of images (image blocks, Slide.image and region images, placed or not), video, organization logos (each shape and background), header/footer images (a logo reference such as var:organization.logo.icon is checked on the organization), quote photos and speaker photos, following asset: references to the assets registry. Whether the text describes the picture well is not judged here (see opf/poor-alt-text). Charts carry `chart.alt` and are checked by opf/chart-text-alternative. Picture backgrounds are decorative unless they carry their own alt, and watermarks are decorative, so neither is checked.\n\n### `opf/poor-alt-text`\n\nDefault severity: **info**. Cost: structure. Alt text is a file name, a URL, a generic word or very long.\n\n**Why.** Alt text such as \"image\", \"IMG_2041.png\" or a 400-character paragraph does not do the job of describing a picture: it is read out and adds noise without information.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Pattern checks only: file extensions and camera-style names, a bare generic word, a URL, a leading \"image of\", and more than 250 characters. Chart alt text (chart.alt) is checked too: a bare chart word, a URL, a leading \"chart of\" or more than 250 characters. It cannot tell whether a plausible sentence is accurate.\n\n### `opf/missing-slide-title`\n\nDefault severity: **warning**. Cost: structure. A slide has no title.\n\n**Why.** Slide titles are how people using a screen reader, an outline view or keyboard navigation find and tell slides apart; PowerPoint's own accessibility checker reports a missing title too.\n\n**Basis.** WCAG 2.2 SC 2.4.2 Page Titled and SC 2.4.6 Headings and Labels, level AA (PowerPoint: \"Missing slide title\")\n\n**Approximations.** Only the slide-level title field counts. Text that merely looks like a heading inside a block does not.\n\n### `opf/duplicate-slide-title`\n\nDefault severity: **info**. Cost: structure. Two slides have the same title.\n\n**Why.** Identical titles make slides indistinguishable in an outline or a screen reader's slide list.\n\n**Approximations.** Titles are compared case-insensitively with whitespace collapsed. Slides that continue one another are not exempt; give a continuation a distinct title such as \"(continued)\".\n\n### `opf/reading-order`\n\nDefault severity: **warning**. Cost: composition. The order content is read differs from the order it appears on the slide.\n\n**Why.** Screen readers, keyboard focus and PowerPoint's selection pane follow the composed order. When it differs from the visual order (top to bottom, then start to end of the reading direction), the slide is read out of sequence.\n\n**Basis.** WCAG 2.2 SC 1.3.2 Meaningful Sequence, level A (PowerPoint: \"Check reading order\")\n\n**Approximations.** Compares the composed content order with a visual order recomputed from the composed boxes: items whose vertical centres fall in the same row are ordered along the reading direction (a right-to-left deck is checked by rows only), rows from top to bottom. Headings are expected first. Free-form overlap is not analysed. On a slide with a template layout (OPF 0.19) the expected order is the binding order instead (the regions in reading order, then the blocks of each region), and items outside a region (placed images, the row below the grid) are not compared.\n\n### `opf/link-text`\n\nDefault severity: **warning**. Cost: structure. Link text does not say where the link goes.\n\n**Why.** People who scan links out of context (a screen reader's links list, a link tab order) hear only the text. \"click here\", \"read more\" or a long raw URL tells them nothing about the destination.\n\n**Basis.** WCAG 2.2 SC 2.4.4 Link Purpose (In Context), level A; PowerPoint: \"Hyperlink text is not meaningful\"\n\n**Approximations.** Matches a short list of generic phrases in English (after lower-casing and removing punctuation), blank link text, and raw URLs longer than 40 characters. Adjacent runs sharing one link are read as one link.\n\n### `opf/chart-color-only`\n\nDefault severity: **info**. Cost: structure. Chart series may be indistinguishable without colour vision.\n\n**Why.** OPF charts have no data labels or patterns, so series are told apart by colour alone (and legend order). Colours that look alike to someone with colour-vision deficiency, or when printed in greyscale, make series impossible to tell apart.\n\n**Basis.** WCAG 2.2 SC 1.4.1 Use of Color, level A\n\n**Thresholds.** `minSeriesColorDifference` (default 10)\n\n**Approximations.** Uses the engine's series palette (ValidateOptions.chartPalette; default chartSeriesPalette of each slide colour scheme (accent1 to accent6, then their tints), adjusted for the card surface like the preview does) in series order: series i takes colour i, pie/doughnut/treemap/funnel slices take colours per category. Pairs are compared by CIE76 distance after simulating protanopia, deuteranopia, tritanopia (Machado 2009, severity 1) and greyscale. Single-series charts and chart types without a series legend are skipped.\n\n### `opf/chart-colors-adjusted`\n\nDefault severity: **warning**. Cost: structure. A chart colour cannot be used as authored.\n\n**Why.** `chart.colors` sets the colour of series 1, 2, 3 and so on. A colour that cannot be resolved (a `var:` colour variable the deck does not define, or a name that is not a colour) is ignored and the deck palette colour of that position is used. A colour with under 3:1 contrast against the chart panel is kept for its hue but drawn lighter or darker, so the chart does not show the colour that was written.\n\n**Basis.** WCAG 2.2 SC 1.4.11 Non-text Contrast, level AA\n\n**Approximations.** Resolves each entry like any colour reference (hex, colour-scheme slot or role, `var:` variable) against the slide's scheme and compares it with the chart panel (the slide surface colour) by WCAG contrast; 3:1 is the threshold the engines enforce. Translucent colours and unresolved panels are not measured. Colours that look alike to colour-vision deficiency are reported by opf/chart-color-only, which judges the authored colours.\n\n### `opf/chart-text-alternative`\n\nDefault severity: **info**. Cost: structure. A chart has no text alternative, or is marked decorative.\n\n**Why.** A chart conveys a message; people who cannot see it need the message and ideally the numbers in text. The chart's alt field is that text alternative (the preview exposes it as the chart's accessible name and the PowerPoint export writes it as the frame's alternative text); a sentence or table beside the chart also serves. An empty alt marks a chart decorative, which is reported as info so the choice is reviewed: a chart rarely carries no message.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** A chart passes when chart.alt has text. Without alt, it passes when the slide has any other text, list, table, quote or metric content besides title and tag, or a subtitle. It does not judge whether alt or that text states the chart's point (see opf/poor-alt-text for generic alt text). alt: \"\" is reported as a decorative chart, whatever else is on the slide.\n\n### `opf/missing-language`\n\nDefault severity: **info**. Cost: structure. The presentation does not declare its language.\n\n**Why.** Screen readers and text-to-speech choose pronunciation and hyphenation from the declared language; spell checkers and translation tools use it too.\n\n**Basis.** WCAG 2.2 SC 3.1.1 Language of Page, level A\n\n**Approximations.** Only the presentation-level `language` is checked, not the language of individual runs (`TextRun.lang`; opf/language-tag checks how those tags are written).\n\n\n## Layout rules\n\n*Will it present as authored?*\n\n### `opf/chart-option-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart option cannot be shown by the chart type.\n\n**Why.** An axis title, legend or data label option the chart type does not support is adapted by every engine, so the chart is not drawn exactly as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/text-overflow`\n\nDefault severity: **warning**. Cost: composition. Text or a table does not fit its space at the smallest allowed size.\n\n**Why.** Core composition shrinks text to the readable minimum and then reports what still does not fit. Overflowing text is clipped or runs over other content in the preview and in PowerPoint.\n\n**Approximations.** Uses composeSlide at the deck's slide size with the fonts of the resolved font scheme. Without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimate, which can differ from the real font by a few percent; pass the renderer's measurement for font-exact results. A composition that sets overflow: \"error\" is reported as a warning here, so that `valid` never depends on font metrics; rendering and export still refuse such a slide when it does not fit.\n\n### `opf/unresolved-content`\n\nDefault severity: **warning**. Cost: composition. Content cannot be drawn as authored.\n\n**Why.** composeSlide reports content it cannot place or an effect it does not support (for example a picture bullet without a logo, a date field without a date, or an unsupported image treatment). The preview and the export fall back.\n\n### `opf/layout-failed`\n\nDefault severity: **warning**. Cost: composition. The slide layout could not be computed.\n\n**Why.** Composition threw for a slide that passed schema validation, so geometry-based rules (contrast, overflow, reading order, resolution) were skipped for it.\n\n### `opf/min-font-size`\n\nDefault severity: **warning**. Cost: composition. Text is drawn smaller than the readable minimum.\n\n**Why.** Small type is unreadable from the back of a room and on a phone. The engine's own default floor is 12 pt (16 px); anything much below that was lowered on purpose or by a composition that could not fit the text.\n\n**Basis.** Common presentation guidance (12 pt minimum for body text; 18 pt or more is easier to read from a distance).\n\n**Thresholds.** `minFontSizePt` (default 11)\n\n**Approximations.** Sizes are those composeSlide fitted, expressed on the 13.33 x 7.5 in reference slide (96 px per inch, so 1 px is 0.75 pt; a smaller canvas scales type down with it) and explicit run fontSize values in points. Per payload, the smallest part (a metric label or a quote attribution) is reported. Table cell text (default 11.25 pt), code and header/footer furniture are not checked.\n\n### `opf/region-unknown`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region the layout does not have.\n\n**Why.** A region pin (`region` on a root block) names a region of the slide's layout. A name the layout does not have is ignored: the block is placed by its kind, which may not be where the author meant.\n\n### `opf/region-kind`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that does not accept its kind.\n\n**Why.** A region says which content kinds it takes. A pin to a region that does not take the block's kind is ignored and the block is placed by its kind.\n\n### `opf/region-full`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that is already full.\n\n**Why.** A region holds at most `max` blocks on one slide. A pin to a full region is ignored and the block is placed by its kind.\n\n### `opf/layout-unplaced`\n\nDefault severity: **warning**. Cost: composition. No region of the layout has room for a block.\n\n**Why.** Content is never dropped: a block no region has room for is drawn in the layout's overflow region beyond its max (or below the grid), where it crowds the others. Paginate moves it to a continuation slide with the same layout.\n\n### `opf/furniture-wraps`\n\nDefault severity: **warning**. Cost: composition. A header or footer zone wraps onto more than 2 lines.\n\n**Why.** Header and footer text repeats on every slide in a band at the slide edge. A zone that wraps onto three or more lines grows into the body, and long source lines read poorly at footer size. Footers carry short repeated text (a title, a date, a page number); sources belong in a caption or the speaker notes.\n\n**Approximations.** Uses the lines core composed for the zone at the deck's slide size; without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimates. A deck-level header or footer is reported once.\n\n### `opf/font-outside-scheme`\n\nDefault severity: **warning**. Cost: structure. Text uses a font family that is not in the deck's font scheme.\n\n**Why.** The font scheme is the deck's font choice. A run that names another family will not follow a font-scheme change, may be missing on the viewer's machine, and breaks the deck's typographic consistency.\n\n**Approximations.** Compares the run's fontFamily (case-insensitively) with the heading, body, code and accent families of the slide's resolved font scheme and the scheme's major/minor fonts. Families that the host substitutes are still different names here.\n\n### `opf/image-resolution`\n\nDefault severity: **warning**. Cost: composition. An image has too few pixels for the size it is shown at.\n\n**Why.** An image stretched beyond its pixel size looks blurry or blocky on a projector or a high-density screen.\n\n**Thresholds.** `minImagePpi` (default 96)\n\n**Approximations.** Only embedded data: images (and asset: references to them) have readable pixel sizes; URLs and files are never fetched, so they are not checked. The displayed size is the composed frame (cover and stretch are measured as the cover scale, contain as the contain scale). Effective ppi is the image pixels per inch of the 96 px/inch reference slide. SVG is vector and exempt.\n\n\n## Content rules\n\n*Is anything left unfinished?*\n\n### `opf/chart-value-not-numeric`\n\nDefault severity: **warning**. Cost: structure. A chart value is text, not a number.\n\n**Why.** A chart plots numbers. A text cell such as \"12%\" is plotted as a gap. When every text value of a column is written in one display style the finding carries the fix that stores the numbers and gives the column the matching format.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart mapping entry is ignored.\n\n**Why.** A mapping that names a column the chart type cannot use is left out of the chart.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart highlight is adapted by the chart type.\n\n**Why.** A highlight the chart type cannot show as authored (a category highlight on a chart without categories, for example) is adapted by every engine, so the emphasis is not exactly what was written.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-out-of-range`\n\nDefault severity: **warning**. Cost: structure. A code highlight marks a line the code does not have.\n\n**Why.** Marked lines count from 1 by line break in code.source; an entry past the last line marks nothing.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-range-reversed`\n\nDefault severity: **warning**. Cost: structure. A code highlight range ends before it starts.\n\n**Why.** A range written end before start marks nothing; write the smaller line number first.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-builtin-missing`\n\nDefault severity: **warning**. Cost: structure. A built-in variable has no value in this document.\n\n**Why.** A `{{deck.*}}`, `{{speaker.*}}` or `{{organization.*}}` token whose field the document does not set resolves to nothing, so the text shows a gap; a `var:organization.logo` reference to an organization without a logo draws no image. `{{slide.section}}` is checked per slide: a slide without a `section` that uses it, in its own text or in the header or footer it inherits, is reported at that slide.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/duration-outside-narrative`\n\nDefault severity: **warning**. Cost: structure. The target duration is outside the range the narrative suits.\n\n**Why.** A narrative records the talk lengths it suits. A deck whose duration is outside that range probably needs a different narrative or a different length; nothing is drawn or exported differently.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/narrative-duration-range`\n\nDefault severity: **warning**. Cost: structure. An inline narrative duration range has min greater than max.\n\n**Why.** A range with its bounds swapped matches no duration, so the duration check cannot help.\n\n**Basis.** spec/schemas/narrative.schema.json\n\n### `opf/placeholder-text`\n\nDefault severity: **warning**. Cost: structure. Placeholder text was left in the deck.\n\n**Why.** \"Lorem ipsum\", \"Click to add title\", \"TBD\" and bracketed prompts are scaffolding. Shipping them reads as unfinished work.\n\n**Approximations.** Case-insensitive patterns for lorem ipsum, template prompts (\"Click to add...\", \"Your title here\", \"[Insert ...]\", a bare \"Title\" or \"Text\"), and the markers TBD, TBC, TODO, FIXME and XXX in capitals. A deliberate use of the words is flagged too; disable the rule for that slide with ignorePaths.\n\n### `opf/empty-text`\n\nDefault severity: **info**. Cost: structure. A text field is present but empty.\n\n**Why.** An empty title, text block or list item draws nothing, and leaves a hole in the outline and for assistive technology.\n\n### `opf/unused-beat`\n\nDefault severity: **info**. Cost: structure. A beat of the narrative has no slide that references it.\n\n**Why.** The narrative is the plan and the slides are the product. When some slides name a beat (slides[].beat) and a beat of the plan has none, the deck skips a step of the story or the plan is out of date.\n\n**Approximations.** Resolved like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked. A deck in which no slide references any beat is not checked either, because it has not linked its slides to the plan. A slide that lists several beats covers each of them.\n\n### `opf/empty-slide`\n\nDefault severity: **warning**. Cost: structure. A slide has no content at all.\n\n**Why.** A slide with no title, no content and no picture is almost always an accident of editing. (A deliberately blank slide can use the blank layout.)\n\n### `opf/language-tag`\n\nDefault severity: **warning**. Cost: structure. A language tag is malformed, uses an unassigned region or is not in canonical case.\n\n**Why.** Engines read the deck language and each run's `lang` as BCP 47 tags to choose script fonts, text direction, proofing language and the PowerPoint `lang` attribute. A tag such as `en-a`, or a region that does not exist (`en-UK`; the United Kingdom is `GB`), is guessed at or dropped, and a wrong-case tag (`EN-us`) is not what a locale lookup expects.\n\n**Basis.** BCP 47 (RFC 5646, section 2.1) and the platform locale data (Intl.getCanonicalLocales, Intl.DisplayNames)\n\n**Approximations.** Checks the root `language` (a tag string, or the `bcp47` of an inline Language object) and `lang` on the TextRun objects of titles, subtitles, tags, text, lists, tables, quotes, metrics and timelines. A region is assigned when the runtime's CLDR data names it (ISO 3166-1 codes and UN M.49 areas such as 419); private-use regions (`AA`, `QM`-`QZ`, `XA`-`XZ`) and `ZZ` are accepted, as are private-use (`x-...`) tags. Only the spelling of a tag is checked, not whether the language subtag is assigned or whether the language suits the text. The schema rejects tags with characters other than letters, digits and hyphens (`en_US` is a schema error, with no suggestion), and `en-UK` as the deck language is already an error of the schema rule, so those are not reported twice.\n\n<!-- validate-rules:end -->\n"
|
|
355
|
+
"markdown": "# OPF validate: one checker for format, references, accessibility, layout and content\n\n`validate` in `@openpresentation/opf` and `opf validate` in the CLI check an OPF presentation for everything a tool can tell without an opinion: whether it is well-formed OPF, whether everything it points at resolves, whether a host's house rules hold, and the problems a schema cannot see, such as text that is hard to read, text that does not fit, pictures without alt text, content read out of order, fonts outside the deck's scheme, placeholder text and slides left empty. It is the only checker. Releases before 0.14 had several (a schema validator, a lint and an audit, in the library and the CLI); 0.14 replaces them all with `validate`.\n\n`validate` is read-only, local and deterministic. It fetches nothing (no images, fonts or catalogs), consults no clock and calls no model. The same document, options and fonts give byte-identical findings.\n\n```sh\nopf validate deck.opf.json # everything, JSON report\nopf validate deck.opf.json --format text # one line per finding\nopf validate deck.opf.json --only format,references --fail-on warning\nopf validate deck.opf.json --ignore layout --config house-rules.json\nopf validate --list-rules\n```\n\n```js\nimport { validate } from '@openpresentation/opf';\n\nconst report = validate(presentation); // a parsed presentation\nconst fromText = validate(fileText); // strict JSON text: also syntax, duplicate keys and line/column\nvalidate(presentation, { only: ['format'] }); // the cheap check: schema and semantic rules only\nvalidate(presentation, { ignore: ['layout'] }); // everything except the checks that build slide layouts\nvalidate(presentation, { contracts, severity: { 'opf/text-contrast': 'error' } }); // house rules\n```\n\n## Categories\n\nEvery finding has one of six categories. They say what kind of question the finding answers, and they select what runs.\n\n| Category | Asks | Errors by default | Examples |\n| --- | --- | --- | --- |\n| `format` | Is it well-formed OPF? | yes | JSON syntax, duplicate keys, schema, a citation of an unknown reference, an unknown dataset, a required variable with no value (a warning in a template), a `name:id` prefix that names no catalog group |\n| `references` | Does everything it points at resolve? | missing or circular asset, invalid catalog record; a reference that resolves nowhere and an unused dataset are warnings | `asset:` references, catalog references, citations and datasets nobody uses |\n| `policy` | Does it follow the host's house rules? | the host sets it (default `error`) | \"only these layouts\", brand fonts: the `contracts` option |\n| `accessibility` | Can everyone read it? | no: warning and info | contrast, alt text, slide titles, reading order, link text, language |\n| `layout` | Will it present as authored? | no: warning | overflow, minimum type size, image resolution, fonts outside the scheme |\n| `content` | Is anything left unfinished? | no: warning and info | placeholder text, an empty slide, a chart value that is text |\n\n`valid` keeps meaning \"correct OPF\": it is true when no finding has severity `error`, and by default only `format`, `references` and `policy` can produce one. A low-contrast deck is still a valid deck. A team promotes the rules it cares about with `severity`.\n\nEvery rule is listed, with its category, default severity, cost and the basis it rests on, in [Rules](#rules) below. A rule stays in core only when it has a measurable basis (a standard such as WCAG 2.2, a print-preflight check, or a number computed from the layout); judgments that need an audience and a purpose, such as how many fonts or words a deck should have, are not rules here.\n\n## Input and report\n\n`validate(input, options?)` takes a parsed presentation (an object) or **strict JSON text**. A string is read as JSON and nothing else: it is never sniffed for YAML or Markdown. When the text begins like YAML (`key: value`, `---`) or Markdown (`#`), the `help` of the syntax finding says to read it with `fromYaml` or `fromMarkdown` first, which return a presentation and call `validate` for you.\n\nWith text, a syntax error or a duplicate key comes back with `location` (original-source UTF-16 offset and length, one-based line and column), and so does every finding about a value in the document; a finding about a missing field is located at the object that lacks it. Text that is not JSON reports its syntax errors (`opf/json-syntax`) whatever `only` says, because nothing else can run. A syntax finding names the root cause in plain words and not the token where a parser gave up: for `\"design\":` followed by the next key, one finding `` `design` needs a value. `` located at `\"design\":` (its `help` says what could come next), not four findings from the token after it. There is one finding per cause, in position order, at most five; the cascade of one mistake is not reported again. `analyzeJsonSyntax` (`@openpresentation/opf/source`, browser-safe) returns the same causes with the fixes that repair each one (`Insert comma`, `Add { }`, `Remove \"design\"`, ...) for editors; `locate` and `pathAt` in the same module turn a JSON Pointer into a range of the text and a caret into a path. A finding for an unknown property (`opf/schema`, keyword `additionalProperties`) carries up to three `suggestions` of the nearest property names of the same object (edit distance 2, or 1 for names of three characters or fewer; case-insensitive), so `slids` suggests `slides`.\n\n### Reading a deck in any form\n\n`parse(text, { format, filename, catalogs })` (the root and `@openpresentation/opf/deck`, RR-60, RR-62) reads a deck from JSON, YAML or Markdown text and returns the presentation; a syntax, schema or reference error throws `OPFValidationError`, whose `report` is the report `validate` gives the text. The format is the `format` option (`\"json\"`, `\"yaml\"` or `\"markdown\"`), else the `filename` (`.opf.md` is Markdown, `.yaml` and `.yml` are YAML, anything else is JSON), and never the content: a plain `.md` name is not a deck. Findings are located in the text that was read: `opf/json-syntax` (line and column) for JSON, `yaml/<rule>` for YAML and `markdown/<rule>` for Markdown, and every `opf/` finding at the field it names. `parse` checks format and references; warnings do not throw, so call `validate` for the full report (`validate(text)` for JSON text; `fromYaml` and `fromMarkdown` return it for the other forms). It throws a `TypeError` for a text that is not a string, an unknown format or a bad option. `stringify(presentation, { format | filename })` is the other direction: JSON as two-space JSON, YAML and Markdown in canonical form. Both are browser-safe; `open` and `save` of `@openpresentation/opf` do the same for files in Node. See [OPF as YAML](yaml.md) and [Markdown and outlines](markdown.md).\n\n[Validation results](finding-schema-reference.md) use the `FindingReport` contract (`spec/schemas/finding.schema.json`). Core's `ValidationReport` adds a few more fields:\n\n| Field | Meaning |\n| --- | --- |\n| `valid` | No finding has severity `error` |\n| `findings` | Every finding, ordered by slide (document-level findings first), then category, then rule |\n| `counts` | `{ error, warning, info }` |\n| `schemaValid` | The document passes the schema and the semantic format checks; `null` for text that is not JSON. A required variable with no value does not make a document schema-invalid |\n| `checks` | What ran and what was not measured (below) |\n| `template`, `unfilledVariables` | Documents that declare content variables or are templates: whether they were checked as a template, and which required variables have no value |\n\nA finding has a stable `ruleId` (`opf/text-contrast`), a `severity` (`error`, `warning`, `info`), a `category`, a JSON Pointer `path`, a `message`, a `help` sentence and a `definition` link to the rule below or to the schema constraint. It may carry `slide` and `slideId` (zero-based index and id of the slide it belongs to), `location` (from text), `measured` (the numbers behind it: ratios, sizes, counts), `suggestions` (catalog records that exist, never silently applied), `lookup` (arguments for `opf schema` or `opf catalog`, not a shell string), `validation` (the raw schema issue, with every union alternative) and `fixes`.\n\n`checks` says what each part did: `syntax` (`checked` for text), `schema`, `references`, `policy`, `accessibility` and `content` are `checked` or `not-run`; `layout` is `not-run`, `estimated` (text widths are core's portable estimate) or `measured` (the host passed `fonts.textMeasurement`); background picture pixels are never read (`backgroundPixels`), image resolution reads only embedded `data:` images (`imageBytes`), and native PowerPoint rendering is not checked (`nativeExport`).\n\nA document the schema rejects is not audited: its accessibility, layout and content rules do not run, and its format errors are reported even when `only` did not ask for them, so the report says why nothing else ran (switch them off with `ignore: ['format']`).\n\n## Options\n\n```js\nvalidate(presentation, {\n only: ['format', 'opf/missing-alt-text'], // run just these rules or categories\n ignore: ['layout'], // do not run these\n severity: { 'opf/text-contrast': 'error', content: 'off' }, // per rule or category: error, warning, info or off\n ignorePaths: [{ rule: 'opf/text-contrast', path: '/slides/3' }], // an accepted exception, by JSON Pointer prefix\n catalogs: [gallery, acmeCatalog], // the catalogs the host registered (the first is the default); nothing is fetched\n contracts, // host policy (below)\n thresholds: { contrastNormal: 7 }, // see DEFAULT_VALIDATION_THRESHOLDS\n fonts, // the host's fonts: layout rules read fonts.textMeasurement\n chartPalette: ['#0072B2', '#E69F00'], // the engine's chart colours\n template: true, values: { client: 'Acme' }, // check as a template, or fill variables first\n});\n```\n\n- **Names.** `only`, `ignore`, `severity` and `ignorePaths` take full rule ids (`opf/text-contrast`), bare rule names (`text-contrast`) or category names (`accessibility`). No rule is named like a category. A rule beats its category in `severity`. An unknown rule, category, threshold or option throws a `TypeError`, so a typo cannot silently disable a check.\n- **`fonts`.** An object with a `textMeasurement` (the renderer's `loadFonts()` result has one). Without it the layout rules use core's portable estimate and `checks.layout` says `estimated`; with it, `measured`. `textMeasurement` may also be a function of the zero-based slide index, for a script-aware measurement per slide. When the measurement has a `forScripts(profile)` method (a script planner), the layout rules ask it for each slide's script-aware measurement, built from the slide's `resolveScriptFonts` profile and a run language's own profile, the way the renderer and the exporter measure, so Arabic or Japanese text is measured in its script face rather than the Latin face its style names.\n- **`template` and `values`.** A deck that declares content variables is checked as the deck it resolves to; see [templates and variables](templates-and-variables.md).\n- **Thresholds.** `DEFAULT_VALIDATION_THRESHOLDS` holds `contrastNormal`, `contrastLarge`, `minFontSizePt`, `minImagePpi` and `minSeriesColorDifference`; each rule below lists the ones it reads.\n\n`validationRules` lists every rule with its metadata (`id`, `name`, `category`, `severity`, `cost`, `summary`, `rationale`, `standard`, `approximations`, `thresholds`), and `findValidationRule(name)` finds one by id or bare name.\n\n`assertValid(value, options?)` throws `OPFValidationError`, carrying the `report` and its error `findings`, unless `validate(value, options)` is valid, and tells TypeScript that `value` is a `Presentation`. It takes a parsed value, not text.\n\n`validateCatalogRecord(kind, record)` and `assertValidCatalogRecord(kind, record)` check one catalog record (an audience, theme, layout, font scheme and so on) against its schema. The report has the same shape and checks the record format only (`checks.references` is `not-run`): without an enclosing catalog it does not resolve linked ids. Validate a presentation with its registered catalogs to check those references.\n\n## Pass `fonts`, otherwise the estimate\n\nThe layout rules (`opf/text-overflow`, `opf/min-font-size`, reading order, image resolution) depend on how wide the text is. `validate` measures it with `options.fonts`, the handle that opf-render's `loadFonts()` returns:\n\n```js\nconst fonts = await loadFonts({ pack: 'office' });\nconst report = validate(deck, { fonts }); // report.checks.layout === 'measured'\n```\n\nPass the same `fonts` handle to preview (`toSvg`), pagination (`paginate`) and export (`toPng`, `toPdf`, `toPptx`), so that validation measures with the faces the preview draws and a finding means what the slide shows. The same handle gives identical geometry across engines; opf-pptx's `layout-parity` test checks that the exported PowerPoint paragraphs agree with the SVG preview and with core's composed items, with and without `fonts`. See [Pass `fonts`, otherwise the estimate](font-fidelity.md#pass-fonts-otherwise-the-estimate) for the full list.\n\nWithout `fonts` the rules use core's built-in estimate and `checks.layout` says `estimated`: base widths are 0.54 em per character, 0.62 em for capitals and digits, 0.32 em for a space and 1 em for CJK characters. Spacing combining marks outside Latin/CJK contribute; nonspacing/enclosing marks do not. Robust script factors calibrated against the FF-44 corpus reduce underestimation for other scripts; see [script width evidence](evidence/script-width-estimates/README.md). It needs no font files and is stable from run to run, but an estimate can be wider or narrower than a particular face, so an estimated overflow finding (or its absence) is a first pass. `opf validate` on the command line loads no fonts, so its layout checks are estimated too. Node `convert` and the CLI's `opf convert` and `opf paginate` prepare an office-pack handle for you; a library call to `validate` never loads fonts itself.\n\n## Cost\n\n`validate` is lazy: it builds only what the rules you ask for need. Measured with Node 24 on three bundled example decks (milliseconds per call, warm; your machine will differ, the ratios hold):\n\n| Call | `minimal` (3 slides) | `renewal-save-plan` (6 slides, 34 KB) | `broadband-public-hearing-deck` (10 slides, 41 KB) |\n| --- | --- | --- | --- |\n| `only: ['format']` | 0.06 | 0.23 | 0.64 |\n| `only: ['format', 'references']` | 0.7 | 5.2 | 7.6 |\n| everything (the default) | 1.7 | 24 | 32 |\n\n`only: ['format']` costs what the schema check alone costs; it builds no layout. The references walk reads the schema alongside the document and costs about ten times as much. The rules of `layout` (and `opf/text-contrast`, `opf/text-on-image` and `opf/reading-order`) compose every slide and are most of the cost of a full run. Each rule's `cost` says which it needs: `syntax` reads the JSON text, `structure` reads the document alone, `composition` builds slide layouts.\n\nThe default runs every category, including overflow findings; `valid` means no error findings, so an overflow warning alone does not make it false. Hot paths select what they need: pagination, conversion, patch validation and the editor's per-edit check use `{ only: ['format'] }`; CLI document-writing commands check `format` and `references`, so a contrast warning does not block an export unless its severity or failure policy is explicitly raised.\n\n## Quick fixes\n\nA fix is a suggestion that core never applies. A host such as the editor's Review panel shows it and applies it through its own undoable edit path.\n\n- A `patch` fix (`kind: \"patch\"`, the default) carries RFC 6902 JSON Patch operations in `patch` (`add`, `remove`, `replace`, `move`, `copy`, `test`, with JSON Pointer paths). `safe: true` means the change cannot alter what the content says: for example switching a failing text colour to the colour the theme chose for that background.\n- A `focus` fix (`kind: \"focus\"`) names the field the author must fill in (`focus.path`, `focus.field`: `alt`, `title`, `text`, `link`, `language`, `fontSize`).\n- `safe: false` patches change meaning or appearance and need a deliberate click, for example marking a picture decorative with an empty `alt`.\n\n`title` says what applying the fix does.\n\n## Catalog context and contracts\n\nContent references resolve the way every engine resolves them ([catalogs](default-catalog.md)): `catalogs.custom`, then the records embedded under `catalogs.default`, then the catalog the host registered (the `catalogs` option) for its source; `name:id` in `catalogs.<name>`, then the catalog registered for its source. A reference that resolves nowhere is a warning (`opf/unresolved-reference`), never an error; a prefix that names no group is a format error (`opf/undeclared-catalog`): it can never resolve, so engines reject the document at their format check (`validate(document, { only: [\"format\"] })`) instead of drawing a fallback. A `catalogs` option that is not an array of registered catalogs throws `OPFCatalogsOptionError` (`code: \"invalid-catalogs\"`) at every entry point. Nothing is fetched, and without the `catalogs` option only the records the document embeds resolve. Free-form audience and purpose descriptions (any string that is not a bare id or `name:id`) and arbitrary `extensions` data are never references; the `language` tag, `chart.type` and the `socials` keys are engine vocabularies the schema checks. Embedded records, and registered records the document uses, are validated against their companion schema (`opf/catalog-record`). A record under `catalogs.default` or a named group that the catalog registered for the group's source does not publish is `opf/catalog-record-not-in-source` (move it to `catalogs.custom`); with no catalog registered for that source the check is silent. Suggestions name records actually present in the checked context (`origin` `document` or `registered`).\n\nAn `asset:` reference that names no entry of the document's `assets` registry is an error (`opf/asset-reference`), and so is a cycle of `asset:` sources (`opf/asset-cycle`); resource bytes are never fetched. Chart and table data findings keep their code as the rule id (`opf/dataset-unknown`, `opf/chart-value-not-numeric`, ...), and a `opf/chart-value-not-numeric` cell whose column is written in one display style (`\"12%\"`, `\"$1,234\"`) carries `fixes` ([chart and table data](chart-table-data.md#migration-help)). Citation and caption errors keep theirs too (`opf/cite-unknown-reference`, `opf/reference-id-duplicate`, `opf/cite-unsupported-location`, `opf/caption-unsupported-payload`).\n\nA narrative the document defines is a record in `catalogs.custom.narratives`, and `narrative` is always a reference string. When the narrative resolves, a `slides[].beat` id it does not define is `opf/unknown-beat`, a root `duration` outside the record's `duration` range is `opf/duration-outside-narrative`, an embedded record whose `duration.min` exceeds `duration.max` is `opf/narrative-duration-range`, and a beat no slide references is `opf/unused-beat` (only once some slide names a beat). A slide-level `design.theme` whose resolved dimensions differ from the deck's is `opf/slide-theme-dimensions`: a PPTX has one slide size, and a slide's `design` cannot set `dimensions` at all (a schema error). An undeclared `var:<id>` in a solid, gradient-stop or pattern background colour warns like any other colour reference (`opf/variable-reference-unknown`).\n\nHost policy comes from explicit options, never from the document: fields under `extensions` are data and cannot install policy. `contracts` and `catalogs` may live in a local JSON file for the CLI (`--config`).\n\n```json\n{\n \"catalogs\": [\n {\n \"source\": \"pkg:@partner/opf-catalog\",\n \"layouts\": {\n \"partner-title\": {\"name\":\"Partner title\",\"regions\":[{\"name\":\"title\",\"size\":1}]}\n }\n }\n ],\n \"contracts\": [\n {\n \"path\":\"/slides/*/layout\",\n \"allowedValues\":[\"partner-title\",\"text\"],\n \"message\":\"Use the brand layouts {{allowed}} at {{path}}. See {{file}}.\",\n \"documentation\":\"brand-guide.md#layouts\",\n \"severity\":\"error\"\n }\n ]\n}\n```\n\nContract paths are JSON Pointer patterns: `~0` escapes `~`, `~1` escapes `/`, and a whole `*` segment matches one property or array index. Contracts check existing fields; they do not require an omitted field or insert defaults. Allowed values are JSON primitives. Optional message placeholders are `{{path}}`, `{{value}}`, `{{allowed}}`, and `{{file}}`. A contract's severity is `error` unless it says otherwise. Invalid or misspelled configuration keys fail instead of being ignored. Messages and catalog labels are data, not executable instructions.\n\n## The CLI\n\n`opf validate <file|-> [--config <file>] [--only <list>] [--ignore <list>] [--fail-on <error|warning|info>] [--asset-dir <dir>] [--format <json|text>] [--list-rules]`\n\nThe JSON report (the default) is the `validate` report plus the source's SHA-256 (`sha256`), the installed core version (`opfVersion`) and, with `--config`, the configuration file's path and hash (`context`). `--format text` prints one line per finding (`file:line:column severity rule message`), the JSON Pointer path, a hint and any fix, then a summary. `--list-rules` prints every rule (`--format json` with its full metadata).\n\nA file ending `.yaml` or `.yml` (or stdin with `--from yaml`) is read as YAML through `fromYaml`, so the report is the same and every finding is located in the YAML; a YAML syntax or dialect error is a `yaml/<rule>` finding (`format`, exit 1). A file ending `.opf.md` (or stdin with `--from md`) is read as a [Markdown deck](markdown.md#markdown-decks-in-every-command) the same way, and a Markdown error is a `markdown/<rule>` finding. A plain `.md` file, and any other text that does not start with `{` or `[`, needs `--from`.\n\n**Local images (RR-93).** The CLI, not core `validate` (which reads no file), also checks that each relative image or logo path exists: a `cli/asset-missing` warning when `opf convert` could not find it, `cli/asset-blocked` when it is outside the folder or not an image file. It uses convert's resolver and rules: paths are relative to the deck's folder (the working directory for stdin), or to `--asset-dir`; URLs, `data:` URIs and `asset:` references to them are not read. `--only` skips the check and `--ignore asset-missing` (or `asset-blocked`) drops one kind.\n\n**Schema errors in a union.** Where a value can take several forms (a table cell is a scalar, runs or a styled object), a failing value reports the errors of the form it was written for, and each finding's `validation.alternatives` lists the other forms in one line each (`--format text` prints them as `other forms: ...`). When no form fits the value's type, one error names the types accepted.\n\n`--only` and `--ignore` take comma-separated rule ids, bare names or category names. `--config` takes an explicit local JSON file, never `-`, holding `{ catalogs, contracts, severity, only, ignore, ignorePaths, thresholds, chartPalette }`; `--only` replaces the file's `only` and `--ignore` adds to its `ignore`.\n\n| Exit | Meaning |\n| --- | --- |\n| 0 | No finding at or above `--fail-on` (default `error`) |\n| 1 | At least one finding at or above `--fail-on`, or a document that is not valid JSON (or YAML) |\n| 2 | Usage, configuration or I/O error |\n\n`--fail-on` is the one way to say \"fail on warnings\" on every command that checks a document: `--fail-on warning`. Other commands (`create`, `edit`, `fill`, `paginate`, `embed`, `ingest`, `convert`) check only `format` and `references` of what they write and take `--fail-on` the same way.\n\n## How contrast is computed\n\n`opf/text-contrast` reasons about the colours the preview draws, because a check against a different colour than the one on screen is worse than none.\n\n1. **Background.** Design resolves per field: slide design, deck design, theme, engine default. The background is a theme slot or colour (`solid`), the card surface for content on a `contentBox` card, a table cell's fill, a gradient or a pattern. Solid, gradient-stop and pattern colours are read as literal hex colours, as the preview reads them (anything else falls back as it does there). Translucent backgrounds are composited over white.\n2. **Text colour.** Titles, body, lists and tags use the scheme's `dark1`, or `light1` when the background is dark (luminance below 0.179). Furniture and quote attributions use the muted colour, metric values the primary colour, and explicit run and table colours the author's `ColorRef` resolved the same way the renderer resolves it. As the preview does, a gradient background has no single colour and counts as light, so a dark gradient behind default text is reported.\n3. **Gradients.** The gradient is sampled on a 5 by 5 grid over the area the text covers (the lines' measured ink, aligned as the text is), using the preview's gradient geometry, and the worst colour decides. A pattern contributes both of its colours.\n4. **Pictures.** Pixels are never read. A background picture is bounded by a grey ramp from black to white, composited through the picture opacity and any full-frame overlay of the image background (`design.background.overlay`). Text passes only if every step passes; otherwise `opf/text-on-image` says the result cannot be guaranteed. An overlay limited to an edge band is not counted.\n5. **Ratio and size.** The WCAG 2.x relative-luminance contrast ratio is compared with 4.5:1, or 3:1 for large text: at least 18 pt, or 14 pt and bold. Sizes are the fitted sizes, expressed at the 13.33 by 7.5 in reference slide (96 px per inch). The default body size, 18.75 pt, is large text under WCAG, so default body text passes at 3:1; set `contrastLarge` to 4.5 for a stricter policy.\n\nApproximations: anti-aliasing, text shadows, font weight and the exact glyph coverage are not modelled; chart and code text are not checked (charts pick label colours against their surface; code uses its own panel colours). See each rule below for what it cannot see.\n\n## Layout rules use composition\n\nOverflow, minimum type size, reading order and image resolution use `composeSlide` at the deck's slide size with the resolved font scheme, the same composition the preview and the PPTX export consume. Without `fonts.textMeasurement` the text widths are core's portable estimate and can differ from the real fonts by a few percent; hosts that load fonts (the editor, the renderer) pass their measurement for font-exact results. A composition with `overflow: \"error\"` fails at composition time; `validate` still computes the geometry, relaxes the setting, and reports the overflow as a **warning**, so that `valid` never depends on font metrics. Rendering and export still refuse such a slide when it does not fit, and a host that wants the finding to fail promotes it with `severity: { 'opf/text-overflow': 'error' }`.\n\nPromoted region keys (`left`, `center`, `right`, `top:left` and the rest) are composed in visual reading order (rows from top to bottom, then along the row), whatever order the keys are written in. The PPTX export writes shapes in that order, which is the reading order of assistive technology; `opf/reading-order` checks that the composed order still matches the geometry (the same `visualReadingOrder` that composes the regions). Authoring with `blocks` (an ordered list) keeps the reading order equal to the visual order.\n\n## Templates and variables\n\n`opf/variable-unfilled` is one rule for a required variable that has no value. In a deck that is not a template it is an error (the deck is incomplete), so `valid` is false; in a template (`\"template\": true` in the document, or `template: true` in the options) it is a warning, because a template is incomplete on purpose; `unfilledVariables` lists the ids either way. In a document that declares no content variables, a `{{id}}` token left in a string (outside `variables`, `extensions`, `catalogs` and `assets`) is a warning. A `var:` colour that names no declared variable is `opf/variable-reference-unknown`. It is a `format` rule, so `only: ['format']` (what pagination, patching, conversion and every write command run) rejects a deck with an unfilled required variable. Fill a template with `opf fill` or `resolveVariables`, or pass `values`, to clear the findings.\n\n## Rules\n\nThe reference below is generated from the rule registry (`validationRules`); `node scripts/build-validate-docs.mjs --check` (and a core test) fails when it drifts.\n\n<!-- validate-rules:start (generated by scripts/build-validate-docs.mjs; do not edit by hand) -->\n\n| Rule | Category | Default | Cost | What it reports |\n| --- | --- | --- | --- | --- |\n| [`opf/json-syntax`](#opfjson-syntax) | Format | error | syntax | The text is not strict JSON. |\n| [`opf/duplicate-key`](#opfduplicate-key) | Format | error | syntax | A JSON object repeats a key. |\n| [`opf/schema`](#opfschema) | Format | error | structure | The document breaks the OPF schema or a structural rule the schema cannot express. |\n| [`opf/reference-id-duplicate`](#opfreference-id-duplicate) | Format | error | structure | Two references share an id. |\n| [`opf/cite-unknown-reference`](#opfcite-unknown-reference) | Format | error | structure | A citation names a reference that does not exist. |\n| [`opf/cite-unsupported-location`](#opfcite-unsupported-location) | Format | error | structure | A citation or footnote sits where no engine draws it. |\n| [`opf/caption-unsupported-payload`](#opfcaption-unsupported-payload) | Format | error | structure | A caption is on a payload that cannot carry one. |\n| [`opf/image-option-unsupported-payload`](#opfimage-option-unsupported-payload) | Format | error | structure | An image option is on a payload that is not an image. |\n| [`opf/image-placement-invalid`](#opfimage-placement-invalid) | Format | error | structure | An image placement cannot be honoured. |\n| [`opf/layout-placeholder-group`](#opflayout-placeholder-group) | Format | error | structure | A layout record's placeholder group breaks a nesting rule. |\n| [`opf/dataset-unknown`](#opfdataset-unknown) | Format | error | structure | A chart or table names a dataset that does not exist. |\n| [`opf/dataset-field-unknown`](#opfdataset-field-unknown) | Format | error | structure | A table asks a dataset for a column it does not have. |\n| [`opf/data-column-duplicate`](#opfdata-column-duplicate) | Format | error | structure | A dataset or table has two columns with the same name. |\n| [`opf/chart-mapping-unknown-column`](#opfchart-mapping-unknown-column) | Format | error | structure | A chart mapping names a column the data does not have. |\n| [`opf/number-format-invalid`](#opfnumber-format-invalid) | Format | error | structure | A number format cannot be read. |\n| [`opf/variable-invalid-value`](#opfvariable-invalid-value) | Format | error | structure | A variable value does not match its declared type. |\n| [`opf/variable-unknown-builtin`](#opfvariable-unknown-builtin) | Format | error | structure | A token names a built-in variable that does not exist. |\n| [`opf/chart-highlight-unknown-name`](#opfchart-highlight-unknown-name) | Format | error | structure | A chart highlight names a series or category the chart does not have. |\n| [`opf/variable-format`](#opfvariable-format) | Format | error | structure | A variable cannot be formatted. |\n| [`opf/variable-unfilled`](#opfvariable-unfilled) | Format | error | structure | A required template variable has no value. |\n| [`opf/run-color-unrecognized`](#opfrun-color-unrecognized) | Format | warning | structure | A text run colour is none of the documented forms. |\n| [`opf/numbering-start-ignored`](#opfnumbering-start-ignored) | Format | warning | structure | A list entry sets `start` where nothing is numbered. |\n| [`opf/slide-size-out-of-range`](#opfslide-size-out-of-range) | Format | warning | structure | The slide size is outside PowerPoint's 1 to 56 inch range. |\n| [`opf/undeclared-catalog`](#opfundeclared-catalog) | Format | error | structure | A reference names a catalog group the document does not declare. |\n| [`opf/asset-reference`](#opfasset-reference) | References | error | structure | An `asset:` reference names an asset that is not in the registry. |\n| [`opf/asset-cycle`](#opfasset-cycle) | References | error | structure | Asset references form a cycle. |\n| [`opf/catalog-record`](#opfcatalog-record) | References | error | structure | An embedded or registered catalog record is invalid. |\n| [`opf/layout-template`](#opflayout-template) | References | error | structure | A layout template's region list is malformed. |\n| [`opf/layout-region`](#opflayout-region) | References | error | structure | A layout template's regions are not valid. |\n| [`opf/unresolved-reference`](#opfunresolved-reference) | References | warning | structure | A content reference resolves nowhere. |\n| [`opf/catalog-record-not-in-source`](#opfcatalog-record-not-in-source) | References | warning | structure | A record embedded under a catalog group is not in that catalog. |\n| [`opf/unused-reference`](#opfunused-reference) | References | warning | structure | A reference is never cited. |\n| [`opf/unused-dataset`](#opfunused-dataset) | References | warning | structure | A dataset is never used. |\n| [`opf/unknown-beat`](#opfunknown-beat) | References | warning | structure | A slide names a beat its narrative does not define. |\n| [`opf/slide-theme-dimensions`](#opfslide-theme-dimensions) | References | warning | structure | A slide theme sets a slide size that differs from the deck. |\n| [`opf/variable-unknown`](#opfvariable-unknown) | References | warning | structure | A `{{token}}` names a variable that is not declared. |\n| [`opf/variable-unknown-value`](#opfvariable-unknown-value) | References | warning | structure | A value was supplied for a variable that is not declared. |\n| [`opf/variable-unused`](#opfvariable-unused) | References | warning | structure | A variable is declared but never used. |\n| [`opf/variable-reference-unknown`](#opfvariable-reference-unknown) | References | warning | structure | A `var:` colour names a variable that is not declared. |\n| [`opf/contract`](#opfcontract) | Policy | error | structure | A field is outside the values a host policy allows. |\n| [`opf/text-contrast`](#opftext-contrast) | Accessibility | warning | composition | Text colour has too little contrast against the background it sits on. |\n| [`opf/text-on-image`](#opftext-on-image) | Accessibility | info | composition | Text sits on a background picture whose pixels cannot be measured. |\n| [`opf/missing-alt-text`](#opfmissing-alt-text) | Accessibility | warning | structure | A picture has no alt text and is not marked decorative. |\n| [`opf/poor-alt-text`](#opfpoor-alt-text) | Accessibility | info | structure | Alt text is a file name, a URL, a generic word or very long. |\n| [`opf/missing-slide-title`](#opfmissing-slide-title) | Accessibility | warning | structure | A slide has no title. |\n| [`opf/duplicate-slide-title`](#opfduplicate-slide-title) | Accessibility | info | structure | Two slides have the same title. |\n| [`opf/reading-order`](#opfreading-order) | Accessibility | warning | composition | The order content is read differs from the order it appears on the slide. |\n| [`opf/link-text`](#opflink-text) | Accessibility | warning | structure | Link text does not say where the link goes. |\n| [`opf/chart-color-only`](#opfchart-color-only) | Accessibility | info | structure | Chart series may be indistinguishable without colour vision. |\n| [`opf/chart-colors-adjusted`](#opfchart-colors-adjusted) | Accessibility | warning | structure | A chart colour cannot be used as authored. |\n| [`opf/chart-text-alternative`](#opfchart-text-alternative) | Accessibility | info | structure | A chart has no text alternative, or is marked decorative. |\n| [`opf/missing-language`](#opfmissing-language) | Accessibility | info | structure | The presentation does not declare its language. |\n| [`opf/chart-option-adapted`](#opfchart-option-adapted) | Layout | warning | structure | A chart option cannot be shown by the chart type. |\n| [`opf/text-overflow`](#opftext-overflow) | Layout | warning | composition | Text or a table does not fit its space at the smallest allowed size. |\n| [`opf/unresolved-content`](#opfunresolved-content) | Layout | warning | composition | Content cannot be drawn as authored. |\n| [`opf/layout-failed`](#opflayout-failed) | Layout | warning | composition | The slide layout could not be computed. |\n| [`opf/min-font-size`](#opfmin-font-size) | Layout | warning | composition | Text is drawn smaller than the readable minimum. |\n| [`opf/region-unknown`](#opfregion-unknown) | Layout | warning | composition | A block is pinned to a region the layout does not have. |\n| [`opf/region-kind`](#opfregion-kind) | Layout | warning | composition | A block is pinned to a region that does not accept its kind. |\n| [`opf/region-full`](#opfregion-full) | Layout | warning | composition | A block is pinned to a region that is already full. |\n| [`opf/layout-unplaced`](#opflayout-unplaced) | Layout | warning | composition | No region of the layout has room for a block. |\n| [`opf/furniture-wraps`](#opffurniture-wraps) | Layout | warning | composition | A header or footer zone wraps onto more than 2 lines. |\n| [`opf/font-outside-scheme`](#opffont-outside-scheme) | Layout | warning | structure | Text uses a font family that is not in the deck's font scheme. |\n| [`opf/image-resolution`](#opfimage-resolution) | Layout | warning | composition | An image has too few pixels for the size it is shown at. |\n| [`opf/chart-value-not-numeric`](#opfchart-value-not-numeric) | Content | warning | structure | A chart value is text, not a number. |\n| [`opf/chart-mapping-adapted`](#opfchart-mapping-adapted) | Content | warning | structure | A chart mapping entry is ignored. |\n| [`opf/chart-highlight-adapted`](#opfchart-highlight-adapted) | Content | warning | structure | A chart highlight is adapted by the chart type. |\n| [`opf/code-highlight-out-of-range`](#opfcode-highlight-out-of-range) | Content | warning | structure | A code highlight marks a line the code does not have. |\n| [`opf/code-highlight-range-reversed`](#opfcode-highlight-range-reversed) | Content | warning | structure | A code highlight range ends before it starts. |\n| [`opf/variable-builtin-missing`](#opfvariable-builtin-missing) | Content | warning | structure | A built-in variable has no value in this document. |\n| [`opf/duration-outside-narrative`](#opfduration-outside-narrative) | Content | warning | structure | The target duration is outside the range the narrative suits. |\n| [`opf/narrative-duration-range`](#opfnarrative-duration-range) | Content | warning | structure | An inline narrative duration range has min greater than max. |\n| [`opf/placeholder-text`](#opfplaceholder-text) | Content | warning | structure | Placeholder text was left in the deck. |\n| [`opf/empty-text`](#opfempty-text) | Content | info | structure | A text field is present but empty. |\n| [`opf/unused-beat`](#opfunused-beat) | Content | info | structure | A beat of the narrative has no slide that references it. |\n| [`opf/empty-slide`](#opfempty-slide) | Content | warning | structure | A slide has no content at all. |\n| [`opf/language-tag`](#opflanguage-tag) | Content | warning | structure | A language tag is malformed, uses an unassigned region or is not in canonical case. |\n\n## Format rules\n\n*Is it well-formed OPF?*\n\n### `opf/json-syntax`\n\nDefault severity: **error**. Cost: syntax. The text is not strict JSON.\n\n**Why.** Every OPF tool reads JSON. A misplaced comma, quote, comment or trailing comma makes the file unreadable, and the range says where.\n\n**Basis.** RFC 8259 (strict JSON: no comments, no trailing commas)\n\n**Approximations.** Only text input is checked. A string is always read as JSON. One finding per root cause, in position order and at most five (a key with no value, a missing comma, a bracket never closed; not the damage each one causes further on): `message` says what is wrong, `help` what to do next and `location` is the range to underline. A text that looks like YAML or Markdown gets one finding with a hint to use fromYaml or fromMarkdown first. Source editors get the same causes with their quick fixes from analyzeJsonSyntax (@openpresentation/opf/source).\n\n### `opf/duplicate-key`\n\nDefault severity: **error**. Cost: syntax. A JSON object repeats a key.\n\n**Why.** JSON.parse keeps only the last value of a repeated key, so the earlier one disappears without a trace. The check compares escaped and literal spellings of the same key.\n\n**Basis.** RFC 8259 section 4 (names within an object should be unique)\n\n### `opf/schema`\n\nDefault severity: **error**. Cost: structure. The document breaks the OPF schema or a structural rule the schema cannot express.\n\n**Why.** The schema is the contract every engine, exporter and editor relies on. A field of the wrong type, a missing required field, an unknown property or content fields that cannot be mixed make the document ambiguous or undrawable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** All alternatives of a union are reported, with the failing schema location in `validation`. A required variable with no value is reported by `opf/variable-unfilled`, in the same category.\n\n### `opf/reference-id-duplicate`\n\nDefault severity: **error**. Cost: structure. Two references share an id.\n\n**Why.** A citation names a reference by id; a repeated id makes it ambiguous which entry is cited.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unknown-reference`\n\nDefault severity: **error**. Cost: structure. A citation names a reference that does not exist.\n\n**Why.** A cited id with no entry in `references` has nothing to show in the footnote area.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unsupported-location`\n\nDefault severity: **error**. Cost: structure. A citation or footnote sits where no engine draws it.\n\n**Why.** `cite` and `footnote` apply to runs in text, bullets and list items; table cells, captions, and reference and footnote texts have no marker.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/caption-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. A caption is on a payload that cannot carry one.\n\n**Why.** A caption belongs to exactly one image, chart, table or video; a group, text or code block has no place to draw it.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-option-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. An image option is on a payload that is not an image.\n\n**Why.** `fit`, `focus`, the image treatments (`shape`, `cornerRadius`, `border`, `opacity`, `recolor`, `overlay`, `aspectRatio`) and `placement` describe how a picture is drawn; a group, text or chart block has no picture for them to apply to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-placement-invalid`\n\nDefault severity: **error**. Cost: structure. An image placement cannot be honoured.\n\n**Why.** A placed image bleeds to a slide edge and the rest of the slide composes beside it. Only a top-level block (slides.N.blocks.I) can do that, and a slide edge holds one placed image; a block in a group or a promoted region, or a second block on the same edge, has nowhere to go.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/layout-placeholder-group`\n\nDefault severity: **error**. Cost: structure. A layout record's placeholder group breaks a nesting rule.\n\n**Why.** A placeholder group arranges body regions inside one cell of its layout record. Groups nest at most three levels inside a record, and a group holds body regions and groups only: the headings (title, subtitle, tag) stay at the record's top level, where the engine places them. Composition cannot use a record that breaks either rule.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-unknown`\n\nDefault severity: **error**. Cost: structure. A chart or table names a dataset that does not exist.\n\n**Why.** The data cannot be drawn without the dataset it refers to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-field-unknown`\n\nDefault severity: **error**. Cost: structure. A table asks a dataset for a column it does not have.\n\n**Why.** The column cannot be shown when the dataset has no column of that name.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/data-column-duplicate`\n\nDefault severity: **error**. Cost: structure. A dataset or table has two columns with the same name.\n\n**Why.** Series, mappings and fields address columns by name, so a repeated name makes them ambiguous.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-unknown-column`\n\nDefault severity: **error**. Cost: structure. A chart mapping names a column the data does not have.\n\n**Why.** The series cannot be built from a column that is not there.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/number-format-invalid`\n\nDefault severity: **error**. Cost: structure. A number format cannot be read.\n\n**Why.** A format code that no engine can parse is drawn as the plain number or fails to export.\n\n**Basis.** Excel number format codes, as far as OPF supports them\n\n### `opf/variable-invalid-value`\n\nDefault severity: **error**. Cost: structure. A variable value does not match its declared type.\n\n**Why.** A number variable given text, or a date given a non-date, cannot be filled into the deck.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-builtin`\n\nDefault severity: **error**. Cost: structure. A token names a built-in variable that does not exist.\n\n**Why.** The `deck.`, `speaker.`, `organization.` and `slide.` names (and `speakers`) are reserved for built-in variables; a name under them that is not a built-in can never be filled, so the token would show literally. The slide-scoped `slide.number`, `slide.section` and `deck.slideCount` are inline tokens only, so `var:slide.*` and `var:deck.slideCount` are reported too; organization logos are whole-field references only (`var:organization.logo.icon`), so a `{{organization.logo}}` token, an unknown logo shape or an unknown organization id is reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-unknown-name`\n\nDefault severity: **error**. Cost: structure. A chart highlight names a series or category the chart does not have.\n\n**Why.** `chart.highlight` emphasises marks by name; a name that matches no series or category in the chart data highlights nothing, so the chart is not drawn as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-format`\n\nDefault severity: **error**. Cost: structure. A variable cannot be formatted.\n\n**Why.** A number or date format that does not apply to the value leaves the token unreadable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unfilled`\n\nDefault severity: **error**. Cost: structure. A required template variable has no value.\n\n**Why.** A variable a deck declares as required, with no value, is template scaffolding: `{{name}}` tokens show up literally and colour variables fall back to a default. A normal deck must be filled, so this is an error and the deck is not valid; a template (`template: true`) is incomplete on purpose, so it is a warning.\n\n**Approximations.** Required variables are those the variable machinery reports as unfilled for the document (declared, no value, not `required: false`, not given by the `values` option). For a document that declares no content variables, `{{id}}` / `{{id|format}}` tokens (`{{` escapes) left in any string outside `variables`, `extensions`, `catalogs` and `assets` are reported as warnings. `var:` colour references with no declaration are reported as `opf/variable-reference-unknown`. Fill a template with `opf fill` or `resolveVariables` to clear the findings.\n\n### `opf/run-color-unrecognized`\n\nDefault severity: **warning**. Cost: structure. A text run colour is none of the documented forms.\n\n**Why.** Run colours are open strings so imported decks stay valid, but a value that is not a hex colour, a colour-scheme name or a `var:` reference falls back to the theme colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/numbering-start-ignored`\n\nDefault severity: **warning**. Cost: structure. A list entry sets `start` where nothing is numbered.\n\n**Why.** A start value only restarts an auto-number; without `numbering` on the payload it has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/slide-size-out-of-range`\n\nDefault severity: **warning**. Cost: structure. The slide size is outside PowerPoint's 1 to 56 inch range.\n\n**Why.** The schema accepts any positive custom `widthInches` and `heightInches`, but a PPTX stores the size as `p:sldSz` with each side between 914400 and 51206400 EMU, which is 1 to 56 inches. A custom size outside that range exports a file that does not conform to the standard, which PowerPoint may reject or clamp. It is a warning because the document is still correct OPF and the preview draws it.\n\n**Basis.** ECMA-376 / ISO/IEC 29500-1 section 19.2.1.39 (p:sldSz) and section 19.7.17 (ST_SlideSizeCoordinate): 914400 to 51206400 EMU\n\n**Approximations.** Checks the deck's resolved size: `design.dimensions`, else the resolved theme's `dimensions`. Inches are converted to EMU as the PPTX exporter does (`Math.round(inches * 914400)`), so a side of exactly 1 in or 56 in passes. A slide-level theme with another size is `opf/slide-theme-dimensions`. Presets are always in range. PowerPoint's own behaviour at the limits was not measured.\n\n### `opf/undeclared-catalog`\n\nDefault severity: **error**. Cost: structure. A reference names a catalog group the document does not declare.\n\n**Why.** The prefix of a `name:id` reference names the group of `catalogs` it resolves in. A prefix with no group can never resolve, whatever the host registers, so it is a format error: engines reject the document at their format check instead of drawing a fallback.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## References rules\n\n*Does everything it points at resolve?*\n\n### `opf/asset-reference`\n\nDefault severity: **error**. Cost: structure. An `asset:` reference names an asset that is not in the registry.\n\n**Why.** An image, logo or video that points at a missing asset cannot be drawn or exported. The check reads the document's own registry only; no file or URL is fetched.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/asset-cycle`\n\nDefault severity: **error**. Cost: structure. Asset references form a cycle.\n\n**Why.** An asset whose source points back at itself never resolves to a file.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/catalog-record`\n\nDefault severity: **error**. Cost: structure. An embedded or registered catalog record is invalid.\n\n**Why.** A record that fails its companion schema cannot be resolved. Invalid records are left out of the lookup, so the references to them are reported too.\n\n**Basis.** spec/schemas/<kind>.schema.json\n\n### `opf/layout-template`\n\nDefault severity: **error**. Cost: structure. A layout template's region list is malformed.\n\n**Why.** A layout is one ordered list of regions, nested through splits for two dimensions. Engines cannot compose a list that is not 1 to 12 entries, an entry that is neither a region nor a split, a direction or size that is not valid, an \"auto\" size anywhere but in a column, splits nested more than three levels deep (the record's own list is level 1), or regions that compile to a grid larger than 12 x 12. composeSlide refuses such a record, so a slide that names it composes automatically.\n\n**Basis.** spec/schemas/layout.schema.json and the region list rules (docs/layout-templates.md)\n\n**Approximations.** Checked on every layout record the document embeds and every registered record it uses, once the record passes its schema.\n\n### `opf/layout-region`\n\nDefault severity: **error**. Cost: structure. A layout template's regions are not valid.\n\n**Why.** Region names are unique across the whole record and may not be tag, auto or a promoted-region word (left, center, right, top, middle, bottom), so a Markdown block's region= stays unambiguous; title and subtitle name the heading regions, which take only a size; a subtitle region needs a title region; a region accepts at least one content kind; a region with flow 'none' holds one block; overflowRegion must name a body region. composeSlide refuses such a record.\n\n**Basis.** spec/schemas/layout.schema.json and the region rules (docs/layout-templates.md)\n\n### `opf/unresolved-reference`\n\nDefault severity: **warning**. Cost: structure. A content reference resolves nowhere.\n\n**Why.** A layout, theme, colour scheme, font scheme, narrative, audience, purpose or tone reference that neither the document embeds nor a registered catalog defines falls back: a slide composes automatically, a design uses the engine default. A strict export fails instead. A slide with no layout is automatic composition and is never reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Resolved like every engine resolves it: catalogs.custom, then the records embedded under catalogs.default, then the catalog registered for its source (the first registered catalog when `default` is omitted); `name:id` in catalogs.<name>, then the catalog registered for its source. Nothing is fetched. Free-form audience and purpose text is not a reference.\n\n### `opf/catalog-record-not-in-source`\n\nDefault severity: **warning**. Cost: structure. A record embedded under a catalog group is not in that catalog.\n\n**Why.** A record under catalogs.default or a named group says it came from the catalog its source names. When the catalog the host registered for that source has no record of that kind and id, the record is the document's own and belongs in catalogs.custom, where an update from the catalog never looks for it. Rendering is unchanged: the embedded record still wins.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Checked only against a catalog the host registered for the group's source (the first registered catalog for an omitted default.source); with none registered for it the check is silent. A record the catalog has with different content is an update difference (updateFromCatalog), not this finding. catalogs.custom is never checked.\n\n### `opf/unused-reference`\n\nDefault severity: **warning**. Cost: structure. A reference is never cited.\n\n**Why.** A listed reference that no run cites is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unused-dataset`\n\nDefault severity: **warning**. Cost: structure. A dataset is never used.\n\n**Why.** A dataset no chart or table references is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unknown-beat`\n\nDefault severity: **warning**. Cost: structure. A slide names a beat its narrative does not define.\n\n**Why.** `slides[].beat` links a slide to a step of the narrative plan. A beat id the narrative does not have links to nothing; nothing is drawn from a beat.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** The narrative resolves like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked.\n\n### `opf/slide-theme-dimensions`\n\nDefault severity: **warning**. Cost: structure. A slide theme sets a slide size that differs from the deck.\n\n**Why.** A PPTX has one slide size. The size a slide-level theme carries is never used: design.dimensions of the deck (or of its theme) wins in every engine, and without them the exporter stops with mixed slide sizes.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown`\n\nDefault severity: **warning**. Cost: structure. A `{{token}}` names a variable that is not declared.\n\n**Why.** An undeclared token is shown literally.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-value`\n\nDefault severity: **warning**. Cost: structure. A value was supplied for a variable that is not declared.\n\n**Why.** The value is never used.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unused`\n\nDefault severity: **warning**. Cost: structure. A variable is declared but never used.\n\n**Why.** A declaration nothing refers to has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-reference-unknown`\n\nDefault severity: **warning**. Cost: structure. A `var:` colour names a variable that is not declared.\n\n**Why.** An undeclared colour variable falls back to a default colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## Policy rules\n\n*Does it follow the host's house rules?*\n\n### `opf/contract`\n\nDefault severity: **error**. Cost: structure. A field is outside the values a host policy allows.\n\n**Why.** A host that needs brand layouts, fonts or other fixed choices states them as contracts. The default severity is error; a contract can set its own. Document metadata never installs policy.\n\n**Approximations.** Only the `contracts` option supplies policy, and it checks existing fields: a contract does not require an omitted field or insert a default.\n\n\n## Accessibility rules\n\n*Can everyone read it?*\n\n### `opf/text-contrast`\n\nDefault severity: **warning**. Cost: composition. Text colour has too little contrast against the background it sits on.\n\n**Why.** Low-contrast text is hard or impossible to read for people with low vision, colour-vision differences, glare on a projector or a poor display. WCAG sets 4.5:1 for normal text and 3:1 for large text.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Computed on sRGB colours with the WCAG relative-luminance formula, against the background the preview draws: a solid or theme colour, the card surface, a table cell fill, every colour a gradient takes under the text box (sampled on a 5x5 grid, angle respected), or both colours of a pattern. Anti-aliasing, text shadows and font weight are not modelled. Text colour is the preview's (the scheme's text role or dark1 on a light background and light1 on a dark one, chosen from the background luminance, where a gradient or picture background uses the scheme's default background colour), so a default can fail on a dark gradient. A link with no colour of its own is measured in the scheme's hyperlink colour.\n\n### `opf/text-on-image`\n\nDefault severity: **info**. Cost: composition. Text sits on a background picture whose pixels cannot be measured.\n\n**Why.** Contrast over a photograph depends on the photograph. Without a full-frame overlay that guarantees readability for every possible image, the result cannot be certified from the document.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Core never reads picture pixels. The picture is bounded by a grey ramp from black to white, composited through the image opacity and a full-frame design.background overlay; the text passes only when every step of that ramp passes. An edge-banded overlay is not counted.\n\n### `opf/missing-alt-text`\n\nDefault severity: **warning**. Cost: structure. A picture has no alt text and is not marked decorative.\n\n**Why.** People using a screen reader get nothing for a picture without alternative text. A picture that is purely decorative should say so with an empty alt (alt: \"\"), which is an explicit, reviewed choice instead of an omission.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Checks the alt field of images (image blocks, Slide.image and region images, placed or not), video, organization logos (each shape and background), header/footer images (a logo reference such as var:organization.logo.icon is checked on the organization), quote photos and speaker photos, following asset: references to the assets registry. Whether the text describes the picture well is not judged here (see opf/poor-alt-text). Charts carry `chart.alt` and are checked by opf/chart-text-alternative. Picture backgrounds are decorative unless they carry their own alt, and watermarks are decorative, so neither is checked.\n\n### `opf/poor-alt-text`\n\nDefault severity: **info**. Cost: structure. Alt text is a file name, a URL, a generic word or very long.\n\n**Why.** Alt text such as \"image\", \"IMG_2041.png\" or a 400-character paragraph does not do the job of describing a picture: it is read out and adds noise without information.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Pattern checks only: file extensions and camera-style names, a bare generic word, a URL, a leading \"image of\", and more than 250 characters. Chart alt text (chart.alt) is checked too: a bare chart word, a URL, a leading \"chart of\" or more than 250 characters. It cannot tell whether a plausible sentence is accurate.\n\n### `opf/missing-slide-title`\n\nDefault severity: **warning**. Cost: structure. A slide has no title.\n\n**Why.** Slide titles are how people using a screen reader, an outline view or keyboard navigation find and tell slides apart; PowerPoint's own accessibility checker reports a missing title too.\n\n**Basis.** WCAG 2.2 SC 2.4.2 Page Titled and SC 2.4.6 Headings and Labels, level AA (PowerPoint: \"Missing slide title\")\n\n**Approximations.** Only the slide-level title field counts. Text that merely looks like a heading inside a block does not.\n\n### `opf/duplicate-slide-title`\n\nDefault severity: **info**. Cost: structure. Two slides have the same title.\n\n**Why.** Identical titles make slides indistinguishable in an outline or a screen reader's slide list.\n\n**Approximations.** Titles are compared case-insensitively with whitespace collapsed. Slides that continue one another are not exempt; give a continuation a distinct title such as \"(continued)\".\n\n### `opf/reading-order`\n\nDefault severity: **warning**. Cost: composition. The order content is read differs from the order it appears on the slide.\n\n**Why.** Screen readers, keyboard focus and PowerPoint's selection pane follow the composed order. When it differs from the visual order (top to bottom, then start to end of the reading direction), the slide is read out of sequence.\n\n**Basis.** WCAG 2.2 SC 1.3.2 Meaningful Sequence, level A (PowerPoint: \"Check reading order\")\n\n**Approximations.** Compares the composed content order with a visual order recomputed from the composed boxes: items whose vertical centres fall in the same row are ordered along the reading direction (a right-to-left deck is checked by rows only), rows from top to bottom. Headings are expected first. Free-form overlap is not analysed. On a slide with a template layout (OPF 0.19) the expected order is the binding order instead (the regions in reading order, then the blocks of each region), and items outside a region (placed images, the row below the grid) are not compared.\n\n### `opf/link-text`\n\nDefault severity: **warning**. Cost: structure. Link text does not say where the link goes.\n\n**Why.** People who scan links out of context (a screen reader's links list, a link tab order) hear only the text. \"click here\", \"read more\" or a long raw URL tells them nothing about the destination.\n\n**Basis.** WCAG 2.2 SC 2.4.4 Link Purpose (In Context), level A; PowerPoint: \"Hyperlink text is not meaningful\"\n\n**Approximations.** Matches a short list of generic phrases in English (after lower-casing and removing punctuation), blank link text, and raw URLs longer than 40 characters. Adjacent runs sharing one link are read as one link.\n\n### `opf/chart-color-only`\n\nDefault severity: **info**. Cost: structure. Chart series may be indistinguishable without colour vision.\n\n**Why.** OPF charts have no data labels or patterns, so series are told apart by colour alone (and legend order). Colours that look alike to someone with colour-vision deficiency, or when printed in greyscale, make series impossible to tell apart.\n\n**Basis.** WCAG 2.2 SC 1.4.1 Use of Color, level A\n\n**Thresholds.** `minSeriesColorDifference` (default 10)\n\n**Approximations.** Uses the engine's series palette (ValidateOptions.chartPalette; default chartSeriesPalette of each slide colour scheme (accent1 to accent6, then their tints), adjusted for the card surface like the preview does) in series order: series i takes colour i, pie/doughnut/treemap/funnel slices take colours per category. Pairs are compared by CIE76 distance after simulating protanopia, deuteranopia, tritanopia (Machado 2009, severity 1) and greyscale. Single-series charts and chart types without a series legend are skipped.\n\n### `opf/chart-colors-adjusted`\n\nDefault severity: **warning**. Cost: structure. A chart colour cannot be used as authored.\n\n**Why.** `chart.colors` sets the colour of series 1, 2, 3 and so on. A colour that cannot be resolved (a `var:` colour variable the deck does not define, or a name that is not a colour) is ignored and the deck palette colour of that position is used. A colour with under 3:1 contrast against the chart panel is kept for its hue but drawn lighter or darker, so the chart does not show the colour that was written.\n\n**Basis.** WCAG 2.2 SC 1.4.11 Non-text Contrast, level AA\n\n**Approximations.** Resolves each entry like any colour reference (hex, colour-scheme slot or role, `var:` variable) against the slide's scheme and compares it with the chart panel (the slide surface colour) by WCAG contrast; 3:1 is the threshold the engines enforce. Translucent colours and unresolved panels are not measured. Colours that look alike to colour-vision deficiency are reported by opf/chart-color-only, which judges the authored colours.\n\n### `opf/chart-text-alternative`\n\nDefault severity: **info**. Cost: structure. A chart has no text alternative, or is marked decorative.\n\n**Why.** A chart conveys a message; people who cannot see it need the message and ideally the numbers in text. The chart's alt field is that text alternative (the preview exposes it as the chart's accessible name and the PowerPoint export writes it as the frame's alternative text); a sentence or table beside the chart also serves. An empty alt marks a chart decorative, which is reported as info so the choice is reviewed: a chart rarely carries no message.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** A chart passes when chart.alt has text. Without alt, it passes when the slide has any other text, list, table, quote or metric content besides title and tag, or a subtitle. It does not judge whether alt or that text states the chart's point (see opf/poor-alt-text for generic alt text). alt: \"\" is reported as a decorative chart, whatever else is on the slide.\n\n### `opf/missing-language`\n\nDefault severity: **info**. Cost: structure. The presentation does not declare its language.\n\n**Why.** Screen readers and text-to-speech choose pronunciation and hyphenation from the declared language; spell checkers and translation tools use it too.\n\n**Basis.** WCAG 2.2 SC 3.1.1 Language of Page, level A\n\n**Approximations.** Only the presentation-level `language` is checked, not the language of individual runs (`TextRun.lang`; opf/language-tag checks how those tags are written).\n\n\n## Layout rules\n\n*Will it present as authored?*\n\n### `opf/chart-option-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart option cannot be shown by the chart type.\n\n**Why.** An axis title, legend or data label option the chart type does not support is adapted by every engine, so the chart is not drawn exactly as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/text-overflow`\n\nDefault severity: **warning**. Cost: composition. Text or a table does not fit its space at the smallest allowed size.\n\n**Why.** Core composition shrinks text to the readable minimum and then reports what still does not fit. Overflowing text is clipped or runs over other content in the preview and in PowerPoint.\n\n**Approximations.** Uses composeSlide at the deck's slide size with the fonts of the resolved font scheme. Without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimate, which can differ from the real font by a few percent; pass the renderer's measurement for font-exact results. A composition that sets overflow: \"error\" is reported as a warning here, so that `valid` never depends on font metrics; rendering and export still refuse such a slide when it does not fit.\n\n### `opf/unresolved-content`\n\nDefault severity: **warning**. Cost: composition. Content cannot be drawn as authored.\n\n**Why.** composeSlide reports content it cannot place or an effect it does not support (for example a picture bullet without a logo, a date field without a date, or an unsupported image treatment). The preview and the export fall back.\n\n### `opf/layout-failed`\n\nDefault severity: **warning**. Cost: composition. The slide layout could not be computed.\n\n**Why.** Composition threw for a slide that passed schema validation, so geometry-based rules (contrast, overflow, reading order, resolution) were skipped for it.\n\n### `opf/min-font-size`\n\nDefault severity: **warning**. Cost: composition. Text is drawn smaller than the readable minimum.\n\n**Why.** Small type is unreadable from the back of a room and on a phone. The engine's own default floor is 12 pt (16 px); anything much below that was lowered on purpose or by a composition that could not fit the text.\n\n**Basis.** Common presentation guidance (12 pt minimum for body text; 18 pt or more is easier to read from a distance).\n\n**Thresholds.** `minFontSizePt` (default 11)\n\n**Approximations.** Sizes are those composeSlide fitted, expressed on the 13.33 x 7.5 in reference slide (96 px per inch, so 1 px is 0.75 pt; a smaller canvas scales type down with it) and explicit run fontSize values in points. Per payload, the smallest part (a metric label or a quote attribution) is reported. Table cell text (default 11.25 pt), code and header/footer furniture are not checked.\n\n### `opf/region-unknown`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region the layout does not have.\n\n**Why.** A region pin (`region` on a root block) names a region of the slide's layout. A name the layout does not have is ignored: the block is placed by its kind, which may not be where the author meant.\n\n### `opf/region-kind`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that does not accept its kind.\n\n**Why.** A region says which content kinds it takes. A pin to a region that does not take the block's kind is ignored and the block is placed by its kind.\n\n### `opf/region-full`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that is already full.\n\n**Why.** A region holds at most `max` blocks on one slide. A pin to a full region is ignored and the block is placed by its kind.\n\n### `opf/layout-unplaced`\n\nDefault severity: **warning**. Cost: composition. No region of the layout has room for a block.\n\n**Why.** Content is never dropped: a block no region has room for is drawn in the layout's overflow region beyond its max (or below the grid), where it crowds the others. Paginate moves it to a continuation slide with the same layout.\n\n### `opf/furniture-wraps`\n\nDefault severity: **warning**. Cost: composition. A header or footer zone wraps onto more than 2 lines.\n\n**Why.** Header and footer text repeats on every slide in a band at the slide edge. A zone that wraps onto three or more lines grows into the body, and long source lines read poorly at footer size. Footers carry short repeated text (a title, a date, a page number); sources belong in a caption or the speaker notes.\n\n**Approximations.** Uses the lines core composed for the zone at the deck's slide size; without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimates. A deck-level header or footer is reported once.\n\n### `opf/font-outside-scheme`\n\nDefault severity: **warning**. Cost: structure. Text uses a font family that is not in the deck's font scheme.\n\n**Why.** The font scheme is the deck's font choice. A run that names another family will not follow a font-scheme change, may be missing on the viewer's machine, and breaks the deck's typographic consistency.\n\n**Approximations.** Compares the run's fontFamily (case-insensitively) with the heading, body, code and accent families of the slide's resolved font scheme and the scheme's major/minor fonts. Families that the host substitutes are still different names here.\n\n### `opf/image-resolution`\n\nDefault severity: **warning**. Cost: composition. An image has too few pixels for the size it is shown at.\n\n**Why.** An image stretched beyond its pixel size looks blurry or blocky on a projector or a high-density screen.\n\n**Thresholds.** `minImagePpi` (default 96)\n\n**Approximations.** Only embedded data: images (and asset: references to them) have readable pixel sizes; URLs and files are never fetched, so they are not checked. The displayed size is the composed frame (cover and stretch are measured as the cover scale, contain as the contain scale). Effective ppi is the image pixels per inch of the 96 px/inch reference slide. SVG is vector and exempt.\n\n\n## Content rules\n\n*Is anything left unfinished?*\n\n### `opf/chart-value-not-numeric`\n\nDefault severity: **warning**. Cost: structure. A chart value is text, not a number.\n\n**Why.** A chart plots numbers. A text cell such as \"12%\" is plotted as a gap. When every text value of a column is written in one display style the finding carries the fix that stores the numbers and gives the column the matching format.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart mapping entry is ignored.\n\n**Why.** A mapping that names a column the chart type cannot use is left out of the chart.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart highlight is adapted by the chart type.\n\n**Why.** A highlight the chart type cannot show as authored (a category highlight on a chart without categories, for example) is adapted by every engine, so the emphasis is not exactly what was written.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-out-of-range`\n\nDefault severity: **warning**. Cost: structure. A code highlight marks a line the code does not have.\n\n**Why.** Marked lines count from 1 by line break in code.source; an entry past the last line marks nothing.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-range-reversed`\n\nDefault severity: **warning**. Cost: structure. A code highlight range ends before it starts.\n\n**Why.** A range written end before start marks nothing; write the smaller line number first.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-builtin-missing`\n\nDefault severity: **warning**. Cost: structure. A built-in variable has no value in this document.\n\n**Why.** A `{{deck.*}}`, `{{speaker.*}}` or `{{organization.*}}` token whose field the document does not set resolves to nothing, so the text shows a gap; a `var:organization.logo` reference to an organization without a logo draws no image. `{{slide.section}}` is checked per slide: a slide without a `section` that uses it, in its own text or in the header or footer it inherits, is reported at that slide.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/duration-outside-narrative`\n\nDefault severity: **warning**. Cost: structure. The target duration is outside the range the narrative suits.\n\n**Why.** A narrative records the talk lengths it suits. A deck whose duration is outside that range probably needs a different narrative or a different length; nothing is drawn or exported differently.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/narrative-duration-range`\n\nDefault severity: **warning**. Cost: structure. An inline narrative duration range has min greater than max.\n\n**Why.** A range with its bounds swapped matches no duration, so the duration check cannot help.\n\n**Basis.** spec/schemas/narrative.schema.json\n\n### `opf/placeholder-text`\n\nDefault severity: **warning**. Cost: structure. Placeholder text was left in the deck.\n\n**Why.** \"Lorem ipsum\", \"Click to add title\", \"TBD\" and bracketed prompts are scaffolding. Shipping them reads as unfinished work.\n\n**Approximations.** Case-insensitive patterns for lorem ipsum, template prompts (\"Click to add...\", \"Your title here\", \"[Insert ...]\", a bare \"Title\" or \"Text\"), and the markers TBD, TBC, TODO, FIXME and XXX in capitals. A deliberate use of the words is flagged too; disable the rule for that slide with ignorePaths.\n\n### `opf/empty-text`\n\nDefault severity: **info**. Cost: structure. A text field is present but empty.\n\n**Why.** An empty title, text block or list item draws nothing, and leaves a hole in the outline and for assistive technology.\n\n### `opf/unused-beat`\n\nDefault severity: **info**. Cost: structure. A beat of the narrative has no slide that references it.\n\n**Why.** The narrative is the plan and the slides are the product. When some slides name a beat (slides[].beat) and a beat of the plan has none, the deck skips a step of the story or the plan is out of date.\n\n**Approximations.** Resolved like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked. A deck in which no slide references any beat is not checked either, because it has not linked its slides to the plan. A slide that lists several beats covers each of them.\n\n### `opf/empty-slide`\n\nDefault severity: **warning**. Cost: structure. A slide has no content at all.\n\n**Why.** A slide with no title, no content and no picture is almost always an accident of editing. (A deliberately blank slide can use the blank layout.)\n\n### `opf/language-tag`\n\nDefault severity: **warning**. Cost: structure. A language tag is malformed, uses an unassigned region or is not in canonical case.\n\n**Why.** Engines read the deck language and each run's `lang` as BCP 47 tags to choose script fonts, text direction, proofing language and the PowerPoint `lang` attribute. A tag such as `en-a`, or a region that does not exist (`en-UK`; the United Kingdom is `GB`), is guessed at or dropped, and a wrong-case tag (`EN-us`) is not what a locale lookup expects.\n\n**Basis.** BCP 47 (RFC 5646, section 2.1) and the platform locale data (Intl.getCanonicalLocales, Intl.DisplayNames)\n\n**Approximations.** Checks the root `language` (a tag string, or the `bcp47` of an inline Language object) and `lang` on the TextRun objects of titles, subtitles, tags, text, lists, tables, quotes, metrics and timelines. A region is assigned when the runtime's CLDR data names it (ISO 3166-1 codes and UN M.49 areas such as 419); private-use regions (`AA`, `QM`-`QZ`, `XA`-`XZ`) and `ZZ` are accepted, as are private-use (`x-...`) tags. Only the spelling of a tag is checked, not whether the language subtag is assigned or whether the language suits the text. The schema rejects tags with characters other than letters, digits and hyphens (`en_US` is a schema error, with no suggestion), and `en-UK` as the deck language is already an error of the schema rule, so those are not reported twice.\n\n<!-- validate-rules:end -->\n"
|
|
356
356
|
},
|
|
357
357
|
{
|
|
358
358
|
"slug": "yaml",
|