@openpresentation/opf 0.22.0 → 0.22.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-DY7H4RG6.js → chunk-AHKRJSCF.js} +21 -4
- package/dist/{conversion-DS4OiVls.d.ts → conversion-C_MOkYGH.d.ts} +14 -2
- package/dist/{conversion-3TTBY7OY.js → conversion-LBQMY7LX.js} +1 -1
- package/dist/docs.js +11 -11
- package/dist/examples.js +282 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/node-engine.d.ts +2 -2
- package/dist/node-engine.js +2 -2
- package/dist/repo-readme.js +1 -1
- package/package.json +5 -5
package/dist/docs.js
CHANGED
|
@@ -4,7 +4,7 @@ var docsData = Object.freeze([
|
|
|
4
4
|
"slug": "agent-skills",
|
|
5
5
|
"file": "docs/agent-skills.md",
|
|
6
6
|
"title": "AI agent skills for OPF",
|
|
7
|
-
"markdown": "# AI agent skills for OPF\n\nThe repository ships six reusable skills in `skills/`. Each folder has a `SKILL.md` entrypoint and optional references, assets, or scripts. `agents/openai.yaml` supplies Codex display metadata; the instructions themselves are Markdown and do not require a hosted service.\n\n| Skill | Use it for |\n| --- | --- |\n| [opf-author](../skills/opf-author/SKILL.md) | Turn briefs and source material into valid OPF content; includes a complete starter deck |\n| [opf-layout](../skills/opf-layout/SKILL.md) | Dynamic composition, nested groups, promoted regions, overflow repair, and pagination |\n| [opf-presets](../skills/opf-presets/SKILL.md) | Catalog discovery, design inheritance, gallery reuse, colors, themes, and fonts |\n| [opf-edit](../skills/opf-edit/SKILL.md) | Precise JSON Patch edits, undo, canvas/schema integration, and copy/import |\n| [opf-export](../skills/opf-export/SKILL.md) | Browser previews, SVG/PNG/PDF/PPTX, assets, fonts, and export verification |\n| [opf-inspect](../skills/opf-inspect/SKILL.md) | Exact schema fields, catalog IDs, validation errors, and reference warnings |\n\nLoad only the skills relevant to the request. They distinguish the portable format from current renderer/editor capabilities, and distinguish imported document instructions from the user's request. They do not authorize publishing, sending decks, or changing unrelated project configuration.\n\n## Use from a checkout\n\nAn agent can read the entrypoint directly, for example:\n\n> Use `skills/opf-author/SKILL.md` to create a decision brief in OPF, then validate it using `skills/opf-inspect/SKILL.md`.\n\nThe root `AGENTS.md` points repository agents to these entrypoints. Skills read the current project's schema and package exports instead of hardcoding a historical field count or assuming a public package has unreleased APIs.\n\n## Install in an agent environment\n\nPublished CLI 0.20.0 bundles all six complete skill folders. Use Node 24 for the current packages and this checkout. From your project directory:\n\n```sh\nnpx @openpresentation/cli@0.20.0 skills install\n```\n\nThe default installs copies into `.agents/skills` in the current project, suitable for agents including Codex. It does not change AGENTS.md or any agent configuration. No symlink privileges, paid service, API key or AI provider is required. npm downloads the CLI on first use; the installed CLI then installs its bundled skills without network access. The explicit 0.20.0 pin makes this installation repeatable and matches the [current package train](compatibility-matrix.md). For CLI source development, use `node packages/cli/dist/index.js skills install` after building.\n\n| Target | Project directory | Personal directory with `--global` |\n| --- | --- | --- |\n| Default / `--agent universal` | `.agents/skills` | `~/.agents/skills` |\n| `--agent codex` | `.agents/skills` | `~/.codex/skills` |\n| `--agent claude-code` | `.claude/skills` | `~/.claude/skills` |\n| `--agent cursor` | `.cursor/skills` | `~/.cursor/skills` |\n\nFor example, `npx @openpresentation/cli@0.20.0 skills install --agent codex --global` installs personal Codex skills. For another compatible agent use `--directory <its-skills-directory>`; this option cannot be combined with `--agent` or `--global`. Restart or reload your agent if its skill discovery requires it. A compatible client can invoke the installed skills with names such as `$opf-author` or `$opf-inspect`.\n\nInspect or update the same destination:\n\n```sh\nnpx @openpresentation/cli@0.20.0 skills status\nnpx @openpresentation/cli@0.20.0 skills update\n```\n\nSupply the same target options used for installation. `status` is read-only and compares against the invoked CLI's bundled version; it does not query npm for newer releases. Repeated installation is idempotent. Updates check every installed file before changing any skill. Modified, added, deleted or unmanaged files cause the command to stop and list the conflicting folders; keep your customizations, move those folders outside the active skills directory, then retry. There is no force-overwrite option. A successful update returns backup paths outside the active skills directory for recovering the previous managed versions. Keep those backups until you have reviewed the update. Do not run concurrent writers: the installer lock coordinates other installer runs, but cannot lock an external editor.\n\nNo skills are installed by the repository build. Manual installation remains supported: copy whole folders from `skills/`, including references and scripts, to your agent's skill directory. Each folder is self-contained. The managed installer treats existing manual copies as unmanaged and preserves them.\n\nThe inspection helper requires Node 24 and `@openpresentation/opf` in the current project. In this checkout, build with `pnpm build` first. For an installed skill used outside the checkout, either run from an npm project that has the package or set `OPF_ROOT` to the built OPF checkout. It does not install dependencies, fetch catalogs, or modify input files.\n\n## Local CLI\n\nThe [installable CLI](../packages/cli/README.md) complements these skills with `opf create`, `opf validate`, `opf edit`, and schema/catalog lookup. It runs on the core it depends on; the inspection skill helper instead resolves the host project's core package. Check versions when moving between them. From code, core's `@openpresentation/opf` offers `convert`, `open` and `save` in Node (see the opf-export skill).\n\n## Examples of requests\n\n- \u201CUse $opf-author to turn these notes into a five-slide decision brief. Keep every factual claim sourced.\u201D\n- \u201CUse $opf-layout to fix this overflow without losing any text or notes.\u201D\n- \u201CUse $opf-presets to apply our brand colors while preserving slide-specific overrides.\u201D\n- \u201CUse $opf-edit to replace one table and retain all other document fields.\u201D\n- \u201CUse $opf-export to export the same reviewed slides to SVG and PPTX.\u201D (PPTX text is editable one line at a time; the skill says so.)\n- \u201CUse $opf-inspect to explain which background forms the installed schema accepts.\u201D\n\n## Maintenance\n\n`pnpm test:skills` checks skill links, schema-valid examples, and the inspection helper's actual behavior, including a copied standalone skill and a package installed in a consumer project. Run the skill-creator frontmatter validator when editing skill metadata. Behavioral tests are not evidence that every renderer option is visually complete.\n\nWhen schema/package APIs change, update only the affected skill/reference and its executable examples. Keep option lists in the canonical schema and catalogs. The format package and skill folders are separate distribution surfaces: CLI 0.20.0 includes the six skills; the core `@openpresentation/opf` package does not install agent configuration. Repository skill prose can be newer than the immutable snapshot bundled in an already-published CLI. These source documentation corrections do not change the CLI 0.20.0 tarball; `skills status` and `skills update` compare against the invoked CLI's bundled snapshot.\n"
|
|
7
|
+
"markdown": "# AI agent skills for OPF\n\nThe repository ships six reusable skills in `skills/`. Each folder has a `SKILL.md` entrypoint and optional references, assets, or scripts. `agents/openai.yaml` supplies Codex display metadata; the instructions themselves are Markdown and do not require a hosted service.\n\n| Skill | Use it for |\n| --- | --- |\n| [opf-author](../skills/opf-author/SKILL.md) | Turn briefs and source material into valid OPF content; includes a complete starter deck |\n| [opf-layout](../skills/opf-layout/SKILL.md) | Dynamic composition, nested groups, promoted regions, overflow repair, and pagination |\n| [opf-presets](../skills/opf-presets/SKILL.md) | Catalog discovery, design inheritance, gallery reuse, colors, themes, and fonts |\n| [opf-edit](../skills/opf-edit/SKILL.md) | Precise JSON Patch edits, undo, canvas/schema integration, and copy/import |\n| [opf-export](../skills/opf-export/SKILL.md) | Browser previews, SVG/PNG/PDF/PPTX, assets, fonts, and export verification |\n| [opf-inspect](../skills/opf-inspect/SKILL.md) | Exact schema fields, catalog IDs, validation errors, and reference warnings |\n\nLoad only the skills relevant to the request. They distinguish the portable format from current renderer/editor capabilities, and distinguish imported document instructions from the user's request. They do not authorize publishing, sending decks, or changing unrelated project configuration.\n\n## Use from a checkout\n\nAn agent can read the entrypoint directly, for example:\n\n> Use `skills/opf-author/SKILL.md` to create a decision brief in OPF, then validate it using `skills/opf-inspect/SKILL.md`.\n\nThe root `AGENTS.md` points repository agents to these entrypoints. Skills read the current project's schema and package exports instead of hardcoding a historical field count or assuming a public package has unreleased APIs.\n\n## Install in an agent environment\n\nPublished CLI 0.20.0 bundles all six complete skill folders. Use Node 24 for the current packages and this checkout. From your project directory:\n\n```sh\nnpx @openpresentation/cli@0.20.0 skills install\n```\n\nThe default installs copies into `.agents/skills` in the current project, suitable for agents including Codex. It does not change AGENTS.md or any agent configuration. No symlink privileges, paid service, API key or AI provider is required. npm downloads the CLI on first use; the installed CLI then installs its bundled skills without network access. The explicit 0.20.0 pin makes this installation repeatable and matches the [current package train](compatibility-matrix.md). For CLI source development, use `node packages/cli/dist/index.js skills install` after building.\n\n| Target | Project directory | Personal directory with `--global` |\n| --- | --- | --- |\n| Default / `--agent universal` | `.agents/skills` | `~/.agents/skills` |\n| `--agent codex` | `.agents/skills` | `~/.codex/skills` |\n| `--agent claude-code` | `.claude/skills` | `~/.claude/skills` |\n| `--agent cursor` | `.cursor/skills` | `~/.cursor/skills` |\n\nFor example, `npx @openpresentation/cli@0.20.0 skills install --agent codex --global` installs personal Codex skills. For another compatible agent use `--directory <its-skills-directory>`; this option cannot be combined with `--agent` or `--global`. Restart or reload your agent if its skill discovery requires it. A compatible client can invoke the installed skills with names such as `$opf-author` or `$opf-inspect`.\n\nInspect or update the same destination:\n\n```sh\nnpx @openpresentation/cli@0.20.0 skills status\nnpx @openpresentation/cli@0.20.0 skills update\n```\n\nSupply the same target options used for installation. `status` is read-only and compares against the invoked CLI's bundled version; it does not query npm for newer releases. Repeated installation is idempotent. Updates check every installed file before changing any skill. Modified, added, deleted or unmanaged files cause the command to stop and list the conflicting folders; keep your customizations, move those folders outside the active skills directory, then retry. There is no force-overwrite option. A successful update returns backup paths outside the active skills directory for recovering the previous managed versions. Keep those backups until you have reviewed the update. Do not run concurrent writers: the installer lock coordinates other installer runs, but cannot lock an external editor.\n\nNo skills are installed by the repository build. Manual installation remains supported: copy whole folders from `skills/`, including references and scripts, to your agent's skill directory. Each folder is self-contained. The managed installer treats existing manual copies as unmanaged and preserves them.\n\nThe inspection helper requires Node 24 and `@openpresentation/opf` in the current project. In this checkout, build with `pnpm build` first. For an installed skill used outside the checkout, either run from an npm project that has the package or set `OPF_ROOT` to the built OPF checkout. It does not install dependencies, fetch catalogs, or modify input files.\n\n## Local CLI\n\nThe [installable CLI](../packages/cli/README.md) complements these skills with `opf create`, `opf validate`, `opf edit`, and schema/catalog lookup. It runs on the core it depends on; the inspection skill helper instead resolves the host project's core package. Check versions when moving between them. From code, core's `@openpresentation/opf` offers `convert`, `open` and `save` in Node (see the opf-export skill).\n\n## Examples of requests\n\n- \u201CUse $opf-author to turn these notes into a five-slide decision brief. Keep every factual claim sourced.\u201D\n- \u201CUse $opf-layout to fix this overflow without losing any text or notes.\u201D\n- \u201CUse $opf-presets to apply our brand colors while preserving slide-specific overrides.\u201D\n- \u201CUse $opf-edit to replace one table and retain all other document fields.\u201D\n- \u201CUse $opf-export to export the same reviewed slides to SVG and PPTX.\u201D (PPTX text is editable one line at a time unless you opt in to `--body-text native`, from opf-pptx and CLI 0.22.0; the skill says so.)\n- \u201CUse $opf-inspect to explain which background forms the installed schema accepts.\u201D\n\n## Maintenance\n\n`pnpm test:skills` checks skill links, schema-valid examples, and the inspection helper's actual behavior, including a copied standalone skill and a package installed in a consumer project. Run the skill-creator frontmatter validator when editing skill metadata. Behavioral tests are not evidence that every renderer option is visually complete.\n\nWhen schema/package APIs change, update only the affected skill/reference and its executable examples. Keep option lists in the canonical schema and catalogs. The format package and skill folders are separate distribution surfaces: CLI 0.20.0 includes the six skills; the core `@openpresentation/opf` package does not install agent configuration. Repository skill prose can be newer than the immutable snapshot bundled in an already-published CLI. These source documentation corrections do not change the CLI 0.20.0 tarball; `skills status` and `skills update` compare against the invoked CLI's bundled snapshot.\n"
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"slug": "catalog-schema-reference",
|
|
@@ -15,26 +15,26 @@ var docsData = Object.freeze([
|
|
|
15
15
|
{
|
|
16
16
|
"slug": "chart-options",
|
|
17
17
|
"file": "docs/chart-options.md",
|
|
18
|
-
"title": "Chart options: axis titles, legend
|
|
19
|
-
"markdown": '# Chart options: axis titles, legend position, data labels and highlight\n\nStatus: RR-35 (release readiness) for axis titles, legend and data labels, FA-14 (format audit) for `highlight`. These shipped fields change geometry only for charts that use the first three fields, and colors only for charts that use `highlight`. Use the coordinated package set in the [compatibility matrix](compatibility-matrix.md); schema support alone does not establish native visual fidelity.\n\n## The fields\n\nFour optional fields on the `Chart` object (the fourth, `highlight`, has [its own section](#chart-emphasis-highlight) below):\n\n```json\n{\n "chart": {\n "type": "column",\n "data": { "columns": ["Quarter", "Revenue"], "rows": [["Q1", 12], ["Q2", 18]] },\n "axisTitles": { "category": "Quarter", "value": "Revenue ($M)" },\n "legend": "bottom",\n "dataLabels": { "content": ["value"], "position": "outside-end" }\n }\n}\n```\n\n| Field | Values | Absent means |\n| --- | --- | --- |\n| `axisTitles` | `{ category?: string, value?: string, secondary?: string }` (`secondary`: combo charts) | no axis titles (today) |\n| `legend` | `none`, `top`, `bottom`, `left`, `right` | today\'s behaviour exactly: a legend at the right of multi-series charts and of pie and doughnut charts, none for a single-series chart |\n| `dataLabels` | `true`, `false` or `{ content?, position?, separator? }` | no data labels (today); the funnel and treemap constructs keep the labels they draw by default (values, category names) |\n\n`axisTitles.category` titles the axis that carries the row labels: the horizontal axis of a column, line, area, histogram, pareto, waterfall and box-and-whisker chart, the **vertical** axis of a bar chart and the X axis of a scatter chart. `axisTitles.value` titles the other one. A named `legend` position shows the legend there even for a single-series chart; `none` hides it.\n\n`dataLabels: true` is `{ "content": ["value"], "position": "auto" }`. `dataLabels: false` is the same as leaving the field out, except on the funnel and treemap constructs, which label their marks by default: there `false` removes those labels (`resolveChartOptions` reports `dataLabelsOff`). In the object form:\n\n- `content`: any of `category`, `value`, `percent` (default `["value"]`). A label shows the selected parts in the fixed order category, value, percent, joined by `separator` (default `", "`). Values print in the General number format (twelve significant digits), `percent` as the integer share of the total (`0%`), `category` as the category name (the X value on a scatter chart).\n- `position`: `auto` (default), `center`, `inside-end`, `inside-base`, `outside-end`, `above`, `below`, `left`, `right`. `auto` is the type\'s default in the table below; the engines write that concrete position, so the preview and PowerPoint do not each pick their own.\n- `separator`: text between the parts.\n\n## Chart emphasis: `highlight`\n\nHighlighting the one series or category that carries the message, and muting the rest, is the main data-storytelling technique. The content stays semantic: `highlight` names series and categories, and the engines pick the colors from the theme. There is no color field.\n\n```json\n{\n "chart": {\n "type": "column",\n "data": { "columns": ["Quarter", "Revenue", "Costs"], "rows": [["Q1", 12, 8], ["Q2", 18, 11], ["Q3", 24, 15]] },\n "highlight": { "series": ["Revenue"], "categories": ["Q3"] }\n }\n}\n```\n\n| Field | Meaning |\n| --- | --- |\n| `highlight.series` | Value columns (series) whose marks are highlighted: every mark of each named series. Names match the data column names after any `fields` selection; a series is a plotted value column. |\n| `highlight.categories` | Row labels (the category column, or `mapping.category`) whose marks are highlighted: a column or bar of every series, a line\'s points, a pie or doughnut slice. Labels match as text (the number `2024` as `"2024"`) and a label shared by several rows highlights each of them. |\n\n**The rule.** When `highlight` is present, a mark is highlighted if its series OR its category is listed. Highlighted marks take the deck\'s primary (accent) color; every other mark takes a muted neutral derived from the theme. With both lists given, a bar of series `Revenue` in any category is highlighted, and so is a bar of category `Q3` in any series. A chart without `highlight` keeps its colors, byte for byte. A highlight that names nothing the chart plots (for example a column that is the category column) leaves the chart as if it had none.\n\n**The colors** come from core\'s `chartHighlightColors(surface, primary, text)`, which the preview and the PPTX export both call with the chart panel, the deck primary and the chart text color:\n\n- the accent is the primary color, kept at 3:1 contrast against the panel by `chartColorForFill` (a primary that already has that contrast is used unchanged);\n- the muted color is the panel mixed 30% of the way toward the text color: a light grey on a light panel and a dim grey on a dark one, in the panel\'s own hue. If that is under 1.6:1 against the panel, the mix moves toward the text color until it holds, so a muted mark is always visible and always quieter than the text.\n\nData labels stay readable: a label inside a mark takes the text color that contrasts with that mark\'s own color (core `textColorForFill`), so a label on a muted bar contrasts with the muted fill, as one on an accent bar does with the accent. Labels outside marks keep the chart text color. The legend shows each series in the color its marks have: accent for a highlighted series, muted otherwise. (A multi-series chart that highlights only categories mutes every series alike, so its legend keys look the same; name the series too when the legend matters.)\n\n**Validation.** A name that matches no data column (`series`) or no row label (`categories`) is a `chart-highlight-unknown-name` **error**, found by `resolveChartData` like `chart-mapping-unknown-column`. A series name that is a column but not plotted as a series (the category or X column, or a column `mapping.series` leaves out) is a `chart-highlight-adapted` warning and highlights nothing. A part the chart type cannot highlight is dropped with a `chart-option-adapted` diagnostic (option `highlight.series` or `highlight.categories`). `highlight` must name at least one series or category, and each list has no duplicates.\n\n### What each chart type highlights\n\n`chartOptionSupport(target).highlight` returns `{ series, categories }`; the validator, the preview, the exporter and the editor all follow it.\n\n| Type (catalog ids) | Series | Categories | What is drawn |\n| --- | --- | --- | --- |\n| clustered column, bar | yes | yes | the series\' columns or bars; a category\'s column or bar in every series |\n| stacked and 100% stacked column, bar | yes | yes | the same, per segment |\n| line, stacked line, with markers | yes | yes | a series\' line and markers; a category highlight draws a marker on each series at that category (on a line without markers, only there) |\n| area, stacked area, 100% stacked area | yes | no | the series\' area |\n| scatter | yes | no | the series\' markers (a scatter point label is not a category) |\n| radar, with markers, filled | yes | no | the series\' line (or fill) and markers |\n| pie, doughnut | no | yes | the slice of each named category (a pie plots one series, so a series name is meaningless) |\n| histogram, pareto, waterfall, funnel, treemap, box and whisker, world, combo | no | no | not supported: dropped with `chart-option-adapted` |\n\nThe Office 2016 (chartex) constructs are left out on purpose. Their marks take their colors from the construct\'s chart style, and a per-point color (`cx:dataPt`) would need native verification this change does not have; the waterfall also already colors its marks by increase, decrease and total. They draw and export as before.\n\n## Series colors\n\nSeries colors follow the deck\'s color scheme. They are not a field of the chart: the same chart takes the colors of whichever scheme the deck uses (`design.colorScheme`, the theme\'s scheme, a catalog record, a built-in such as `vibes`, or an inline scheme with slot overrides).\n\nCore\'s `chartSeriesPalette(colorScheme)` (on `@openpresentation/opf/composition`) returns the twelve colors the preview, the PPTX export and the `chart-color-only` accessibility rule all use, in this order:\n\n| Series | Color |\n| --- | --- |\n| 1 to 6 | `accent1` to `accent6` of the resolved color scheme (a slot the scheme leaves out takes the engine default scheme\'s slot) |\n| 7 to 12 | the same six accents mixed 40% toward white, in the same order (series 7 is a tint of `accent1`) |\n\nSeries `i` takes color `i`; a pie, doughnut, treemap or funnel colors category `i` with color `i`. A chart with more than twelve series repeats the list (and `chart-color-only` says so). The `primary`, `secondary` and `accent` role overrides do not reorder the list: series 1 to 3 are always `accent1` to `accent3`, the slots the PPTX export writes as theme colors.\n\nBoth engines then pass the list through `chartPaletteForFill(panel, palette)`, which keeps every series at 3:1 contrast against the chart panel (the card surface) and apart from its neighbours, so on a dark panel the colors are lightened, never merged. The palette is read in tiers of six (series 1 to 6, then 7 to 12): a color is kept apart from the colors of its own and earlier tiers only, so the tints never pull the six accents towards each other. Plan a chart\'s series order for the scheme: put the series that carries the message first (accent1 is the deck\'s primary), and keep a chart to six series or fewer so no tint is needed. The `highlight` colors above are separate: they come from the primary and the panel, not from this list.\n\nA host that validates with its own palette passes `ValidateOptions.chartPalette`; without it the rule judges each slide\'s own scheme.\n\n### Setting colors: `chart.colors`\n\n`colors` lists colors by position and replaces the palette color of that position; the rest of the palette is unchanged.\n\n```json\n{\n "chart": {\n "type": "column",\n "data": { "columns": ["Quarter", "Revenue", "Costs"], "rows": [["Q1", 12, 8], ["Q2", 18, 11]] },\n "colors": ["accent3", "#7B241C"]\n }\n}\n```\n\n- **Position.** Entry `i` is the color of series `i` (the `i`-th plotted value column), of slice or category `i` on a pie, doughnut, treemap or funnel chart, and of the `i`-th color a histogram, pareto, waterfall or box-and-whisker chart draws with (the first color for its bars, the second for its line or negative bars). Up to 24 entries; positions past the last entry keep the deck palette.\n- **Values.** Any `ColorRef`: a hex color, a color-scheme slot or role (`accent3`, `secondary`), or a `var:<id>` color variable. A scheme name follows the deck if its scheme changes and is written to PowerPoint as a theme color; a hex color stays as written. Alpha is dropped. An entry that cannot be resolved is ignored (the palette color of its position is used) and `opf/chart-colors-adjusted` says so.\n- **Contrast.** Colors still pass through `chartPaletteForFill`: one with under 3:1 contrast against the chart panel is drawn lighter or darker, and series are kept apart. `opf/chart-colors-adjusted` warns when that happens to an authored color, and `opf/chart-color-only` judges the authored colors for colour-blind and greyscale collisions.\n- **Highlight wins.** With `chart.highlight`, highlighted marks take the primary color and every other mark the muted neutral, whatever `colors` says.\n- **Engines.** Core resolves the list with `chartSeriesPalette(colorScheme, chart.colors, { roles, variables })` (and `resolveChartColors` for the entries alone); the preview and the PPTX export pass the result to `chartPaletteForFill` as before.\n\n## Value axis\n\nA column, bar, line or area chart (standard, stacked and 100% stacked) has one value axis, and composition computes it: `composeSlide` puts `{ min, max, majorUnit }` on the composed chart item as `chart.valueAxis`. The preview draws that axis and the PPTX export writes it as `c:min`, `c:max` and `c:majorUnit`, so PowerPoint and Keynote show the same gridlines as the preview instead of choosing their own major unit. A 100% stacked chart\'s axis is a fraction (`max: 1` is 100%) and carries `percent: true`. Combo, scatter, radar, pie and the Office 2016 constructs have no computed axis (`chart` is absent); each engine keeps drawing them as before.\n\nThe scale is core\'s `niceScale(dataMin, dataMax, maxIntervals, { percent })`: zero is on the axis (always for bars and areas; for a line only when the data does not sit in the top sixth of its range), the data gets 5% headroom, and the major unit is the smallest 1, 2 or 5 times a power of ten that fits `maxIntervals` intervals. `maxIntervals` is `maxIntervalsFor(length, labelExtent)`, at most 10, where `length` is the plot\'s extent along the value axis estimated from the chart box (without measuring text) and `labelExtent` is the space one tick label needs: a column, line or area chart has `box height - 2 x pad - 1.5 x line height` (less a line for a category axis title, and a line plus a pad for a top or bottom legend) over 1.2 line heights, a bar chart `70% of the box width - 2 x pad - text size` over 4 text sizes. Here pad is `10 x scale`, text size is `chartTextSize(...) x scale` px and line height is 1.5 text sizes. `chartTextSize({ width, height, scale, minFontSize })` is the chart text size the preview draws: `CHART_TEXT_PX` (20 px at the 1280 x 720 reference, 15 pt), shrunk only when the box is small (at most 1/18 of the box height and 1/30 of its width, whole pixels) and never below `minFontSize` (default 16), with `scale = min(slide width, slide height) / 720`. A chart in a short box therefore has fewer, larger intervals (0 to 800 in steps of 100 in a tall box, steps of 200 in a short one); a tall box always gets ten at most.\n\n## What each chart type supports (axis titles, legend, data labels)\n\n`chartOptionSupport(target)` in `@openpresentation/opf/composition` returns this table; the validator, the preview, the exporter and the importer all follow it. An option a type cannot show is **adapted** (dropped, or reset to the default) and reported as a `chart-option-adapted` diagnostic by the validator (a warning), the renderer and the exporter (`onDiagnostic`). It is never silently lost and never fails the render.\n\n| Type (catalog ids) | Axis titles | Legend | Data label content | Data label positions (default) |\n| --- | --- | --- | --- | --- |\n| clustered column, bar (`column`, `bar`) | category, value | yes | category, value | center, inside-end, inside-base, outside-end (outside-end) |\n| stacked and 100% stacked column, bar | category, value | yes | category, value | center, inside-end, inside-base (center) |\n| line, stacked line, with markers | category, value | yes | category, value | above, below, left, right, center (above) |\n| area, stacked area, 100% stacked area | category, value | yes | category, value | none: the label sits in the area at each category |\n| scatter | category (X), value (Y) | yes | category (X value), value (Y) | above, below, left, right, center (above) |\n| pie | none | yes | category, value, percent | center, inside-end, outside-end (outside-end) |\n| doughnut | none | yes | category, value, percent | none: labels sit in the ring |\n| radar, with markers, filled | none | yes | category, value | none |\n| histogram, pareto | category, value | none | category, value | center, inside-end, inside-base, outside-end (outside-end) |\n| waterfall | category, value | none | category, value | center, inside-end, inside-base, outside-end (outside-end) |\n| funnel | category | none | category, value | none (center) |\n| treemap | none | none | category, value | none (center) |\n| box and whisker | category, value | yes | none | none |\n| combo (`combo`) | category, value, secondary | yes | category, value | columns: center, inside-end, inside-base, outside-end (outside-end); lines: above, below, left, right, center (above) |\n| world (region map) | none | none | none | none |\n\nA chart type outside the catalog is never adapted.\n\n## Combo charts\n\nStatus: FA-15 (format audit). A `combo` chart draws clustered columns and line series in one plot, for an amount beside a rate such as revenue and margin %:\n\n```json\n{\n "chart": {\n "type": "combo",\n "data": {\n "columns": ["Quarter", { "name": "Revenue", "format": "$#,##0.0" }, { "name": "Margin", "format": "0%" }],\n "rows": [["Q1", 12.4, 0.31], ["Q2", 18.1, 0.34], ["Q3", 21.7, 0.29], ["Q4", 26.3, 0.37]]\n },\n "line": ["Margin"],\n "secondaryAxis": ["Margin"],\n "axisTitles": { "category": "Quarter", "value": "Revenue ($M)", "secondary": "Margin" }\n }\n}\n```\n\n| Field | Values | Absent means |\n| --- | --- | --- |\n| `line` | plotted series names, at least one | the last plotted series is the line |\n| `secondaryAxis` | line series names | every series uses the primary (left) value axis |\n| `axisTitles.secondary` | string | no secondary axis title |\n\n- **The plan.** Every plotted series (the `mapping.series` or the positional rule) is drawn as clustered columns, except the ones `line` names, which are lines with markers. At least one series stays columns, so a `line` that names every series draws the first as columns. A one-series combo chart is drawn as columns. Core\'s `resolveChartData` returns the plan as `combo` (`{ role: "bar" | "line", axis: "primary" | "secondary" }` per series) and orders the series columns first, then the primary-axis lines, then the secondary-axis lines, so the preview, the PPTX series order and the legend agree.\n- **The secondary axis.** `secondaryAxis` puts line series on a value axis at the right (at the left right to left). It has its own automatic scale and its tick labels use the number format of its first series\' column (`DataColumn.format`, for example `0%`); the primary axis uses the first column series\' format. Gridlines follow the primary axis. `axisTitles.secondary` titles it, rotated like the primary value title.\n- **Labels.** One `dataLabels` applies to both parts: column labels sit outside the end and line labels above the points by default; `center` applies to both; a position only one part takes (`inside-end`, `inside-base`, `outside-end` for columns; `above`, `below`, `left`, `right` for lines) applies to that part and the other keeps its default, with a `chart-option-adapted` note. `resolveChartOptions` returns the column position as `dataLabels.position` and the line position as `dataLabels.linePosition`.\n- **Legend.** As for other multi-series charts (right by default, or `legend`); a column series has a square key and a line series a line with a marker.\n- **Validation.** A `line` or `secondaryAxis` name that is not a column of the data is a `chart-mapping-unknown-column` error. A name that is not plotted, a `secondaryAxis` name that is not a line, a `line` that names every series and a one-series combo chart are `chart-mapping-adapted` warnings. `line`, `secondaryAxis` and `axisTitles.secondary` on any other chart type, and `axisTitles.secondary` on a combo chart without `secondaryAxis`, are `chart-option-adapted` warnings.\n- **PPTX.** One `c:plotArea` holds a clustered column `c:barChart` (primary axes) and a `c:lineChart` with markers. Secondary-axis lines form a second `c:lineChart` on a second `c:valAx` (`axPos r`, `crosses max`) and a deleted second `c:catAx`; the embedded workbook holds every series. Import reads such a plot area (one clustered column group, one or two line groups) back as `combo` with `line` (when it is not the default), `secondaryAxis`, the secondary title and the label position.\n- **Editor.** The chart type picker offers `combo` for data with two or more series, the chart options panel sets which series are lines and which lines use the secondary axis, and switching to another type removes the three fields.\n\n## How the engines draw and write them\n\nThe preview (opf-render) and the PPTX export (opf-pptx) both read `resolveChartOptions(chart, chartOptionTarget(chart.type))`, so they agree on which options apply and with which content and position. The highlight adds `resolveChartOptions(...).highlight` (the names that survive the type\'s support table), `chartHighlightMarks(highlight, resolvedData)` (which series and which rows are named) and `chartHighlightColors` (the two colors).\n\n- **Preview.** A legend, and the axis titles, are carved from the chart box before the plot is laid out: the legend at its edge, then the titles next to the axes. Only a chart that uses a field changes; a chart without them draws the same SVG as before. A vertical axis title is rotated 270 degrees, as PowerPoint draws it.\n- **PPTX, classic charts** (column, bar, line, area, pie, doughnut, scatter, radar): `c:catAx/c:title` and `c:valAx/c:title` (rich text, `c:overlay val="0"`), `c:legend/c:legendPos` (`t`, `b`, `l`, `r`; no `c:legend` for `none`) and a `c:dLbls` per series with `c:dLblPos`, `c:showVal`, `c:showCatName`, `c:showPercent` and an explicit `c:separator`, number format `General` (RR-54: the series column format, as `toExcelNumberFormat` writes it, when its `DataColumn` has one; see [chart-table-data.md](chart-table-data.md)).\n- **PPTX, chartex** (histogram, pareto, waterfall, funnel, treemap, box and whisker): `cx:axis/cx:title`, `cx:legend pos`, and `cx:dataLabels pos` with `cx:visibility` and `cx:separator`. The classic fallback chart that precedes every chartex part carries the same classic options.\n- **Highlight (FA-14), preview.** A highlighted mark is filled with the accent color, every other mark with the muted color; series keep their order (the highlighted one is not moved on top), as PowerPoint draws them. A chart without a highlight never calls the highlight code.\n- **Highlight (FA-14), PPTX.** Series colors go into `c:ser/c:spPr` (and `c:marker/c:spPr` on line, scatter and radar series); a per-point color is a `c:dPt` with `c:idx`: `c:dPt/c:spPr` on a column or bar, a circular `c:dPt/c:marker` on a line, the slice fill on a pie or doughnut. The accent is `a:schemeClr val="accent1"` when the deck theme holds the primary exactly (so re-theming in PowerPoint recolors it) and an `a:srgbClr` literal when the contrast adjustment moved it; the muted color is always an `a:srgbClr` literal, because a mix of two theme colors is not one scheme color with `lumMod`/`lumOff`. Data labels inside a mark are written per point where the mark colors differ. Chartex constructs are unchanged.\n- **Import.** `fromPptx` reads the same parts back into `axisTitles`, `legend` and `dataLabels`. A legend equal to the default for that chart (right for multi-series, pie and doughnut; none otherwise) is not recorded, so decks that never set the field import unchanged. Anything the three fields cannot express (per-series label overrides, number formats, rich-text titles, manual layouts) is reported with a diagnostic and not invented. The highlight is not inferred from the colors (a native chart\'s fills do not say which names were highlighted, and a hand-colored chart must not turn into a highlight): in `full` provenance mode the exporter records the authored `highlight` in the chart\'s `OPF_DATA_V1` data record, and `fromPptx` restores it while the chart\'s cached data is unchanged. With `provenance: false` or `\'references-only\'`, or after the data was edited in PowerPoint (`chart-data-provenance-changed`), the chart imports without a highlight and keeps the colors it shows.\n\n## Text alternative\n\n`Chart.alt` (FA-09) is a string: what the chart shows, in a sentence or two (the point and the key numbers), not "a chart". Unlike the three options above it changes no geometry.\n\n- **Preview.** The chart\'s SVG group carries `role="img"` and `aria-label`; the role makes the drawn marks inside presentational, so the label is the chart\'s one announcement. The text beside the chart stays reachable: the slide is a labelled group, not an image (see [Accessibility of the preview](content-payloads.md#accessibility-of-the-preview)).\n- **PPTX.** The value is the chart frame\'s `p:nvGraphicFramePr/p:cNvPr/@descr`, the same attribute pictures use, on classic charts and on chartex (Office 2016) frames. `fromPptx` reads it back into `alt`.\n- **Empty string.** `""` marks the chart decorative, like an empty `Asset.alt`: the preview writes no label and hides the group (`aria-hidden`), and the PPTX frame carries PowerPoint\'s own "Mark as decorative" marker (the `adec:decorative` extension of `p:cNvPr`), not `descr=""`, which PowerPoint writes for any shape without alt text. Import reads the marker back as `alt: ""`. A chart nearly always carries a message, so `opf/chart-text-alternative` reports it as info.\n- **Validate.** `opf/chart-text-alternative` passes a chart with a non-blank `alt`. Without `alt` it still passes when text, a table or a subtitle sits beside the chart; otherwise it reports the chart and offers a quick fix that focuses the `alt` field. `opf/poor-alt-text` flags a bare "chart", a URL, a leading "chart of" and alt text over 250 characters.\n\n## Defaults and geometry\n\nA chart that sets none of the four fields renders and exports byte-for-byte as before; the preview tests and the PPTX goldens assert it. A chart with options reserves space for its legend and titles, so its geometry differs from a chart without them. The implementations require coordinated Core, renderer, PPTX and editor versions; use the [compatibility matrix](compatibility-matrix.md).\n\n## Not in this change\n\nPer-series data label overrides (column number formats are RR-54, [chart-table-data.md](chart-table-data.md)), a rotated or rich-text axis title, a chart title, a legend that overlays the plot, manual plot-area layout, secondary axes outside combo charts, and trendlines. Charts from external spreadsheets stay descoped (the `ChartDataSource` form was removed from the schema).\n'
|
|
18
|
+
"title": "Chart options: axis titles, legend, data labels, highlight, series colors and the value axis",
|
|
19
|
+
"markdown": '# Chart options: axis titles, legend, data labels, highlight, series colors and the value axis\n\nStatus: RR-35 (release readiness) for axis titles, legend and data labels, FA-14 (format audit) for `highlight`, and RR-89 and RR-92 (core 0.21.0) for series colors (`chart.colors`) and the computed value axis. The first four fields change geometry only for charts that use the first three fields, and colors only for charts that use `highlight`. Use the coordinated package set in the [compatibility matrix](compatibility-matrix.md); schema support alone does not establish native visual fidelity.\n\n## The fields\n\nFour optional fields on the `Chart` object (the fourth, `highlight`, has [its own section](#chart-emphasis-highlight) below):\n\n```json\n{\n "chart": {\n "type": "column",\n "data": { "columns": ["Quarter", "Revenue"], "rows": [["Q1", 12], ["Q2", 18]] },\n "axisTitles": { "category": "Quarter", "value": "Revenue ($M)" },\n "legend": "bottom",\n "dataLabels": { "content": ["value"], "position": "outside-end" }\n }\n}\n```\n\n| Field | Values | Absent means |\n| --- | --- | --- |\n| `axisTitles` | `{ category?: string, value?: string, secondary?: string }` (`secondary`: combo charts) | no axis titles (today) |\n| `legend` | `none`, `top`, `bottom`, `left`, `right` | today\'s behaviour exactly: a legend at the right of multi-series charts and of pie and doughnut charts, none for a single-series chart |\n| `dataLabels` | `true`, `false` or `{ content?, position?, separator? }` | no data labels (today); the funnel and treemap constructs keep the labels they draw by default (values, category names) |\n\n`axisTitles.category` titles the axis that carries the row labels: the horizontal axis of a column, line, area, histogram, pareto, waterfall and box-and-whisker chart, the **vertical** axis of a bar chart and the X axis of a scatter chart. `axisTitles.value` titles the other one. A named `legend` position shows the legend there even for a single-series chart; `none` hides it.\n\n`dataLabels: true` is `{ "content": ["value"], "position": "auto" }`. `dataLabels: false` is the same as leaving the field out, except on the funnel and treemap constructs, which label their marks by default: there `false` removes those labels (`resolveChartOptions` reports `dataLabelsOff`). In the object form:\n\n- `content`: any of `category`, `value`, `percent` (default `["value"]`). A label shows the selected parts in the fixed order category, value, percent, joined by `separator` (default `", "`). Values print in the General number format (twelve significant digits), `percent` as the integer share of the total (`0%`), `category` as the category name (the X value on a scatter chart).\n- `position`: `auto` (default), `center`, `inside-end`, `inside-base`, `outside-end`, `above`, `below`, `left`, `right`. `auto` is the type\'s default in the table below; the engines write that concrete position, so the preview and PowerPoint do not each pick their own.\n- `separator`: text between the parts.\n\n## Chart emphasis: `highlight`\n\nHighlighting the one series or category that carries the message, and muting the rest, is the main data-storytelling technique. The content stays semantic: `highlight` names series and categories, and the engines pick the colors from the theme. There is no color field.\n\n```json\n{\n "chart": {\n "type": "column",\n "data": { "columns": ["Quarter", "Revenue", "Costs"], "rows": [["Q1", 12, 8], ["Q2", 18, 11], ["Q3", 24, 15]] },\n "highlight": { "series": ["Revenue"], "categories": ["Q3"] }\n }\n}\n```\n\n| Field | Meaning |\n| --- | --- |\n| `highlight.series` | Value columns (series) whose marks are highlighted: every mark of each named series. Names match the data column names after any `fields` selection; a series is a plotted value column. |\n| `highlight.categories` | Row labels (the category column, or `mapping.category`) whose marks are highlighted: a column or bar of every series, a line\'s points, a pie or doughnut slice. Labels match as text (the number `2024` as `"2024"`) and a label shared by several rows highlights each of them. |\n\n**The rule.** When `highlight` is present, a mark is highlighted if its series OR its category is listed. Highlighted marks take the deck\'s primary (accent) color; every other mark takes a muted neutral derived from the theme. With both lists given, a bar of series `Revenue` in any category is highlighted, and so is a bar of category `Q3` in any series. A chart without `highlight` keeps its colors, byte for byte. A highlight that names nothing the chart plots (for example a column that is the category column) leaves the chart as if it had none.\n\n**The colors** come from core\'s `chartHighlightColors(surface, primary, text)`, which the preview and the PPTX export both call with the chart panel, the deck primary and the chart text color:\n\n- the accent is the primary color, kept at 3:1 contrast against the panel by `chartColorForFill` (a primary that already has that contrast is used unchanged);\n- the muted color is the panel mixed 30% of the way toward the text color: a light grey on a light panel and a dim grey on a dark one, in the panel\'s own hue. If that is under 1.6:1 against the panel, the mix moves toward the text color until it holds, so a muted mark is always visible and always quieter than the text.\n\nData labels stay readable: a label inside a mark takes the text color that contrasts with that mark\'s own color (core `textColorForFill`), so a label on a muted bar contrasts with the muted fill, as one on an accent bar does with the accent. Labels outside marks keep the chart text color. The legend shows each series in the color its marks have: accent for a highlighted series, muted otherwise. (A multi-series chart that highlights only categories mutes every series alike, so its legend keys look the same; name the series too when the legend matters.)\n\n**Validation.** A name that matches no data column (`series`) or no row label (`categories`) is a `chart-highlight-unknown-name` **error**, found by `resolveChartData` like `chart-mapping-unknown-column`. A series name that is a column but not plotted as a series (the category or X column, or a column `mapping.series` leaves out) is a `chart-highlight-adapted` warning and highlights nothing. A part the chart type cannot highlight is dropped with a `chart-option-adapted` diagnostic (option `highlight.series` or `highlight.categories`). `highlight` must name at least one series or category, and each list has no duplicates.\n\n### What each chart type highlights\n\n`chartOptionSupport(target).highlight` returns `{ series, categories }`; the validator, the preview, the exporter and the editor all follow it.\n\n| Type (catalog ids) | Series | Categories | What is drawn |\n| --- | --- | --- | --- |\n| clustered column, bar | yes | yes | the series\' columns or bars; a category\'s column or bar in every series |\n| stacked and 100% stacked column, bar | yes | yes | the same, per segment |\n| line, stacked line, with markers | yes | yes | a series\' line and markers; a category highlight draws a marker on each series at that category (on a line without markers, only there) |\n| area, stacked area, 100% stacked area | yes | no | the series\' area |\n| scatter | yes | no | the series\' markers (a scatter point label is not a category) |\n| radar, with markers, filled | yes | no | the series\' line (or fill) and markers |\n| pie, doughnut | no | yes | the slice of each named category (a pie plots one series, so a series name is meaningless) |\n| histogram, pareto, waterfall, funnel, treemap, box and whisker, world, combo | no | no | not supported: dropped with `chart-option-adapted` |\n\nThe Office 2016 (chartex) constructs are left out on purpose. Their marks take their colors from the construct\'s chart style, and a per-point color (`cx:dataPt`) would need native verification this change does not have; the waterfall also already colors its marks by increase, decrease and total. They draw and export as before.\n\n## Series colors\n\nSeries colors follow the deck\'s color scheme by default: the same chart takes the colors of whichever scheme the deck uses (`design.colorScheme`, the theme\'s scheme, a catalog record, a built-in such as `vibes`, or an inline scheme with slot overrides). To set the color of one series, slice or category, list colors in [`chart.colors`](#setting-colors-chartcolors); a position it does not list keeps the deck palette.\n\nCore\'s `chartSeriesPalette(colorScheme)` (on `@openpresentation/opf/composition`) returns the twelve colors the preview, the PPTX export and the `chart-color-only` accessibility rule all use, in this order:\n\n| Series | Color |\n| --- | --- |\n| 1 to 6 | `accent1` to `accent6` of the resolved color scheme (a slot the scheme leaves out takes the engine default scheme\'s slot) |\n| 7 to 12 | the same six accents mixed 40% toward white, in the same order (series 7 is a tint of `accent1`) |\n\nSeries `i` takes color `i`; a pie, doughnut, treemap or funnel colors category `i` with color `i`. A chart with more than twelve series repeats the list (and `chart-color-only` says so). The `primary`, `secondary` and `accent` role overrides do not reorder the list: series 1 to 3 are always `accent1` to `accent3`, the slots the PPTX export writes as theme colors.\n\nBoth engines then pass the list through `chartPaletteForFill(panel, palette)`, which keeps every series at 3:1 contrast against the chart panel (the card surface) and apart from its neighbours, so on a dark panel the colors are lightened, never merged. The palette is read in tiers of six (series 1 to 6, then 7 to 12): a color is kept apart from the colors of its own and earlier tiers only, so the tints never pull the six accents towards each other. Plan a chart\'s series order for the scheme: put the series that carries the message first (accent1 is the deck\'s primary), and keep a chart to six series or fewer so no tint is needed. The `highlight` colors above are separate: they come from the primary and the panel, not from this list.\n\nA host that validates with its own palette passes `ValidateOptions.chartPalette`; without it the rule judges each slide\'s own scheme.\n\n### Setting colors: `chart.colors`\n\n`colors` lists colors by position and replaces the palette color of that position; the rest of the palette is unchanged.\n\n```json\n{\n "chart": {\n "type": "column",\n "data": { "columns": ["Quarter", "Revenue", "Costs"], "rows": [["Q1", 12, 8], ["Q2", 18, 11]] },\n "colors": ["accent3", "#7B241C"]\n }\n}\n```\n\n- **Position.** Entry `i` is the color of series `i` (the `i`-th plotted value column), of slice or category `i` on a pie, doughnut, treemap or funnel chart, and of the `i`-th color a histogram, pareto, waterfall or box-and-whisker chart draws with (the first color for its bars, the second for its line or negative bars). Up to 24 entries; positions past the last entry keep the deck palette.\n- **Values.** Any `ColorRef`: a hex color, a color-scheme slot or role (`accent3`, `secondary`), or a `var:<id>` color variable. A scheme name follows the deck if its scheme changes and is written to PowerPoint as a theme color; a hex color stays as written. Alpha is dropped. An entry that cannot be resolved is ignored (the palette color of its position is used) and `opf/chart-colors-adjusted` says so.\n- **Contrast.** Colors still pass through `chartPaletteForFill`: one with under 3:1 contrast against the chart panel is drawn lighter or darker, and series are kept apart. `opf/chart-colors-adjusted` warns when that happens to an authored color, and `opf/chart-color-only` judges the authored colors for colour-blind and greyscale collisions.\n- **Highlight wins.** With `chart.highlight`, highlighted marks take the primary color and every other mark the muted neutral, whatever `colors` says.\n- **Engines.** Core resolves the list with `chartSeriesPalette(colorScheme, chart.colors, { roles, variables })` (and `resolveChartColors` for the entries alone); the preview and the PPTX export pass the result to `chartPaletteForFill` as before.\n\n## Value axis\n\nA column, bar, line or area chart (standard, stacked and 100% stacked) has one value axis, and composition computes it: `composeSlide` puts `{ min, max, majorUnit }` on the composed chart item as `chart.valueAxis`. The preview draws that axis and the PPTX export writes it as `c:min`, `c:max` and `c:majorUnit`, so PowerPoint and Keynote show the same gridlines as the preview instead of choosing their own major unit. A 100% stacked chart\'s axis is a fraction (`max: 1` is 100%) and carries `percent: true`. Combo, scatter, radar, pie and the Office 2016 constructs have no computed axis (`chart` is absent); each engine keeps drawing them as before.\n\nThe scale is core\'s `niceScale(dataMin, dataMax, maxIntervals, { percent })`: zero is on the axis (always for bars and areas; for a line only when the data does not sit in the top sixth of its range), the data gets 5% headroom, and the major unit is the smallest 1, 2 or 5 times a power of ten that fits `maxIntervals` intervals. `maxIntervals` is `maxIntervalsFor(length, labelExtent)`, at most 10, where `length` is the plot\'s extent along the value axis estimated from the chart box (without measuring text) and `labelExtent` is the space one tick label needs: a column, line or area chart has `box height - 2 x pad - 1.5 x line height` (less a line for a category axis title, and a line plus a pad for a top or bottom legend) over 1.2 line heights, a bar chart `70% of the box width - 2 x pad - text size` over 4 text sizes. Here pad is `10 x scale`, text size is `chartTextSize(...) x scale` px and line height is 1.5 text sizes. `chartTextSize({ width, height, scale, minFontSize })` is the chart text size the preview draws: `CHART_TEXT_PX` (20 px at the 1280 x 720 reference, 15 pt), shrunk only when the box is small (at most 1/18 of the box height and 1/30 of its width, whole pixels) and never below `minFontSize` (default 16), with `scale = min(slide width, slide height) / 720`. A chart in a short box therefore has fewer, larger intervals (0 to 800 in steps of 100 in a tall box, steps of 200 in a short one); a tall box always gets ten at most.\n\n## What each chart type supports (axis titles, legend, data labels)\n\n`chartOptionSupport(target)` in `@openpresentation/opf/composition` returns this table; the validator, the preview, the exporter and the importer all follow it. An option a type cannot show is **adapted** (dropped, or reset to the default) and reported as a `chart-option-adapted` diagnostic by the validator (a warning), the renderer and the exporter (`onDiagnostic`). It is never silently lost and never fails the render.\n\n| Type (catalog ids) | Axis titles | Legend | Data label content | Data label positions (default) |\n| --- | --- | --- | --- | --- |\n| clustered column, bar (`column`, `bar`) | category, value | yes | category, value | center, inside-end, inside-base, outside-end (outside-end) |\n| stacked and 100% stacked column, bar | category, value | yes | category, value | center, inside-end, inside-base (center) |\n| line, stacked line, with markers | category, value | yes | category, value | above, below, left, right, center (above) |\n| area, stacked area, 100% stacked area | category, value | yes | category, value | none: the label sits in the area at each category |\n| scatter | category (X), value (Y) | yes | category (X value), value (Y) | above, below, left, right, center (above) |\n| pie | none | yes | category, value, percent | center, inside-end, outside-end (outside-end) |\n| doughnut | none | yes | category, value, percent | none: labels sit in the ring |\n| radar, with markers, filled | none | yes | category, value | none |\n| histogram, pareto | category, value | none | category, value | center, inside-end, inside-base, outside-end (outside-end) |\n| waterfall | category, value | none | category, value | center, inside-end, inside-base, outside-end (outside-end) |\n| funnel | category | none | category, value | none (center) |\n| treemap | none | none | category, value | none (center) |\n| box and whisker | category, value | yes | none | none |\n| combo (`combo`) | category, value, secondary | yes | category, value | columns: center, inside-end, inside-base, outside-end (outside-end); lines: above, below, left, right, center (above) |\n| world (region map) | none | none | none | none |\n\nA chart type outside the catalog is never adapted.\n\n## Combo charts\n\nStatus: FA-15 (format audit). A `combo` chart draws clustered columns and line series in one plot, for an amount beside a rate such as revenue and margin %:\n\n```json\n{\n "chart": {\n "type": "combo",\n "data": {\n "columns": ["Quarter", { "name": "Revenue", "format": "$#,##0.0" }, { "name": "Margin", "format": "0%" }],\n "rows": [["Q1", 12.4, 0.31], ["Q2", 18.1, 0.34], ["Q3", 21.7, 0.29], ["Q4", 26.3, 0.37]]\n },\n "line": ["Margin"],\n "secondaryAxis": ["Margin"],\n "axisTitles": { "category": "Quarter", "value": "Revenue ($M)", "secondary": "Margin" }\n }\n}\n```\n\n| Field | Values | Absent means |\n| --- | --- | --- |\n| `line` | plotted series names, at least one | the last plotted series is the line |\n| `secondaryAxis` | line series names | every series uses the primary (left) value axis |\n| `axisTitles.secondary` | string | no secondary axis title |\n\n- **The plan.** Every plotted series (the `mapping.series` or the positional rule) is drawn as clustered columns, except the ones `line` names, which are lines with markers. At least one series stays columns, so a `line` that names every series draws the first as columns. A one-series combo chart is drawn as columns. Core\'s `resolveChartData` returns the plan as `combo` (`{ role: "bar" | "line", axis: "primary" | "secondary" }` per series) and orders the series columns first, then the primary-axis lines, then the secondary-axis lines, so the preview, the PPTX series order and the legend agree.\n- **The secondary axis.** `secondaryAxis` puts line series on a value axis at the right (at the left right to left). It has its own automatic scale and its tick labels use the number format of its first series\' column (`DataColumn.format`, for example `0%`); the primary axis uses the first column series\' format. Gridlines follow the primary axis. `axisTitles.secondary` titles it, rotated like the primary value title.\n- **Labels.** One `dataLabels` applies to both parts: column labels sit outside the end and line labels above the points by default; `center` applies to both; a position only one part takes (`inside-end`, `inside-base`, `outside-end` for columns; `above`, `below`, `left`, `right` for lines) applies to that part and the other keeps its default, with a `chart-option-adapted` note. `resolveChartOptions` returns the column position as `dataLabels.position` and the line position as `dataLabels.linePosition`.\n- **Legend.** As for other multi-series charts (right by default, or `legend`); a column series has a square key and a line series a line with a marker.\n- **Validation.** A `line` or `secondaryAxis` name that is not a column of the data is a `chart-mapping-unknown-column` error. A name that is not plotted, a `secondaryAxis` name that is not a line, a `line` that names every series and a one-series combo chart are `chart-mapping-adapted` warnings. `line`, `secondaryAxis` and `axisTitles.secondary` on any other chart type, and `axisTitles.secondary` on a combo chart without `secondaryAxis`, are `chart-option-adapted` warnings.\n- **PPTX.** One `c:plotArea` holds a clustered column `c:barChart` (primary axes) and a `c:lineChart` with markers. Secondary-axis lines form a second `c:lineChart` on a second `c:valAx` (`axPos r`, `crosses max`) and a deleted second `c:catAx`; the embedded workbook holds every series. Import reads such a plot area (one clustered column group, one or two line groups) back as `combo` with `line` (when it is not the default), `secondaryAxis`, the secondary title and the label position.\n- **Editor.** The chart type picker offers `combo` for data with two or more series, the chart options panel sets which series are lines and which lines use the secondary axis, and switching to another type removes the three fields.\n\n## How the engines draw and write them\n\nThe preview (opf-render) and the PPTX export (opf-pptx) both read `resolveChartOptions(chart, chartOptionTarget(chart.type))`, so they agree on which options apply and with which content and position. The highlight adds `resolveChartOptions(...).highlight` (the names that survive the type\'s support table), `chartHighlightMarks(highlight, resolvedData)` (which series and which rows are named) and `chartHighlightColors` (the two colors).\n\n- **Preview.** A legend, and the axis titles, are carved from the chart box before the plot is laid out: the legend at its edge, then the titles next to the axes. Only a chart that uses a field changes; a chart without them draws the same SVG as before. A vertical axis title is rotated 270 degrees, as PowerPoint draws it.\n- **PPTX, classic charts** (column, bar, line, area, pie, doughnut, scatter, radar): `c:catAx/c:title` and `c:valAx/c:title` (rich text, `c:overlay val="0"`), `c:legend/c:legendPos` (`t`, `b`, `l`, `r`; no `c:legend` for `none`) and a `c:dLbls` per series with `c:dLblPos`, `c:showVal`, `c:showCatName`, `c:showPercent` and an explicit `c:separator`, number format `General` (RR-54: the series column format, as `toExcelNumberFormat` writes it, when its `DataColumn` has one; see [chart-table-data.md](chart-table-data.md)).\n- **PPTX, chartex** (histogram, pareto, waterfall, funnel, treemap, box and whisker): `cx:axis/cx:title`, `cx:legend pos`, and `cx:dataLabels pos` with `cx:visibility` and `cx:separator`. The classic fallback chart that precedes every chartex part carries the same classic options.\n- **Highlight (FA-14), preview.** A highlighted mark is filled with the accent color, every other mark with the muted color; series keep their order (the highlighted one is not moved on top), as PowerPoint draws them. A chart without a highlight never calls the highlight code.\n- **Highlight (FA-14), PPTX.** Series colors go into `c:ser/c:spPr` (and `c:marker/c:spPr` on line, scatter and radar series); a per-point color is a `c:dPt` with `c:idx`: `c:dPt/c:spPr` on a column or bar, a circular `c:dPt/c:marker` on a line, the slice fill on a pie or doughnut. The accent is `a:schemeClr val="accent1"` when the deck theme holds the primary exactly (so re-theming in PowerPoint recolors it) and an `a:srgbClr` literal when the contrast adjustment moved it; the muted color is always an `a:srgbClr` literal, because a mix of two theme colors is not one scheme color with `lumMod`/`lumOff`. Data labels inside a mark are written per point where the mark colors differ. Chartex constructs are unchanged.\n- **Import.** `fromPptx` reads the same parts back into `axisTitles`, `legend` and `dataLabels`. A legend equal to the default for that chart (right for multi-series, pie and doughnut; none otherwise) is not recorded, so decks that never set the field import unchanged. Anything the three fields cannot express (per-series label overrides, number formats, rich-text titles, manual layouts) is reported with a diagnostic and not invented. The highlight is not inferred from the colors (a native chart\'s fills do not say which names were highlighted, and a hand-colored chart must not turn into a highlight): in `full` provenance mode the exporter records the authored `highlight` in the chart\'s `OPF_DATA_V1` data record, and `fromPptx` restores it while the chart\'s cached data is unchanged. With `provenance: false` or `\'references-only\'`, or after the data was edited in PowerPoint (`chart-data-provenance-changed`), the chart imports without a highlight and keeps the colors it shows.\n\n## Text alternative\n\n`Chart.alt` (FA-09) is a string: what the chart shows, in a sentence or two (the point and the key numbers), not "a chart". Unlike the three options above it changes no geometry.\n\n- **Preview.** The chart\'s SVG group carries `role="img"` and `aria-label`; the role makes the drawn marks inside presentational, so the label is the chart\'s one announcement. The text beside the chart stays reachable: the slide is a labelled group, not an image (see [Accessibility of the preview](content-payloads.md#accessibility-of-the-preview)).\n- **PPTX.** The value is the chart frame\'s `p:nvGraphicFramePr/p:cNvPr/@descr`, the same attribute pictures use, on classic charts and on chartex (Office 2016) frames. `fromPptx` reads it back into `alt`.\n- **Empty string.** `""` marks the chart decorative, like an empty `Asset.alt`: the preview writes no label and hides the group (`aria-hidden`), and the PPTX frame carries PowerPoint\'s own "Mark as decorative" marker (the `adec:decorative` extension of `p:cNvPr`), not `descr=""`, which PowerPoint writes for any shape without alt text. Import reads the marker back as `alt: ""`. A chart nearly always carries a message, so `opf/chart-text-alternative` reports it as info.\n- **Validate.** `opf/chart-text-alternative` passes a chart with a non-blank `alt`. Without `alt` it still passes when text, a table or a subtitle sits beside the chart; otherwise it reports the chart and offers a quick fix that focuses the `alt` field. `opf/poor-alt-text` flags a bare "chart", a URL, a leading "chart of" and alt text over 250 characters.\n\n## Defaults and geometry\n\nA chart that sets none of the four fields renders and exports byte-for-byte as before; the preview tests and the PPTX goldens assert it. A chart with options reserves space for its legend and titles, so its geometry differs from a chart without them. The implementations require coordinated Core, renderer, PPTX and editor versions; use the [compatibility matrix](compatibility-matrix.md).\n\n## Not in this change\n\nPer-series data label overrides (column number formats are RR-54, [chart-table-data.md](chart-table-data.md)), a rotated or rich-text axis title, a chart title, a legend that overlays the plot, manual plot-area layout, secondary axes outside combo charts, and trendlines. Charts from external spreadsheets stay descoped (the `ChartDataSource` form was removed from the schema).\n'
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"slug": "chart-table-data",
|
|
23
23
|
"file": "docs/chart-table-data.md",
|
|
24
24
|
"title": "Chart and table data: numbers, formats, datasets and series mapping",
|
|
25
|
-
"markdown": '# Chart and table data: numbers, formats, datasets and series mapping\n\nStatus: RR-54 (release readiness). Additive schema plus one behaviour fix (strict chart numbers). These features are shipped in the coordinated package line; use the [compatibility matrix](compatibility-matrix.md) for the verified set. This page is the contract the four packages implement; change it here first.\n\n## Decisions (vetoable)\n\nRecorded 2026-10-05 for the owner, who asked for all five recommendations of the chart/table data review at once:\n\n1. **One strict chart number rule in core.** The preview read `"12%"` as a gap and `"1e6"` as 1000000; the exporter stripped every non-numeric character (`"1e6"` became 16, `"(5)"` became 5, `"Q1"` became 1). Both engines now call core `chartNumber`, which follows the existing data-import rule. A string that is not a strict decimal number is a gap in both engines and a `chart-value-not-numeric` warning, never a guessed value. Decks whose chart cells held `"12%"` or `"$5"` export a gap where they used to export 12 or 5.\n2. **`ChartDataSource` is removed from the schema.** No engine resolved it ([opf#240](https://github.com/OpenPresentation/opf/issues/240), descoped), so the format-audit program (FA-07, 2026-10-06) deleted it, with its `chart-data-source-unresolved` warning. A future data-resolver design re-adds it when it is built. Inline data records where it came from with `data.source`, which engines never read.\n3. **Number formats use the variables syntax.** One pattern language for `NumberVariable.format`, column formats and cell formats. Core converts it to an Excel format code for the PPTX export and back on import.\n4. **Datasets are top-level and shared.** A chart or a table references one with `{ "dataset": "<id>" }`. Core inlines references before composition, so the engines keep plotting and laying out inline data.\n5. **Series mapping is by column name.** `chart.mapping` picks the category, X and series columns. Absent keeps today\'s positional rule exactly.\n\nA document that uses none of the new fields validates, previews and exports byte-for-byte as before, except for decision 1 on non-numeric strings.\n\n## Schema\n\n### Number formats\n\n`NumberFormat` is a string with the `NumberVariable.format` syntax: an optional literal prefix, a numeric part made of `#`, `0`, `,` (grouping) and `.` (decimals), and an optional literal suffix. A `%` in the prefix or suffix multiplies the value by 100. Examples: `#,##0`, `0.0%`, `$#,##0.00`, `#,##0 units`. An invalid pattern is a `number-format-invalid` error. A format applies only to number values; strings, booleans, `null` and rich runs display unchanged.\n\nTwo spellings display differently in Excel, so prefer the alternatives in charts that are exported: optional decimals only (`0.##`) show a whole number as `5` in core and the preview but as `5.` (with a trailing point) in Excel, and an all-`#` integer part (`#`, `#,###`) shows zero as `0` in core but as nothing in Excel. Use `0.0#`-style decimals with at least one `0`, or a whole-number format, and an integer part that ends in `0`.\n\n### Columns\n\n`DataColumn` is `{ "name": string, "format"?: NumberFormat }`. Anywhere a chart or dataset column is a string, it may be a `DataColumn`; the string form is `{ "name": value }` with no format.\n\n```json\n"chart": {\n "type": "column",\n "data": {\n "columns": ["Quarter", { "name": "Revenue", "format": "$#,##0.0" }, { "name": "Margin", "format": "0%" }],\n "rows": [["Q1", 12.4, 0.31], ["Q2", 18.1, 0.34]]\n }\n}\n```\n\nA table column header may also be a `DataColumn`. A `StyledTableCell` gains an optional `format`:\n- on a body cell, it formats that cell\'s number value;\n- on a header cell (in `columns`), it is the column\'s format, the same as a `DataColumn` header.\n\nA body cell\'s own format wins over the column\'s.\n\n```json\n"table": {\n "columns": ["Region", { "name": "Revenue", "format": "$#,##0.0" }, { "value": "Growth", "style": { "align": "right" }, "format": "0%" }],\n "rows": [["EMEA", 8.2, 0.4], ["APAC", 6.1, { "value": 0.52, "format": "0.0%" }]]\n}\n```\n\n### Data source provenance\n\n`DataSourceRef` is `{ "src": string, "sheet"?, "range"?, "fields"?: string[], "retrieved"?: string (ISO date or date-time), "description"?: string }`. It records where inline data or a dataset came from. Engines never read, fetch or refresh it; they keep it through editing, export and re-import. It may appear as `ChartData.source` and `Dataset.source`.\n\nThere is no chart data source by file or asset (`chart.data` with `src`, `sheet`, `range`, `columns`): that form is removed (opf#240, descoped) and is a schema error. Import the data inline with `columns`/`rows` and record its origin in `source`.\n\n### Datasets\n\nTop-level `datasets` maps ids (the `assets` id pattern) to a `Dataset`:\n\n```json\n{\n "datasets": {\n "revenue": {\n "title": "Revenue by quarter",\n "columns": ["Quarter", { "name": "Revenue", "format": "$#,##0.0" }, "Costs"],\n "rows": [["Q1", 12, 8], ["Q2", 18, 11], ["Q3", 24, 15]],\n "source": { "src": "./data/revenue.csv", "retrieved": "2026-10-05" }\n }\n }\n}\n```\n\nRows hold `ChartDataCell` scalars (string, number, boolean, null). A `DatasetRef` is `{ "dataset": "<id>", "fields"?: string[] }`; `fields` selects and orders columns by name, each at most once (a repeated name is a schema error).\n- **Chart:** `chart.data` may be a `DatasetRef`. It is the second `oneOf` branch beside `ChartData`.\n- **Table:** a table is either inline (`rows` required, optional `columns`) or dataset-backed (`dataset` required, optional `fields`, and no `rows` or `columns`). Dataset tables take their headers and column formats from the dataset; per-cell styles need an inline table.\n\nValidation:\n- an unknown dataset id is a `dataset-unknown` error;\n- an unknown field is a `dataset-field-unknown` error;\n- duplicate column names in a dataset, or in chart data that a `mapping` or `fields` addresses, are a `data-column-duplicate` error;\n- a dataset nothing references is the warning `opf/unused-dataset` (category `references`).\n\n### Series mapping\n\n`chart.mapping` is `{ "category"?: string, "x"?: string, "series"?: string[] }`, naming columns after any `fields` selection:\n- `category`: the label column. Default: the first column.\n- `x`: the X column of an XY (scatter) chart. Default, today\'s rule: the second column (the first when `category` names the second), and only when the chart has three or more columns. With two columns there is no X column: the second column is the one series, plotted against row numbers. An `x` that leaves no series (two columns, or every series dropped) is a `chart-mapping-adapted` warning and that column is plotted as the series. A chart is XY when its type resolves (deprecated ids through their replacement) to the scatter construct of `chartOptionTarget`. An `x` equal to the category is a `chart-mapping-adapted` warning and the default is used. On a chart type without an X axis it is dropped with a `chart-mapping-adapted` warning.\n- `series`: the plotted columns, in order. Default: every column that is not `category` or `x`.\n\nAn unknown name is a `chart-mapping-unknown-column` error. A series that repeats the category or X column is a `chart-mapping-adapted` warning and is dropped. Mapping only reorders and selects columns; pie, doughnut and single-series constructs still plot one series (`series-dropped`, as today).\n\n## Core runtime API\n\nExported from `@openpresentation/opf` and `@openpresentation/opf/data` (module `src/chart-data.ts`; the resolvers are also on `@openpresentation/opf/composition`). Inputs are typed `unknown` so engines can pass any parsed chart, table or document; the result types are exported (`ResolvedChartData`, `ResolvedTableData`, `DataDiagnostic`, `DataColumn`, `DataSourceRef`, `Dataset`, `DatasetRef`, `ChartMapping`, `DataCellValue`, `DataTableCell`, `DataTableHeader`, `DataStyledCell`).\n\n```ts\n/** Strict chart number: finite numbers, and strings in strict decimal syntax (trimmed). Everything else is null (a gap), including "+5", ".5", "5.", "007" and plain digit strings beyond the safe integer range (usually identifiers that would lose digits); a decimal or exponent form such as "1e20" is a number. */\nexport function chartNumber(value: unknown): number | null;\n/** Format a number with a NumberFormat; an absent or invalid format prints the General form (String(value)). */\nexport function formatDataNumber(value: number, format?: string): string;\n/** Why a NumberFormat is invalid (the number-format-invalid message), or undefined when it is valid or absent. */\nexport function numberFormatError(format: unknown): string | undefined;\n/** NumberFormat -> Excel format code ("General" when absent or invalid). Literal prefix/suffix text is quoted as one run ("0.0 m/s" -> \'0.0 "m/s"\'), so \'/\' (Excel\'s fraction bar), \'E+\', \'@\', \'*\', \'_\' and \'?\' are never bare (native check opf#387); placeholders are written \'#\' before \'0\' ("0#" -> "#0"). */\nexport function toExcelNumberFormat(format?: string): string;\n/** Excel format code -> NumberFormat, or undefined when the code has no exact NumberFormat equivalent (General, sections, scaling commas, and placeholder orders Excel reads by position such as "0#" or "0.#0"). */\nexport function fromExcelNumberFormat(code: string): string | undefined;\n/** Pure: a copy of the document where every chart and table DatasetRef is replaced by inline data (columns as DataColumn when a format applies, rows copied; a chart also takes the dataset\'s `source`). `datasets` stays in place. Unknown ids, and references naming an unknown field, are left as they are. */\nexport function inlineDatasets<T>(document: T): T;\n/** One table or chart in inline form (a dataset reference copied from document.datasets); anything else is returned as is. */\nexport function inlineTableData<T>(table: T, document?: unknown): T;\nexport function inlineChartData<T>(chart: T, document?: unknown): T;\n/**\n * Resolve a chart\'s data to the canonical positional table: [category, (x,) ...series]. hasX is true when columns[1] is the X\n * column: an XY chart with three or more resolved columns (with two, the second column is the series against row numbers).\n * Category cells are kept as authored; X and series cells pass through chartNumber. Data with a single column has no\n * category: that column is the chart\'s values and passes through chartNumber too (with chart-value-not-numeric per cell).\n * Inline ChartData, a DatasetRef (needs the document) and mapping are resolved here.\n * options.path is the chart\'s JSON Pointer, the base of diagnostic paths (dataset cells report at /datasets/<id>/...).\n */\nexport function resolveChartData(chart: unknown, document?: unknown, options?: { path?: string }):\n | { ok: true; columns: string[]; hasX: boolean; formats: (string | undefined)[]; rows: (string | number | boolean | null)[][]; source?: DataSourceRef; dataset?: string; diagnostics: DataDiagnostic[] }\n | { ok: false; reason: "dataset-unknown" | "no-rows" | "no-columns"; message: string; diagnostics: DataDiagnostic[] };\n/** Resolve a table (inline or dataset-backed) to headers, rows and per-column formats. */\nexport function resolveTableData(table: unknown, document?: unknown, options?: { path?: string }):\n { columns?: DataTableHeader[]; rows: DataTableCell[][]; formats: (string | undefined)[]; dataset?: string; diagnostics: DataDiagnostic[] };\n/** Display value of a table cell: a number with a valid format (cell, else column) becomes formatted text (a styled cell keeps its style with the text as value); anything else is returned unchanged. */\nexport function tableCellDisplayValue(cell: DataTableCell, columnFormat?: string): DataTableCell;\n/** Migration help (see below): the patch that stores one chart value column\'s text as numbers with the format that shows the same text, or undefined. */\nexport function suggestChartNumberFix(chart: unknown, document?: unknown, options?: { path?: string; column?: number | string }):\n { patches: { op: "replace"; path: string; value: unknown }[]; column: number; name: string; format: string } | undefined;\n/** Helpers: dataset-level diagnostics (duplicate names, invalid formats), unreferenced dataset ids, the XY test, the DatasetRef test. */\nexport function datasetDiagnostics(document: unknown): DataDiagnostic[];\nexport function unusedDatasets(document: unknown): string[];\nexport function isXYChartType(type: unknown): boolean;\nexport function isDatasetRef(value: unknown): value is DatasetRef;\n```\n\n`DataDiagnostic` is `{ code, severity: "error" | "warning", path, message }`, with the codes above plus `chart-value-not-numeric`. `chartNumber` runs after variables are filled. Before filling, the validator does not warn on a `var:<id>` cell whose variable is a number. `chart-value-not-numeric` is reported for any value cell that `chartNumber` rejects (strings and booleans), never for `null` or `""`. An unknown `fields` entry is a `dataset-field-unknown` error and `resolveChartData`/`resolveTableData` leave that column out. `number-format-invalid` applies to `DataColumn.format` and `StyledTableCell.format`; `NumberVariable.format` keeps its existing check (`variable-format` where it is used), so no existing document gains an error.\n\n`validate` reports the codes as `opf/<code>` rule ids: the errors are `format` findings, `opf/chart-value-not-numeric` and `opf/chart-mapping-adapted` are `content` warnings, `opf/chart-data-source-unresolved` is a `references` warning, and `references` adds `opf/unused-dataset`. The raw schema issue of a finding is in its `validation` field (`validation.params.code`).\n\n### Migration help\n\nDecks written before the strict rule often hold display text in chart cells. When every text cell of a value column (an X or series column, or a lone column) is written in one display style that a NumberFormat reproduces exactly, `suggestChartNumberFix` returns the fix, and `validate` attaches it to each of the column\'s `opf/chart-value-not-numeric` warnings as `fixes: [{ id: "store-chart-numbers", title, kind: "patch", safe: false, patch }]` (a [finding fix](finding-schema-reference.md); core never applies it):\n\n- `"12%"`, `"8.5%"` become 0.12 and 0.085 with the column format `0.#%`;\n- `"$1,234"`, `"$56"` become 1234 and 56 with `$#,##0` (also `\u20AC`, `\xA3`, `\xA5`);\n- `"1,234.5"`, `"999"` become 1234.5 (the plain number stays) with `#,##0.##`.\n\nThe patch replaces each text cell with its number and the column with a `DataColumn` that has the format; a dataset column is fixed in the dataset, so every chart and table that uses it changes, and its tables show the same text. A style is one optional leading minus, one optional currency symbol, digits (grouped in threes or plain), optional decimals and an optional `%`. There is no fix when the column already has a format, when styles are mixed (`"12%"` beside `"$5"` or beside the number 0.5), for accounting negatives `"(5)"`, `"$-5"`, a decimal comma (`"1.234,5"`), units (`"5 units"`), `".5"` or `"007"`, for mixed grouping (`"1,234"` beside `"5678"`), or when one format cannot show every value as written (`"$5"` beside `"$5.50"`). Every fixed value is checked: `formatDataNumber(value, format)` equals the text as written.\n\nCore composition, table layout and pagination accept dataset-backed tables and charts by inlining first. Table layout measures the formatted text. Markdown conversion, diff, merge, patch, validate and format keep the new fields. In detail:\n\n- `composeSlide` inlines a dataset-backed `table` or `chart` from `options.presentation.datasets`, so the composed item\'s `value` (and `payload`) is the inline copy; the item `path` still points at the authored field. `layoutTable(value, box, { presentation })` does the same, and each body cell\'s `TableCellLayout.value` is its display value (`tableCellDisplayValue`); `input` stays the authored cell. A DataColumn header shows its `name` (`path` ends in `.name`).\n- `paginateSlide`/`paginate` validate a slide with the deck\'s `datasets` (and `references`). A dataset table that has to be split is written to the continuation slides as inline tables (the slide that fits is returned unchanged, with its reference).\n- `convertContent` keeps a dataset reference between chart and table (`fields` too; `mapping` is reported as lost) and needs `options.presentation` to convert a dataset table to any other kind. Inline conversions keep DataColumn formats and report lost number formats and `source`. Slide-level conversions validate with the document\'s datasets; slide-only structure edits skip the dataset checks they cannot make.\n- Markdown writes the new fields in its embedded YAML form (lossless); `datasets` go to the front matter. `format` orders `datasets`, `mapping`, `source` and `DataColumn` keys by the schema. `diff` reports changes under `datasets` in their own category. The chart rules of `validate` read resolved chart data (DataColumn names, dataset charts).\n- Variables: `resolveVariables` walks `datasets` like any other content, so `{{id}}` tokens and whole `var:<id>` cells in dataset rows are filled before engines inline the reference.\n- `opf ingest --dataset <id>` (CLI) writes the imported columns and rows into `datasets.<id>` and references it. Re-importing into an existing dataset keeps its title, description and the format of each column whose name is unchanged; `source.src` names the imported file relative to the deck file\'s folder with `/` separators (stdin: no source), and `source.retrieved` is set only by `--date YYYY-MM-DD`; the command never reads the clock.\n- The generated `Table` type now has `rows?` (a dataset table has none) and `dataset?`/`fields?`; TypeScript callers that read `table.rows` handle the dataset form (or call `resolveTableData`).\n\n## Engines\n\nAll three engines read core\'s functions when they exist and fall back to their previous behaviour on a core without RR-54, so each can ship before or after core. A document that uses none of the new fields renders, exports and edits byte for byte as before (renderer: all 805 slides of the core examples; PPTX: 21 golden digests; editor: identical patches).\n\n### Preview (opf-render, [opf-render#127](https://github.com/OpenPresentation/opf-render/pull/127))\n\n- **Data:** every chart path reads `resolveChartData`: the catalog charts, the chartex constructs, scatter, pie and doughnut, single-column charts, and the legacy sketch. Its own `chartNumber` delegates to core\'s, so `"0x10"` is now a gap too. Data that does not resolve keeps the "No chart data" placeholder. A document with an unknown dataset or mapping column fails validation (`invalid-opf`) like any other invalid document.\n- **Formats:**\n - Data labels use their series\' format, through `formatDataNumber`.\n - Value-axis ticks use the first plotted series\' format, as Excel does for a source-linked axis.\n - The scatter X axis and X labels use the X column\'s format.\n - Percent axes (100% stacked, pareto) and pie percent labels stay percent.\n - A histogram of a lone column plots counts and takes no format.\n- **Tables:** cells draw core layout\'s display text, so a body cell\'s own `format` wins over its column\'s.\n- **Tracing:** the editor relies on each drawn part\'s authored path.\n - Every part of a dataset-backed chart or table traces to its authored `slides.N.chart` or `slides.N.table` path, because it has no rows of its own.\n - A chart with `mapping` traces each part to the authored column index.\n\n### PPTX export (opf-pptx, [opf-pptx#171](https://github.com/OpenPresentation/opf-pptx/pull/171))\n\n- **Numbers:** the export resolves data through `resolveChartData`, and the lenient `parsedNumber`/`numericValue` are removed. Non-numeric strings export as gaps, with one `chart-value-not-numeric` diagnostic per chart that carries a `count`.\n- **Classic charts:** `toExcelNumberFormat` codes are written as:\n - the series `c:numCache/c:formatCode` (added where PptxGenJS writes none, such as pie and doughnut);\n - the series and chart-group data-label `c:numFmt sourceLinked="0"`;\n - the value-axis `c:numFmt`. The first plotted series\' format is used, a scatter X axis takes the X column\'s, and a 100% stacked axis keeps `0%`.\n - Labels that show percentages keep PowerPoint\'s percent form.\n- **Chartex:** `cx:lvl formatCode` and the value-label and value-axis `cx:numFmt`, except on binned constructs.\n- **Embedded workbook:** custom `numFmt` entries from id 164, with cell styles on every value cell, so Edit Data shows the formats.\n- **Tables:** tables export core layout\'s display text.\n- **Datasets and mapping:** a chart that uses them exports exactly like its inline equivalent.\n- **Provenance:** in `full` mode, `OPF_DATASETS_V1` holds the `datasets` map. `OPF_DATA_V1` sits on each chart or table frame that uses a new field and holds the authored `data`, `mapping` and table form (and, FA-14, a chart\'s `highlight`, which then also records its inline `data`; see [chart-options.md](chart-options.md)), plus a hash of the cached names, values and format codes. `references-only` mode and `provenance: false` write neither tag.\n\n### PPTX import (opf-pptx)\n\n- **With provenance:** `datasets` is restored before document provenance validates. A frame\'s dataset reference, `mapping`, formats and `source` are restored only while the cache hash still matches. If PowerPoint (or a person) changed the values, the native values import and a diagnostic says why, such as `chart-data-provenance-changed` or `table-dataset-unavailable`. The hash compares format codes in core\'s canonical form (`fromExcelNumberFormat`), so PowerPoint\'s re-spellings on save (`\\$#,##0.0` for `$#,##0.0`, `#,##0\\ "units"` for `#,##0 "units"`, native check [opf#385](https://github.com/OpenPresentation/opf/pull/385)) still match; a General code counts as no code.\n- **Without provenance:** a cache `formatCode` that `fromExcelNumberFormat` maps back becomes that column\'s `{ name, format }`, for both classic and chartex charts. Core returns the canonical spelling, such as `#,##0 units` for `#,##0 "units"`. Other codes are reported as `chart-number-format-adapted` and never invented.\n- **Cached values:** these follow the strict rule as well. XML decimal forms (`+5`, `.5`, `007`) are numbers; anything else is a gap with a diagnostic, never a stripped number or 0.\n\n### Editor (opf-editor, [opf-editor#92](https://github.com/OpenPresentation/opf-editor/pull/92))\n\n- **DataColumn headers:** the data grid shows a `DataColumn` header\'s `name`. Rename, insert, delete, move and paste keep each column\'s format.\n- **Chart cells:** they follow `chartNumber`. Text that is not a number is refused with the reason, and a stored string read as a gap is flagged.\n- **Shared datasets:** a dataset-backed chart or table is edited at `/datasets/<id>` through `fields`.\n - Hidden columns keep their data.\n - A column added while `fields` is set goes to both the dataset and `fields`.\n - A rename updates every `fields` and `mapping` that names the column, in the same patch.\n - Deleting a column another item uses is refused with `dataset-column-in-use`.\n - The status line says how many items share the dataset, and "Use a copy of the data" (`detachGridDataset`) inlines one item\'s data.\n- **Format and mapping:**\n - A per-column format field (`setGridColumnFormat`) is checked with `numberFormatError`.\n - A "Chart columns" panel (`setChartMapping`) sets the category, the X column (XY charts only) and the series, and writes only what differs from the default.\n- **Import:** "Store as a shared dataset" (`prepareDatasetImport`) imports into `datasets.<id>`, as `opf ingest --dataset` does.\n- **Other panels:** the table style panel and `table-options` refuse a dataset table with `table-dataset-backed`. Find and replace also searches dataset text.\n- **Undo:** each edit is one undoable patch.\n\n### Native PowerPoint checks\n\nWanted before release:\n- formatted data labels and value axes;\n- Edit Data shows the workbook cell formats with no repair prompt;\n- no repair prompt for the frame `custDataLst`;\n- after a plain save in PowerPoint, the data tags and cache hash survive;\n- the two spellings above (`0.##` on a whole number, `#` on zero) show as described.\n\nSample decks are written by the PPTX branch under `artifacts/rr-54-native/`.\n\n## Not in this change\n\n- Resolving a data source by file or asset, or refreshing `source` ([opf#240](https://github.com/OpenPresentation/opf/issues/240)).\n- Date axes and date formats.\n- Per-series colours. (Columns with line series, optionally on a secondary axis, are the `combo` chart type: [chart-options.md](chart-options.md#combo-charts).)\n- Formulas.\n- Rich text in dataset cells.\n'
|
|
25
|
+
"markdown": '# Chart and table data: numbers, formats, datasets and series mapping\n\nStatus: RR-54 (release readiness). Additive schema plus one behaviour fix (strict chart numbers). These features are shipped in the coordinated package line; use the [compatibility matrix](compatibility-matrix.md) for the verified set. This page is the contract the four packages implement; change it here first.\n\n## Decisions (vetoable)\n\nRecorded 2026-10-05 for the owner, who asked for all five recommendations of the chart/table data review at once:\n\n1. **One strict chart number rule in core.** The preview read `"12%"` as a gap and `"1e6"` as 1000000; the exporter stripped every non-numeric character (`"1e6"` became 16, `"(5)"` became 5, `"Q1"` became 1). Both engines now call core `chartNumber`, which follows the existing data-import rule. A string that is not a strict decimal number is a gap in both engines and a `chart-value-not-numeric` warning, never a guessed value. Decks whose chart cells held `"12%"` or `"$5"` export a gap where they used to export 12 or 5.\n2. **`ChartDataSource` is removed from the schema.** No engine resolved it ([opf#240](https://github.com/OpenPresentation/opf/issues/240), descoped), so the format-audit program (FA-07, 2026-10-06) deleted it, with its `chart-data-source-unresolved` warning. A future data-resolver design re-adds it when it is built. Inline data records where it came from with `data.source`, which engines never read.\n3. **Number formats use the variables syntax.** One pattern language for `NumberVariable.format`, column formats and cell formats. Core converts it to an Excel format code for the PPTX export and back on import.\n4. **Datasets are top-level and shared.** A chart or a table references one with `{ "dataset": "<id>" }`. Core inlines references before composition, so the engines keep plotting and laying out inline data.\n5. **Series mapping is by column name.** `chart.mapping` picks the category, X and series columns. Absent keeps today\'s positional rule exactly.\n\nA document that uses none of the new fields validates, previews and exports byte-for-byte as before, except for decision 1 on non-numeric strings.\n\n## Schema\n\n### Number formats\n\n`NumberFormat` is a string with the `NumberVariable.format` syntax: an optional literal prefix, a numeric part made of `#`, `0`, `,` (grouping) and `.` (decimals), and an optional literal suffix. A `%` in the prefix or suffix multiplies the value by 100. Examples: `#,##0`, `0.0%`, `$#,##0.00`, `#,##0 units`. An invalid pattern is a `number-format-invalid` error. A format applies only to number values; strings, booleans, `null` and rich runs display unchanged.\n\nTwo spellings display differently in Excel, so prefer the alternatives in charts that are exported: optional decimals only (`0.##`) show a whole number as `5` in core and the preview but as `5.` (with a trailing point) in Excel, and an all-`#` integer part (`#`, `#,###`) shows zero as `0` in core but as nothing in Excel. Use `0.0#`-style decimals with at least one `0`, or a whole-number format, and an integer part that ends in `0`.\n\n### Columns\n\n`DataColumn` is `{ "name": string, "format"?: NumberFormat }`. Anywhere a chart or dataset column is a string, it may be a `DataColumn`; the string form is `{ "name": value }` with no format.\n\n```json\n"chart": {\n "type": "column",\n "data": {\n "columns": ["Quarter", { "name": "Revenue", "format": "$#,##0.0" }, { "name": "Margin", "format": "0%" }],\n "rows": [["Q1", 12.4, 0.31], ["Q2", 18.1, 0.34]]\n }\n}\n```\n\nA table column header may also be a `DataColumn`. A `StyledTableCell` gains an optional `format`:\n- on a body cell, it formats that cell\'s number value;\n- on a header cell (in `columns`), it is the column\'s format, the same as a `DataColumn` header.\n\nA body cell\'s own format wins over the column\'s.\n\n```json\n"table": {\n "columns": ["Region", { "name": "Revenue", "format": "$#,##0.0" }, { "value": "Growth", "style": { "align": "right" }, "format": "0%" }],\n "rows": [["EMEA", 8.2, 0.4], ["APAC", 6.1, { "value": 0.52, "format": "0.0%" }]]\n}\n```\n\n### Data source provenance\n\n`DataSourceRef` is `{ "src": string, "sheet"?, "range"?, "fields"?: string[], "retrieved"?: string (ISO date or date-time), "description"?: string }`. It records where inline data or a dataset came from. Engines never read, fetch or refresh it; they keep it through editing, export and re-import. It may appear as `ChartData.source` and `Dataset.source`.\n\nThere is no chart data source by file or asset (`chart.data` with `src`, `sheet`, `range`, `columns`): that form is removed (opf#240, descoped) and is a schema error. Import the data inline with `columns`/`rows` and record its origin in `source`.\n\n### Datasets\n\nTop-level `datasets` maps ids (the `assets` id pattern) to a `Dataset`:\n\n```json\n{\n "datasets": {\n "revenue": {\n "title": "Revenue by quarter",\n "columns": ["Quarter", { "name": "Revenue", "format": "$#,##0.0" }, "Costs"],\n "rows": [["Q1", 12, 8], ["Q2", 18, 11], ["Q3", 24, 15]],\n "source": { "src": "./data/revenue.csv", "retrieved": "2026-10-05" }\n }\n }\n}\n```\n\nRows hold `ChartDataCell` scalars (string, number, boolean, null). A `DatasetRef` is `{ "dataset": "<id>", "fields"?: string[] }`; `fields` selects and orders columns by name, each at most once (a repeated name is a schema error).\n- **Chart:** `chart.data` may be a `DatasetRef`. It is the second `oneOf` branch beside `ChartData`.\n- **Table:** a table is either inline (`rows` required, optional `columns`) or dataset-backed (`dataset` required, optional `fields`, and no `rows` or `columns`). Dataset tables take their headers and column formats from the dataset; per-cell styles need an inline table.\n\nValidation:\n- an unknown dataset id is a `dataset-unknown` error;\n- an unknown field is a `dataset-field-unknown` error;\n- duplicate column names in a dataset, or in chart data that a `mapping` or `fields` addresses, are a `data-column-duplicate` error;\n- a dataset nothing references is the warning `opf/unused-dataset` (category `references`).\n\n### Series mapping\n\n`chart.mapping` is `{ "category"?: string, "x"?: string, "series"?: string[] }`, naming columns after any `fields` selection:\n- `category`: the label column. Default: the first column.\n- `x`: the X column of an XY (scatter) chart. Default, today\'s rule: the second column (the first when `category` names the second), and only when the chart has three or more columns. With two columns there is no X column: the second column is the one series, plotted against row numbers. An `x` that leaves no series (two columns, or every series dropped) is a `chart-mapping-adapted` warning and that column is plotted as the series. A chart is XY when its type resolves (deprecated ids through their replacement) to the scatter construct of `chartOptionTarget`. An `x` equal to the category is a `chart-mapping-adapted` warning and the default is used. On a chart type without an X axis it is dropped with a `chart-mapping-adapted` warning.\n- `series`: the plotted columns, in order. Default: every column that is not `category` or `x`.\n\nAn unknown name is a `chart-mapping-unknown-column` error. A series that repeats the category or X column is a `chart-mapping-adapted` warning and is dropped. Mapping only reorders and selects columns; pie, doughnut and single-series constructs still plot one series (`series-dropped`, as today).\n\n## Core runtime API\n\nExported from `@openpresentation/opf` and `@openpresentation/opf/data` (module `src/chart-data.ts`; the resolvers are also on `@openpresentation/opf/composition`). Inputs are typed `unknown` so engines can pass any parsed chart, table or document; the result types are exported (`ResolvedChartData`, `ResolvedTableData`, `DataDiagnostic`, `DataColumn`, `DataSourceRef`, `Dataset`, `DatasetRef`, `ChartMapping`, `DataCellValue`, `DataTableCell`, `DataTableHeader`, `DataStyledCell`).\n\n```ts\n/** Strict chart number: finite numbers, and strings in strict decimal syntax (trimmed). Everything else is null (a gap), including "+5", ".5", "5.", "007" and plain digit strings beyond the safe integer range (usually identifiers that would lose digits); a decimal or exponent form such as "1e20" is a number. */\nexport function chartNumber(value: unknown): number | null;\n/** Format a number with a NumberFormat; an absent or invalid format prints the General form (String(value)). */\nexport function formatDataNumber(value: number, format?: string): string;\n/** Why a NumberFormat is invalid (the number-format-invalid message), or undefined when it is valid or absent. */\nexport function numberFormatError(format: unknown): string | undefined;\n/** NumberFormat -> Excel format code ("General" when absent or invalid). Literal prefix/suffix text is quoted as one run ("0.0 m/s" -> \'0.0 "m/s"\'), so \'/\' (Excel\'s fraction bar), \'E+\', \'@\', \'*\', \'_\' and \'?\' are never bare (native check opf#387); placeholders are written \'#\' before \'0\' ("0#" -> "#0"). */\nexport function toExcelNumberFormat(format?: string): string;\n/** Excel format code -> NumberFormat, or undefined when the code has no exact NumberFormat equivalent (General, sections, scaling commas, and placeholder orders Excel reads by position such as "0#" or "0.#0"). */\nexport function fromExcelNumberFormat(code: string): string | undefined;\n/** Pure: a copy of the document where every chart and table DatasetRef is replaced by inline data (columns as DataColumn when a format applies, rows copied; a chart also takes the dataset\'s `source`). `datasets` stays in place. Unknown ids, and references naming an unknown field, are left as they are. */\nexport function inlineDatasets<T>(document: T): T;\n/** One table or chart in inline form (a dataset reference copied from document.datasets); anything else is returned as is. */\nexport function inlineTableData<T>(table: T, document?: unknown): T;\nexport function inlineChartData<T>(chart: T, document?: unknown): T;\n/**\n * Resolve a chart\'s data to the canonical positional table: [category, (x,) ...series]. hasX is true when columns[1] is the X\n * column: an XY chart with three or more resolved columns (with two, the second column is the series against row numbers).\n * Category cells are kept as authored; X and series cells pass through chartNumber. Data with a single column has no\n * category: that column is the chart\'s values and passes through chartNumber too (with chart-value-not-numeric per cell).\n * Inline ChartData, a DatasetRef (needs the document) and mapping are resolved here.\n * options.path is the chart\'s JSON Pointer, the base of diagnostic paths (dataset cells report at /datasets/<id>/...).\n */\nexport function resolveChartData(chart: unknown, document?: unknown, options?: { path?: string }):\n | { ok: true; columns: string[]; hasX: boolean; formats: (string | undefined)[]; rows: (string | number | boolean | null)[][]; source?: DataSourceRef; dataset?: string; diagnostics: DataDiagnostic[] }\n | { ok: false; reason: "dataset-unknown" | "no-rows" | "no-columns"; message: string; diagnostics: DataDiagnostic[] };\n/** Resolve a table (inline or dataset-backed) to headers, rows and per-column formats. */\nexport function resolveTableData(table: unknown, document?: unknown, options?: { path?: string }):\n { columns?: DataTableHeader[]; rows: DataTableCell[][]; formats: (string | undefined)[]; dataset?: string; diagnostics: DataDiagnostic[] };\n/** Display value of a table cell: a number with a valid format (cell, else column) becomes formatted text (a styled cell keeps its style with the text as value); anything else is returned unchanged. */\nexport function tableCellDisplayValue(cell: DataTableCell, columnFormat?: string): DataTableCell;\n/** Migration help (see below): the patch that stores one chart value column\'s text as numbers with the format that shows the same text, or undefined. */\nexport function suggestChartNumberFix(chart: unknown, document?: unknown, options?: { path?: string; column?: number | string }):\n { patches: { op: "replace"; path: string; value: unknown }[]; column: number; name: string; format: string } | undefined;\n/** Helpers: dataset-level diagnostics (duplicate names, invalid formats), unreferenced dataset ids, the XY test, the DatasetRef test. */\nexport function datasetDiagnostics(document: unknown): DataDiagnostic[];\nexport function unusedDatasets(document: unknown): string[];\nexport function isXYChartType(type: unknown): boolean;\nexport function isDatasetRef(value: unknown): value is DatasetRef;\n```\n\n`DataDiagnostic` is `{ code, severity: "error" | "warning", path, message }`, with the codes above plus `chart-value-not-numeric`. `chartNumber` runs after variables are filled. Before filling, the validator does not warn on a `var:<id>` cell whose variable is a number. `chart-value-not-numeric` is reported for any value cell that `chartNumber` rejects (strings and booleans), never for `null` or `""`. An unknown `fields` entry is a `dataset-field-unknown` error and `resolveChartData`/`resolveTableData` leave that column out. `number-format-invalid` applies to `DataColumn.format` and `StyledTableCell.format`; `NumberVariable.format` keeps its existing check (`variable-format` where it is used), so no existing document gains an error.\n\n`validate` reports the codes as `opf/<code>` rule ids: the errors are `format` findings, `opf/chart-value-not-numeric` and `opf/chart-mapping-adapted` are `content` warnings, `opf/chart-data-source-unresolved` is a `references` warning, and `references` adds `opf/unused-dataset`. The raw schema issue of a finding is in its `validation` field (`validation.params.code`).\n\n### Migration help\n\nDecks written before the strict rule often hold display text in chart cells. When every text cell of a value column (an X or series column, or a lone column) is written in one display style that a NumberFormat reproduces exactly, `suggestChartNumberFix` returns the fix, and `validate` attaches it to each of the column\'s `opf/chart-value-not-numeric` warnings as `fixes: [{ id: "store-chart-numbers", title, kind: "patch", safe: false, patch }]` (a [finding fix](finding-schema-reference.md); core never applies it):\n\n- `"12%"`, `"8.5%"` become 0.12 and 0.085 with the column format `0.#%`;\n- `"$1,234"`, `"$56"` become 1234 and 56 with `$#,##0` (also `\u20AC`, `\xA3`, `\xA5`);\n- `"1,234.5"`, `"999"` become 1234.5 (the plain number stays) with `#,##0.##`.\n\nThe patch replaces each text cell with its number and the column with a `DataColumn` that has the format; a dataset column is fixed in the dataset, so every chart and table that uses it changes, and its tables show the same text. A style is one optional leading minus, one optional currency symbol, digits (grouped in threes or plain), optional decimals and an optional `%`. There is no fix when the column already has a format, when styles are mixed (`"12%"` beside `"$5"` or beside the number 0.5), for accounting negatives `"(5)"`, `"$-5"`, a decimal comma (`"1.234,5"`), units (`"5 units"`), `".5"` or `"007"`, for mixed grouping (`"1,234"` beside `"5678"`), or when one format cannot show every value as written (`"$5"` beside `"$5.50"`). Every fixed value is checked: `formatDataNumber(value, format)` equals the text as written.\n\nCore composition, table layout and pagination accept dataset-backed tables and charts by inlining first. Table layout measures the formatted text. Markdown conversion, diff, merge, patch, validate and format keep the new fields. In detail:\n\n- `composeSlide` inlines a dataset-backed `table` or `chart` from `options.presentation.datasets`, so the composed item\'s `value` (and `payload`) is the inline copy; the item `path` still points at the authored field. `layoutTable(value, box, { presentation })` does the same, and each body cell\'s `TableCellLayout.value` is its display value (`tableCellDisplayValue`); `input` stays the authored cell. A DataColumn header shows its `name` (`path` ends in `.name`).\n- `paginateSlide`/`paginate` validate a slide with the deck\'s `datasets` (and `references`). A dataset table that has to be split is written to the continuation slides as inline tables (the slide that fits is returned unchanged, with its reference).\n- `convertContent` keeps a dataset reference between chart and table (`fields` too; `mapping` is reported as lost) and needs `options.presentation` to convert a dataset table to any other kind. Inline conversions keep DataColumn formats and report lost number formats and `source`. Slide-level conversions validate with the document\'s datasets; slide-only structure edits skip the dataset checks they cannot make.\n- Markdown writes the new fields in its embedded YAML form (lossless); `datasets` go to the front matter. `format` orders `datasets`, `mapping`, `source` and `DataColumn` keys by the schema. `diff` reports changes under `datasets` in their own category. The chart rules of `validate` read resolved chart data (DataColumn names, dataset charts).\n- Variables: `resolveVariables` walks `datasets` like any other content, so `{{id}}` tokens and whole `var:<id>` cells in dataset rows are filled before engines inline the reference.\n- `opf ingest --dataset <id>` (CLI) writes the imported columns and rows into `datasets.<id>` and references it. Re-importing into an existing dataset keeps its title, description and the format of each column whose name is unchanged; `source.src` names the imported file relative to the deck file\'s folder with `/` separators (stdin: no source), and `source.retrieved` is set only by `--date YYYY-MM-DD`; the command never reads the clock.\n- The generated `Table` type now has `rows?` (a dataset table has none) and `dataset?`/`fields?`; TypeScript callers that read `table.rows` handle the dataset form (or call `resolveTableData`).\n\n## Engines\n\nAll three engines read core\'s functions when they exist and fall back to their previous behaviour on a core without RR-54, so each can ship before or after core. A document that uses none of the new fields renders, exports and edits byte for byte as before (renderer: all 805 slides of the core examples; PPTX: 21 golden digests; editor: identical patches).\n\n### Preview (opf-render, [opf-render#127](https://github.com/OpenPresentation/opf-render/pull/127))\n\n- **Data:** every chart path reads `resolveChartData`: the catalog charts, the chartex constructs, scatter, pie and doughnut, single-column charts, and the legacy sketch. Its own `chartNumber` delegates to core\'s, so `"0x10"` is now a gap too. Data that does not resolve keeps the "No chart data" placeholder. A document with an unknown dataset or mapping column fails validation (`invalid-opf`) like any other invalid document.\n- **Formats:**\n - Data labels use their series\' format, through `formatDataNumber`.\n - Value-axis ticks use the first plotted series\' format, as Excel does for a source-linked axis.\n - The scatter X axis and X labels use the X column\'s format.\n - Percent axes (100% stacked, pareto) and pie percent labels stay percent.\n - A histogram of a lone column plots counts and takes no format.\n- **Tables:** cells draw core layout\'s display text, so a body cell\'s own `format` wins over its column\'s.\n- **Tracing:** the editor relies on each drawn part\'s authored path.\n - Every part of a dataset-backed chart or table traces to its authored `slides.N.chart` or `slides.N.table` path, because it has no rows of its own.\n - A chart with `mapping` traces each part to the authored column index.\n\n### PPTX export (opf-pptx, [opf-pptx#171](https://github.com/OpenPresentation/opf-pptx/pull/171))\n\n- **Numbers:** the export resolves data through `resolveChartData`, and the lenient `parsedNumber`/`numericValue` are removed. Non-numeric strings export as gaps, with one `chart-value-not-numeric` diagnostic per chart that carries a `count`.\n- **Classic charts:** `toExcelNumberFormat` codes are written as:\n - the series `c:numCache/c:formatCode` (added where PptxGenJS writes none, such as pie and doughnut);\n - the series and chart-group data-label `c:numFmt sourceLinked="0"`;\n - the value-axis `c:numFmt`. The first plotted series\' format is used, a scatter X axis takes the X column\'s, and a 100% stacked axis keeps `0%`.\n - Labels that show percentages keep PowerPoint\'s percent form.\n- **Chartex:** `cx:lvl formatCode` and the value-label and value-axis `cx:numFmt`, except on binned constructs.\n- **Embedded workbook:** custom `numFmt` entries from id 164, with cell styles on every value cell, so Edit Data shows the formats.\n- **Tables:** tables export core layout\'s display text.\n- **Datasets and mapping:** a chart that uses them exports exactly like its inline equivalent.\n- **Provenance:** in `full` mode, `OPF_DATASETS_V1` holds the `datasets` map. `OPF_DATA_V1` sits on each chart or table frame that uses a new field and holds the authored `data`, `mapping` and table form (and, FA-14, a chart\'s `highlight`, which then also records its inline `data`; see [chart-options.md](chart-options.md)), plus a hash of the cached names, values and format codes. `references-only` mode and `provenance: false` write neither tag.\n\n### PPTX import (opf-pptx)\n\n- **With provenance:** `datasets` is restored before document provenance validates. A frame\'s dataset reference, `mapping`, formats and `source` are restored only while the cache hash still matches. If PowerPoint (or a person) changed the values, the native values import and a diagnostic says why, such as `chart-data-provenance-changed` or `table-dataset-unavailable`. The hash compares format codes in core\'s canonical form (`fromExcelNumberFormat`), so PowerPoint\'s re-spellings on save (`\\$#,##0.0` for `$#,##0.0`, `#,##0\\ "units"` for `#,##0 "units"`, native check [opf#385](https://github.com/OpenPresentation/opf/pull/385)) still match; a General code counts as no code.\n- **Without provenance:** a cache `formatCode` that `fromExcelNumberFormat` maps back becomes that column\'s `{ name, format }`, for both classic and chartex charts. Core returns the canonical spelling, such as `#,##0 units` for `#,##0 "units"`. Other codes are reported as `chart-number-format-adapted` and never invented.\n- **Cached values:** these follow the strict rule as well. XML decimal forms (`+5`, `.5`, `007`) are numbers; anything else is a gap with a diagnostic, never a stripped number or 0.\n\n### Editor (opf-editor, [opf-editor#92](https://github.com/OpenPresentation/opf-editor/pull/92))\n\n- **DataColumn headers:** the data grid shows a `DataColumn` header\'s `name`. Rename, insert, delete, move and paste keep each column\'s format.\n- **Chart cells:** they follow `chartNumber`. Text that is not a number is refused with the reason, and a stored string read as a gap is flagged.\n- **Shared datasets:** a dataset-backed chart or table is edited at `/datasets/<id>` through `fields`.\n - Hidden columns keep their data.\n - A column added while `fields` is set goes to both the dataset and `fields`.\n - A rename updates every `fields` and `mapping` that names the column, in the same patch.\n - Deleting a column another item uses is refused with `dataset-column-in-use`.\n - The status line says how many items share the dataset, and "Use a copy of the data" (`detachGridDataset`) inlines one item\'s data.\n- **Format and mapping:**\n - A per-column format field (`setGridColumnFormat`) is checked with `numberFormatError`.\n - A "Chart columns" panel (`setChartMapping`) sets the category, the X column (XY charts only) and the series, and writes only what differs from the default.\n- **Import:** "Store as a shared dataset" (`prepareDatasetImport`) imports into `datasets.<id>`, as `opf ingest --dataset` does.\n- **Other panels:** the table style panel and `table-options` refuse a dataset table with `table-dataset-backed`. Find and replace also searches dataset text.\n- **Undo:** each edit is one undoable patch.\n\n### Native PowerPoint checks\n\nWanted before release:\n- formatted data labels and value axes;\n- Edit Data shows the workbook cell formats with no repair prompt;\n- no repair prompt for the frame `custDataLst`;\n- after a plain save in PowerPoint, the data tags and cache hash survive;\n- the two spellings above (`0.##` on a whole number, `#` on zero) show as described.\n\nSample decks are written by the PPTX branch under `artifacts/rr-54-native/`.\n\n## Not in this change\n\n- Resolving a data source by file or asset, or refreshing `source` ([opf#240](https://github.com/OpenPresentation/opf/issues/240)).\n- Date axes and date formats.\n- Per-series colors: shipped later as `chart.colors`; see [chart-options.md](chart-options.md#setting-colors-chartcolors). (Columns with line series, optionally on a secondary axis, are the `combo` chart type: [chart-options.md](chart-options.md#combo-charts).)\n- Formulas.\n- Rich text in dataset cells.\n'
|
|
26
26
|
},
|
|
27
27
|
{
|
|
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**Verified package set (2026-10-16):** CLI 0.20.0 with core 0.20.3, renderer\n0.20.2, PPTX/editor 0.20.3 and Gallery 3.0.0. Use the complete pinned\n[quickstart installation](quickstart.md#versions) and its required font/converter\npeers. [Artifact verification](evidence/rr-85-final-published-set-2026-10-16/README.md).\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` |\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## 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 an unreadable or blocked local path stops the export (`pptx/asset-unresolved`), and an\nunresolved remote URL becomes an unavailable-image placeholder with a `pptx/unresolved-asset` warning. Use `--fail-on warning` to\nreject that output before any file is written.\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**Verified package set (2026-10-16):** CLI 0.20.0 with core 0.20.3, renderer\n0.20.2, PPTX/editor 0.20.3 and Gallery 3.0.0. Use the complete pinned\n[quickstart installation](quickstart.md#versions) and its required font/converter\npeers. [Artifact verification](evidence/rr-85-final-published-set-2026-10-16/README.md).\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 an unreadable or blocked local path stops the export (`pptx/asset-unresolved`), and an\nunresolved remote URL becomes an unavailable-image placeholder with a `pptx/unresolved-asset` warning. Use `--fail-on warning` to\nreject that output before any file is written.\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**Verified artifacts (2026-10-17):** core 0.20.5, renderer/PPTX/editor 0.20.4,\nCLI 0.20.1 and Gallery 3.0.0.\n[Artifact verification](https://github.com/OpenPresentation/opf/blob/main/docs/evidence-rr-87-final-published-set-2026-10-17.md) binds\nthe selected archives to their release commits and trusted publication checks.\nEarlier matrices and native observations retain their historical identities.\nComplete native compatibility remains a separate gate.\n\n## Published 0.20 contract\n\nLayout `areas` uses nested arrays of cell names. Repeated names span rectangular\nregions and `.` denotes an empty cell. Gallery owns every default record;\nGallery 3 is required for catalog compatibility generation 3, while per-kind\nschema versions remain v2. Core 0.19 and later consume the Gallery package and\ndo not export `@openpresentation/opf/catalog` or ship a catalog snapshot.\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.20.5 | Gallery ^3.0.0; optional renderer/PPTX ^0.20.0 |\n| CLI | 0.20.1 | Core ^0.20.5, Gallery ^3.0.0; optional renderer/PPTX ^0.20.0 |\n| Renderer | 0.20.4 | Core ^0.20.5; optional pinned font/converter peers |\n| PPTX | 0.20.4 | Core ^0.20.5, Gallery ^3.0.0; optional renderer ^0.20.4 |\n| Editor | 0.20.4 | Core ^0.20.5; optional renderer ^0.20.4 |\n| Gallery | 3.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.20.2 and PPTX/editor 0.20.3 require core ^0.20.2;\nCLI 0.20.0 accepts core ^0.20.1. Pin core 0.20.3 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` | 3.0.0 ([independent artifact verification](evidence/rr-85-final-published-set-2026-10-16/README.md); prior 2.0.0 and 1.0.0 remain historical) | nothing (its `GalleryCatalog` type has the shape of `@openpresentation/opf`'s `Catalog`) |\n\nThe gallery (RR-78) is released from its own repository, OpenPresentation/gallery, on its own version line (`vX.Y.Z`\ntags, `gallery-publish.yml` there), outside the lockstep set above and\nrecorded under `independentPackages` in `release-plan.json`: new catalog records are a minor release, a removed id or\na changed drawing a major one ([release process](release-process.md#the-gallery-lane-rr-78)). 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; an opt-in native body mode is in progress. 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**Verified artifacts (2026-10-17):** core 0.20.5, renderer/PPTX/editor 0.20.4,\nCLI 0.20.1 and Gallery 3.0.0.\n[Artifact verification](https://github.com/OpenPresentation/opf/blob/main/docs/evidence-rr-87-final-published-set-2026-10-17.md) binds\nthe selected archives to their release commits and trusted publication checks.\nEarlier matrices and native observations retain their historical identities.\nComplete native compatibility remains a separate gate.\n\n## Published 0.20 contract\n\nLayout `areas` uses nested arrays of cell names. Repeated names span rectangular\nregions and `.` denotes an empty cell. Gallery owns every default record;\nGallery 3 is required for catalog compatibility generation 3, while per-kind\nschema versions remain v2. Core 0.19 and later consume the Gallery package and\ndo not export `@openpresentation/opf/catalog` or ship a catalog snapshot.\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.20.5 | Gallery ^3.0.0; optional renderer/PPTX ^0.20.0 |\n| CLI | 0.20.1 | Core ^0.20.5, Gallery ^3.0.0; optional renderer/PPTX ^0.20.0 |\n| Renderer | 0.20.4 | Core ^0.20.5; optional pinned font/converter peers |\n| PPTX | 0.20.4 | Core ^0.20.5, Gallery ^3.0.0; optional renderer ^0.20.4 |\n| Editor | 0.20.4 | Core ^0.20.5; optional renderer ^0.20.4 |\n| Gallery | 3.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.20.2 and PPTX/editor 0.20.3 require core ^0.20.2;\nCLI 0.20.0 accepts core ^0.20.1. Pin core 0.20.3 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` | 3.0.0 ([independent artifact verification](evidence/rr-85-final-published-set-2026-10-16/README.md); prior 2.0.0 and 1.0.0 remain historical) | nothing (its `GalleryCatalog` type has the shape of `@openpresentation/opf`'s `Catalog`) |\n\nThe gallery (RR-78) is released from its own repository, OpenPresentation/gallery, on its own version line (`vX.Y.Z`\ntags, `gallery-publish.yml` there), outside the lockstep set above and\nrecorded under `independentPackages` in `release-plan.json`: new catalog records are a minor release, a removed id or\na changed drawing a major one ([release process](release-process.md#the-gallery-lane-rr-78)). 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",
|
|
@@ -46,7 +46,7 @@ var docsData = Object.freeze([
|
|
|
46
46
|
"slug": "content-payloads",
|
|
47
47
|
"file": "docs/content-payloads.md",
|
|
48
48
|
"title": "Content Payloads",
|
|
49
|
-
"markdown": '# Content Payloads\n\nSlide content lives directly on a slide as a full-slide payload, in layout-agnostic `blocks`, or inside a promoted region key such as `left`, `center+right`, or `top:left`.\n\nThe slide-level `title`, `subtitle` and `tag` are not payloads, but like `text` and `quote.text` they accept a string or `TextRun[]` (colored words, bold, links, `cite`/`footnote` markers); see [Rich text](rich-text.md#rich-headings-and-quotes).\n\nThe optional payload `type` can make intent explicit, but OPF should usually infer the content kind from the field present:\n\n| Field | Inferred type | Notes |\n| --- | --- | --- |\n| `text` | `text` | Plain string or `TextRun[]`. |\n| `bullets` | `text` | Prose bullets: plain talking points, usually `string[]`. |\n| `items` | `list` | Structured list: entries that share a shape or carry a `description`; `string[]` or `ListItem[]`. See [items versus bullets](#items-versus-bullets). |\n| `image` | `image` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `video` | `video` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `chart` | `chart` | Chart object with `type` and tabular `data`. |\n| `table` | `table` | Table object with optional `columns` and required `rows`. |\n| `code` | `code` | String shorthand or `Code` object with `source`, `language`, `filename`, and `highlight`. |\n| `metric` | `metric` | String/number shorthand or `Metric` object with `value`, `label`, `description`, `unit`, `delta`, `trend`, and `sentiment`. |\n| `quote` | `quote` | String shorthand or `Quote` object with `text` (string or `TextRun[]`), `attribution`, `role`, `photo`, and `source`. |\n| `timeline` | `timeline` | Array shorthand or `Timeline` object with `name`, `description`, and `events`. |\n\n## Items versus bullets\n\nBoth fields draw the same bulleted (or numbered) list: one marker, the same nesting `level`, rich-text entries and `numbering`, fitted by the same list engine. Nothing about the drawing differs. They differ in the kind they infer and in what an entry may carry:\n\n| | `bullets` | `items` |\n| --- | --- | --- |\n| Inferred kind | `text` | `list` |\n| Entry | string, `TextRun[]` or `{ text, level, start }` | string, `TextRun[]` or `{ text, description, level, start }` |\n| `description` | not allowed (a schema error) | supporting detail drawn under the entry |\n| Beside `text` in one block or region | allowed | an error (`list` and `text` are different kinds) |\n\n**The rule.** Use `bullets` for plain talking points: short lines that each stand alone. Use `items` when the entries share a shape (features, steps, options, risks) or any entry needs a `description`; never fold a heading and its detail into one string with a separator, that is what `description` is for. When in doubt, use `items`, because it can grow a `description` later without changing the payload kind. The editor helper `convertListForm` switches a payload between the two; going to `bullets` drops the descriptions.\n\nDo not give one payload both. In a block or a promoted region, `items` next to `bullets` is the error "content payload mixes fields from incompatible content types". At slide root it validates, as the shorthand for two blocks, and composes as two lists side by side; write two blocks on purpose if you want that. `numbering` applies to every list in the payload.\n\nA layout\'s placeholder kind (`list` or `text`) is what layout pickers match on (`layoutContent`), but composition does not refuse the other field and draws both the same way: the choice states what the content is, it is not a switch for the look.\n\n## Code highlight\n\n`code.highlight` marks lines of a code block: a line number or an inclusive `[start, end]` range, 1-based, for example `[3, [5, 7]]`. Lines are counted by line break in `code.source`; a line that wraps is still one line. The preview and the PPTX export draw a theme-derived band behind the marked lines and dim the others slightly; every line keeps at least 4.5:1 contrast against what it sits on (`codeHighlightColors`). The PPTX export draws one native rectangle per run of marked lines behind the per-line text boxes, and import restores `highlight` from the code provenance tag. A line past the last line, or a range written end before start, is the validation warning `code-highlight-out-of-range` or `code-highlight-range-reversed`, and engines ignore it. Pagination keeps each page\'s marked lines, renumbered from 1.\n\n## Color references\n\nEvery content color field \u2014 `TextRun.color`, styled table cell `style.fill` and `style.color`, and table cell border `color` \u2014 accepts three forms:\n\n- A literal hex color: `"#0F172A"`, `"#B42318CC"`.\n- A color-scheme slot or role name, resolved through the effective color scheme after design resolution: slots `accent1`\u2013`accent6`, `dark1`, `dark2`, `light1`, `light2`, `hyperlink`, `followedHyperlink`; roles `primary`, `secondary`, `accent`, `background`, `surface`, `surfaceAlt`, `text`, `textSecondary`. `surfaceAlt` is the alternate surface for banded table rows: core derives it from `surface` and `text` (`surfaceAltColor`), so it always differs visibly from the `surface` of the plain rows, on light and dark slides, and body text keeps 4.5:1 on it.\n- A variable reference `var:<id>` into the top-level `variables` map.\n\n```json\n{\n "variables": { "risk": "#B42318" },\n "slides": [\n {\n "title": "What Could Go Wrong",\n "items": [\n ["Two regions at ", { "text": "85% utilization", "color": "var:risk", "bold": true }],\n ["Mitigations ship in ", { "text": "November", "color": "accent2" }]\n ]\n }\n ]\n}\n```\n\nPrefer names and variables over literal hex: re-theming the deck updates every named reference, while a hex value stays frozen at authoring time. An unknown `var:` id is a validation warning, never an error; engines fall back to their default text color. The styled table cell and border color fields enforce the three forms at the schema level; run colors additionally accept any string so imported decks keep validating \u2014 unrecognized values warn, and renderers fall back to the theme color. See [`design-resolution.md`](./design-resolution.md) for the resolution rules.\n\n## Numbered lists\n\n`numbering` on an `items` or `bullets` payload draws numbers instead of bullets: a style name (`arabic`, `roman-upper`, `roman-lower`, `alpha-upper`, `alpha-lower`), a `{ style, start, suffix }` object, or an array with one entry per list level. PowerPoint export writes native auto-numbers and the preview draws the same numbers. See [numbered lists](numbered-lists.md).\n\n```json\n{ "items": ["Define", "Build", "Ship"], "numbering": { "style": "roman-lower", "suffix": "paren" } }\n```\n\n## Blocks\n\nUse slide-level `blocks` when a slide contains multiple content payloads, but exact placement should be inferred by the renderer. Blocks may contain a concrete content payload or a nested group with its own `blocks` and optional `composition`. Groups cannot mix child blocks with leaf payload fields. See [dynamic composition](dynamic-composition.md) for nesting and inheritance rules.\n\n```json\n{\n "title": "Customer Feedback Summary",\n "blocks": [\n {\n "table": {\n "columns": ["Theme", "Mentions"],\n "rows": [\n ["Speed", 42],\n ["Ease of use", 31]\n ]\n }\n },\n {\n "quote": {\n "text": "The new workflow cut review time in half.",\n "attribution": "Operations Lead",\n "source": "Customer interview"\n }\n }\n ]\n}\n```\n\nAt slide root only, multiple content payload kinds are accepted as shorthand for the equivalent blocks form when there is no explicit `type`, no `blocks`, and no promoted region keys:\n\n```json\n{\n "title": "Habitat & Territory",\n "text": "Jaguars are strongly associated with presence of water and dense cover.",\n "items": [\n "Primary habitats include dense rainforests, swamps, and seasonally flooded wetlands.",\n "Solitary animals that establish and defend large territories."\n ]\n}\n```\n\nThe same shorthand works for other content kinds:\n\n```json\n{\n "title": "Evidence Snapshot",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Sightings"],\n "rows": [\n ["Q1", 12],\n ["Q2", 18]\n ]\n }\n },\n "quote": {\n "text": "Jaguar conservation depends on connected habitat.",\n "attribution": "Field researcher"\n }\n}\n```\n\n## Chart\n\nChart-specific fields are grouped under `chart`. Do not put loose chart data directly on a slide or region.\n\n```json\n{\n "title": "Revenue Trend",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Revenue", "Costs"],\n "rows": [\n ["Q1", 12, 8],\n ["Q2", 18, 11],\n ["Q3", 24, 15]\n ]\n }\n }\n}\n```\n\nInline chart data is tabular by default. Renderers convert `columns` and `rows` into series, axes, legends, and workbook data internally.\n\nGive every chart an `alt`: one or two sentences that say what the data shows (the point and the key numbers), not "a chart" or "bar chart". The preview names the chart with it (`role="img"`, `aria-label`) and the PowerPoint export writes it as the chart frame\'s alternative text (`descr`), which `fromPptx` reads back. `"alt": ""` marks a chart decorative, as an empty image `alt` does; `validate` (`opf/chart-text-alternative`) still reports it as info so the choice is reviewed. Without `alt`, text beside the chart (a subtitle, text or table) satisfies the rule. See [chart options](chart-options.md#text-alternative).\n\nValue cells are numbers. A string is read only in strict decimal syntax (`"12"`, `" -3.5 "`, `"1e6"`); anything else (`"12%"`, `"$5"`, `"(5)"`, `"1,234"`, `"Q1"`) is a gap in the preview and the export and a `chart-value-not-numeric` warning, never a guessed value. `null` and `""` are gaps without a warning. Core `chartNumber` is the one rule every engine uses (RR-54; before it the PPTX exporter stripped non-numeric characters, so `"12%"` exported as 12).\n\nA column is a string or a `DataColumn` with a number format, which the data labels, the value axis and the exported workbook use:\n\n```json\n{\n "chart": {\n "type": "column",\n "data": {\n "columns": ["Quarter", { "name": "Revenue", "format": "$#,##0.0" }, { "name": "Margin", "format": "0%" }],\n "rows": [["Q1", 12.4, 0.31], ["Q2", 18.1, 0.34]],\n "source": { "src": "./data/revenue.csv", "retrieved": "2026-10-05" }\n },\n "mapping": { "category": "Quarter", "series": ["Revenue"] }\n }\n}\n```\n\nFormats use the `NumberVariable.format` syntax (`#,##0`, `0.0%`, `$#,##0.00`, `#,##0 units`; a `%` multiplies by 100). `source` records provenance only; engines never read or refresh it. `mapping` picks the category, the X column of a scatter chart and the plotted series by column name; without it the first column is the category and every other column a series. Shared data lives in the top-level `datasets` map and a chart plots it with `"data": { "dataset": "revenue", "fields": ["Quarter", "Revenue"] }`. The full contract, with validation codes and the engine behaviour, is [Chart and table data](chart-table-data.md).\n\nChart data is inline columns and rows or a dataset. A data source by file or asset (`"data": { "src": "asset:revenue-csv", "columns": [...] }`) is not part of the format: no engine loaded it, and the validator rejects it. Import the data inline (with a `source`) or into a dataset instead.\n\n## Table\n\nTable-specific fields are grouped under `table`. Do not put loose `columns` or `rows` directly on a slide or region.\n\n```json\n{\n "title": "Pipeline",\n "table": {\n "columns": ["Stage", "Count", "Value"],\n "rows": [\n ["Qualified", 42, "$1.2M"],\n ["Proposal", 18, "$840K"]\n ]\n }\n}\n```\n\nA table may instead show a dataset: `"table": { "dataset": "revenue", "fields": ["Quarter", "Revenue"] }` (no `rows` or `columns`); it takes its headers, rows and column formats from the dataset, and per-cell styles need an inline table. A column header may be a `DataColumn` (`{ "name": "Revenue", "format": "$#,##0.0" }`) and a styled header or body cell takes a `format`; a number cell displays formatted (the body cell\'s own format wins over the column\'s), and `layoutTable` measures the formatted text. See [Chart and table data](chart-table-data.md).\n\nTable body cells accept strings, numbers, booleans, or `null`. Since core 0.5.0, a cell or column header also accepts the same `TextRun[]` used by rich text:\n\n```json\n{\n "table": {\n "columns": [["Quarter ", {"text": "growth", "bold": true}], "Value"],\n "rows": [\n [["Up ", {"text": "12%", "color": "#008800"}], 12]\n ]\n }\n}\n```\n\nUse the coordinated Node 24 set recorded in [release-plan.json](../release-plan.json) and the [compatibility matrix](compatibility-matrix.md) for exact pins and evidence. Core measures run styles when checking overflow and keeps each row intact when paginating. The renderer traces rich cells for the editor\'s existing formatting, typing and undo controls; the exporter emits editable native text runs. PPTX 0.4.0 introduced import of supported native character styles, paragraph defaults, theme fonts/colors, external links and significant whitespace as rich runs. Unstyled body cells remain strings, and cached display text cannot recover original scalar types or live fields. Conditional table styles, merged geometry and cell fills/borders/alignment remain limited; native PowerPoint visual parity is not yet verified.\n\nA table may carry an `alt` (FA-27), the same text alternative as `Chart.alt`: one or two sentences that say what the table shows (the point and the key numbers), not "a table". The preview names the table with it (a `role="group"` with `aria-label`; the cells stay readable) and the PowerPoint export writes it as the table frame\'s alternative text (`descr`), which `fromPptx` reads back. `"alt": ""` hides a purely decorative table, as an empty image `alt` does. Unlike a chart, a table needs no `alt`: a screen reader reads a native table cell by cell, so `validate` reports nothing when it is missing, and a table beside a chart still counts as that chart\'s text alternative. The `alt` is a summary read before the cells, which stay readable text (in PowerPoint too, where `descr` on a table frame does not hide its cells), so it is most useful for a large or dense table.\n\nCore 0.6.0 adds `layoutTable` from `@openpresentation/opf/composition`. It measures scalar and rich cells, keeps short rows compact, and gives wrapped or multiline rows the height they need. When space is constrained it reduces spare row height before shrinking text, and reports overflow when the minimum fitting size cannot fit. Pass the same `scale`, font family, measurement provider and effective `minFontSize` to each consumer. The returned row boxes, cell text boxes and fits are shared by the coordinated SVG and PPTX implementations; rich table cells use uniform line advances to match native cell paragraph spacing. Native viewer fidelity remains a separate verification boundary.\n\n## Captions\n\nAn `image`, `chart`, `table` or `video` payload takes a `caption`: a string, `TextRun[]`, or `{ "text", "position": "below" | "above", "align": "left" | "center" | "right" }` (defaults `below`, `left`). It sits beside the payload field on a block or promoted-region payload, or on the slide root when the root holds exactly one of those payloads; anywhere else it is a `caption-unsupported-payload` error.\n\n```json\n{\n "title": "Pipeline",\n "blocks": [\n { "image": "asset:funnel", "caption": "Figure 1. Pipeline by stage, Q3" },\n { "table": { "columns": ["Stage", "Count"], "rows": [["Qualified", 42]] }, "caption": { "text": "Table 1. Counts", "position": "above", "align": "center" } }\n ]\n}\n```\n\nCore composition reserves the caption band inside the block\'s region and shrinks the media by its height (`item.caption` carries the band, `item.box` is the media box); the preview and the PPTX export draw that band in the muted text colour at the caption size (0.6 of the body size, never under the readable floor). Captioned blocks are the only ones whose geometry changes. See [footnotes, citations and captions](footnotes-citations-captions.md).\n\n## Code\n\nCode-specific fields are grouped under `code`. A string value is shorthand for `code.source`; use object form when syntax highlighting or a file label matters. In object form, `source` is required. `language` colours the code in the preview and the PowerPoint export (comments, strings, numbers, keywords, names and types, in colours from the deck theme); an unknown language stays plain and the text is never changed. See [dynamic composition](dynamic-composition.md#preview-polish-shared-by-preview-and-export-rr-07) for the supported languages.\n\n```json\n{\n "title": "Decision Rule",\n "code": {\n "source": "if risk > threshold:\\n escalate(owner)\\nelse:\\n approve(change)",\n "language": "python",\n "filename": "decision.py"\n }\n}\n```\n\n## Metric\n\nMetric-specific fields are grouped under `metric`. A string or number value is shorthand for `metric.value`; numeric values stay numeric and are formatted by renderers at display time. Use object form when labels, descriptions, units, deltas, trends, or sentiment matter. A `trend` (`up`, `down`, `flat`) draws an arrow beside its word, coloured with the delta text, in the preview and the PowerPoint export; the arrow always points the way the trend does, and its colour follows `sentiment` (`positive` green, `negative` red, `neutral` the neutral text colour). When `sentiment` is absent up is positive, down is negative and flat is neutral, so set it when the direction is not the verdict: a falling churn, cost or latency is `"trend": "down", "sentiment": "positive"`. Only the trend arrow and the trend and delta text take the colour, so `sentiment` has no visible effect on a metric without a `trend`; the word stays editable text and the arrow carries "Trend: up" as its alternative text (see [dynamic composition](dynamic-composition.md#preview-polish-shared-by-preview-and-export-rr-07)).\n\nThe `metrics` v2 layout accepts metric blocks in its body region and flows them as a grid. For several metrics, use separate `{ "metric": ... }` entries in `blocks`; the content kind and payload key are `metric`. Choosing a layout does not reinterpret existing text as numeric data.\n\n```json\n{\n "title": "Operating Metric",\n "metric": {\n "value": "42%",\n "label": "Review cycle reduction",\n "description": "Median reduction across customer review workflows.",\n "delta": "+11 pts",\n "trend": "up"\n }\n}\n```\n\nA falling value that is good news keeps its downward arrow and takes the green:\n\n```json\n{\n "title": "Churn",\n "metric": {\n "value": "3.1%",\n "label": "Monthly churn",\n "delta": "-0.6 pts",\n "trend": "down",\n "sentiment": "positive"\n }\n}\n```\n\n## Quote\n\nQuote-specific fields are grouped under `quote`. A string value is shorthand for `quote.text`; use object form when attribution or citation matters. `text` is a string or `TextRun[]` with inline formatting; `attribution`, `role` and `source` are plain strings.\n\n```json\n{\n "title": "Customer Proof",\n "quote": {\n "text": "The new workflow made exceptions visible before they became escalations.",\n "attribution": "Priya Raman",\n "source": "Customer interview"\n }\n}\n```\n\nA testimonial attributes the quote to a person with a title and a face. `role` is the person\'s title and organization, and `photo` is their headshot, an `Asset` (a source string or an object with `src` and `alt`):\n\n```json\n{\n "title": "Customer Proof",\n "quote": {\n "text": "The new workflow made exceptions visible before they became escalations.",\n "attribution": "Priya Raman",\n "role": "VP Operations, Acme",\n "photo": { "src": "asset:priya-raman", "alt": "Priya Raman" },\n "source": "Customer interview, March 2026"\n }\n}\n```\n\nThe footer under the quote is the attribution, then the role on its own line, then the source after ` - ` on the last line. Without a `role` it is the single line it always was (`attribution - source`). With a `photo`, the headshot is a circle at the start edge of the footer row (the left in a left-to-right deck, the right in a right-to-left one), three times the footer font size across, and the footer lines sit beside it, centered on it. The photo is cropped to fill the circle, and its size follows the footer\'s font size when the readability floor shrinks the footer. Without a `photo` nothing else changes. The preview draws it with the circular clip that an image block uses for shape `circle`, and PowerPoint export writes a native picture with the `ellipse` geometry and the alt text as its description; importing that file restores `role` and `photo`. A photo needs alt text (the `opf/missing-alt-text` rule of `validate` checks `quote.photo`); an SVG photo exports as its PNG raster. A photo with no attribution, role or source still draws, alone in the footer row. The Markdown dialect has no native form for `role` and `photo`: a quote that carries them is written as an `opf` block, which round trips exactly, and a plain `> \u2014 Name, Title` line stays an attribution.\n\n## Timeline\n\nTimeline-specific fields are grouped under `timeline`. An array value is shorthand for `timeline.events`; use object form when the timeline needs a name or description. Timeline events use `when`, `what`, `description` and `status`. `status` is `done`, `current` or `planned` and marks progress: `done` is a filled marker with normal text, `current` ("we are here") is a ringed marker with a bold label, and `planned` is a hollow outlined marker with muted text. An event without a status draws as a plain filled marker. Schedule health (at risk, blocked) is not a status; say it in the event text. See [Dynamic composition](dynamic-composition.md#timeline-internals) for the exact drawing.\n\n```json\n{\n "title": "Rollout Plan",\n "timeline": {\n "name": "Regional Rollout",\n "description": "Major milestones for the rollout.",\n "events": [\n {\n "when": "Q1",\n "what": "Pilot",\n "description": "Launch with one operations team."\n },\n {\n "when": "Q2",\n "what": "Rollout",\n "description": "Expand to all regions."\n }\n ]\n }\n}\n```\n\nA roadmap with progress states:\n\n```json\n{\n "title": "Product roadmap",\n "timeline": [\n { "when": "Q1", "what": "Discovery", "status": "done" },\n { "when": "Q2", "what": "Pilot", "status": "current" },\n { "when": "Q3", "what": "Rollout", "status": "planned" }\n ]\n}\n```\n\n## Regions\n\nRegion keys address a 3\xD73 grid of rows (`top`, `middle`, `bottom`) and columns (`left`, `center`, `right`):\n\n```\n left center right\n +--------------------+--------------------+--------------------+\n top | top:left | top:center | top:right |\n +--------------------+--------------------+--------------------+\n middle | middle:left | middle:center | middle:right |\n +--------------------+--------------------+--------------------+\n bottom | bottom:left | bottom:center | bottom:right |\n +--------------------+--------------------+--------------------+\n```\n\n- A bare column key (`left`) spans all three rows; a bare row key (`top`) spans all three columns.\n- `+` spans adjacent rows or columns: `center+right`, `top+middle`.\n- `row:column` combines the two: `top:left`, `middle+bottom:center+right`.\n- Keys on one slide must not overlap, and regions cannot be mixed with root payload fields.\n\nSpans compose into common slide shapes:\n\n```\n "left" + "center+right" "top" + "middle+bottom"\n (sidebar + main) (headline band + body)\n +----------+------------------+ +-------------------------------+\n | | | | top |\n | | | +-------------------------------+\n | left | center+right | | |\n | | | | middle+bottom |\n | | | | |\n +----------+------------------+ +-------------------------------+\n\n "top" + "middle+bottom:left" + "middle+bottom:center+right"\n (headline band, then sidebar + main)\n +---------------------------------------------+\n | top |\n +---------------+-----------------------------+\n | | |\n | middle+bottom | middle+bottom:center+right |\n | :left | |\n | | |\n +---------------+-----------------------------+\n```\n\nThe same payload objects work inside regions \u2014 here, the sidebar-plus-main shape:\n\n```json\n{\n "title": "Operating Snapshot",\n "left": {\n "table": {\n "columns": ["Metric", "Value"],\n "rows": [\n ["Revenue", "$4.2M"],\n ["Gross margin", "68%"]\n ]\n }\n },\n "center+right": {\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Month", "Revenue"],\n "rows": [\n ["Jan", 3.4],\n ["Feb", 3.8],\n ["Mar", 4.2]\n ]\n }\n }\n }\n}\n```\n\n## Accessibility of the preview\n\nThe SVG preview and the PowerPoint export agree on what a reader can reach. The slide root is a labelled container (`role="group"`, `aria-roledescription="slide"`, `aria-label` set to the slide title), never an image: an image makes everything inside it presentational, so a screen reader would hear only the title. Text stays reachable, in the order it is drawn: tag, title and subtitle, the content in layout order, footnotes, then header and footer. That covers body text, list entries, quotes, metric values and labels, table cells, timeline entries, captions and furniture text.\n\nA picture with `alt` is an image with that name. Purely decorative drawing is hidden (`aria-hidden`): the background and its overlay, card frames, table cell fills and borders, dividers, timeline markers, watermarks, list bullets, a picture or chart whose `alt` is `""`, and a header or footer picture without `alt` (a logo repeated on every slide). A chart with `alt` is an image with that name, and its drawn marks are not announced separately. The deck element, the player and `renderDeckHtml` in `@openpresentation/opf-render` put the one slide container (region or figure) around the drawing, so a slide is announced once.\n\nTesting status (opf-render#170): this contract is the DOM and ARIA structure. It is checked with axe-core, the browser\'s accessibility tree and focus-order assertions, and it is **not tested with assistive technology**: no screen reader (NVDA, JAWS, VoiceOver, TalkBack) has been run against the preview, the deck element or the player (owner decision 2026-10-10). Forced colors are verified with Chromium\'s emulation only, not the Windows contrast theme ([evidence](evidence/rr-59-forced-colors-emulated-20261010/README.md)).\n'
|
|
49
|
+
"markdown": '# Content Payloads\n\nSlide content lives directly on a slide as a full-slide payload, in layout-agnostic `blocks`, or inside a promoted region key such as `left`, `center+right`, or `top:left`.\n\nThe slide-level `title`, `subtitle` and `tag` are not payloads, but like `text` and `quote.text` they accept a string or `TextRun[]` (colored words, bold, links, `cite`/`footnote` markers); see [Rich text](rich-text.md#rich-headings-and-quotes).\n\nThe optional payload `type` can make intent explicit, but OPF should usually infer the content kind from the field present:\n\n| Field | Inferred type | Notes |\n| --- | --- | --- |\n| `text` | `text` | Plain string or `TextRun[]`. |\n| `bullets` | `text` | Prose bullets: plain talking points, usually `string[]`. |\n| `items` | `list` | Structured list: entries that share a shape or carry a `description`; `string[]` or `ListItem[]`. See [items versus bullets](#items-versus-bullets). |\n| `image` | `image` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `video` | `video` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `chart` | `chart` | Chart object with `type` and tabular `data`. |\n| `table` | `table` | Table object with optional `columns` and required `rows`. |\n| `code` | `code` | String shorthand or `Code` object with `source`, `language`, `filename`, and `highlight`. |\n| `metric` | `metric` | String/number shorthand or `Metric` object with `value`, `label`, `description`, `unit`, `delta`, `trend`, and `sentiment`. |\n| `quote` | `quote` | String shorthand or `Quote` object with `text` (string or `TextRun[]`), `attribution`, `role`, `photo`, and `source`. |\n| `timeline` | `timeline` | Array shorthand or `Timeline` object with `name`, `description`, and `events`. |\n\n## Items versus bullets\n\nBoth fields draw the same bulleted (or numbered) list: one marker, the same nesting `level`, rich-text entries and `numbering`, fitted by the same list engine. Nothing about the drawing differs. They differ in the kind they infer and in what an entry may carry:\n\n| | `bullets` | `items` |\n| --- | --- | --- |\n| Inferred kind | `text` | `list` |\n| Entry | string, `TextRun[]` or `{ text, level, start }` | string, `TextRun[]` or `{ text, description, level, start }` |\n| `description` | not allowed (a schema error) | supporting detail drawn under the entry |\n| Beside `text` in one block or region | allowed | an error (`list` and `text` are different kinds) |\n\n**The rule.** Use `bullets` for plain talking points: short lines that each stand alone. Use `items` when the entries share a shape (features, steps, options, risks) or any entry needs a `description`; never fold a heading and its detail into one string with a separator, that is what `description` is for. When in doubt, use `items`, because it can grow a `description` later without changing the payload kind. The editor helper `convertListForm` switches a payload between the two; going to `bullets` drops the descriptions.\n\nDo not give one payload both. In a block or a promoted region, `items` next to `bullets` is the error "content payload mixes fields from incompatible content types". At slide root it validates, as the shorthand for two blocks, and composes as two lists side by side; write two blocks on purpose if you want that. `numbering` applies to every list in the payload.\n\nA layout\'s placeholder kind (`list` or `text`) is what layout pickers match on (`layoutContent`), but composition does not refuse the other field and draws both the same way: the choice states what the content is, it is not a switch for the look.\n\n## Code highlight\n\n`code.highlight` marks lines of a code block: a line number or an inclusive `[start, end]` range, 1-based, for example `[3, [5, 7]]`. Lines are counted by line break in `code.source`; a line that wraps is still one line. The preview and the PPTX export draw a theme-derived band behind the marked lines and dim the others slightly; every line keeps at least 4.5:1 contrast against what it sits on (`codeHighlightColors`). The PPTX export draws one native rectangle per run of marked lines behind the per-line text boxes, and import restores `highlight` from the code provenance tag. A line past the last line, or a range written end before start, is the validation warning `code-highlight-out-of-range` or `code-highlight-range-reversed`, and engines ignore it. Pagination keeps each page\'s marked lines, renumbered from 1.\n\n## Color references\n\nEvery content color field \u2014 `TextRun.color`, styled table cell `style.fill` and `style.color`, and table cell border `color` \u2014 accepts three forms:\n\n- A literal hex color: `"#0F172A"`, `"#B42318CC"`.\n- A color-scheme slot or role name, resolved through the effective color scheme after design resolution: slots `accent1`\u2013`accent6`, `dark1`, `dark2`, `light1`, `light2`, `hyperlink`, `followedHyperlink`; roles `primary`, `secondary`, `accent`, `background`, `surface`, `surfaceAlt`, `text`, `textSecondary`. `surfaceAlt` is the alternate surface for banded table rows: core derives it from `surface` and `text` (`surfaceAltColor`), so it always differs visibly from the `surface` of the plain rows, on light and dark slides, and body text keeps 4.5:1 on it.\n- A variable reference `var:<id>` into the top-level `variables` map.\n\n```json\n{\n "variables": { "risk": "#B42318" },\n "slides": [\n {\n "title": "What Could Go Wrong",\n "items": [\n ["Two regions at ", { "text": "85% utilization", "color": "var:risk", "bold": true }],\n ["Mitigations ship in ", { "text": "November", "color": "accent2" }]\n ]\n }\n ]\n}\n```\n\nPrefer names and variables over literal hex: re-theming the deck updates every named reference, while a hex value stays frozen at authoring time. An unknown `var:` id is a validation warning, never an error; engines fall back to their default text color. The styled table cell and border color fields enforce the three forms at the schema level; run colors additionally accept any string so imported decks keep validating \u2014 unrecognized values warn, and renderers fall back to the theme color. See [`design-resolution.md`](./design-resolution.md) for the resolution rules.\n\n## Numbered lists\n\n`numbering` on an `items` or `bullets` payload draws numbers instead of bullets: a style name (`arabic`, `roman-upper`, `roman-lower`, `alpha-upper`, `alpha-lower`), a `{ style, start, suffix }` object, or an array with one entry per list level. PowerPoint export writes native auto-numbers and the preview draws the same numbers. See [numbered lists](numbered-lists.md).\n\n```json\n{ "items": ["Define", "Build", "Ship"], "numbering": { "style": "roman-lower", "suffix": "paren" } }\n```\n\n## Blocks\n\nUse slide-level `blocks` when a slide contains multiple content payloads, but exact placement should be inferred by the renderer. Blocks may contain a concrete content payload or a nested group with its own `blocks` and optional `composition`. Groups cannot mix child blocks with leaf payload fields. See [dynamic composition](dynamic-composition.md) for nesting and inheritance rules.\n\n```json\n{\n "title": "Customer Feedback Summary",\n "blocks": [\n {\n "table": {\n "columns": ["Theme", "Mentions"],\n "rows": [\n ["Speed", 42],\n ["Ease of use", 31]\n ]\n }\n },\n {\n "quote": {\n "text": "The new workflow cut review time in half.",\n "attribution": "Operations Lead",\n "source": "Customer interview"\n }\n }\n ]\n}\n```\n\nAt slide root only, multiple content payload kinds are accepted as shorthand for the equivalent blocks form when there is no explicit `type`, no `blocks`, and no promoted region keys:\n\n```json\n{\n "title": "Habitat & Territory",\n "text": "Jaguars are strongly associated with presence of water and dense cover.",\n "items": [\n "Primary habitats include dense rainforests, swamps, and seasonally flooded wetlands.",\n "Solitary animals that establish and defend large territories."\n ]\n}\n```\n\nThe same shorthand works for other content kinds:\n\n```json\n{\n "title": "Evidence Snapshot",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Sightings"],\n "rows": [\n ["Q1", 12],\n ["Q2", 18]\n ]\n }\n },\n "quote": {\n "text": "Jaguar conservation depends on connected habitat.",\n "attribution": "Field researcher"\n }\n}\n```\n\n## Chart\n\nChart-specific fields are grouped under `chart`. Do not put loose chart data directly on a slide or region.\n\n```json\n{\n "title": "Revenue Trend",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Revenue", "Costs"],\n "rows": [\n ["Q1", 12, 8],\n ["Q2", 18, 11],\n ["Q3", 24, 15]\n ]\n }\n }\n}\n```\n\nInline chart data is tabular by default. Renderers convert `columns` and `rows` into series, axes, legends, and workbook data internally.\n\nGive every chart an `alt`: one or two sentences that say what the data shows (the point and the key numbers), not "a chart" or "bar chart". The preview names the chart with it (`role="img"`, `aria-label`) and the PowerPoint export writes it as the chart frame\'s alternative text (`descr`), which `fromPptx` reads back. `"alt": ""` marks a chart decorative, as an empty image `alt` does; `validate` (`opf/chart-text-alternative`) still reports it as info so the choice is reviewed. Without `alt`, text beside the chart (a subtitle, text or table) satisfies the rule. See [chart options](chart-options.md#text-alternative).\n\nValue cells are numbers. A string is read only in strict decimal syntax (`"12"`, `" -3.5 "`, `"1e6"`); anything else (`"12%"`, `"$5"`, `"(5)"`, `"1,234"`, `"Q1"`) is a gap in the preview and the export and a `chart-value-not-numeric` warning, never a guessed value. `null` and `""` are gaps without a warning. Core `chartNumber` is the one rule every engine uses (RR-54; before it the PPTX exporter stripped non-numeric characters, so `"12%"` exported as 12).\n\nA column is a string or a `DataColumn` with a number format, which the data labels, the value axis and the exported workbook use:\n\n```json\n{\n "chart": {\n "type": "column",\n "data": {\n "columns": ["Quarter", { "name": "Revenue", "format": "$#,##0.0" }, { "name": "Margin", "format": "0%" }],\n "rows": [["Q1", 12.4, 0.31], ["Q2", 18.1, 0.34]],\n "source": { "src": "./data/revenue.csv", "retrieved": "2026-10-05" }\n },\n "mapping": { "category": "Quarter", "series": ["Revenue"] }\n }\n}\n```\n\nFormats use the `NumberVariable.format` syntax (`#,##0`, `0.0%`, `$#,##0.00`, `#,##0 units`; a `%` multiplies by 100). `source` records provenance only; engines never read or refresh it. `mapping` picks the category, the X column of a scatter chart and the plotted series by column name; without it the first column is the category and every other column a series. Shared data lives in the top-level `datasets` map and a chart plots it with `"data": { "dataset": "revenue", "fields": ["Quarter", "Revenue"] }`. The full contract, with validation codes and the engine behaviour, is [Chart and table data](chart-table-data.md).\n\nChart data is inline columns and rows or a dataset. A data source by file or asset (`"data": { "src": "asset:revenue-csv", "columns": [...] }`) is not part of the format: no engine loaded it, and the validator rejects it. Import the data inline (with a `source`) or into a dataset instead.\n\n## Table\n\nTable-specific fields are grouped under `table`. Do not put loose `columns` or `rows` directly on a slide or region.\n\n```json\n{\n "title": "Pipeline",\n "table": {\n "columns": ["Stage", "Count", "Value"],\n "rows": [\n ["Qualified", 42, "$1.2M"],\n ["Proposal", 18, "$840K"]\n ]\n }\n}\n```\n\nA table may instead show a dataset: `"table": { "dataset": "revenue", "fields": ["Quarter", "Revenue"] }` (no `rows` or `columns`); it takes its headers, rows and column formats from the dataset, and per-cell styles need an inline table. A column header may be a `DataColumn` (`{ "name": "Revenue", "format": "$#,##0.0" }`) and a styled header or body cell takes a `format`; a number cell displays formatted (the body cell\'s own format wins over the column\'s), and `layoutTable` measures the formatted text. See [Chart and table data](chart-table-data.md).\n\nTable body cells accept strings, numbers, booleans, or `null`. Since core 0.5.0, a cell or column header also accepts the same `TextRun[]` used by rich text:\n\n```json\n{\n "table": {\n "columns": [["Quarter ", {"text": "growth", "bold": true}], "Value"],\n "rows": [\n [["Up ", {"text": "12%", "color": "#008800"}], 12]\n ]\n }\n}\n```\n\nUse the coordinated Node 24 set recorded in [release-plan.json](../release-plan.json) and the [compatibility matrix](compatibility-matrix.md) for exact pins and evidence. Core measures run styles when checking overflow and keeps each row intact when paginating. The renderer traces rich cells for the editor\'s existing formatting, typing and undo controls; the exporter emits editable native text runs. PPTX 0.4.0 introduced import of supported native character styles, paragraph defaults, theme fonts/colors, external links and significant whitespace as rich runs. Unstyled body cells remain strings, and cached display text cannot recover original scalar types or live fields. Conditional table styles, merged geometry and cell fills/borders/alignment remain limited; native PowerPoint visual parity is not yet verified.\n\n**Table text size and bold.** Table text draws at the body size (about 19 pt, 25 reference pixels) and the whole table shrinks together toward `composition.minFontSize` when its rows do not fit, so a table next to bullets no longer reads smaller than they do. A run\'s own `fontSize` is drawn as written. `TableCellStyle` takes `bold` to make the whole cell bold; a numeric cell can be bold and still use its number `format` (`{"value": 18.1, "style": {"bold": true}}`), and a cell written as runs in a numeric column keeps the column\'s alignment.\n\nA table may carry an `alt` (FA-27), the same text alternative as `Chart.alt`: one or two sentences that say what the table shows (the point and the key numbers), not "a table". The preview names the table with it (a `role="group"` with `aria-label`; the cells stay readable) and the PowerPoint export writes it as the table frame\'s alternative text (`descr`), which `fromPptx` reads back. `"alt": ""` hides a purely decorative table, as an empty image `alt` does. Unlike a chart, a table needs no `alt`: a screen reader reads a native table cell by cell, so `validate` reports nothing when it is missing, and a table beside a chart still counts as that chart\'s text alternative. The `alt` is a summary read before the cells, which stay readable text (in PowerPoint too, where `descr` on a table frame does not hide its cells), so it is most useful for a large or dense table.\n\nCore 0.6.0 adds `layoutTable` from `@openpresentation/opf/composition`. It measures scalar and rich cells, keeps short rows compact, and gives wrapped or multiline rows the height they need. When space is constrained it reduces spare row height before shrinking text, and reports overflow when the minimum fitting size cannot fit. Pass the same `scale`, font family, measurement provider and effective `minFontSize` to each consumer. The returned row boxes, cell text boxes and fits are shared by the coordinated SVG and PPTX implementations; rich table cells use uniform line advances to match native cell paragraph spacing. Native viewer fidelity remains a separate verification boundary.\n\n## Captions\n\nAn `image`, `chart`, `table` or `video` payload takes a `caption`: a string, `TextRun[]`, or `{ "text", "position": "below" | "above", "align": "left" | "center" | "right" }` (defaults `below`, `left`). It sits beside the payload field on a block or promoted-region payload, or on the slide root when the root holds exactly one of those payloads; anywhere else it is a `caption-unsupported-payload` error.\n\n```json\n{\n "title": "Pipeline",\n "blocks": [\n { "image": "asset:funnel", "caption": "Figure 1. Pipeline by stage, Q3" },\n { "table": { "columns": ["Stage", "Count"], "rows": [["Qualified", 42]] }, "caption": { "text": "Table 1. Counts", "position": "above", "align": "center" } }\n ]\n}\n```\n\nCore composition reserves the caption band inside the block\'s region and shrinks the media by its height (`item.caption` carries the band, `item.box` is the media box); the preview and the PPTX export draw that band in the muted text colour at the caption size (0.6 of the body size, never under the readable floor). Captioned blocks are the only ones whose geometry changes. See [footnotes, citations and captions](footnotes-citations-captions.md).\n\n## Code\n\nCode-specific fields are grouped under `code`. A string value is shorthand for `code.source`; use object form when syntax highlighting or a file label matters. In object form, `source` is required. `language` colours the code in the preview and the PowerPoint export (comments, strings, numbers, keywords, names and types, in colours from the deck theme); an unknown language stays plain and the text is never changed. See [dynamic composition](dynamic-composition.md#preview-polish-shared-by-preview-and-export-rr-07) for the supported languages.\n\n```json\n{\n "title": "Decision Rule",\n "code": {\n "source": "if risk > threshold:\\n escalate(owner)\\nelse:\\n approve(change)",\n "language": "python",\n "filename": "decision.py"\n }\n}\n```\n\n## Metric\n\nMetric-specific fields are grouped under `metric`. A string or number value is shorthand for `metric.value`; numeric values stay numeric and are formatted by renderers at display time. Use object form when labels, descriptions, units, deltas, trends, or sentiment matter. A `trend` (`up`, `down`, `flat`) draws an arrow beside its word, coloured with the delta text, in the preview and the PowerPoint export; the arrow always points the way the trend does, and its colour follows `sentiment` (`positive` green, `negative` red, `neutral` the neutral text colour). When `sentiment` is absent up is positive, down is negative and flat is neutral, so set it when the direction is not the verdict: a falling churn, cost or latency is `"trend": "down", "sentiment": "positive"`. Only the trend arrow and the trend and delta text take the colour, so `sentiment` has no visible effect on a metric without a `trend`; the word stays editable text and the arrow carries "Trend: up" as its alternative text (see [dynamic composition](dynamic-composition.md#preview-polish-shared-by-preview-and-export-rr-07)).\n\nThe `metrics` v2 layout accepts metric blocks in its body region and flows them as a grid. For several metrics, use separate `{ "metric": ... }` entries in `blocks`; the content kind and payload key are `metric`. Choosing a layout does not reinterpret existing text as numeric data.\n\n```json\n{\n "title": "Operating Metric",\n "metric": {\n "value": "42%",\n "label": "Review cycle reduction",\n "description": "Median reduction across customer review workflows.",\n "delta": "+11 pts",\n "trend": "up"\n }\n}\n```\n\nA falling value that is good news keeps its downward arrow and takes the green:\n\n```json\n{\n "title": "Churn",\n "metric": {\n "value": "3.1%",\n "label": "Monthly churn",\n "delta": "-0.6 pts",\n "trend": "down",\n "sentiment": "positive"\n }\n}\n```\n\n## Quote\n\nQuote-specific fields are grouped under `quote`. A string value is shorthand for `quote.text`; use object form when attribution or citation matters. `text` is a string or `TextRun[]` with inline formatting; `attribution`, `role` and `source` are plain strings.\n\n```json\n{\n "title": "Customer Proof",\n "quote": {\n "text": "The new workflow made exceptions visible before they became escalations.",\n "attribution": "Priya Raman",\n "source": "Customer interview"\n }\n}\n```\n\nA testimonial attributes the quote to a person with a title and a face. `role` is the person\'s title and organization, and `photo` is their headshot, an `Asset` (a source string or an object with `src` and `alt`):\n\n```json\n{\n "title": "Customer Proof",\n "quote": {\n "text": "The new workflow made exceptions visible before they became escalations.",\n "attribution": "Priya Raman",\n "role": "VP Operations, Acme",\n "photo": { "src": "asset:priya-raman", "alt": "Priya Raman" },\n "source": "Customer interview, March 2026"\n }\n}\n```\n\nThe footer under the quote is the attribution, then the role on its own line, then the source after ` - ` on the last line. Without a `role` it is the single line it always was (`attribution - source`). With a `photo`, the headshot is a circle at the start edge of the footer row (the left in a left-to-right deck, the right in a right-to-left one), three times the footer font size across, and the footer lines sit beside it, centered on it. The photo is cropped to fill the circle, and its size follows the footer\'s font size when the readability floor shrinks the footer. Without a `photo` nothing else changes. The preview draws it with the circular clip that an image block uses for shape `circle`, and PowerPoint export writes a native picture with the `ellipse` geometry and the alt text as its description; importing that file restores `role` and `photo`. A photo needs alt text (the `opf/missing-alt-text` rule of `validate` checks `quote.photo`); an SVG photo exports as its PNG raster. A photo with no attribution, role or source still draws, alone in the footer row. The Markdown dialect has no native form for `role` and `photo`: a quote that carries them is written as an `opf` block, which round trips exactly, and a plain `> \u2014 Name, Title` line stays an attribution.\n\n## Timeline\n\nTimeline-specific fields are grouped under `timeline`. An array value is shorthand for `timeline.events`; use object form when the timeline needs a name or description. Timeline events use `when`, `what`, `description` and `status`. `status` is `done`, `current` or `planned` and marks progress: `done` is a filled marker with normal text, `current` ("we are here") is a ringed marker with a bold label, and `planned` is a hollow outlined marker with muted text. An event without a status draws as a plain filled marker. Schedule health (at risk, blocked) is not a status; say it in the event text. See [Dynamic composition](dynamic-composition.md#timeline-internals) for the exact drawing.\n\n```json\n{\n "title": "Rollout Plan",\n "timeline": {\n "name": "Regional Rollout",\n "description": "Major milestones for the rollout.",\n "events": [\n {\n "when": "Q1",\n "what": "Pilot",\n "description": "Launch with one operations team."\n },\n {\n "when": "Q2",\n "what": "Rollout",\n "description": "Expand to all regions."\n }\n ]\n }\n}\n```\n\nA roadmap with progress states:\n\n```json\n{\n "title": "Product roadmap",\n "timeline": [\n { "when": "Q1", "what": "Discovery", "status": "done" },\n { "when": "Q2", "what": "Pilot", "status": "current" },\n { "when": "Q3", "what": "Rollout", "status": "planned" }\n ]\n}\n```\n\n## Regions\n\nRegion keys address a 3\xD73 grid of rows (`top`, `middle`, `bottom`) and columns (`left`, `center`, `right`):\n\n```\n left center right\n +--------------------+--------------------+--------------------+\n top | top:left | top:center | top:right |\n +--------------------+--------------------+--------------------+\n middle | middle:left | middle:center | middle:right |\n +--------------------+--------------------+--------------------+\n bottom | bottom:left | bottom:center | bottom:right |\n +--------------------+--------------------+--------------------+\n```\n\n- A bare column key (`left`) spans all three rows; a bare row key (`top`) spans all three columns.\n- `+` spans adjacent rows or columns: `center+right`, `top+middle`.\n- `row:column` combines the two: `top:left`, `middle+bottom:center+right`.\n- Keys on one slide must not overlap, and regions cannot be mixed with root payload fields.\n\nSpans compose into common slide shapes:\n\n```\n "left" + "center+right" "top" + "middle+bottom"\n (sidebar + main) (headline band + body)\n +----------+------------------+ +-------------------------------+\n | | | | top |\n | | | +-------------------------------+\n | left | center+right | | |\n | | | | middle+bottom |\n | | | | |\n +----------+------------------+ +-------------------------------+\n\n "top" + "middle+bottom:left" + "middle+bottom:center+right"\n (headline band, then sidebar + main)\n +---------------------------------------------+\n | top |\n +---------------+-----------------------------+\n | | |\n | middle+bottom | middle+bottom:center+right |\n | :left | |\n | | |\n +---------------+-----------------------------+\n```\n\nThe same payload objects work inside regions \u2014 here, the sidebar-plus-main shape:\n\n```json\n{\n "title": "Operating Snapshot",\n "left": {\n "table": {\n "columns": ["Metric", "Value"],\n "rows": [\n ["Revenue", "$4.2M"],\n ["Gross margin", "68%"]\n ]\n }\n },\n "center+right": {\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Month", "Revenue"],\n "rows": [\n ["Jan", 3.4],\n ["Feb", 3.8],\n ["Mar", 4.2]\n ]\n }\n }\n }\n}\n```\n\n## Accessibility of the preview\n\nThe SVG preview and the PowerPoint export agree on what a reader can reach. The slide root is a labelled container (`role="group"`, `aria-roledescription="slide"`, `aria-label` set to the slide title), never an image: an image makes everything inside it presentational, so a screen reader would hear only the title. Text stays reachable, in the order it is drawn: tag, title and subtitle, the content in layout order, footnotes, then header and footer. That covers body text, list entries, quotes, metric values and labels, table cells, timeline entries, captions and furniture text.\n\nA picture with `alt` is an image with that name. Purely decorative drawing is hidden (`aria-hidden`): the background and its overlay, card frames, table cell fills and borders, dividers, timeline markers, watermarks, list bullets, a picture or chart whose `alt` is `""`, and a header or footer picture without `alt` (a logo repeated on every slide). A chart with `alt` is an image with that name, and its drawn marks are not announced separately. The deck element, the player and `renderDeckHtml` in `@openpresentation/opf-render` put the one slide container (region or figure) around the drawing, so a slide is announced once.\n\nTesting status (opf-render#170): this contract is the DOM and ARIA structure. It is checked with axe-core, the browser\'s accessibility tree and focus-order assertions, and it is **not tested with assistive technology**: no screen reader (NVDA, JAWS, VoiceOver, TalkBack) has been run against the preview, the deck element or the player (owner decision 2026-10-10). Forced colors are verified with Chromium\'s emulation only, not the Windows contrast theme ([evidence](evidence/rr-59-forced-colors-emulated-20261010/README.md)).\n'
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"slug": "conversions",
|
|
@@ -100,7 +100,7 @@ var docsData = Object.freeze([
|
|
|
100
100
|
"slug": "examples",
|
|
101
101
|
"file": "docs/examples.md",
|
|
102
102
|
"title": "OPF Examples Guide",
|
|
103
|
-
"markdown": "# OPF Examples Guide\n\nThe `examples/` directory has two shipped layers, plus a docs fixture kept outside the catalog:\n\n- `examples/technical/` contains compact fixtures that isolate one or two schema behaviors.\n- `examples/gallery/` contains scenario-oriented decks that show OPF working across industries, functions, education, government, international, presentation-type, and design/media use cases.\n- The representative deck for [the published-package quickstart](quickstart.md) lives at [`docs/quickstart/developer-quickstart.opf.json`](quickstart/developer-quickstart.opf.json), outside the catalog, so `@openpresentation/opf/examples` stays at the published example count (currently
|
|
103
|
+
"markdown": "# OPF Examples Guide\n\nThe `examples/` directory has two shipped layers, plus a docs fixture kept outside the catalog:\n\n- `examples/technical/` contains compact fixtures that isolate one or two schema behaviors.\n- `examples/gallery/` contains scenario-oriented decks that show OPF working across industries, functions, education, government, international, presentation-type, and design/media use cases.\n- The representative deck for [the published-package quickstart](quickstart.md) lives at [`docs/quickstart/developer-quickstart.opf.json`](quickstart/developer-quickstart.opf.json), outside the catalog, so `@openpresentation/opf/examples` stays at the published example count (currently 128 decks); the renderer golden corpus tracks that catalog on its own release cadence.\n- The runnable first deck lives at [`docs/first-deck/`](first-deck/README.md), also outside the catalog (a deck under `examples/` would change the examples corpus that the renderer golden covers): six slides (bullets, a metric, a chart, a styled table, a local image) and a README that goes from install to validate, PNG, PPTX and opening the file in PowerPoint or Keynote, with the known limits.\n- `examples/markdown/` holds decks written in the [Markdown dialect](markdown.md) (`.opf.md`, not `.opf.json`, so they are outside the catalog count): `quarterly-review.opf.md` is a Markdown deck that every command reads (`opf validate examples/markdown/quarterly-review.opf.md`); it uses every block kind and is canonical. `outline.md` is a plain outline, not a deck (a plain `.md` file is never taken for one), read with `opf convert outline.md deck.opf.json` (a plain `.md` input is OPF Markdown, and an outline with no `---` is cut at its headings). The tests convert, validate, compose and round trip them.\n- The examples root is kept as an organizing directory rather than a home for standalone OPF files.\n\n## Technical Fixtures\n\nUse `examples/technical/` when you want a small file that exercises a specific schema surface:\n\n- content payloads, rich text, blocks, charts, tables, media, metrics, quotes, and timelines\n- promoted region keys and span combinations\n- asset string/object forms and asset-backed chart data\n- design backgrounds, organization logo shapes, headers, footers, watermarks, and slide-level overrides\n- metadata array forms, language metadata, custom narrative records, and catalog overrides\n\n## Gallery Folders\n\n| Folder | What It Demonstrates |\n| --- | --- |\n| `industries/` | Vertical market decks with operating plans, investment briefs, readiness reviews, and launch coordination. |\n| `business-functions/` | Department-specific decks for sales, marketing, product, engineering, finance, HR, legal, security, support, procurement, and strategy. |\n| `education/` | K-12, higher education, research, advising, workforce, advancement, and student services scenarios. |\n| `government/` | Public health, transit, emergency management, utilities, regulators, courts, parks, workforce, tax, and civic engagement decks. |\n| `presentation-types/` | Reusable deck archetypes such as pitches, board updates, QBRs, conference talks, workshops, postmortems, launches, policy briefings, training, and research reports. |\n| `international/` | Region- or language-specific decks, including examples of language object metadata and right-to-left direction. |\n| `design-and-media/` | Decks that emphasize design controls, image/video assets, data storytelling, and self-running orientation patterns. |\n\n## Patterns To Look For\n\n- Technical fixtures that isolate validator and renderer behavior.\n- Sparse gallery documents that use shorthand catalog references and a small slide list.\n- Medium documents with schema ids, metadata, organization and speaker records, design overrides, assets, and richer slide payloads.\n- Dense documents with their own records in `catalogs.custom`, promoted region keys, `blocks`, media assets, code payloads, header/footer configuration, organization logo shapes, watermarks, and extensions.\n- Mixed content payloads across text, bullets, lists, image, video, chart, table, code, metric, quote, and timeline slides.\n- Catalog references across narratives, layouts, themes, color schemes, font schemes, audiences, purposes and tones, every record embedded under `catalogs.default` or `catalogs.custom`, so each deck validates and renders with no catalog registered; chart types, languages and social platforms as engine vocabularies.\n\n## Validation\n\nRun the example validator after changing any `*.opf.json` file:\n\n```sh\nnode scripts/validate-examples.mjs\n```\n\nThe script walks every OPF document under `examples/` and reports schema or semantic validation issues with file paths.\n"
|
|
104
104
|
},
|
|
105
105
|
{
|
|
106
106
|
"slug": "finding-schema-reference",
|
|
@@ -286,13 +286,13 @@ var docsData = Object.freeze([
|
|
|
286
286
|
"slug": "quickstart",
|
|
287
287
|
"file": "docs/quickstart.md",
|
|
288
288
|
"title": "Developer quickstart",
|
|
289
|
-
"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**Verified artifacts (2026-10-17):** core **0.20.5**, CLI **0.20.1**,\nrenderer **0.20.4**, PPTX **0.20.4**, editor **0.20.4** and Gallery **3.0.0**.\n[Artifact identities and verification](https://github.com/OpenPresentation/opf/blob/main/docs/evidence-rr-87-final-published-set-2026-10-17.md) records\nthe releases selected in `release-plan.json`; artifact verification is separate\nfrom 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.20.5 \\\n @openpresentation/opf-render@0.20.4 \\\n @openpresentation/opf-editor@0.20.4 \\\n @openpresentation/opf-pptx@0.20.4 \\\n @openpresentation/cli@0.20.1 \\\n @openpresentation/gallery@3.0.0\n# opf-render 0.20 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. An opt-in native\nbody mode is in progress and is not published yet. 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"
|
|
289
|
+
"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**Verified artifacts (2026-10-17):** core **0.20.5**, CLI **0.20.1**,\nrenderer **0.20.4**, PPTX **0.20.4**, editor **0.20.4** and Gallery **3.0.0**.\n[Artifact identities and verification](https://github.com/OpenPresentation/opf/blob/main/docs/evidence-rr-87-final-published-set-2026-10-17.md) records\nthe releases selected in `release-plan.json`; artifact verification is separate\nfrom 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.20.5 \\\n @openpresentation/opf-render@0.20.4 \\\n @openpresentation/opf-editor@0.20.4 \\\n @openpresentation/opf-pptx@0.20.4 \\\n @openpresentation/cli@0.20.1 \\\n @openpresentation/gallery@3.0.0\n# opf-render 0.20 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"
|
|
290
290
|
},
|
|
291
291
|
{
|
|
292
292
|
"slug": "release-process",
|
|
293
293
|
"file": "docs/release-process.md",
|
|
294
294
|
"title": "OPF Release Process",
|
|
295
|
-
"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 jobs, which run in the merge queue 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 public JavaScript package,\n[`@openpresentation/opf`](https://www.npmjs.com/package/@openpresentation/opf).\n\nThe canonical release path is:\n\n1. Merge the release commit to `main`.\n2. Push a semver tag whose name matches the package version.\n3. Let GitHub Actions publish to npm through npm trusted publishing.\n4. Verify npm and the GitHub Release the publish workflow creates from the changelog section.\n\n## Agent authorization and coordinated release order\n\nThe owner authorized agents to prepare and publish npm releases on 2026-09-29\nand for future releases whenever a release is required. This does not waive any\ngate: a release still needs a merged release-prep PR, green required checks and\nthe verification below. Agents keep publishing on the trusted-publishing\nworkflows (GitHub Actions OIDC with `--provenance`). If a workflow cannot run,\nstop and repair the trusted-publishing path; do not substitute a local publish\nor an npm-token publish that omits the required provenance.\n\nThe engine packages depend on each other, so publish in this order and wait for\neach version to appear on the registry before starting the next:\n\n1. `@openpresentation/opf` (this repository, `opf-vX.Y.Z` tag).\n2. `@openpresentation/opf-render` (`opf-render-vX.Y.Z` tag) and\n `@openpresentation/opf-pptx` (`opf-pptx-vX.Y.Z` tag). Both depend on core; PPTX\n also devDepends on the renderer, so publish the renderer first.\n3. `@openpresentation/opf-editor` (`opf-editor-vX.Y.Z` tag), which depends on core\n and peers/devDepends on the renderer and PPTX.\n\nEach sibling's release-prep PR raises its dependency floors to the just-published\nversions. Its lockfile can only be refreshed after the upstream version exists on\nnpm (`npm install --package-lock-only`), so merge sibling release PRs only after\nthe upstream publish. `@openpresentation/cli` depends on core (a regular dependency; its floor is raised by the release-prep PR like the others, the commands run that core's Node engine) and is released\nseparately by `cli-publish.yml` (`cli-vX.Y.Z`) when a CLI release is needed.\n\nThe CLI release gate includes the installed-package regression `packages/cli/test/installed-regression.mjs` (RR-59, [opf#476](https://github.com/OpenPresentation/opf/issues/476)). It drives the installed `opf` binary beside the published opf-render and opf-pptx, the render font packages and the Noto JP and SC script packages, with no network after the install (a preload refuses every connection and fetch, and a local server that the image URLs point at counts zero requests): `opf convert deck.opf.json deck.svg` embeds `Noto Sans JP` 400 and 700 for a `ja-JP` deck, nothing under `--text system` and no CJK face for a Latin-only deck; `opf convert deck.opf.json deck.pptx` of a deck with an `https:` content image (an image block and a quote photo) exits 0 with a `pptx/unresolved-asset` warning at the slide path and the placeholder, exits 1 and writes no file under `--fail-on warning`, and exports a native picture for an embedded or local PNG under `--fail-on warning`. It runs in `pnpm test:cli:packed:peers` (the candidate CLI and core tarballs; OPF CI, the Windows and engines-range CLI jobs, and `cli-publish.yml` before `npm publish`) and in `pnpm test:registry-ecosystem` (the published set of `release-plan.json`, from plan 0.16).\n\nA sibling pull request that needs a core that is not on npm yet (a breaking core release and its sibling PRs, `Depends-On: OpenPresentation/opf#N`) declares it in its `package.json` as `\"opf\": { \"requiresUnreleasedCore\": \"X.Y.Z\" }` (RR-55). Its CI then skips only the packed install against published dependencies (`npm run test:packed`, with a `::notice::`) while the installed published `@openpresentation/opf` is lower than `X.Y.Z`; every linked-ecosystem step still runs. The field is for pull requests only: `release-train.mjs prep` deletes it (and refuses a field above the core of the train), and `plan` flags a release commit whose `package.json` still carries it.\n\nRelease-prep PRs contain only version bumps, changelog entries, dependency ranges\nand lockfile changes (plus current-instruction docs). The changelog entries are\nnot written by hand: every change adds a fragment `changes/<slug>.md` in its own\nPR (RR-46, [changes/README.md](../changes/README.md)), and the release-prep PR\nruns the assembler, which moves the fragments into the new release section of\n`CHANGELOG.md` and deletes them. Each sibling repository has the same\n`changes/` directory and script:\n\n```sh\n# core (packages are named in each fragment: opf = CHANGELOG.md, cli = packages/cli/CHANGELOG.md)\nnode scripts/changelog-fragments.mjs assemble --version X.Y.Z --package opf --date YYYY-MM-DD\nnode scripts/changelog-fragments.mjs assemble --version A.B.C --package cli # only when the CLI is released\n# opf-render, opf-pptx, opf-editor\nnode scripts/changelog-fragments.mjs assemble --version X.Y.Z --date YYYY-MM-DD [--summary \"Patch release: ...\"]\n```\n\nUse `--dry-run` to preview, and check that no `changes/*.md` other than\n`README.md` remains for the released package before opening the PR. After the whole set is on\nthe registry, a follow-up docs change updates `release-plan.json`, the\ncompatibility matrix and the quickstart to the published set, and the gallery\nconsumer dependencies are bumped.\n\n## Release train (scripted, RR-51)\n\n`scripts/release-train.mjs` runs the coordinated release above in lockstep order. It is run by the supervisor with\ntheir own `gh` login (or `GH_TOKEN`); every command is a dry run unless `--execute` is given. It only reads, opens\nrelease-prep pull requests and creates tags: it never merges a pull request and never publishes. Each package is still\npublished by its own repository's trusted-publishing workflow (OIDC, `--provenance`) when its tag appears.\n\nName the versions of the train, any subset: `--core X.Y.Z --render X.Y.Z --pptx X.Y.Z --editor X.Y.Z --cli X.Y.Z`.\nThe order is always core, then opf-render, then opf-pptx, then opf-editor and the CLI.\n\n```sh\n# 1. What is missing (read only; exit 1 until every package is on npm)\nnode scripts/release-train.mjs plan --core 0.12.1 --render 0.12.1 --pptx 0.12.3 --editor 0.11.3 --cli 0.10.1\n\n# 2. The release-prep PR of the next package, once its upstream is on npm (dry run first: a scratch clone and the diff)\nnode scripts/release-train.mjs prep render --core 0.12.1 --render 0.12.1 [--item RR-nn]\nnode scripts/release-train.mjs prep render --core 0.12.1 --render 0.12.1 --item RR-nn --execute\n\n# 3. After a person merged it with green CI: tag the release commit, wait for the publish run and npm, verify\nnode scripts/release-train.mjs tag render --core 0.12.1 --render 0.12.1 --execute\n\n# 4. Verify any published version (also run by `tag` and `run`)\nnode scripts/release-train.mjs verify @openpresentation/opf-render@0.12.1\n\n# Or the whole sequence: it stops at the first step that needs a person and prints the command to resume\nnode scripts/release-train.mjs run --core 0.12.1 --render 0.12.1 --pptx 0.12.3 --editor 0.11.3 --cli 0.10.1 --execute\n```\n\nWhat each step checks:\n\n- `plan` reads, per package: whether the version is already on npm (then it is only verified), the version in the\n manifest at `main`'s head, the release commit (the commit that set the version) and its merged release-prep PR, the\n required checks on the release commit (the `main` ruleset's required checks; for a repository without a ruleset,\n every reported check apart from existing advisory bots and the identity-verified scheduled Editor flake measurement), the tag, whether the upstream versions of the train are on npm, and the dependency floors.\n It flags every sibling whose `@openpresentation/opf` floor stays below a new core in the train, with hints from core's\n fragments or changelog section: the tool cannot know whether a core release moves geometry, so the release owner\n decides whether the lockstep rule below applies (flag, never decided).\n- `prep` refuses until every upstream version of the train is on npm. In a scratch clone of `main` it bumps the\n version, runs `node scripts/changelog-fragments.mjs assemble --version X.Y.Z --date <today>` (with `--package opf` or\n `--package cli` in core, `--summary` when given), raises the floors that name a package of the train (caret and\n tilde ranges keep their operator; exact devDependency pins move to the exact version; `workspace:*` is untouched;\n for CLI preparation, already-compatible patch peer ranges stay unchanged while development pins advance;\n core runtime files are never edited by CLI prep) and refreshes the lockfile\n (`npm install --package-lock-only` in the siblings, `pnpm install --lockfile-only` in core). It fails if a fragment\n for the package is left or if anything other than the manifest, changelog, fragments, lockfile and peers file\n changed. The PR body lists README lines that name the previous version for a person to review; prose is not\n rewritten. It also deletes `opf.requiresUnreleasedCore` from the manifest when the sibling declares it (see above; `plan` flags a release commit that keeps it). Branch `codex/release-<package>-<x-y-z>` unless `--branch` is given. An open release-prep PR (found by\n branch or by a \"release <package> X.Y.Z\" title) or a merged one is detected and nothing is written.\n- `tag` re-verifies right before tagging: the version at the release commit, that the commit is on `main`, green\n required checks, every upstream of the train on npm and every runtime floor naming a version npm has. It then creates\n `refs/tags/<prefix>X.Y.Z` (`opf-v`, `opf-render-v`, `opf-pptx-v`, `opf-editor-v`, `cli-v`) with\n `POST /repos/{repo}/git/refs` on the release commit (checks that are still running, or not reported yet right after\n the merge, stop it unless `--checks-wait-minutes N` is given: it then polls them every `--poll-seconds` for up to N\n minutes, and a red check still stops it at once), polls that repository's publish workflow run for the tag\n (default every 120 s, up to 90 min), polls npm until the version is visible, and runs `verify`. A tag that already\n exists on the release commit is not created again; one on another commit stops the train. A failed publish run stops\n with its link: never move or re-push the tag; re-run a transient failure (`gh run rerun <id> --failed`), fix a real\n one on `main` with a new version.\n- `verify` checks the registry manifest, that `gitHead` equals the tagged commit (and that the commit is on `main` and\n carries the version), `dist.attestations` with the SLSA v1 provenance predicate, the provenance statement itself\n (built by `.github/workflows/<publish workflow>` of the package's repository on `refs/tags/<tag>` from the tagged\n commit, subject digest equal to `dist.integrity`), `npm audit signatures --include-attestations` in a scratch\n project that installs exactly that version (no invalid or missing signatures; the package has a verified\n attestation), and the GitHub release for the tag (every lockstep package's publish workflow creates one from 0.18\n on, RR-20; a gallery release has none). A release that is not \"Latest\" passes: the CLI's is created with\n `--latest=false` because core owns \"Latest\" in `OpenPresentation/opf`. The dist-tag is reported for information.\n - Propagation: right after a publish, npm serves the packument (with `dist.attestations.url`) minutes before the\n attestation bundle (HTTP 404 `{\"error\":\"Not found\"}`) and before a fresh `npm install` can resolve the version\n (`notarget`, \"No matching version found\"); the first live use, core 0.12.1, failed `verify` on exactly these two\n for 2-4 minutes. Only those two checks wait: `tag` and `run` retry the bundle fetch and the scratch install every\n 30 s (`--attest-poll-seconds`) for up to 15 min (`--attest-wait-minutes`; 0 turns the wait off) and say what they\n wait for in the log. Any answer that is not \"not yet there\" fails at once without waiting: a bundle that is\n present but has no SLSA provenance or names another repository, workflow, ref, commit or digest, an invalid or\n missing signature, any other install error. If the wait runs out, the check fails with \"still not propagated\n after N min\". Standalone `verify` keeps failing fast; `verify <package>@X.Y.Z --wait <minutes>` gives it the same\n retry (for example right after a publish run you started by hand).\n- `run` repeats `plan` per package in order and does the next step: verify what is on npm, stop at an open release-prep\n PR, open a missing one (`prep`), wait for pending checks on a merged release commit, then `tag`. Re-running it after\n a stop skips every step already done; a version already on npm is never published again.\n\nThe follow-up docs PR (`release-plan.json`, the compatibility matrix and the quickstart) and the gallery consumer bumps\nstay by hand after the train.\n\n`.github/workflows/release-train.yml` is the same script as a `workflow_dispatch` (inputs: the versions and `mode`).\nUntil the GitHub App of [opf#298](https://github.com/OpenPresentation/opf/issues/298) exists it is plan-only: tags\npushed with a workflow's `GITHUB_TOKEN` start no other workflow (so the publish workflows would never run), and\n`GITHUB_TOKEN` cannot push branches or open pull requests in the sibling repositories, so `mode: execute` fails at\nonce. With the App (`ECOSYSTEM_APP_ID` variable and `ECOSYSTEM_APP_PRIVATE_KEY` secret, the roller's App) `mode:\nexecute` runs `run --execute` with the App's token.\n\nThe steps by hand below remain the fallback when the script cannot run.\n\n## The gallery lane (RR-78)\n\n`@openpresentation/gallery` (the pptx.gallery catalog records) is released from its own repository,\n[OpenPresentation/gallery](https://github.com/OpenPresentation/gallery), on its own version line, outside the lockstep\ntrain: `release-train.mjs` has it in `INDEPENDENT_PACKAGES` (repository `gallery`, tags `vX.Y.Z`, workflow\n`gallery-publish.yml`), never in `PACKAGES`, so no train plans, preps or tags it, and `release-plan.json` lists it under\n`independentPackages`. A gallery release needs no core release, and a core release does not release the gallery.\n\n- **Versions.** New records are a minor release. Removing or renaming an id, changing how an existing record draws, or\n a change that needs a newer record schema (`opf.catalogSchema`) is a major release. The gallery repository's\n pixel-stability check (`npm run check:stability`, on every pull request there and before every publish) fails a\n minor or patch release that changes an existing record's drawing or removes an id.\n- **Release and publish.** In the gallery repository: a pull request that sets the version and the `CHANGELOG.md`\n section, then a `vX.Y.Z` tag on main. Its `gallery-publish.yml` refuses a tag that is not `v` plus the version, a\n tag off main and a `repository` other than OpenPresentation/gallery, runs every check, and publishes with\n `npm publish --access public --provenance` (see its CONTRIBUTING.md).\n- **Verify.** `node scripts/release-train.mjs verify gallery@X.Y.Z` here (tag `vX.Y.Z` and gitHead in\n OpenPresentation/gallery, provenance built by `gallery-publish.yml` on `refs/tags/vX.Y.Z`, signatures). The workflow\n creates no GitHub release.\n- **Trusted publisher on npm** (`@openpresentation/gallery`, Settings, Trusted publishing): GitHub Actions,\n organization `OpenPresentation`, repository `gallery`, workflow filename `gallery-publish.yml`, no environment.\n- **First release.** `1.0.0` is the first version published from OpenPresentation/gallery; `0.0.1` was a placeholder\n published by hand. Against 0.0.1 the stability check runs as a major release and reports no changed record.\n\nFrom OPF 0.19, core and the CLI consume the independently published Gallery package: their Node defaults register\n`gallery`, and core has no `/catalog` subpath. OPF 0.19 used Gallery 2; OPF 0.20 uses Gallery 3.\nThe current ranges and exact verified set are in the package manifests and [release-plan.json](../release-plan.json).\nA core or CLI release needs a Gallery version its range accepts on npm first. A reviewed registry-adoption PR\nupdates those ranges and the authentic lockfile after independent publication verification. Stable release\npreparation follows that adoption; it does not perform the source configuration transition.\n\nBefore registry adoption, explicit authenticated candidate CI can stage reviewed gallery and engine tarballs\n(`scripts/candidate-gallery-build.mjs`, `scripts/pack-cli-candidate.mjs`, `scripts/pack-ecosystem.mjs`). Its recorded\noriginal/staged metadata and runtime checks qualify development consumers; they do not prove the declared source\nranges are registry-installable or authorize publication. Ordinary registry and publication gates stay unchanged.\nCore's registry tests use the exact gallery version and integrity recorded in the workspace lockfile.\n\n## Geometry-moving core releases: lockstep floors\n\nCore composition changes that move geometry (for example opf#169 cover centering) make the preview and the PPTX export drift when `@openpresentation/opf-render` and `@openpresentation/opf-pptx` resolve different core versions (measured: 186-300 pt title offsets on covers).\n\nRule: when a core release contains composition or geometry changes, the same release train must raise BOTH the renderer's and PPTX's core floor (`dependencies` and, where present, `peerDependencies`) to that core version, publish them together, and raise the editor's floor too. Do not release core alone and leave a sibling on the older floor.\n\nThe parity harness must always run with `--import <opf>/scripts/register-local-opf.mjs` (as `run.ps1` does) so every engine shares one core.\n\n## Release Preconditions\n\nBefore tagging, confirm that the release commit on `main` already contains:\n\n- `packages/javascript/package.json` with the intended version.\n- `CHANGELOG.md` with the matching release section (assembled from `changes/`, see above).\n- Passing `OPF CI` on the release commit.\n\nThe publish workflow validates the tag name against\n`packages/javascript/package.json`, so the tag must point at the release commit.\n\n## Tag And Publish\n\nUse the `opf-vX.Y.Z` tag form for the package release:\n\n```sh\ngit checkout main\ngit pull origin main\ngrep '\"version\"' packages/javascript/package.json\ngit tag opf-vX.Y.Z\ngit push origin opf-vX.Y.Z\n```\n\nFor example, version `0.3.0` used:\n\n```sh\ngit tag opf-v0.3.0\ngit push origin opf-v0.3.0\n```\n\nPushing the tag triggers `.github/workflows/npm-publish.yml`. The workflow:\n\n- runs on tags matching `opf-v*` or `@openpresentation/opf@v*`\n- installs dependencies with pnpm on Node 24\n- verifies the tag matches `packages/javascript/package.json`\n- runs typecheck and tests\n- runs the npm package dry-run check\n- publishes from `packages/javascript` with `npm publish --access public --provenance`\n\nDo not rerun a successful publish for the same version. npm package versions are\nimmutable; a second publish for an already-published version should fail.\n\n## Trusted Publishing\n\nnpm publishing is configured to use GitHub Actions OIDC trusted publishing, not\na long-lived npm token.\n\nExpected npm package trusted-publisher settings:\n\n| Setting | Value |\n|---|---|\n| Package | `@openpresentation/opf` |\n| Publisher | GitHub Actions |\n| Organization/repository | `OpenPresentation/opf` |\n| Workflow filename | `npm-publish.yml` |\n| Environment | empty, unless the workflow is later moved behind a GitHub Environment |\n| Permission | `npm publish` |\n\nExpected workflow settings:\n\n```yaml\npermissions:\n contents: read\n id-token: write\n```\n\nThe publish step should not set `NODE_AUTH_TOKEN`:\n\n```yaml\n- name: Publish to npm\n working-directory: packages/javascript\n run: npm publish --access public\n```\n\nIf a future release fails with npm authentication errors, check the npm\ntrusted-publisher settings first and repair the OIDC workflow. Do not add an\n`NPM_TOKEN` fallback or publish locally; the required trusted-publishing and\nprovenance gates still apply.\n\n## Verify The Release\n\n`node scripts/release-train.mjs verify <package>@X.Y.Z` runs every registry check of this section and the provenance\nchecks (see \"Release train\" above). By hand, after the workflow completes, verify npm:\n\n```sh\nnpm view @openpresentation/opf version\n```\n\nThe output should equal the package version that was tagged.\n\nSpot-check the validator API from a clean project or temporary directory:\n\n```sh\nnpm install @openpresentation/opf@X.Y.Z\nnode --input-type=module -e \"import {validate} from '@openpresentation/opf'; console.log(validate({name:'t', narrative:'not-a-real-id', slides:[{title:'t'}]}, {only:['format','references']}).findings)\"\n```\n\nThe expected result is one `opf/unresolved-reference` warning for the narrative (no catalog is registered).\n\n## GitHub Release Notes\n\nFrom 0.18 on, every lockstep package's tag workflow ends in a `release` job that creates a GitHub Release after the npm\npublish (RR-20). The job holds `contents: write`; the publish job keeps only `id-token: write` for provenance. It takes\nthe notes from the matching `## X.Y.Z` section of the package's changelog and fails when the section is missing.\n\n| Package | Repository, workflow | Tag | Title | Changelog | Latest |\n| --- | --- | --- | --- | --- | --- |\n| `@openpresentation/opf` | `opf`, `npm-publish.yml` | `opf-vX.Y.Z` | `@openpresentation/opf X.Y.Z` | `CHANGELOG.md` | yes |\n| `@openpresentation/opf-render` | `opf-render`, `npm-publish.yml` | `opf-render-vX.Y.Z` | `@openpresentation/opf-render X.Y.Z` | `CHANGELOG.md` | yes |\n| `@openpresentation/opf-pptx` | `opf-pptx`, `release.yml` | `opf-pptx-vX.Y.Z` | `@openpresentation/opf-pptx X.Y.Z` | `CHANGELOG.md` | yes |\n| `@openpresentation/opf-editor` | `opf-editor`, `release.yml` | `opf-editor-vX.Y.Z` | `@openpresentation/opf-editor X.Y.Z` | `CHANGELOG.md` | yes |\n| `@openpresentation/cli` | `opf`, `cli-publish.yml` | `cli-vX.Y.Z` | `@openpresentation/cli X.Y.Z` | `packages/cli/CHANGELOG.md` | no (core owns it) |\n\nReleases before 0.18 are not backfilled. `verify` requires the release for each of these packages. If release creation\nfailed, rerun the failed `release` job (it keeps an existing release and its notes), or create the missing release for\nthe existing tag, for example:\n\n```sh\ngh release create opf-render-vX.Y.Z \\\n --repo OpenPresentation/opf-render \\\n --title '@openpresentation/opf-render X.Y.Z' \\\n --notes-file /path/to/release-notes.md \\\n --verify-tag --latest\n```\n\nUse the matching `## X.Y.Z` section of the changelog as the release notes.\n\n## Troubleshooting\n\nIf the tag/version check fails, the tag does not point at the release commit or\nthe tag name does not match `packages/javascript/package.json`. Stop and inspect\nthe exact tag and merged release source. Never move or re-push a release tag.\nFix a real source or version error on `main` through a new release-prep PR and\nuse a new version/tag after its gates pass. A transient workflow failure may be\nretried on the same unchanged tag; it does not authorize replacing the tag.\n\nIf npm publish fails with `ENEEDAUTH`, confirm:\n\n- npm has a trusted publisher for `OpenPresentation/opf`\n- the trusted publisher uses workflow filename `npm-publish.yml`\n- `.github/workflows/npm-publish.yml` has `id-token: write`\n- the publish job is running on a modern Node/npm toolchain\n\nIf npm publish fails after the version is already present on npm, do not retry\nthe same publish. Verify the package and treat the failure as a duplicate\npublish attempt.\n\n### CLI peer contracts before a new minor train\n\nA CLI release prep advances development pins, but does not define a new runtime peer contract.\nBefore a train crosses an incompatible peer minor (for example 0.18 to 0.19), the reviewed feature changes\nmust align the CLI's peer metadata with core's peer manifest and `packages/javascript/src/node/peers.ts`.\nRelease core's new contract first. Then CLI prep keeps those aligned ranges and advances its core dependency\nand development pins. If CLI metadata still names the old incompatible line, prep stops before cloning;\nmake the contract change in its own reviewed PR, rather than editing core runtime files in release prep.\n\n\n### Scheduled Editor measurement policy (RR-68)\n\nThe Editor's `.github/workflows/flake-repeat.yml` declares that its scheduled repeat measures rather than gates.\nThe fallback-all mode in `pr-gate.mjs` and `release-train.mjs` therefore treats that measurement as advisory only\nwhen authenticated workflow-run metadata verifies `OpenPresentation/opf-editor`, GitHub Actions app 15368,\nworkflow 373564717, that exact path, event `schedule`, branch `main`, the observed commit SHA and check-suite\nassociation. Both the check runs and the running suite follow this policy; advisory activity and failures remain\nvisible. Explicitly required contexts always gate, including the same job name. Commit statuses, other workflows,\nevents, repositories or apps, and missing or inaccessible identity metadata continue to gate.\n\nIdentity lookups are cached per candidate run and commit for each observer's lifetime, including failed lookups;\nthey do not poll workflow state. The policy changes no checks, thresholds, retries or workflow executions, and does\nnot authorize using an unmerged tooling change for a release.\n"
|
|
295
|
+
"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 jobs, which run in the merge queue 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 public JavaScript package,\n[`@openpresentation/opf`](https://www.npmjs.com/package/@openpresentation/opf).\n\nThe canonical release path is:\n\n1. Merge the release commit to `main`.\n2. Push a semver tag whose name matches the package version.\n3. Let GitHub Actions publish to npm through npm trusted publishing.\n4. Verify npm and the GitHub Release the publish workflow creates from the changelog section.\n\n## Agent authorization and coordinated release order\n\nThe owner authorized agents to prepare and publish npm releases on 2026-09-29\nand for future releases whenever a release is required. This does not waive any\ngate: a release still needs a merged release-prep PR, green required checks and\nthe verification below. Agents keep publishing on the trusted-publishing\nworkflows (GitHub Actions OIDC with `--provenance`). If a workflow cannot run,\nstop and repair the trusted-publishing path; do not substitute a local publish\nor an npm-token publish that omits the required provenance.\n\nThe engine packages depend on each other, so publish in this order and wait for\neach version to appear on the registry before starting the next:\n\n1. `@openpresentation/opf` (this repository, `opf-vX.Y.Z` tag).\n2. `@openpresentation/opf-render` (`opf-render-vX.Y.Z` tag) and\n `@openpresentation/opf-pptx` (`opf-pptx-vX.Y.Z` tag). Both depend on core; PPTX\n also devDepends on the renderer, so publish the renderer first.\n3. `@openpresentation/opf-editor` (`opf-editor-vX.Y.Z` tag), which depends on core\n and peers/devDepends on the renderer and PPTX.\n\nEach sibling's release-prep PR raises its dependency floors to the just-published\nversions. Its lockfile can only be refreshed after the upstream version exists on\nnpm (`npm install --package-lock-only`), so merge sibling release PRs only after\nthe upstream publish. `@openpresentation/cli` depends on core (a regular dependency; its floor is raised by the release-prep PR like the others, the commands run that core's Node engine) and is released\nseparately by `cli-publish.yml` (`cli-vX.Y.Z`) when a CLI release is needed.\n\nThe CLI release gate includes the installed-package regression `packages/cli/test/installed-regression.mjs` (RR-59, [opf#476](https://github.com/OpenPresentation/opf/issues/476)). It drives the installed `opf` binary beside the published opf-render and opf-pptx, the render font packages and the Noto JP and SC script packages, with no network after the install (a preload refuses every connection and fetch, and a local server that the image URLs point at counts zero requests): `opf convert deck.opf.json deck.svg` embeds `Noto Sans JP` 400 and 700 for a `ja-JP` deck, nothing under `--text system` and no CJK face for a Latin-only deck; `opf convert deck.opf.json deck.pptx` of a deck with an `https:` content image (an image block and a quote photo) exits 0 with a `pptx/unresolved-asset` warning at the slide path and the placeholder, exits 1 and writes no file under `--fail-on warning`, and exports a native picture for an embedded or local PNG under `--fail-on warning`. It runs in `pnpm test:cli:packed:peers` (the candidate CLI and core tarballs; OPF CI, the Windows and engines-range CLI jobs, and `cli-publish.yml` before `npm publish`) and in `pnpm test:registry-ecosystem` (the published set of `release-plan.json`, from plan 0.16).\n\nA sibling pull request that needs a core that is not on npm yet (a breaking core release and its sibling PRs, `Depends-On: OpenPresentation/opf#N`) declares it in its `package.json` as `\"opf\": { \"requiresUnreleasedCore\": \"X.Y.Z\" }` (RR-55). Its CI then skips only the packed install against published dependencies (`npm run test:packed`, with a `::notice::`) while the installed published `@openpresentation/opf` is lower than `X.Y.Z`; every linked-ecosystem step still runs. The field is for pull requests only: `release-train.mjs prep` deletes it (and refuses a field above the core of the train), and `plan` flags a release commit whose `package.json` still carries it.\n\nRelease-prep PRs contain only version bumps, changelog entries, dependency ranges\nand lockfile changes (plus current-instruction docs). The changelog entries are\nnot written by hand: every change adds a fragment `changes/<slug>.md` in its own\nPR (RR-46, [changes/README.md](../changes/README.md)), and the release-prep PR\nruns the assembler, which moves the fragments into the new release section of\n`CHANGELOG.md` and deletes them. Each sibling repository has the same\n`changes/` directory and script:\n\n```sh\n# core (packages are named in each fragment: opf = CHANGELOG.md, cli = packages/cli/CHANGELOG.md)\nnode scripts/changelog-fragments.mjs assemble --version X.Y.Z --package opf --date YYYY-MM-DD\nnode scripts/changelog-fragments.mjs assemble --version A.B.C --package cli # only when the CLI is released\n# opf-render, opf-pptx, opf-editor\nnode scripts/changelog-fragments.mjs assemble --version X.Y.Z --date YYYY-MM-DD [--summary \"Patch release: ...\"]\n```\n\nUse `--dry-run` to preview, and check that no `changes/*.md` other than\n`README.md` remains for the released package before opening the PR. After the whole set is on\nthe registry, a follow-up docs change updates `release-plan.json`, the\ncompatibility matrix and the quickstart to the published set, and the gallery\nconsumer dependencies are bumped.\n\n## Release train (scripted, RR-51)\n\n`scripts/release-train.mjs` runs the coordinated release above in lockstep order. It is run by the supervisor with\ntheir own `gh` login (or `GH_TOKEN`); every command is a dry run unless `--execute` is given. It only reads, opens\nrelease-prep pull requests and creates tags: it never merges a pull request and never publishes. Each package is still\npublished by its own repository's trusted-publishing workflow (OIDC, `--provenance`) when its tag appears.\n\nName the versions of the train, any subset: `--core X.Y.Z --render X.Y.Z --pptx X.Y.Z --editor X.Y.Z --cli X.Y.Z`.\nThe order is always core, then opf-render, then opf-pptx, then opf-editor and the CLI.\n\n```sh\n# 1. What is missing (read only; exit 1 until every package is on npm)\nnode scripts/release-train.mjs plan --core 0.12.1 --render 0.12.1 --pptx 0.12.3 --editor 0.11.3 --cli 0.10.1\n\n# 2. The release-prep PR of the next package, once its upstream is on npm (dry run first: a scratch clone and the diff)\nnode scripts/release-train.mjs prep render --core 0.12.1 --render 0.12.1 [--item RR-nn]\nnode scripts/release-train.mjs prep render --core 0.12.1 --render 0.12.1 --item RR-nn --execute\n\n# 3. After a person merged it with green CI: tag the release commit, wait for the publish run and npm, verify\nnode scripts/release-train.mjs tag render --core 0.12.1 --render 0.12.1 --execute\n\n# 4. Verify any published version (also run by `tag` and `run`)\nnode scripts/release-train.mjs verify @openpresentation/opf-render@0.12.1\n\n# Or the whole sequence: it stops at the first step that needs a person and prints the command to resume\nnode scripts/release-train.mjs run --core 0.12.1 --render 0.12.1 --pptx 0.12.3 --editor 0.11.3 --cli 0.10.1 --execute\n```\n\nWhat each step checks:\n\n- `plan` reads, per package: whether the version is already on npm (then it is only verified), the version in the\n manifest at `main`'s head, the release commit (the commit that set the version) and its merged release-prep PR, the\n required checks on the release commit (the `main` ruleset's required checks; for a repository without a ruleset,\n every reported check apart from existing advisory bots and the identity-verified scheduled Editor flake measurement), the tag, whether the upstream versions of the train are on npm, and the dependency floors.\n It flags every sibling whose `@openpresentation/opf` floor stays below a new core in the train, with hints from core's\n fragments or changelog section: the tool cannot know whether a core release moves geometry, so the release owner\n decides whether the lockstep rule below applies (flag, never decided).\n- `prep` refuses until every upstream version of the train is on npm. In a scratch clone of `main` it bumps the\n version, runs `node scripts/changelog-fragments.mjs assemble --version X.Y.Z --date <today>` (with `--package opf` or\n `--package cli` in core, `--summary` when given), raises the floors that name a package of the train (caret and\n tilde ranges keep their operator; exact devDependency pins move to the exact version; `workspace:*` is untouched;\n for CLI preparation, already-compatible patch peer ranges stay unchanged while development pins advance;\n core runtime files are never edited by CLI prep) and refreshes the lockfile\n (`npm install --package-lock-only` in the siblings, `pnpm install --lockfile-only` in core). It fails if a fragment\n for the package is left or if anything other than the manifest, changelog, fragments, lockfile and peers file\n changed. The PR body lists README lines that name the previous version for a person to review; prose is not\n rewritten. It also deletes `opf.requiresUnreleasedCore` from the manifest when the sibling declares it (see above; `plan` flags a release commit that keeps it). Branch `codex/release-<package>-<x-y-z>` unless `--branch` is given. An open release-prep PR (found by\n branch or by a \"release <package> X.Y.Z\" title) or a merged one is detected and nothing is written.\n- `tag` re-verifies right before tagging: the version at the release commit, that the commit is on `main`, green\n required checks, every upstream of the train on npm and every runtime floor naming a version npm has. It then creates\n `refs/tags/<prefix>X.Y.Z` (`opf-v`, `opf-render-v`, `opf-pptx-v`, `opf-editor-v`, `cli-v`) with\n `POST /repos/{repo}/git/refs` on the release commit (checks that are still running, or not reported yet right after\n the merge, stop it unless `--checks-wait-minutes N` is given: it then polls them every `--poll-seconds` for up to N\n minutes, and a red check still stops it at once), polls that repository's publish workflow run for the tag\n (default every 120 s, up to 90 min), polls npm until the version is visible, and runs `verify`. A tag that already\n exists on the release commit is not created again; one on another commit stops the train. A failed publish run stops\n with its link: never move or re-push the tag; re-run a transient failure (`gh run rerun <id> --failed`), fix a real\n one on `main` with a new version.\n- `verify` checks the registry manifest, that `gitHead` equals the tagged commit (and that the commit is on `main` and\n carries the version), `dist.attestations` with the SLSA v1 provenance predicate, the provenance statement itself\n (built by `.github/workflows/<publish workflow>` of the package's repository on `refs/tags/<tag>` from the tagged\n commit, subject digest equal to `dist.integrity`), `npm audit signatures --include-attestations` in a scratch\n project that installs exactly that version (no invalid or missing signatures; the package has a verified\n attestation), and the GitHub release for the tag (every lockstep package's publish workflow creates one from 0.18\n on, RR-20; a gallery release has none). A release that is not \"Latest\" passes: the CLI's is created with\n `--latest=false` because core owns \"Latest\" in `OpenPresentation/opf`. The dist-tag is reported for information.\n - Propagation: right after a publish, npm serves the packument (with `dist.attestations.url`) minutes before the\n attestation bundle (HTTP 404 `{\"error\":\"Not found\"}`) and before a fresh `npm install` can resolve the version\n (`notarget`, \"No matching version found\"); the first live use, core 0.12.1, failed `verify` on exactly these two\n for 2-4 minutes. Only those two checks wait: `tag` and `run` retry the bundle fetch and the scratch install every\n 30 s (`--attest-poll-seconds`) for up to 15 min (`--attest-wait-minutes`; 0 turns the wait off) and say what they\n wait for in the log. Any answer that is not \"not yet there\" fails at once without waiting: a bundle that is\n present but has no SLSA provenance or names another repository, workflow, ref, commit or digest, an invalid or\n missing signature, any other install error. If the wait runs out, the check fails with \"still not propagated\n after N min\". Standalone `verify` keeps failing fast; `verify <package>@X.Y.Z --wait <minutes>` gives it the same\n retry (for example right after a publish run you started by hand).\n- `run` repeats `plan` per package in order and does the next step: verify what is on npm, stop at an open release-prep\n PR, open a missing one (`prep`), wait for pending checks on a merged release commit, then `tag`. Re-running it after\n a stop skips every step already done; a version already on npm is never published again.\n\nThe follow-up docs PR (`release-plan.json`, the compatibility matrix and the quickstart) and the gallery consumer bumps\nstay by hand after the train.\n\n`.github/workflows/release-train.yml` is the same script as a `workflow_dispatch` (inputs: the versions and `mode`).\nUntil the GitHub App of [opf#298](https://github.com/OpenPresentation/opf/issues/298) exists it is plan-only: tags\npushed with a workflow's `GITHUB_TOKEN` start no other workflow (so the publish workflows would never run), and\n`GITHUB_TOKEN` cannot push branches or open pull requests in the sibling repositories, so `mode: execute` fails at\nonce. With the App (`ECOSYSTEM_APP_ID` variable and `ECOSYSTEM_APP_PRIVATE_KEY` secret, the roller's App) `mode:\nexecute` runs `run --execute` with the App's token.\n\nThe steps by hand below remain the fallback when the script cannot run.\n\n## The gallery lane (RR-78)\n\n`@openpresentation/gallery` (the pptx.gallery catalog records) is released from its own repository,\n[OpenPresentation/gallery](https://github.com/OpenPresentation/gallery), on its own version line, outside the lockstep\ntrain: `release-train.mjs` has it in `INDEPENDENT_PACKAGES` (repository `gallery`, tags `vX.Y.Z`, workflow\n`gallery-publish.yml`), never in `PACKAGES`, so no train plans, preps or tags it, and `release-plan.json` lists it under\n`independentPackages`. A gallery release needs no core release, and a core release does not release the gallery.\n\n- **Versions.** New records are a minor release. Removing or renaming an id, changing how an existing record draws, or\n a change that needs a newer record schema (`opf.catalogSchema`) is a major release. The gallery repository's\n pixel-stability check (`npm run check:stability`, on every pull request there and before every publish) fails a\n minor or patch release that changes an existing record's drawing or removes an id.\n- **Release and publish.** In the gallery repository: a pull request that sets the version and the `CHANGELOG.md`\n section, then a `vX.Y.Z` tag on main. Its `gallery-publish.yml` refuses a tag that is not `v` plus the version, a\n tag off main and a `repository` other than OpenPresentation/gallery, runs every check, and publishes with\n `npm publish --access public --provenance` (see its CONTRIBUTING.md).\n- **Verify.** `node scripts/release-train.mjs verify gallery@X.Y.Z` here (tag `vX.Y.Z` and gitHead in\n OpenPresentation/gallery, provenance built by `gallery-publish.yml` on `refs/tags/vX.Y.Z`, signatures). The workflow\n creates no GitHub release.\n- **Trusted publisher on npm** (`@openpresentation/gallery`, Settings, Trusted publishing): GitHub Actions,\n organization `OpenPresentation`, repository `gallery`, workflow filename `gallery-publish.yml`, no environment.\n- **First release.** `1.0.0` is the first version published from OpenPresentation/gallery; `0.0.1` was a placeholder\n published by hand. Against 0.0.1 the stability check runs as a major release and reports no changed record.\n\nFrom OPF 0.19, core and the CLI consume the independently published Gallery package: their Node defaults register\n`gallery`, and core has no `/catalog` subpath. OPF 0.19 used Gallery 2; OPF 0.20 uses Gallery 3.\nThe current ranges and exact verified set are in the package manifests and [release-plan.json](../release-plan.json).\nA core or CLI release needs a Gallery version its range accepts on npm first. A reviewed registry-adoption PR\nupdates those ranges and the authentic lockfile after independent publication verification. Stable release\npreparation follows that adoption; it does not perform the source configuration transition.\n\nBefore registry adoption, explicit authenticated candidate CI can stage reviewed gallery and engine tarballs\n(`scripts/candidate-gallery-build.mjs`, `scripts/pack-cli-candidate.mjs`, `scripts/pack-ecosystem.mjs`). Its recorded\noriginal/staged metadata and runtime checks qualify development consumers; they do not prove the declared source\nranges are registry-installable or authorize publication. Ordinary registry and publication gates stay unchanged.\nCore's registry tests use the exact gallery version and integrity recorded in the workspace lockfile.\n\n## Geometry-moving core releases: lockstep floors\n\nCore composition changes that move geometry (for example opf#169 cover centering) make the preview and the PPTX export drift when `@openpresentation/opf-render` and `@openpresentation/opf-pptx` resolve different core versions (measured: 186-300 pt title offsets on covers).\n\nRule: when a core release contains composition or geometry changes, the same release train must raise BOTH the renderer's and PPTX's core floor (`dependencies` and, where present, `peerDependencies`) to that core version, publish them together, and raise the editor's floor too. Do not release core alone and leave a sibling on the older floor.\n\nThe parity harness must always run with `--import <opf>/scripts/register-local-opf.mjs` (as `run.ps1` does) so every engine shares one core.\n\n## 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 stable version 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- the version is a prerelease: it always needs its own `LAZY_FONT_COUNTS` entry.\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## Release Preconditions\n\nBefore tagging, confirm that the release commit on `main` already contains:\n\n- `packages/javascript/package.json` with the intended version.\n- `CHANGELOG.md` with the matching release section (assembled from `changes/`, see above).\n- Passing `OPF CI` on the release commit.\n\nThe publish workflow validates the tag name against\n`packages/javascript/package.json`, so the tag must point at the release commit.\n\n## Tag And Publish\n\nUse the `opf-vX.Y.Z` tag form for the package release:\n\n```sh\ngit checkout main\ngit pull origin main\ngrep '\"version\"' packages/javascript/package.json\ngit tag opf-vX.Y.Z\ngit push origin opf-vX.Y.Z\n```\n\nFor example, version `0.3.0` used:\n\n```sh\ngit tag opf-v0.3.0\ngit push origin opf-v0.3.0\n```\n\nPushing the tag triggers `.github/workflows/npm-publish.yml`. The workflow:\n\n- runs on tags matching `opf-v*` or `@openpresentation/opf@v*`\n- installs dependencies with pnpm on Node 24\n- verifies the tag matches `packages/javascript/package.json`\n- runs typecheck and tests\n- runs the npm package dry-run check\n- publishes from `packages/javascript` with `npm publish --access public --provenance`\n\nDo not rerun a successful publish for the same version. npm package versions are\nimmutable; a second publish for an already-published version should fail.\n\n## Trusted Publishing\n\nnpm publishing is configured to use GitHub Actions OIDC trusted publishing, not\na long-lived npm token.\n\nExpected npm package trusted-publisher settings:\n\n| Setting | Value |\n|---|---|\n| Package | `@openpresentation/opf` |\n| Publisher | GitHub Actions |\n| Organization/repository | `OpenPresentation/opf` |\n| Workflow filename | `npm-publish.yml` |\n| Environment | empty, unless the workflow is later moved behind a GitHub Environment |\n| Permission | `npm publish` |\n\nExpected workflow settings:\n\n```yaml\npermissions:\n contents: read\n id-token: write\n```\n\nThe publish step should not set `NODE_AUTH_TOKEN`:\n\n```yaml\n- name: Publish to npm\n working-directory: packages/javascript\n run: npm publish --access public\n```\n\nIf a future release fails with npm authentication errors, check the npm\ntrusted-publisher settings first and repair the OIDC workflow. Do not add an\n`NPM_TOKEN` fallback or publish locally; the required trusted-publishing and\nprovenance gates still apply.\n\n## Verify The Release\n\n`node scripts/release-train.mjs verify <package>@X.Y.Z` runs every registry check of this section and the provenance\nchecks (see \"Release train\" above). By hand, after the workflow completes, verify npm:\n\n```sh\nnpm view @openpresentation/opf version\n```\n\nThe output should equal the package version that was tagged.\n\nSpot-check the validator API from a clean project or temporary directory:\n\n```sh\nnpm install @openpresentation/opf@X.Y.Z\nnode --input-type=module -e \"import {validate} from '@openpresentation/opf'; console.log(validate({name:'t', narrative:'not-a-real-id', slides:[{title:'t'}]}, {only:['format','references']}).findings)\"\n```\n\nThe expected result is one `opf/unresolved-reference` warning for the narrative (no catalog is registered).\n\n## GitHub Release Notes\n\nFrom 0.18 on, every lockstep package's tag workflow ends in a `release` job that creates a GitHub Release after the npm\npublish (RR-20). The job holds `contents: write`; the publish job keeps only `id-token: write` for provenance. It takes\nthe notes from the matching `## X.Y.Z` section of the package's changelog and fails when the section is missing.\n\n| Package | Repository, workflow | Tag | Title | Changelog | Latest |\n| --- | --- | --- | --- | --- | --- |\n| `@openpresentation/opf` | `opf`, `npm-publish.yml` | `opf-vX.Y.Z` | `@openpresentation/opf X.Y.Z` | `CHANGELOG.md` | yes |\n| `@openpresentation/opf-render` | `opf-render`, `npm-publish.yml` | `opf-render-vX.Y.Z` | `@openpresentation/opf-render X.Y.Z` | `CHANGELOG.md` | yes |\n| `@openpresentation/opf-pptx` | `opf-pptx`, `release.yml` | `opf-pptx-vX.Y.Z` | `@openpresentation/opf-pptx X.Y.Z` | `CHANGELOG.md` | yes |\n| `@openpresentation/opf-editor` | `opf-editor`, `release.yml` | `opf-editor-vX.Y.Z` | `@openpresentation/opf-editor X.Y.Z` | `CHANGELOG.md` | yes |\n| `@openpresentation/cli` | `opf`, `cli-publish.yml` | `cli-vX.Y.Z` | `@openpresentation/cli X.Y.Z` | `packages/cli/CHANGELOG.md` | no (core owns it) |\n\nReleases before 0.18 are not backfilled. `verify` requires the release for each of these packages. If release creation\nfailed, rerun the failed `release` job (it keeps an existing release and its notes), or create the missing release for\nthe existing tag, for example:\n\n```sh\ngh release create opf-render-vX.Y.Z \\\n --repo OpenPresentation/opf-render \\\n --title '@openpresentation/opf-render X.Y.Z' \\\n --notes-file /path/to/release-notes.md \\\n --verify-tag --latest\n```\n\nUse the matching `## X.Y.Z` section of the changelog as the release notes.\n\n## Troubleshooting\n\nIf the tag/version check fails, the tag does not point at the release commit or\nthe tag name does not match `packages/javascript/package.json`. Stop and inspect\nthe exact tag and merged release source. Never move or re-push a release tag.\nFix a real source or version error on `main` through a new release-prep PR and\nuse a new version/tag after its gates pass. A transient workflow failure may be\nretried on the same unchanged tag; it does not authorize replacing the tag.\n\nIf npm publish fails with `ENEEDAUTH`, confirm:\n\n- npm has a trusted publisher for `OpenPresentation/opf`\n- the trusted publisher uses workflow filename `npm-publish.yml`\n- `.github/workflows/npm-publish.yml` has `id-token: write`\n- the publish job is running on a modern Node/npm toolchain\n\nIf npm publish fails after the version is already present on npm, do not retry\nthe same publish. Verify the package and treat the failure as a duplicate\npublish attempt.\n\n### CLI peer contracts before a new minor train\n\nA CLI release prep advances development pins, but does not define a new runtime peer contract.\nBefore a train crosses an incompatible peer minor (for example 0.18 to 0.19), the reviewed feature changes\nmust align the CLI's peer metadata with core's peer manifest and `packages/javascript/src/node/peers.ts`.\nRelease core's new contract first. Then CLI prep keeps those aligned ranges and advances its core dependency\nand development pins. If CLI metadata still names the old incompatible line, prep stops before cloning;\nmake the contract change in its own reviewed PR, rather than editing core runtime files in release prep.\n\n\n### Scheduled Editor measurement policy (RR-68)\n\nThe Editor's `.github/workflows/flake-repeat.yml` declares that its scheduled repeat measures rather than gates.\nThe fallback-all mode in `pr-gate.mjs` and `release-train.mjs` therefore treats that measurement as advisory only\nwhen authenticated workflow-run metadata verifies `OpenPresentation/opf-editor`, GitHub Actions app 15368,\nworkflow 373564717, that exact path, event `schedule`, branch `main`, the observed commit SHA and check-suite\nassociation. Both the check runs and the running suite follow this policy; advisory activity and failures remain\nvisible. Explicitly required contexts always gate, including the same job name. Commit statuses, other workflows,\nevents, repositories or apps, and missing or inaccessible identity metadata continue to gate.\n\nIdentity lookups are cached per candidate run and commit for each observer's lifetime, including failed lookups;\nthey do not poll workflow state. The policy changes no checks, thresholds, retries or workflow executions, and does\nnot authorize using an unmerged tooling change for a release.\n"
|
|
296
296
|
},
|
|
297
297
|
{
|
|
298
298
|
"slug": "rich-text",
|
|
@@ -358,7 +358,7 @@ var docsData = Object.freeze([
|
|
|
358
358
|
"slug": "yaml",
|
|
359
359
|
"file": "docs/yaml.md",
|
|
360
360
|
"title": "OPF as YAML",
|
|
361
|
-
"markdown": '# OPF as YAML\n\n`@openpresentation/opf/yaml` (RR-56) reads and writes an OPF deck as YAML, in both directions, with no renderer, fonts, network or model: the same input always gives the same output. The CLI exposes it through `opf convert` (`deck.opf.yaml` to `deck.opf.json` and back; `opf from-yaml` and `opf to-yaml` before 0.18), and every command that reads or writes a deck understands `.opf.yaml` files (and, the same way, [`.opf.md` Markdown decks](markdown.md#markdown-decks-in-every-command)); `parse` and `stringify` read and write a deck in JSON, YAML or Markdown with one call, and `open` and `save` of `@openpresentation/opf` read and write the files in Node.\n\n- **JSON stays canonical.** The schema, the interchange form and every package describe the JSON. A `.opf.yaml` file is an authoring serialization of the same data: the parsed YAML is exactly the JSON document, and the [schema](../spec/schemas/opf.schema.json) validates that parsed data.\n- **Write a deck by hand.** YAML has no braces, no quotes around keys and no trailing-comma errors, and a block scalar (`text: |`) keeps a paragraph readable. Comments are allowed in the file (they are not data).\n- **One strict dialect.** JSON-compatible YAML 1.2, core schema. Anything that could mean something else in another reader, or cannot exist in JSON, is an error with a line and a column.\n- **Round trip.** `toYaml` writes canonical text that `fromYaml` reads back to the same deck, and that converts back to itself byte for byte. Every one of the 127 example decks round-trips, JSON to YAML to JSON and YAML to JSON to YAML.\n- **Errors have places.** Findings use the shared Finding format of [validate](validate.md) and carry the source offset, length, line and column of the YAML that caused them, including the OPF findings mapped back to the YAML of the field they name.\n\n```js\nimport { fromYaml, toYaml } from "@openpresentation/opf/yaml";\n\nconst { presentation, valid, findings } = fromYaml(text);\nconst { yaml } = toYaml(presentation, { schemaComment: true });\n```\n\n```sh\nopf convert deck.opf.yaml deck.opf.json\nopf convert deck.opf.json deck.opf.yaml --schema-comment\nopf validate deck.opf.yaml\nopf convert deck.opf.yaml slides/deck.svg\n```\n\n## An example\n\n```yaml\n# yaml-language-server: $schema=https://openpresentation.org/schema/opf/v1\n$schema: https://openpresentation.org/schema/opf/v1\nname: Q3 Business Review\nlanguage: en-US\nslides:\n - id: cover\n layout: cover\n title: Q3 Business Review\n subtitle: Operations and growth\n notes: Two minutes on the agenda, then straight into the numbers.\n - title: Revenue grew every quarter\n blocks:\n - chart:\n type: column\n data:\n columns:\n - Quarter\n - Revenue\n rows:\n - - Q1\n - 12\n - - Q2\n - 18\n - quote:\n text: We would rather spend a week on capacity than a month on an outage.\n attribution: Priya Raman, Head of Platform\n```\n\nis the same deck as the JSON it converts to: `name`, `language` and `slides` with the values above. `cover` is a 0.19 intent layout. To pin a block to a named area, write `region: <name>` on that block after reading the selected layout\'s `regions`; `layout: auto` explicitly asks for automatic composition. A comment and the first line are not data.\n\n## The dialect\n\n`fromYaml` accepts exactly this, and nothing else:\n\n| Rule | What happens otherwise |\n| --- | --- |\n| One document, with a mapping at the root | `yaml/empty` (nothing, or only comments), `yaml/multiple-documents` (a `---` separates two), `yaml/not-mapping` (a list or a bare value) |\n| YAML 1.2 core schema | `2026-10-01`, `yes`, `no`, `on`, `off` and `1_000` stay **strings**: only `true`/`false`, `null`/`~` and numbers (decimal, `0x1F`, `0o17`, exponents) are typed. Quote a value to force text |\n| No duplicate keys | `yaml/duplicate-key`, at the repeated key |\n| Keys are strings | `yaml/key` for `1:`, `true:`, `null:` or a collection used as a key. Quote it: `"1":` |\n| No custom tags | `yaml/tag` for `!custom`, `!!binary`, `!!set` and the like. The core tags `!!str`, `!!int`, `!!float`, `!!bool`, `!!null`, `!!map` and `!!seq` are allowed |\n| Finite numbers only | `yaml/number` for `.inf`, `-.inf` and `.nan`, with the hint to quote them |\n| No anchors, aliases or merge keys | `yaml/alias`, with help that points at `aliases: true` |\n| Syntax | `yaml/syntax` for anything the parser rejects, at the position it reports |\n\nA leading byte order mark and CRLF line endings are accepted. Offsets are UTF-16 units into the text you passed (a BOM counts as one); lines and columns are one-based and ignore the BOM. After the YAML parses, the deck goes through `validate` and each finding (`opf/...` rule ids) is located at the YAML node of the field it names: the value for a scalar, the key through the value for a mapping or a list, and the nearest existing ancestor for something that is missing.\n\n### `aliases: true`\n\nAnchors (`&name`), aliases (`*name`) and the `<<` merge key are valid YAML and handy for repeated values, but they are not JSON, many tools handle them differently, and an alias hides the size of what it expands to. They are refused by default. `fromYaml(text, { aliases: true })` (CLI: `opf convert deck.opf.yaml deck.opf.json --aliases`) expands them to plain data:\n\n```yaml\nslides:\n - title: One\n extensions:\n owner: &owner { team: ops, oncall: true }\n - title: Two\n extensions:\n owner: *owner\n other:\n <<: *owner\n extra: 1\n```\n\nThe expansion is capped by the `yaml` package\'s `maxAliasCount` (100; each alias counts the nodes it expands to, so nested aliases that multiply exponentially are refused with `yaml/alias-limit`), and an alias that sits inside its own anchor is `yaml/alias-cycle`. What comes out is always finite, tree-shaped JSON data, and keys, tags and numbers are checked exactly as without the option. Writing never produces anchors or aliases.\n\n## Writing OPF as YAML\n\n`toYaml(presentation, { schemaComment })` first validates the presentation (`OPFYamlError`, `code: "invalid-document"`, when it is not valid OPF), then writes the canonical form:\n\n- keys in the order of the schema (the same order as [`opf format`](../packages/cli/README.md)): `$schema`, `name`, ..., `slides`, `assets`, ..., with keys the schema does not declare after them in their own order;\n- two-space indentation, block style throughout, no line folding (`lineWidth: 0`), a block scalar (`|`) for text with line breaks, one trailing newline, LF line endings;\n- strings that would read as another type, or as another type in a YAML 1.1 reader, are quoted: `"yes"`, `"No"`, `"on"`, `"2026-10-01"`, `"123"`, `"null"`, `"0x1F"`, `"12:30"`, and any string with leading or trailing spaces, a `#` or `: `. Mapping keys such as `"yes":` follow the same rule;\n- the text is **read back before it is returned**: it is parsed again in the strict dialect and compared with the input, key order included. If it does not read back to the same data, `toYaml` throws `OPFYamlError` with `code: "not-representable"` instead of writing something wrong (for example a lone surrogate in a string).\n\n`schemaComment: true` starts the file with `# yaml-language-server: $schema=<url>`, the deck\'s own `$schema` or else the canonical `https://openpresentation.org/schema/opf/v1`.\n\n### Editor autocomplete\n\nEditors that use the YAML language server (VS Code with the Red Hat YAML extension, Neovim, Zed, JetBrains) read the modeline on the first line and validate and complete the file against the schema:\n\n```yaml\n# yaml-language-server: $schema=https://openpresentation.org/schema/opf/v1\n```\n\n`opf convert deck.opf.json deck.opf.yaml --schema-comment` (and `opf create --schema-comment`) writes it. A command that rewrites a YAML file keeps the file\'s own first-line modeline verbatim (whatever URL it names, even a local schema path) and generates one only for `--schema-comment`. It is a comment: `fromYaml` ignores it.\n\n## Other exports\n\nThese three exports are not part of the `fromYaml`/`toYaml` pair. They exist so the CLI and other tools can reuse the reader.\n\n| Export | Purpose |\n| --- | --- |\n| `parseYamlData(yaml, options)` | The same strict dialect for any JSON-compatible root, for files that are not decks (a JSON Patch, a settings file). `{ value, valid, findings }` |\n| `scanYamlComments(yaml)` | `{ count, modeline? }`: the comments a rewrite would lose, and the editor modeline (the first-line `# yaml-language-server:` comment, verbatim) |\n| `YAML_SCHEMA_URL` | The canonical schema URL |\n\n## Findings\n\n`fromYaml(text, options)` never throws for malformed content. It returns the deck and the [validate](validate.md) report of it, `{ presentation, valid, findings, counts, schemaValid, checks }` (plus `template` and `unfilledVariables` for a deck with content variables), so a YAML file reports exactly what the same deck as JSON text does. `presentation` is a best effort when `valid` is false (an empty object after a YAML error). Each finding is a [Finding](finding-schema-reference.md) (`ruleId`, `severity`, `category`, `path`, `scope`, `message`, `help`) plus `location` (`offset`, `length`, `line`, `column`). Options: `validate` (`true`, the default, checks `format` and `references`; `validate` options pick other rules or categories, and `{}` runs every rule; `false` skips the check, so `schemaValid` is null) and `aliases` (default false).\n\nYAML errors are `format` findings: `yaml/syntax`, `yaml/empty`, `yaml/not-mapping`, `yaml/multiple-documents`, `yaml/duplicate-key`, `yaml/key`, `yaml/tag`, `yaml/number`, `yaml/alias`, `yaml/alias-limit`, `yaml/alias-cycle`. After one, no OPF check runs (`schemaValid: null`). Ids starting `opf/` are the `validate` findings of the deck, located in the YAML.\n\n## Command line\n\n```text\nopf convert <deck.opf.yaml|-> <output.opf.json|-> [--from yaml] [--to json] [--aliases] [--force] [--fail-on <level>]\nopf convert <deck.opf.json|-> <output.opf.yaml|-> [--to yaml] [--schema-comment] [--force] [--fail-on <level>]\n```\n\nOutput `-` writes stdout (with `--to`) and the JSON report goes to stderr. Exit codes: 0 success, 1 a deck that is not valid OPF, an output conflict or a finding at or above `--fail-on`, 2 usage, a YAML syntax error (with the located `findings`), a read error or I/O. Nothing is written on a failure. `--aliases` applies to a YAML input and `--schema-comment` to a YAML output; anywhere else they are usage errors. `opf from-yaml` and `opf to-yaml` were removed in 0.18.\n\n### YAML everywhere\n\nEvery command that reads a deck or a JSON Patch reads YAML too, through one reader:\n\n1. A file whose name ends `.yaml` or `.yml` is YAML, parsed in the strict dialect (no aliases). A name ending `.opf.md` is a [Markdown deck](markdown.md#markdown-decks-in-every-command), read the same way.\n2. Anything else (stdin and every other name) is JSON when the text starts with `{` or `[`; otherwise `--from yaml|md` (`--from json`) says what it is, and without it the command exits 2 and asks for it. `--from` applies to every deck input of the command and wins over a name.\n3. A YAML syntax or dialect error exits **2**, like invalid JSON, and the message carries the position: `Invalid YAML in deck.opf.yaml at line 5, column 5: Map keys must be unique [yaml/duplicate-key]`, with the located `findings` and `code: "invalid-yaml"` in the JSON error report. `opf validate` instead reports a YAML error as a `yaml/<rule>` finding and exits 1, as it does for JSON that does not parse. A deck with a syntax-valid but invalid OPF structure is reported by the command as it is for JSON (`validate` and `convert` locate their findings in the YAML).\n\nA deck is written as YAML when the output name ends `.yaml`/`.yml` or `--to yaml` is given (`--to json` forces JSON, `--to md` writes a Markdown deck). When the output is stdout or has another name, the format of the deck that was read is used (a YAML deck is edited, merged, filled, paginated and embedded back to YAML); `create` has no deck input and defaults to JSON. `opf format` canonicalizes a YAML file in place of the form it has (`-i`, `--check`); `opf convert` changes forms.\n\nThese are not deck serializations: `validate --config` and `--signals` output are JSON; `fill` data and the `ingest` source accept CSV, TSV or JSON. In `fill` and `ingest`, `--data-format csv|tsv|json` names the data format and `--to yaml` selects the output.\n\n**Comments are not preserved.** A command that rewrites a YAML file (`edit`, `format`, `merge`, `paginate`, `embed`, `fill`, `ingest --into`, `convert`) writes the canonical text of the data, so comments and the original quoting and layout are lost. The CLI prints `warning: <file> has N comments; comments are not preserved when OPF rewrites a YAML file.` on stderr, and does not fail. The `# yaml-language-server:` line is kept.\n\n## Decisions and limits\n\nThese choices are the defaults of the module and can be changed.\n\n- **JSON stays canonical and the interchange form.** The schemas, the catalogs, the reports, the renderer, the editor and the PPTX converter all take the JSON document; YAML is converted at the edge, by `fromYaml` and `toYaml`, and `opf convert deck.opf.yaml deck.opf.json` gives you the JSON. Nothing in OPF depends on a YAML library at run time except this subpath and the Markdown front matter.\n- **Comments are not preserved by commands that rewrite.** The YAML is parsed to plain data and written again from the data, which is what makes the output canonical and the same deck always the same bytes. Preserving comments would need a lossless syntax tree through every edit and would make output depend on the history of the file. Use an editor to change a file that you annotate heavily, and keep the notes in `description`, `notes` or `extensions` when they should survive.\n- **Strict by default.** Anchors, aliases, merge keys, custom tags, duplicate keys, non-string keys and non-finite numbers are where YAML readers disagree (YAML 1.1 and 1.2, `<<`, `on`/`yes`, sexagesimal numbers) or where a document stops being JSON. A deck should mean the same thing in every tool, so the reader accepts the intersection and says where the text leaves it. `aliases: true` opts into expansion for files that already use it.\n- **pptx.dev\'s playground uses `aliases: true`** so that YAML pasted into it from other sources keeps working; the default of the module and of the CLI stays strict.\n- **One YAML reader.** The `yaml` package (ISC, no dependencies) reads and writes, with the core schema. The [Markdown](markdown.md) front matter and the `opf-slide` and `opf-block` fences follow the same rules (JSON-compatible YAML 1.2 core schema: no anchors, tags or duplicate keys).\n- **Block style only.** Tables and chart data are lists of lists, which block style spreads over many lines. The text is longer than the JSON but diffs line by line, and one style means one canonical form.\n- **Quoting is conservative.** The core schema reads `yes` and `2026-10-01` as text, but a YAML 1.1 reader does not, so the writer quotes them anyway.\n- **No new fields.** YAML adds nothing to the schema: there is no YAML-only key, and a file that does not validate as JSON does not validate as YAML.\n'
|
|
361
|
+
"markdown": '# OPF as YAML\n\n`@openpresentation/opf/yaml` (RR-56) reads and writes an OPF deck as YAML, in both directions, with no renderer, fonts, network or model: the same input always gives the same output. The CLI exposes it through `opf convert` (`deck.opf.yaml` to `deck.opf.json` and back; `opf from-yaml` and `opf to-yaml` before 0.18), and every command that reads or writes a deck understands `.opf.yaml` files (and, the same way, [`.opf.md` Markdown decks](markdown.md#markdown-decks-in-every-command)); `parse` and `stringify` read and write a deck in JSON, YAML or Markdown with one call, and `open` and `save` of `@openpresentation/opf` read and write the files in Node.\n\n- **JSON stays canonical.** The schema, the interchange form and every package describe the JSON. A `.opf.yaml` file is an authoring serialization of the same data: the parsed YAML is exactly the JSON document, and the [schema](../spec/schemas/opf.schema.json) validates that parsed data.\n- **Write a deck by hand.** YAML has no braces, no quotes around keys and no trailing-comma errors, and a block scalar (`text: |`) keeps a paragraph readable. Comments are allowed in the file (they are not data).\n- **One strict dialect.** JSON-compatible YAML 1.2, core schema. Anything that could mean something else in another reader, or cannot exist in JSON, is an error with a line and a column.\n- **Round trip.** `toYaml` writes canonical text that `fromYaml` reads back to the same deck, and that converts back to itself byte for byte. Every one of the 128 example decks round-trips, JSON to YAML to JSON and YAML to JSON to YAML.\n- **Errors have places.** Findings use the shared Finding format of [validate](validate.md) and carry the source offset, length, line and column of the YAML that caused them, including the OPF findings mapped back to the YAML of the field they name.\n\n```js\nimport { fromYaml, toYaml } from "@openpresentation/opf/yaml";\n\nconst { presentation, valid, findings } = fromYaml(text);\nconst { yaml } = toYaml(presentation, { schemaComment: true });\n```\n\n```sh\nopf convert deck.opf.yaml deck.opf.json\nopf convert deck.opf.json deck.opf.yaml --schema-comment\nopf validate deck.opf.yaml\nopf convert deck.opf.yaml slides/deck.svg\n```\n\n## An example\n\n```yaml\n# yaml-language-server: $schema=https://openpresentation.org/schema/opf/v1\n$schema: https://openpresentation.org/schema/opf/v1\nname: Q3 Business Review\nlanguage: en-US\nslides:\n - id: cover\n layout: cover\n title: Q3 Business Review\n subtitle: Operations and growth\n notes: Two minutes on the agenda, then straight into the numbers.\n - title: Revenue grew every quarter\n blocks:\n - chart:\n type: column\n data:\n columns:\n - Quarter\n - Revenue\n rows:\n - - Q1\n - 12\n - - Q2\n - 18\n - quote:\n text: We would rather spend a week on capacity than a month on an outage.\n attribution: Priya Raman, Head of Platform\n```\n\nis the same deck as the JSON it converts to: `name`, `language` and `slides` with the values above. `cover` is a 0.19 intent layout. To pin a block to a named area, write `region: <name>` on that block after reading the selected layout\'s `regions`; `layout: auto` explicitly asks for automatic composition. A comment and the first line are not data.\n\n## The dialect\n\n`fromYaml` accepts exactly this, and nothing else:\n\n| Rule | What happens otherwise |\n| --- | --- |\n| One document, with a mapping at the root | `yaml/empty` (nothing, or only comments), `yaml/multiple-documents` (a `---` separates two), `yaml/not-mapping` (a list or a bare value) |\n| YAML 1.2 core schema | `2026-10-01`, `yes`, `no`, `on`, `off` and `1_000` stay **strings**: only `true`/`false`, `null`/`~` and numbers (decimal, `0x1F`, `0o17`, exponents) are typed. Quote a value to force text |\n| No duplicate keys | `yaml/duplicate-key`, at the repeated key |\n| Keys are strings | `yaml/key` for `1:`, `true:`, `null:` or a collection used as a key. Quote it: `"1":` |\n| No custom tags | `yaml/tag` for `!custom`, `!!binary`, `!!set` and the like. The core tags `!!str`, `!!int`, `!!float`, `!!bool`, `!!null`, `!!map` and `!!seq` are allowed |\n| Finite numbers only | `yaml/number` for `.inf`, `-.inf` and `.nan`, with the hint to quote them |\n| No anchors, aliases or merge keys | `yaml/alias`, with help that points at `aliases: true` |\n| Syntax | `yaml/syntax` for anything the parser rejects, at the position it reports |\n\nA leading byte order mark and CRLF line endings are accepted. Offsets are UTF-16 units into the text you passed (a BOM counts as one); lines and columns are one-based and ignore the BOM. After the YAML parses, the deck goes through `validate` and each finding (`opf/...` rule ids) is located at the YAML node of the field it names: the value for a scalar, the key through the value for a mapping or a list, and the nearest existing ancestor for something that is missing.\n\n### `aliases: true`\n\nAnchors (`&name`), aliases (`*name`) and the `<<` merge key are valid YAML and handy for repeated values, but they are not JSON, many tools handle them differently, and an alias hides the size of what it expands to. They are refused by default. `fromYaml(text, { aliases: true })` (CLI: `opf convert deck.opf.yaml deck.opf.json --aliases`) expands them to plain data:\n\n```yaml\nslides:\n - title: One\n extensions:\n owner: &owner { team: ops, oncall: true }\n - title: Two\n extensions:\n owner: *owner\n other:\n <<: *owner\n extra: 1\n```\n\nThe expansion is capped by the `yaml` package\'s `maxAliasCount` (100; each alias counts the nodes it expands to, so nested aliases that multiply exponentially are refused with `yaml/alias-limit`), and an alias that sits inside its own anchor is `yaml/alias-cycle`. What comes out is always finite, tree-shaped JSON data, and keys, tags and numbers are checked exactly as without the option. Writing never produces anchors or aliases.\n\n## Writing OPF as YAML\n\n`toYaml(presentation, { schemaComment })` first validates the presentation (`OPFYamlError`, `code: "invalid-document"`, when it is not valid OPF), then writes the canonical form:\n\n- keys in the order of the schema (the same order as [`opf format`](../packages/cli/README.md)): `$schema`, `name`, ..., `slides`, `assets`, ..., with keys the schema does not declare after them in their own order;\n- two-space indentation, block style throughout, no line folding (`lineWidth: 0`), a block scalar (`|`) for text with line breaks, one trailing newline, LF line endings;\n- strings that would read as another type, or as another type in a YAML 1.1 reader, are quoted: `"yes"`, `"No"`, `"on"`, `"2026-10-01"`, `"123"`, `"null"`, `"0x1F"`, `"12:30"`, and any string with leading or trailing spaces, a `#` or `: `. Mapping keys such as `"yes":` follow the same rule;\n- the text is **read back before it is returned**: it is parsed again in the strict dialect and compared with the input, key order included. If it does not read back to the same data, `toYaml` throws `OPFYamlError` with `code: "not-representable"` instead of writing something wrong (for example a lone surrogate in a string).\n\n`schemaComment: true` starts the file with `# yaml-language-server: $schema=<url>`, the deck\'s own `$schema` or else the canonical `https://openpresentation.org/schema/opf/v1`.\n\n### Editor autocomplete\n\nEditors that use the YAML language server (VS Code with the Red Hat YAML extension, Neovim, Zed, JetBrains) read the modeline on the first line and validate and complete the file against the schema:\n\n```yaml\n# yaml-language-server: $schema=https://openpresentation.org/schema/opf/v1\n```\n\n`opf convert deck.opf.json deck.opf.yaml --schema-comment` (and `opf create --schema-comment`) writes it. A command that rewrites a YAML file keeps the file\'s own first-line modeline verbatim (whatever URL it names, even a local schema path) and generates one only for `--schema-comment`. It is a comment: `fromYaml` ignores it.\n\n## Other exports\n\nThese three exports are not part of the `fromYaml`/`toYaml` pair. They exist so the CLI and other tools can reuse the reader.\n\n| Export | Purpose |\n| --- | --- |\n| `parseYamlData(yaml, options)` | The same strict dialect for any JSON-compatible root, for files that are not decks (a JSON Patch, a settings file). `{ value, valid, findings }` |\n| `scanYamlComments(yaml)` | `{ count, modeline? }`: the comments a rewrite would lose, and the editor modeline (the first-line `# yaml-language-server:` comment, verbatim) |\n| `YAML_SCHEMA_URL` | The canonical schema URL |\n\n## Findings\n\n`fromYaml(text, options)` never throws for malformed content. It returns the deck and the [validate](validate.md) report of it, `{ presentation, valid, findings, counts, schemaValid, checks }` (plus `template` and `unfilledVariables` for a deck with content variables), so a YAML file reports exactly what the same deck as JSON text does. `presentation` is a best effort when `valid` is false (an empty object after a YAML error). Each finding is a [Finding](finding-schema-reference.md) (`ruleId`, `severity`, `category`, `path`, `scope`, `message`, `help`) plus `location` (`offset`, `length`, `line`, `column`). Options: `validate` (`true`, the default, checks `format` and `references`; `validate` options pick other rules or categories, and `{}` runs every rule; `false` skips the check, so `schemaValid` is null) and `aliases` (default false).\n\nYAML errors are `format` findings: `yaml/syntax`, `yaml/empty`, `yaml/not-mapping`, `yaml/multiple-documents`, `yaml/duplicate-key`, `yaml/key`, `yaml/tag`, `yaml/number`, `yaml/alias`, `yaml/alias-limit`, `yaml/alias-cycle`. After one, no OPF check runs (`schemaValid: null`). Ids starting `opf/` are the `validate` findings of the deck, located in the YAML.\n\n## Command line\n\n```text\nopf convert <deck.opf.yaml|-> <output.opf.json|-> [--from yaml] [--to json] [--aliases] [--force] [--fail-on <level>]\nopf convert <deck.opf.json|-> <output.opf.yaml|-> [--to yaml] [--schema-comment] [--force] [--fail-on <level>]\n```\n\nOutput `-` writes stdout (with `--to`) and the JSON report goes to stderr. Exit codes: 0 success, 1 a deck that is not valid OPF, an output conflict or a finding at or above `--fail-on`, 2 usage, a YAML syntax error (with the located `findings`), a read error or I/O. Nothing is written on a failure. `--aliases` applies to a YAML input and `--schema-comment` to a YAML output; anywhere else they are usage errors. `opf from-yaml` and `opf to-yaml` were removed in 0.18.\n\n### YAML everywhere\n\nEvery command that reads a deck or a JSON Patch reads YAML too, through one reader:\n\n1. A file whose name ends `.yaml` or `.yml` is YAML, parsed in the strict dialect (no aliases). A name ending `.opf.md` is a [Markdown deck](markdown.md#markdown-decks-in-every-command), read the same way.\n2. Anything else (stdin and every other name) is JSON when the text starts with `{` or `[`; otherwise `--from yaml|md` (`--from json`) says what it is, and without it the command exits 2 and asks for it. `--from` applies to every deck input of the command and wins over a name.\n3. A YAML syntax or dialect error exits **2**, like invalid JSON, and the message carries the position: `Invalid YAML in deck.opf.yaml at line 5, column 5: Map keys must be unique [yaml/duplicate-key]`, with the located `findings` and `code: "invalid-yaml"` in the JSON error report. `opf validate` instead reports a YAML error as a `yaml/<rule>` finding and exits 1, as it does for JSON that does not parse. A deck with a syntax-valid but invalid OPF structure is reported by the command as it is for JSON (`validate` and `convert` locate their findings in the YAML).\n\nA deck is written as YAML when the output name ends `.yaml`/`.yml` or `--to yaml` is given (`--to json` forces JSON, `--to md` writes a Markdown deck). When the output is stdout or has another name, the format of the deck that was read is used (a YAML deck is edited, merged, filled, paginated and embedded back to YAML); `create` has no deck input and defaults to JSON. `opf format` canonicalizes a YAML file in place of the form it has (`-i`, `--check`); `opf convert` changes forms.\n\nThese are not deck serializations: `validate --config` and `--signals` output are JSON; `fill` data and the `ingest` source accept CSV, TSV or JSON. In `fill` and `ingest`, `--data-format csv|tsv|json` names the data format and `--to yaml` selects the output.\n\n**Comments are not preserved.** A command that rewrites a YAML file (`edit`, `format`, `merge`, `paginate`, `embed`, `fill`, `ingest --into`, `convert`) writes the canonical text of the data, so comments and the original quoting and layout are lost. The CLI prints `warning: <file> has N comments; comments are not preserved when OPF rewrites a YAML file.` on stderr, and does not fail. The `# yaml-language-server:` line is kept.\n\n## Decisions and limits\n\nThese choices are the defaults of the module and can be changed.\n\n- **JSON stays canonical and the interchange form.** The schemas, the catalogs, the reports, the renderer, the editor and the PPTX converter all take the JSON document; YAML is converted at the edge, by `fromYaml` and `toYaml`, and `opf convert deck.opf.yaml deck.opf.json` gives you the JSON. Nothing in OPF depends on a YAML library at run time except this subpath and the Markdown front matter.\n- **Comments are not preserved by commands that rewrite.** The YAML is parsed to plain data and written again from the data, which is what makes the output canonical and the same deck always the same bytes. Preserving comments would need a lossless syntax tree through every edit and would make output depend on the history of the file. Use an editor to change a file that you annotate heavily, and keep the notes in `description`, `notes` or `extensions` when they should survive.\n- **Strict by default.** Anchors, aliases, merge keys, custom tags, duplicate keys, non-string keys and non-finite numbers are where YAML readers disagree (YAML 1.1 and 1.2, `<<`, `on`/`yes`, sexagesimal numbers) or where a document stops being JSON. A deck should mean the same thing in every tool, so the reader accepts the intersection and says where the text leaves it. `aliases: true` opts into expansion for files that already use it.\n- **pptx.dev\'s playground uses `aliases: true`** so that YAML pasted into it from other sources keeps working; the default of the module and of the CLI stays strict.\n- **One YAML reader.** The `yaml` package (ISC, no dependencies) reads and writes, with the core schema. The [Markdown](markdown.md) front matter and the `opf-slide` and `opf-block` fences follow the same rules (JSON-compatible YAML 1.2 core schema: no anchors, tags or duplicate keys).\n- **Block style only.** Tables and chart data are lists of lists, which block style spreads over many lines. The text is longer than the JSON but diffs line by line, and one style means one canonical form.\n- **Quoting is conservative.** The core schema reads `yes` and `2026-10-01` as text, but a YAML 1.1 reader does not, so the writer quotes them anyway.\n- **No new fields.** YAML adds nothing to the schema: there is no YAML-only key, and a file that does not validate as JSON does not validate as YAML.\n'
|
|
362
362
|
}
|
|
363
363
|
]);
|
|
364
364
|
var docsRaw = docsData;
|