@openpresentation/opf 0.20.2 → 0.20.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/browser.js +14 -14
  2. package/dist/{chunk-NXYHBBVV.js → chunk-2XVYJMLR.js} +1 -1
  3. package/dist/{chunk-QOQATI37.js → chunk-3XLQI7SW.js} +2 -2
  4. package/dist/{chunk-XWDAXVFZ.js → chunk-AMIQJVJR.js} +2 -2
  5. package/dist/{chunk-SZALQLZP.js → chunk-BDVU4CJV.js} +1 -1
  6. package/dist/{chunk-5YTTTLL2.js → chunk-BWZS3RVK.js} +1 -1
  7. package/dist/{chunk-XGR5HL7R.js → chunk-D3YLFLYO.js} +1 -1
  8. package/dist/{chunk-E662FTHI.js → chunk-HML2TNTA.js} +1 -1
  9. package/dist/{chunk-E4VBQW2Z.js → chunk-QBYGIIS7.js} +1 -1
  10. package/dist/{chunk-AXUY4BHG.js → chunk-QNJMY5GX.js} +1 -1
  11. package/dist/{chunk-SPSAQKNB.js → chunk-R72S2GLS.js} +4 -4
  12. package/dist/{chunk-QEISIGFD.js → chunk-S5IZLAFK.js} +1 -1
  13. package/dist/{chunk-2XIMI2V5.js → chunk-SXNVC2ZU.js} +5 -5
  14. package/dist/{chunk-OGVRAMHE.js → chunk-UFTKB25U.js} +2 -2
  15. package/dist/{chunk-NVNK5BLL.js → chunk-WGNSYVLB.js} +5 -5
  16. package/dist/{chunk-MSWNRD4C.js → chunk-X3F6PRH2.js} +4 -4
  17. package/dist/conversion-5IGYAS4A.js +19 -0
  18. package/dist/{conversion-DRBKSRAB.d.ts → conversion-Ca_346Zv.d.ts} +3 -3
  19. package/dist/convert.js +6 -6
  20. package/dist/{deck-report-4CNhJYk7.d.ts → deck-report-ByHBxLUO.d.ts} +1 -1
  21. package/dist/deck.d.ts +4 -4
  22. package/dist/deck.js +9 -9
  23. package/dist/diff.d.ts +1 -1
  24. package/dist/diff.js +4 -4
  25. package/dist/docs.js +5 -5
  26. package/dist/{finding-CT_f_vbD.d.ts → finding-DqgwzUmr.d.ts} +1 -1
  27. package/dist/format.js +2 -2
  28. package/dist/index.d.ts +9 -9
  29. package/dist/index.js +18 -18
  30. package/dist/markdown.d.ts +2 -2
  31. package/dist/markdown.js +6 -6
  32. package/dist/node-engine.d.ts +5 -5
  33. package/dist/node-engine.js +18 -18
  34. package/dist/pagination.js +3 -3
  35. package/dist/patch.d.ts +1 -1
  36. package/dist/patch.js +3 -3
  37. package/dist/schemas.js +1 -1
  38. package/dist/spec/README.md +1 -1
  39. package/dist/spec/schemas/finding.schema.json +2 -2
  40. package/dist/types.d.ts +1 -1
  41. package/dist/{validation-types-CxbvWnKh.d.ts → validation-types-DRaNnieo.d.ts} +1 -1
  42. package/dist/{validator-Rq4en736.d.ts → validator-DDTYh9sp.d.ts} +2 -2
  43. package/dist/validator.d.ts +3 -3
  44. package/dist/validator.js +2 -2
  45. package/dist/yaml.d.ts +2 -2
  46. package/dist/yaml.js +6 -6
  47. package/package.json +1 -1
  48. package/dist/conversion-6YXKHUAU.js +0 -19
package/dist/docs.js CHANGED
@@ -10,7 +10,7 @@ var docsData = Object.freeze([
10
10
  "slug": "catalog-schema-reference",
11
11
  "file": "docs/catalog-schema-reference.md",
12
12
  "title": "OPF Catalog Schema Reference",
13
- "markdown": "# OPF Catalog Schema Reference\n\nCatalog records are reusable presets that OPF documents reference by id. This page summarizes every catalog companion schema in `spec/schemas/`. The top-level presentation schema has its own [schema reference](schema-reference.md), and the report format every tool shares has the [finding schema reference](finding-schema-reference.md).\n\nOPF documents usually reference these records with string ids such as `design.theme = \"minimal\"`, `tone = \"formal\"`, or `chart.type = \"line\"`. Dense examples may also embed catalog sources or inline records under `catalogs`.\n\n## Audience\n\n- File: `spec/schemas/audience.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-audience/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for audience records. Each record names an audience archetype (e.g. 'executive', 'engineering-team', 'investor') and carries seniority, technical-fluency, decision-power, and attention-budget hints used by AI-driven generation. Documents reference an audience from 'audience', which also accepts free-form text. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-audience/v1\"` | Identifies a published record file as a audience record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The audience's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this f... |\n| `name` | yes | `string` | Human-readable audience name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the audience who they are and what they care about. |\n| `description` | no | `string` | Longer prose describing the audience archetype and how to address them. |\n| `seniority` | no | `enum:ic \\| manager \\| director \\| vp \\| c-suite \\| mixed` | Typical seniority level of the audience. Engines use this as a hint for default depth and pacing. |\n| `technicalFluency` | no | `enum:low \\| medium \\| high \\| mixed` | Typical technical fluency of the audience. AI generation uses this to decide whether to expand or assume technical terminology. |\n| `decisionPower` | no | `enum:informational \\| advisory \\| decision-maker` | Whether the audience is expected to be informed, to advise, or to actually decide. Shapes the strength of the closing ask. |\n| `attentionBudgetMinutes` | no | `number` | Realistic upper bound on this audience's focused attention for a single presentation, in minutes. Used as a hint when comparing against duration and the resolved narrative's duration range. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog that work well for this audience. Used by picker UIs to suggest narratives once an audience is chosen. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: ids of tones in the same catalog that work well for this audience. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n\n## Catalog Index\n\n- File: `spec/schemas/catalog-index.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-catalog-index/v1`\n- Type: `object`\n- Required fields: `$schema`, `version`, `description`, `records`\n- Purpose: Generic shape shared by every `<kind>/index.json` file of the @openpresentation/gallery package (packages/gallery/catalog in the OPF repo) and by the default-catalog index that pptx.gallery publishes at `https://www.pptx.gallery/<kind>/index.json` (the gallery package is a pinned snapshot of that catalog; see docs/default-catalog.md). An index is a lightweight, ordered summary of the full-record JSON files that live alongside it: each entry names the record's stable id, a human-readable name,...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-catalog-index/v1\"` | |\n| `kind` | no | `enum:audiences \\| chart-types \\| color-schemes \\| font-schemes \\| languages \\| layouts \\| narratives \\| purposes \\| social-platforms \\| themes \\| tones` | Catalog kind, as the URL segment of the default catalog (`https://www.pptx.gallery/<kind>`) and the `packages/gallery/catalog/<kind>` directory name. |\n| `version` | yes | `string` | Index format version, as a string. |\n| `description` | yes | `string` | Human-readable description of what this catalog kind holds and how entries are ordered. |\n| `contentSha256` | no | `string` | SHA-256 (lowercase hex) of the canonical JSON of the full records this index lists, in index order, with every top-level `x-*` member removed. Canonical JSON sorts object keys and has no insignificant whitespace. Lets... |\n| `records` | yes | `array<ref:IndexRecord>` | Ordered list of lightweight record summaries. Order defines the catalog's canonical/display order; full record data lives in the sibling JSON file named by `file`. |\n\n### Nested Types\n\n#### IndexRecord\n\n- Type: `object`\n- Required fields: `id`, `name`, `file`\n- Purpose: Lightweight summary of one catalog record. Additional per-kind fields (e.g. `summary`, `tags`, `bcp47`, `duration`, `group`, `label`) are allowed and vary by catalog kind.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable identifier, matching the `id` field inside the record file named by `file`. Lowercase kebab-case; chart-type ids may start with a digit (e.g. '100pct-stacked-bar'). |\n| `name` | yes | `string` | Human-readable name shown in pickers. |\n| `file` | yes | `string` | Filename of the full record, relative to this index file's directory. |\n| `deprecated` | no | `const:true` | Present on the entry of a record that carries `deprecation`. Pickers and default listings hide deprecated entries; the id keeps resolving. |\n| `replacedBy` | no | `string` | The deprecated record's `deprecation.replacedBy`, repeated so a picker can offer the replacement without loading the record. |\n\n## Catalog Snapshot Manifest\n\n- File: `spec/schemas/catalog-manifest.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-catalog-manifest/v1`\n- Type: `object`\n- Required fields: `$schema`, `description`, `publisher`, `source`, `kinds`\n- Purpose: Shape of the `manifest.json` of the @openpresentation/gallery package (packages/gallery/catalog/manifest.json), which pins the snapshot of the default OPF catalog published by pptx.gallery. It records the gallery commit the snapshot came from and, per kind, how the snapshot relates to the published catalog plus a content hash of the bundled records. Written by scripts/sync-gallery-catalog.mjs and checked by scripts/check-spec-integrity.mjs; see docs/default-catalog.md. This schema describes a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-catalog-manifest/v1\"` | |\n| `description` | yes | `string` | |\n| `publisher` | yes | `string` | Base URL of the default-catalog publisher. Each kind is published at `<publisher>/<kind>/index.json`. |\n| `source` | yes | `object` | The published catalog files the snapshot was taken from. |\n| `kinds` | yes | `object` | One entry per catalog kind, keyed by the kind's URL segment. |\n\n### Nested Types\n\n#### KindEntry\n\n- Type: `object`\n- Required fields: `mode`, `records`, `contentSha256`, `gallery`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `mode` | yes | `enum:mirror` | Always 'mirror': the snapshot holds every record the publisher serves for the kind (OPF 0.15). |\n| `records` | yes | `integer` | Number of records bundled for this kind. |\n| `contentSha256` | yes | `string` | contentSha256 of the bundled records, as defined by the catalog index schema. |\n| `gallery` | yes | `object` | The published catalog for this kind at the pinned commit. |\n\n## Chart Type\n\n- File: `spec/schemas/chart-type.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-chart-type/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `mappings`\n- Purpose: Schema for chart-type records: display metadata for chart pickers (name, group, expected series, complexity) and the Open XML mapping a picker can describe. One record per chart type that Aspose.Slides officially supports (see mappings.renderers[\"aspose-slides\"].chartType), plus the column-and-line combination 'combo'. A document's chart.type is an engine vocabulary validated directly; engines never look a chart-type record up. pptx.gallery publishes these records and @openpresentation/galler...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-chart-type/v1\"` | Identifies this record as a chart type in the open presentation catalog. |\n| `id` | yes | `string` | The chart-type value (`chart.type`) this record describes. Lowercase kebab-case; it may start with a digit (e.g., '100pct-stacked-column') to mirror conventional chart naming. |\n| `name` | yes | `string` | Human-readable name of the chart type, as a chart picker shows it. |\n| `summary` | no | `string` | One-sentence positioning: when to reach for this chart variant. |\n| `description` | no | `string` | Longer prose describing the chart and ideal use cases. |\n| `mappings` | yes | `ref:ChartTypeMappings` | Canonical and optional renderer-specific mappings used by engines to render this chart type. |\n| `group` | no | `string` | Top-level grouping in the chart picker. |\n| `groupSort` | no | `integer` | Display ordering hint within the chart group. |\n| `complexity` | no | `enum:simple \\| calculated \\| hierarchical \\| normalized` | Shape of the underlying data: a flat series ('simple'), one with engine-side calculation ('calculated'), parent-child rows ('hierarchical'), or pre-normalized rows ('normalized'). The editor offers only 'simple' types... |\n| `series` | no | `integer` | Number of data series this chart type expects: exactly that many, except that a stacked or percent-stacked type and a combination (composition 'mixed', such as combo) expect at least that many. The editor offers a typ... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### ChartTypeMappings\n\n- Type: `object`\n- Required fields: `openxml`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `openxml` | yes | `ref:OpenXmlChartMapping` | Canonical mapping to Open XML chart structures. |\n| `renderers` | no | `object` | Optional renderer-specific mappings. Keys are renderer ids; values are intentionally opaque to OPF. The bundled catalog records the matching Aspose.Slides ChartType enumeration member under the \"aspose-slides\" key, e.... |\n\n#### OpenXmlChartMapping\n\n- Type: `object`\n- Required fields: none\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `element` | no | `string` | Primary Open XML chart element or extension chart element, such as 'barChart', 'lineChart', 'pieChart', 'treemapChart', or 'waterfallChart'. |\n| `barDir` | no | `enum:bar \\| col` | Bar direction for Open XML barChart mappings. |\n| `grouping` | no | `enum:standard \\| clustered \\| stacked \\| percentStacked` | Open XML chart grouping value when the chart family supports grouping. |\n| `marker` | no | `boolean` | Whether the chart type expects visible data markers. |\n| `radarStyle` | no | `enum:standard \\| marker \\| filled` | Open XML radarStyle value for radarChart mappings. |\n| `scatterStyle` | no | `enum:line \\| lineMarker \\| marker \\| smooth \\| smoothMarker` | Open XML scatterStyle value for scatterChart mappings. |\n| `composition` | no | `enum:single \\| mixed \\| extension` | Whether the chart maps to one standard chart element, multiple combined chart elements, or an Open XML extension chart. |\n| `extension` | no | `string` | Optional Open XML extension namespace or element hint for extension charts. |\n| `series` | no | `array<ref:OpenXmlChartMapping>` | Open XML chart elements used by mixed/composite chart types. |\n| `notes` | no | `string` | Short implementation note for mappings that need renderer interpretation. |\n\n## Color Scheme\n\n- File: `spec/schemas/color-scheme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-color-scheme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for color-scheme records. Each scheme is a named palette with the twelve PowerPoint color slots (six accents, two darks, two lights, plus hyperlink and followed-hyperlink), suitable for being mapped directly into OOXML theme XML. Documents reference a scheme from design.colorScheme (or design.colorScheme.id) and from a theme's colorScheme; slot overrides on design.colorScheme take precedence over the resolved scheme. The slot fields here mirror the inline-override fields on the in-docu...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-color-scheme/v1\"` | Identifies a published record file as a colour scheme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The colour scheme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without t... |\n| `name` | yes | `string` | Human-readable scheme name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the palette what mood it evokes and where to use it. |\n| `description` | no | `string` | Longer prose describing the palette and its intended use. |\n| `accent1` | no | `ref:HexColor` | Accent 1 color (hex). Mirrors the OOXML accent1 slot. |\n| `accent2` | no | `ref:HexColor` | Accent 2 color (hex). Mirrors the OOXML accent2 slot. |\n| `accent3` | no | `ref:HexColor` | Accent 3 color (hex). Mirrors the OOXML accent3 slot. |\n| `accent4` | no | `ref:HexColor` | Accent 4 color (hex). Mirrors the OOXML accent4 slot. |\n| `accent5` | no | `ref:HexColor` | Accent 5 color (hex). Mirrors the OOXML accent5 slot. |\n| `accent6` | no | `ref:HexColor` | Accent 6 color (hex). Mirrors the OOXML accent6 slot. |\n| `dark1` | no | `ref:HexColor` | Dark 1 color (hex). Typically the deepest neutral; OOXML dark1. |\n| `dark2` | no | `ref:HexColor` | Dark 2 color (hex). Secondary dark; OOXML dark2. |\n| `light1` | no | `ref:HexColor` | Light 1 color (hex). Typically the slide canvas; OOXML lt1. |\n| `light2` | no | `ref:HexColor` | Light 2 color (hex). Secondary light surface; OOXML lt2. |\n| `hyperlink` | no | `ref:HexColor` | Hyperlink color (hex). OOXML hlink. |\n| `followedHyperlink` | no | `ref:HexColor` | Followed-hyperlink color (hex). OOXML folHlink. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### HexColor\n\n- Type: `string`\n- Required fields: none\n- Purpose: Hex color: '#RGB', '#RRGGBB' or '#RRGGBBAA'. The same definition as HexColor in opf.schema.json.\n\n_No named properties._\n\n## Font Scheme\n\n- File: `spec/schemas/font-scheme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-font-scheme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `major`, `minor`\n- Purpose: Schema for font-scheme records. Each scheme pairs a major (heading) and minor (body) font family in the OOXML majorFont/minorFont sense, scoped to a target app (powerpoint or google-slides) and a language family (Latin, East Asian, or Complex Script). Documents reference a scheme from design.fontScheme (or design.fontScheme.id), from a theme's fontScheme and from a language's fontScheme / googleFontScheme. Role overrides on design.fontScheme (heading, body, accent, code), each a font family n...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-font-scheme/v1\"` | Identifies a published record file as a font scheme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The font scheme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without thi... |\n| `name` | yes | `string` | Human-readable scheme name shown in pickers. |\n| `major` | yes | `string` | Heading (major) font family mirrors the OOXML majorFont entry. |\n| `minor` | yes | `string` | Body (minor) font family mirrors the OOXML minorFont entry. |\n| `code` | no | `string` | Optional monospaced font family name for code blocks and for inline code runs (TextRun.code). It has the same shape as the OPF FontScheme 'code' role, so a record and an inline design.fontScheme override are interchan... |\n| `eastAsian` | no | `object` | East Asian script fonts. Maps to the OOXML a:ea element of majorFont (major) and minorFont (minor), and to run-level a:ea. When set, they fill the eastAsian slot for every language; when omitted, the slot comes from t... |\n| `complexScript` | no | `object` | Complex-script fonts (for example Arabic, Hebrew, Indic and Thai). Maps to the OOXML a:cs element of majorFont (major) and minorFont (minor), and to run-level a:cs. When set, they fill the complexScript slot for every... |\n| `type` | no | `enum:sans-serif \\| serif \\| monospace` | High-level typographic class of the scheme. |\n| `app` | no | `enum:powerpoint \\| google-slides` | Target application this font pairing is intended for. Metadata for pickers and catalog filters: no engine changes its output by it. |\n| `languageFamily` | no | `enum:latin \\| ea \\| cs \\| eastAsian \\| complexScript` | Font-language family this scheme is intended for: 'latin' for Latin-script content, 'ea' (or 'eastAsian', the same value) for East Asian scripts, 'cs' (or 'complexScript', the same value) for Complex Scripts. The long... |\n| `languages` | no | `array<string>` | Optional list of BCP-47 language tags this scheme is curated for. Useful for picker UIs that group fonts by language coverage. As the design font scheme, an 'ea' or 'cs' scheme fills its script slot only when this lis... |\n| `textSample` | no | `string` | Short specimen string used by picker UIs to preview the scheme. |\n| `summary` | no | `string` | One-sentence positioning of the font pairing. |\n| `description` | no | `string` | Longer prose describing the font scheme and where it shines. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Language\n\n- File: `spec/schemas/language.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-language/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `bcp47`\n- Purpose: Schema for language records: display metadata for the language pickers of authoring tools (name, ISO code, the default font schemes a picker suggests). The document's 'language' is an engine vocabulary, a BCP-47 tag validated directly; engines never look a language record up, and their script, direction, OOXML tag and default script fonts come from spec/reference/engine-vocabularies.json. pptx.gallery publishes these records and @openpresentation/gallery ships them as catalogDisplay.languages.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-language/v1\"` | Identifies this record as a language in the openpresentation.org catalog. |\n| `id` | yes | `string` | The record's id in the gallery's language catalog, lowercase kebab-case. Documents name a language by its BCP-47 tag ('bcp47'), never by this id. |\n| `name` | yes | `string` | Human-readable language name. |\n| `code` | no | `string` | ISO 639-3 (or 639-2) three-letter language code. Carried for engines that prefer ISO codes. |\n| `bcp47` | yes | `string` | BCP-47 language tag for this record. Use 'en-GB' for UK English; 'en-UK' is not a valid BCP-47 region form. |\n| `ooxmlLang` | no | `string` | Curated culture tag for OOXML text-run language attributes (a:rPr/@lang, a:endParaRPr/@lang), in the language-[Script-]REGION form Office recognizes (e.g. 'ja-JP', 'ar-SA', 'ms-MY', 'nb-NO', 'fil-PH', 'zh-CN'). Engine... |\n| `direction` | no | `enum:ltr \\| rtl` | Base text direction for the language. When omitted, engines derive it from the script: Arabic (Arab), Hebrew (Hebr), Syriac (Syrc), Thaana (Thaa), N'Ko (Nkoo), Adlam (Adlm), Samaritan (Samr), Mandaic (Mand) and Hanifi... |\n| `script` | no | `string` | ISO 15924 script code of the language's writing system. The script selects the OOXML font slot the language's text uses: East Asian scripts (Hans, Hant, Hani, Jpan, Kore, Hang, Hira, Kana, Bopo, Yiii) use the eastAsia... |\n| `fontScheme` | no | `string` | The font-scheme id a picker suggests for this language when targeting PowerPoint output. Display metadata: engines take a language's default script fonts from their own vocabulary. |\n| `googleFontScheme` | no | `string` | The font-scheme id a picker suggests for this language when targeting Google Slides output. Display metadata. |\n| `summary` | no | `string` | One-sentence note about coverage or font defaults. |\n| `description` | no | `string` | Longer prose describing the language record and any font-pairing rationale. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Layout Preview Index\n\n- File: `spec/schemas/layout-preview-index.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-layout-preview-index/v1`\n- Type: `object`\n- Required fields: `$schema`, `version`, `description`, `records`\n- Purpose: Shape of previews/layouts/index.json in @openpresentation/gallery 2: one self-contained SVG preview per v2 intent layout, keyed by the matching catalog layout id. Each record names its SVG file and exact UTF-8 byte length. The gallery package owns the files; this checkout materializes ignored copies under packages/gallery/previews/layouts/. This internal index schema is excluded from the package's published/typed schema surface.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-layout-preview-index/v1\"` | |\n| `version` | yes | `const:\"2\"` | Index format version, as a string. |\n| `description` | yes | `string` | Human-readable description of the preview gallery and its rendering conventions. |\n| `records` | yes | `array<ref:PreviewRecord>` | One entry per v2 layout and its SVG preview. |\n\n### Nested Types\n\n#### PreviewRecord\n\n- Type: `object`\n- Required fields: `id`, `file`, `bytes`\n- Purpose: One layout template preview.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | The matching layout record id, such as cover, chart or two-column. |\n| `file` | yes | `string` | SVG filename, relative to this index directory, using the matching layout id. |\n| `bytes` | yes | `integer` | Exact UTF-8 byte length of the referenced SVG file. |\n\n## Slide Layout\n\n- File: `spec/schemas/layout.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-layout/v2`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `areas`\n- Purpose: Schema for slide-layout records (OPF 0.19). A layout is a grid template: 'areas' names the cells of a grid using nested row arrays, 'columns' and 'rows' size its tracks, and 'regions' says, for every body area, which content kinds it takes and how several blocks share it. Slide content stays flat: engines bind a slide's blocks to the regions by reading order, role and kind (bindRegions in @openpresentation/opf), a block may pin itself to a region with 'region', and an empty region collapses i...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-layout/v2\"` | Identifies a published record file as a layout template. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The layout's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this fie... |\n| `name` | yes | `string` | Human-readable layout name, shown in layout pickers and as the PowerPoint layout name. |\n| `summary` | no | `string` | One-sentence positioning of the layout when to reach for it. |\n| `description` | no | `string` | Longer prose describing the layout structure and ideal use cases. |\n| `design` | no | `ref:DesignHints` | The layout's design defaults, with the same keys and values as the deck's design and a slide's design (a slide overrides exactly what its layout sets, by the same name). An absent key means the layout has no opinion.... |\n| `areas` | yes | `array<array<string>>` | The grid as nested arrays: each inner array is a row, each string is one cell, and a name repeated over adjacent cells spans them; '.' is an empty cell. Cells are lowercase names ([a-z][a-z0-9-]*) or '.', and every ro... |\n| `columns` | no | `array<number>` | Relative column widths, one per cell of a row; the columns share the content width in proportion, separated by the gap. Omitted: all 1. Columns are numbers only. |\n| `rows` | no | `array<oneOf:number / const:\"auto\">` | Relative row heights, one per string of 'areas'. 'auto' is the height the row's content needs at the starting text size (at most half the content box; areas that span several rows do not size it); the numeric rows sha... |\n| `regions` | no | `object` | One entry per body area, keyed by the area's name. Region names may not be 'title', 'subtitle', 'tag', 'auto' or one of the promoted-region words ('left', 'center', 'right', 'top', 'middle', 'bottom'), so a Markdown b... |\n| `overflowRegion` | no | `string` | The region where content that no region accepts goes, whatever its 'accepts', while it has room; content beyond every region's room is drawn there too, with the warning opf/layout-unplaced. Omitted: the first primary... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n| `composition` | no | `ref:Composition` | Only 'gap', 'padding', 'minFontSize' and 'overflow', with their 0.18 meaning. A template's regions replace composition.mode, columns and weights. |\n\n### Nested Types\n\n#### Region\n\n- Type: `object`\n- Required fields: `accepts`\n- Purpose: One body region of a layout: which content it takes and how several blocks share it.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `accepts` | yes | `array<enum:text \\| list \\| image \\| video \\| chart \\| table \\| code \\| metric \\| quote \\| timeline \\| group>` | Content kinds the region takes: the body content kinds, and 'group' for a content group of any content. A group is also accepted where every one of its leaf kinds is. |\n| `role` | no | `enum:primary \\| secondary \\| media \\| supporting` | Which content the region wants first. Binding gives a picture (image, video) to a media region first, then primary, secondary, supporting; every other kind to a primary region first, then secondary, supporting, media;... |\n| `flow` | no | `enum:none \\| grid \\| column \\| auto` | How several blocks share the region: 'none' one block fills it; 'grid' by count (1 fills, 2 side by side, 3 in a row, 4 as 2 x 2, 5 as 3 + 2, 6 as 3 x 2, 7 to 12 in four columns; rows and columns swap in a portrait re... |\n| `max` | no | `integer` | Blocks the region holds on one slide. Default 1 for flow 'none' (which requires 1), else 6. Content beyond every region's room is drawn in the overflow region with the warning opf/layout-unplaced, and paginate moves i... |\n| `bleed` | no | `boolean` | The region's outer edges that lie on the content box's edge extend to the slide edge, as a placed image does. A bled region is never drawn as a card. Default false. |\n| `listColumns` | no | `oneOf:const:\"auto\" / const:1` | 'auto': a list in the region that does not fit its cell at the starting size (a lone list, or one of several blocks) flows into up to three columns before it shrinks, as a list payload's columns: 'auto'. Default 1. |\n| `anchor` | no | `enum:top \\| middle \\| bottom` | Vertical position of the region's content when it is shorter than the region. Default 'top'. |\n| `empty` | no | `enum:collapse \\| keep` | What the region does on a slide that gives it no content: 'collapse' (default) joins its cells to a neighbouring area that shares the full length of one of its edges and is not itself empty; 'keep' leaves the space. |\n\n#### DesignHints\n\n- Type: `object`\n- Required fields: none\n- Purpose: Design hints a layout carries. The keys and lowercase values are those of the deck's design and a slide's design in opf.schema.json (Design), so a slide overrides a layout by the same name and layout.design can be copied into slide.design unchanged. An absent key means the layout has no opinion. Only the keys the layout sets are stored. scripts/check-spec-integrity.mjs verifies that every key here has the same typ...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `titleAlignment` | no | `enum:left \\| center \\| right` | Horizontal alignment of the title heading area. |\n| `contentAlignment` | no | `enum:left \\| center \\| right` | Horizontal alignment of the body regions. |\n| `contentBox` | no | `boolean` | Whether the body regions are drawn inside a visible card or surface. |\n| `contentDirection` | no | `enum:horizontal \\| vertical` | Axis along which parallel body content is arranged: 'vertical' is a column, 'horizontal' a row. On a layout slide it turns every flowing region into one row or one column; the slide's own composition.mode, columns and... |\n| `chartPrimary` | no | `enum:none \\| top \\| bottom \\| left \\| right` | Where the primary chart sits relative to supporting content on automatic slides. Template layouts express this arrangement with their regions; chartPrimary does not override a template. |\n| `imageFit` | no | `enum:cover \\| contain \\| stretch` | Default fit of the slide's image blocks: 'cover' fills the frame and crops around focus, 'contain' shows the whole picture, 'stretch' scales it to the frame. |\n| `listBullet` | no | `enum:character \\| image` | Marker style of lists: 'character' draws the glyph, 'image' draws the deck's icon logo as a picture bullet. |\n| `mirror` | no | `boolean` | Draw the layout mirrored: each row's cells and the column sizes reversed, so a 'beside' layout puts its picture, chart or table at the end. Logical: a right-to-left deck mirrors on top. Binding and reading order do no... |\n\n#### Composition\n\n- Type: `object`\n- Required fields: none\n- Purpose: A template's composition: only gap, padding, minFontSize and overflow, with their 0.18 meaning. A slide's composition overrides them; a slide's composition.mode, columns and weights arrange its first primary flowing region.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `gap` | no | `number` | Space between cells as a fraction of the container short edge (canvas at slide root). Default 0.03333333333333333. |\n| `padding` | no | `number` | Inset as a fraction of the container short edge. Default 0.08 on a slide, 0 inside a group. |\n| `minFontSize` | no | `number` | Minimum readable text size in reference pixels at a 720-pixel canvas short edge. Default 16. Overflow is diagnosed when text cannot fit at this size. |\n| `overflow` | no | `enum:warn \\| error` | warn returns diagnostics for content that does not fit; error rejects layout. Content is never silently removed. Default warn. |\n\n## Narrative Template\n\n- File: `spec/schemas/narrative.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-narrative/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `beats`\n- Purpose: Schema for narrative records. A narrative is a plan: a named story arc (e.g. 'problem-solution', 'scqa') as an ordered list of beats, each saying what its slide must do. A document points at one with the string 'narrative' and links its slides to beats with 'slides[].beat'; a narrative the document defines itself is a record in catalogs.custom.narratives. Core validate warns about a 'slides[].beat' id the resolved narrative does not define (opf/unknown-beat) and about a root 'duration' outsid...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-narrative/v1\"` | Identifies a published record file as a narrative record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The narrative's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this... |\n| `name` | yes | `string` | Human-readable template name, e.g. 'Problem Solution'. |\n| `summary` | no | `string` | One-sentence description of when and why to use this narrative. |\n| `description` | no | `string` | Longer prose describing the narrative arc and ideal use cases. Used by AI-driven generation to seed deck-level direction. |\n| `audienceFit` | no | `array<string>` | Audiences this narrative works well for, e.g. ['executive', 'investor', 'customer']. |\n| `duration` | no | `object` | Typical talk length this narrative suits, as a range in minutes. A deck's own target is the root 'duration' (one number); core validate warns when that target lies outside this range, and when 'min' is greater than 'm... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search, e.g. ['business', 'pitch', 'internal']. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n| `beats` | yes | `array<ref:Beat>` | Ordered list of beats that make up the narrative arc. |\n\n### Nested Types\n\n#### Beat\n\n- Type: `object`\n- Required fields: `id`, `name`\n- Purpose: A single narrative beat a labeled segment of the story arc with a specific dramatic purpose. One beat is one slide: 'type' is that slide's Slide.type and 'layout' is its Slide.layout, so a planner copies them straight into a skeleton deck. Split a heavy beat into several beats rather than giving it several slides.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable slug used by Slide.beat to reference this beat. Lowercase kebab-case. |\n| `name` | yes | `string` | Human-readable beat name, e.g. 'The Problem'. |\n| `description` | no | `string` | Curator-written prose that explains what this beat should accomplish. |\n| `instructions` | no | `string` | Short author-facing instruction for the beat typically one phrase. Complements 'description' with a concise directive. |\n| `type` | no | `enum:text \\| list \\| image \\| video \\| chart \\| table \\| code \\| metric \\| quote \\| timeline` | Default content kind for the beat's slide: the same values as Slide.type. Helps engines and planners choose a layout when only the beat is known. |\n| `layout` | no | `string` | Suggested layout id for the beat's slide, e.g. 'section-divider', 'title-slide', 'text-left'. Resolves the same way as Slide.layout against catalogs.layouts and the default catalog at https://www.pptx.gallery/layouts. |\n| `thoughtCues` | no | `array<string>` | Optional speaker or thinking cues for the beat: questions the slide should answer. A planner or author turns them into the slide's notes; no engine copies them there on its own. |\n\n## Purpose\n\n- File: `spec/schemas/purpose.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-purpose/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for purpose records. Each record names a presentation objective such as informing, aligning, persuading, driving a decision, or selling. Documents reference a purpose from 'purpose', which also accepts free-form text and inline Purpose objects. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines never fetch a catalog. A published record file carries '$schema' a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-purpose/v1\"` | Identifies a published record file as a purpose record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The purpose's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this fi... |\n| `name` | yes | `string` | Human-readable purpose name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the purpose what this deck is trying to accomplish. |\n| `description` | no | `string` | Longer prose describing when to use this purpose and how it should shape a deck. |\n| `outcome` | no | `string` | Desired audience outcome after the presentation. |\n| `successCriteria` | no | `array<string>` | Observable signals that the deck accomplished this purpose. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog that work well for this purpose. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: ids of tones in the same catalog that work well for this purpose. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n\n## Social Platform\n\n- File: `spec/schemas/social-platform.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-social-platform/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for social-platform records: display metadata for authoring UIs (name, brand color, handle example) alongside the URL patterns. The keys of a Socials object are an engine vocabulary validated directly; engines link a handle with the patterns in spec/reference/engine-vocabularies.json and never look a social-platform record up. pptx.gallery publishes these records and @openpresentation/gallery ships them as catalogDisplay.socialPlatforms.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-social-platform/v1\"` | Identifies this record as a social-platform entry in the openpresentation.org catalog. |\n| `id` | yes | `string` | The social platform's key, as written in a Socials object. Lowercase kebab-case. |\n| `name` | yes | `string` | Human-readable platform name shown in pickers and footers. |\n| `summary` | no | `string` | One-sentence positioning of the platform what it's used for and who's on it. |\n| `description` | no | `string` | Longer prose describing the platform and any rendering conventions (e.g., handle prefixes, distributed instances). |\n| `baseUrl` | no | `string` | Canonical base URL of the platform used as the prefix when normalizing handles to full URLs. |\n| `profileUrlPattern` | no | `string` | URL pattern for individual member profiles. Use '{handle}' as the placeholder for the handle (with the prefix already stripped). |\n| `companyUrlPattern` | no | `string` | Optional URL pattern for organization / company pages, when the platform distinguishes them from member profiles. Use '{handle}' as the placeholder. |\n| `handlePrefix` | no | `string` | Conventional prefix character displayed before the handle (e.g. '@' for X / Mastodon / Threads / TikTok). Empty string when no prefix is used. Renderers strip it before substituting into URL patterns. |\n| `handleExample` | no | `string` | Example handle in its conventional rendered form, used by picker UIs and validation hints. |\n| `brandColor` | no | `string` | Brand color (hex) for branded icon chips, link styling, or section accents in authoring UIs. Catalog metadata: engines do not draw it. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Theme\n\n- File: `spec/schemas/theme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-theme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for theme records. Each theme is a small, named bundle that pairs a color scheme, a font scheme, a default theme-controlled background, and a slide size. Documents reference a theme from design.theme; design.colorScheme, design.fontScheme, design.background and design.dimensions take precedence over the resolved theme. The theme's own colorScheme and fontScheme references resolve in the theme's group first, so a catalog's theme finds that catalog's schemes. A document references a reco...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-theme/v1\"` | Identifies a published record file as a theme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The theme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this field. |\n| `name` | yes | `string` | Human-readable theme name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the theme when to reach for it. |\n| `description` | no | `string` | Longer prose describing what the theme looks and feels like and the kinds of decks it suits. |\n| `colorScheme` | no | `string` | The theme's default colour scheme: a reference (a bare id or 'name:id') resolved like design.colorScheme, but in the theme's own catalogs group first. |\n| `fontScheme` | no | `string` | The theme's default font scheme: a reference (a bare id or 'name:id') resolved like design.fontScheme, but in the theme's own catalogs group first. |\n| `background` | no | `ref:ThemeBackground` | |\n| `dimensions` | no | `enum:16:9 \\| 4:3 \\| 16:10 \\| 1:1 \\| 4:5 \\| 9:16 \\| letter \\| a4 \\| widescreen \\| standard` | Default slide size for this theme. Accepts the same preset values as design.dimensions.preset. |\n| `design` | no | `object` | The theme's design defaults (OPF 0.19), so a house style such as boxed content or centred titles is set once: the keys and values of the deck's design for titleAlignment, contentAlignment, contentBox, contentDirection... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### ThemeBackgroundSlot\n\n- Type: `enum:light1 | light2 | dark1 | dark2`\n- Required fields: none\n- Purpose: PowerPoint theme-controlled slide background slot from the active color scheme. These are slots, not assumptions about actual colors: light1 is usually white and dark1 is usually black by convention, but the color scheme controls the real values.\n\n_No named properties._\n\n#### ThemeBackground\n\n- Type: `object`\n- Required fields: `type`, `slot`\n- Purpose: Theme-controlled PowerPoint slide background. The slot is resolved through the active color scheme and remains theme-aware.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"theme\"` | Theme-controlled background fill. |\n| `slot` | yes | `ref:ThemeBackgroundSlot` | |\n\n## Tone\n\n- File: `spec/schemas/tone.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-tone/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for tone records. Each record names a presentation tone (e.g. 'formal', 'casual', 'inspirational') and carries voice cues, anti-patterns, and sample phrases that AI-driven generation uses to shape output. Documents reference a tone from 'tone'. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines never fetch a catalog. A published record file carries '$schema' a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-tone/v1\"` | Identifies a published record file as a tone record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The tone's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this field. |\n| `name` | yes | `string` | Human-readable tone name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the tone when to reach for it. |\n| `description` | no | `string` | Longer prose describing the tone and the kinds of decks it suits. |\n| `voiceCues` | no | `array<string>` | Short directives that shape AI generation toward this tone. Phrased as imperatives, e.g. 'use second-person', 'favor short sentences', 'lead with the recommendation'. |\n| `avoid` | no | `array<string>` | Anti-patterns that AI generation should not produce when this tone is active. |\n| `samplePhrases` | no | `array<string>` | Short example phrases that exemplify this tone. Used by picker UIs and as few-shot examples for AI generation. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog this tone pairs well with. Used by picker UIs to suggest narratives once a tone is chosen. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n"
13
+ "markdown": "# OPF Catalog Schema Reference\n\nCatalog records are reusable presets that OPF documents reference by id. This page summarizes every catalog companion schema in `spec/schemas/`. The top-level presentation schema has its own [schema reference](schema-reference.md), and the report format every tool shares has the [validation results reference](finding-schema-reference.md).\n\nOPF documents usually reference these records with string ids such as `design.theme = \"minimal\"`, `tone = \"formal\"`, or `chart.type = \"line\"`. Dense examples may also embed catalog sources or inline records under `catalogs`.\n\n## Audience\n\n- File: `spec/schemas/audience.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-audience/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for audience records. Each record names an audience archetype (e.g. 'executive', 'engineering-team', 'investor') and carries seniority, technical-fluency, decision-power, and attention-budget hints used by AI-driven generation. Documents reference an audience from 'audience', which also accepts free-form text. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-audience/v1\"` | Identifies a published record file as a audience record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The audience's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this f... |\n| `name` | yes | `string` | Human-readable audience name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the audience who they are and what they care about. |\n| `description` | no | `string` | Longer prose describing the audience archetype and how to address them. |\n| `seniority` | no | `enum:ic \\| manager \\| director \\| vp \\| c-suite \\| mixed` | Typical seniority level of the audience. Engines use this as a hint for default depth and pacing. |\n| `technicalFluency` | no | `enum:low \\| medium \\| high \\| mixed` | Typical technical fluency of the audience. AI generation uses this to decide whether to expand or assume technical terminology. |\n| `decisionPower` | no | `enum:informational \\| advisory \\| decision-maker` | Whether the audience is expected to be informed, to advise, or to actually decide. Shapes the strength of the closing ask. |\n| `attentionBudgetMinutes` | no | `number` | Realistic upper bound on this audience's focused attention for a single presentation, in minutes. Used as a hint when comparing against duration and the resolved narrative's duration range. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog that work well for this audience. Used by picker UIs to suggest narratives once an audience is chosen. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: ids of tones in the same catalog that work well for this audience. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n\n## Catalog Index\n\n- File: `spec/schemas/catalog-index.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-catalog-index/v1`\n- Type: `object`\n- Required fields: `$schema`, `version`, `description`, `records`\n- Purpose: Generic shape shared by every `<kind>/index.json` file of the @openpresentation/gallery package (packages/gallery/catalog in the OPF repo) and by the default-catalog index that pptx.gallery publishes at `https://www.pptx.gallery/<kind>/index.json` (the gallery package is a pinned snapshot of that catalog; see docs/default-catalog.md). An index is a lightweight, ordered summary of the full-record JSON files that live alongside it: each entry names the record's stable id, a human-readable name,...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-catalog-index/v1\"` | |\n| `kind` | no | `enum:audiences \\| chart-types \\| color-schemes \\| font-schemes \\| languages \\| layouts \\| narratives \\| purposes \\| social-platforms \\| themes \\| tones` | Catalog kind, as the URL segment of the default catalog (`https://www.pptx.gallery/<kind>`) and the `packages/gallery/catalog/<kind>` directory name. |\n| `version` | yes | `string` | Index format version, as a string. |\n| `description` | yes | `string` | Human-readable description of what this catalog kind holds and how entries are ordered. |\n| `contentSha256` | no | `string` | SHA-256 (lowercase hex) of the canonical JSON of the full records this index lists, in index order, with every top-level `x-*` member removed. Canonical JSON sorts object keys and has no insignificant whitespace. Lets... |\n| `records` | yes | `array<ref:IndexRecord>` | Ordered list of lightweight record summaries. Order defines the catalog's canonical/display order; full record data lives in the sibling JSON file named by `file`. |\n\n### Nested Types\n\n#### IndexRecord\n\n- Type: `object`\n- Required fields: `id`, `name`, `file`\n- Purpose: Lightweight summary of one catalog record. Additional per-kind fields (e.g. `summary`, `tags`, `bcp47`, `duration`, `group`, `label`) are allowed and vary by catalog kind.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable identifier, matching the `id` field inside the record file named by `file`. Lowercase kebab-case; chart-type ids may start with a digit (e.g. '100pct-stacked-bar'). |\n| `name` | yes | `string` | Human-readable name shown in pickers. |\n| `file` | yes | `string` | Filename of the full record, relative to this index file's directory. |\n| `deprecated` | no | `const:true` | Present on the entry of a record that carries `deprecation`. Pickers and default listings hide deprecated entries; the id keeps resolving. |\n| `replacedBy` | no | `string` | The deprecated record's `deprecation.replacedBy`, repeated so a picker can offer the replacement without loading the record. |\n\n## Catalog Snapshot Manifest\n\n- File: `spec/schemas/catalog-manifest.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-catalog-manifest/v1`\n- Type: `object`\n- Required fields: `$schema`, `description`, `publisher`, `source`, `kinds`\n- Purpose: Shape of the `manifest.json` of the @openpresentation/gallery package (packages/gallery/catalog/manifest.json), which pins the snapshot of the default OPF catalog published by pptx.gallery. It records the gallery commit the snapshot came from and, per kind, how the snapshot relates to the published catalog plus a content hash of the bundled records. Written by scripts/sync-gallery-catalog.mjs and checked by scripts/check-spec-integrity.mjs; see docs/default-catalog.md. This schema describes a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-catalog-manifest/v1\"` | |\n| `description` | yes | `string` | |\n| `publisher` | yes | `string` | Base URL of the default-catalog publisher. Each kind is published at `<publisher>/<kind>/index.json`. |\n| `source` | yes | `object` | The published catalog files the snapshot was taken from. |\n| `kinds` | yes | `object` | One entry per catalog kind, keyed by the kind's URL segment. |\n\n### Nested Types\n\n#### KindEntry\n\n- Type: `object`\n- Required fields: `mode`, `records`, `contentSha256`, `gallery`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `mode` | yes | `enum:mirror` | Always 'mirror': the snapshot holds every record the publisher serves for the kind (OPF 0.15). |\n| `records` | yes | `integer` | Number of records bundled for this kind. |\n| `contentSha256` | yes | `string` | contentSha256 of the bundled records, as defined by the catalog index schema. |\n| `gallery` | yes | `object` | The published catalog for this kind at the pinned commit. |\n\n## Chart Type\n\n- File: `spec/schemas/chart-type.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-chart-type/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `mappings`\n- Purpose: Schema for chart-type records: display metadata for chart pickers (name, group, expected series, complexity) and the Open XML mapping a picker can describe. One record per chart type that Aspose.Slides officially supports (see mappings.renderers[\"aspose-slides\"].chartType), plus the column-and-line combination 'combo'. A document's chart.type is an engine vocabulary validated directly; engines never look a chart-type record up. pptx.gallery publishes these records and @openpresentation/galler...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-chart-type/v1\"` | Identifies this record as a chart type in the open presentation catalog. |\n| `id` | yes | `string` | The chart-type value (`chart.type`) this record describes. Lowercase kebab-case; it may start with a digit (e.g., '100pct-stacked-column') to mirror conventional chart naming. |\n| `name` | yes | `string` | Human-readable name of the chart type, as a chart picker shows it. |\n| `summary` | no | `string` | One-sentence positioning: when to reach for this chart variant. |\n| `description` | no | `string` | Longer prose describing the chart and ideal use cases. |\n| `mappings` | yes | `ref:ChartTypeMappings` | Canonical and optional renderer-specific mappings used by engines to render this chart type. |\n| `group` | no | `string` | Top-level grouping in the chart picker. |\n| `groupSort` | no | `integer` | Display ordering hint within the chart group. |\n| `complexity` | no | `enum:simple \\| calculated \\| hierarchical \\| normalized` | Shape of the underlying data: a flat series ('simple'), one with engine-side calculation ('calculated'), parent-child rows ('hierarchical'), or pre-normalized rows ('normalized'). The editor offers only 'simple' types... |\n| `series` | no | `integer` | Number of data series this chart type expects: exactly that many, except that a stacked or percent-stacked type and a combination (composition 'mixed', such as combo) expect at least that many. The editor offers a typ... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### ChartTypeMappings\n\n- Type: `object`\n- Required fields: `openxml`\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `openxml` | yes | `ref:OpenXmlChartMapping` | Canonical mapping to Open XML chart structures. |\n| `renderers` | no | `object` | Optional renderer-specific mappings. Keys are renderer ids; values are intentionally opaque to OPF. The bundled catalog records the matching Aspose.Slides ChartType enumeration member under the \"aspose-slides\" key, e.... |\n\n#### OpenXmlChartMapping\n\n- Type: `object`\n- Required fields: none\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `element` | no | `string` | Primary Open XML chart element or extension chart element, such as 'barChart', 'lineChart', 'pieChart', 'treemapChart', or 'waterfallChart'. |\n| `barDir` | no | `enum:bar \\| col` | Bar direction for Open XML barChart mappings. |\n| `grouping` | no | `enum:standard \\| clustered \\| stacked \\| percentStacked` | Open XML chart grouping value when the chart family supports grouping. |\n| `marker` | no | `boolean` | Whether the chart type expects visible data markers. |\n| `radarStyle` | no | `enum:standard \\| marker \\| filled` | Open XML radarStyle value for radarChart mappings. |\n| `scatterStyle` | no | `enum:line \\| lineMarker \\| marker \\| smooth \\| smoothMarker` | Open XML scatterStyle value for scatterChart mappings. |\n| `composition` | no | `enum:single \\| mixed \\| extension` | Whether the chart maps to one standard chart element, multiple combined chart elements, or an Open XML extension chart. |\n| `extension` | no | `string` | Optional Open XML extension namespace or element hint for extension charts. |\n| `series` | no | `array<ref:OpenXmlChartMapping>` | Open XML chart elements used by mixed/composite chart types. |\n| `notes` | no | `string` | Short implementation note for mappings that need renderer interpretation. |\n\n## Color Scheme\n\n- File: `spec/schemas/color-scheme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-color-scheme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for color-scheme records. Each scheme is a named palette with the twelve PowerPoint color slots (six accents, two darks, two lights, plus hyperlink and followed-hyperlink), suitable for being mapped directly into OOXML theme XML. Documents reference a scheme from design.colorScheme (or design.colorScheme.id) and from a theme's colorScheme; slot overrides on design.colorScheme take precedence over the resolved scheme. The slot fields here mirror the inline-override fields on the in-docu...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-color-scheme/v1\"` | Identifies a published record file as a colour scheme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The colour scheme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without t... |\n| `name` | yes | `string` | Human-readable scheme name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the palette what mood it evokes and where to use it. |\n| `description` | no | `string` | Longer prose describing the palette and its intended use. |\n| `accent1` | no | `ref:HexColor` | Accent 1 color (hex). Mirrors the OOXML accent1 slot. |\n| `accent2` | no | `ref:HexColor` | Accent 2 color (hex). Mirrors the OOXML accent2 slot. |\n| `accent3` | no | `ref:HexColor` | Accent 3 color (hex). Mirrors the OOXML accent3 slot. |\n| `accent4` | no | `ref:HexColor` | Accent 4 color (hex). Mirrors the OOXML accent4 slot. |\n| `accent5` | no | `ref:HexColor` | Accent 5 color (hex). Mirrors the OOXML accent5 slot. |\n| `accent6` | no | `ref:HexColor` | Accent 6 color (hex). Mirrors the OOXML accent6 slot. |\n| `dark1` | no | `ref:HexColor` | Dark 1 color (hex). Typically the deepest neutral; OOXML dark1. |\n| `dark2` | no | `ref:HexColor` | Dark 2 color (hex). Secondary dark; OOXML dark2. |\n| `light1` | no | `ref:HexColor` | Light 1 color (hex). Typically the slide canvas; OOXML lt1. |\n| `light2` | no | `ref:HexColor` | Light 2 color (hex). Secondary light surface; OOXML lt2. |\n| `hyperlink` | no | `ref:HexColor` | Hyperlink color (hex). OOXML hlink. |\n| `followedHyperlink` | no | `ref:HexColor` | Followed-hyperlink color (hex). OOXML folHlink. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### HexColor\n\n- Type: `string`\n- Required fields: none\n- Purpose: Hex color: '#RGB', '#RRGGBB' or '#RRGGBBAA'. The same definition as HexColor in opf.schema.json.\n\n_No named properties._\n\n## Font Scheme\n\n- File: `spec/schemas/font-scheme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-font-scheme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `major`, `minor`\n- Purpose: Schema for font-scheme records. Each scheme pairs a major (heading) and minor (body) font family in the OOXML majorFont/minorFont sense, scoped to a target app (powerpoint or google-slides) and a language family (Latin, East Asian, or Complex Script). Documents reference a scheme from design.fontScheme (or design.fontScheme.id), from a theme's fontScheme and from a language's fontScheme / googleFontScheme. Role overrides on design.fontScheme (heading, body, accent, code), each a font family n...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-font-scheme/v1\"` | Identifies a published record file as a font scheme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The font scheme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without thi... |\n| `name` | yes | `string` | Human-readable scheme name shown in pickers. |\n| `major` | yes | `string` | Heading (major) font family mirrors the OOXML majorFont entry. |\n| `minor` | yes | `string` | Body (minor) font family mirrors the OOXML minorFont entry. |\n| `code` | no | `string` | Optional monospaced font family name for code blocks and for inline code runs (TextRun.code). It has the same shape as the OPF FontScheme 'code' role, so a record and an inline design.fontScheme override are interchan... |\n| `eastAsian` | no | `object` | East Asian script fonts. Maps to the OOXML a:ea element of majorFont (major) and minorFont (minor), and to run-level a:ea. When set, they fill the eastAsian slot for every language; when omitted, the slot comes from t... |\n| `complexScript` | no | `object` | Complex-script fonts (for example Arabic, Hebrew, Indic and Thai). Maps to the OOXML a:cs element of majorFont (major) and minorFont (minor), and to run-level a:cs. When set, they fill the complexScript slot for every... |\n| `type` | no | `enum:sans-serif \\| serif \\| monospace` | High-level typographic class of the scheme. |\n| `app` | no | `enum:powerpoint \\| google-slides` | Target application this font pairing is intended for. Metadata for pickers and catalog filters: no engine changes its output by it. |\n| `languageFamily` | no | `enum:latin \\| ea \\| cs \\| eastAsian \\| complexScript` | Font-language family this scheme is intended for: 'latin' for Latin-script content, 'ea' (or 'eastAsian', the same value) for East Asian scripts, 'cs' (or 'complexScript', the same value) for Complex Scripts. The long... |\n| `languages` | no | `array<string>` | Optional list of BCP-47 language tags this scheme is curated for. Useful for picker UIs that group fonts by language coverage. As the design font scheme, an 'ea' or 'cs' scheme fills its script slot only when this lis... |\n| `textSample` | no | `string` | Short specimen string used by picker UIs to preview the scheme. |\n| `summary` | no | `string` | One-sentence positioning of the font pairing. |\n| `description` | no | `string` | Longer prose describing the font scheme and where it shines. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Language\n\n- File: `spec/schemas/language.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-language/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `bcp47`\n- Purpose: Schema for language records: display metadata for the language pickers of authoring tools (name, ISO code, the default font schemes a picker suggests). The document's 'language' is an engine vocabulary, a BCP-47 tag validated directly; engines never look a language record up, and their script, direction, OOXML tag and default script fonts come from spec/reference/engine-vocabularies.json. pptx.gallery publishes these records and @openpresentation/gallery ships them as catalogDisplay.languages.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-language/v1\"` | Identifies this record as a language in the openpresentation.org catalog. |\n| `id` | yes | `string` | The record's id in the gallery's language catalog, lowercase kebab-case. Documents name a language by its BCP-47 tag ('bcp47'), never by this id. |\n| `name` | yes | `string` | Human-readable language name. |\n| `code` | no | `string` | ISO 639-3 (or 639-2) three-letter language code. Carried for engines that prefer ISO codes. |\n| `bcp47` | yes | `string` | BCP-47 language tag for this record. Use 'en-GB' for UK English; 'en-UK' is not a valid BCP-47 region form. |\n| `ooxmlLang` | no | `string` | Curated culture tag for OOXML text-run language attributes (a:rPr/@lang, a:endParaRPr/@lang), in the language-[Script-]REGION form Office recognizes (e.g. 'ja-JP', 'ar-SA', 'ms-MY', 'nb-NO', 'fil-PH', 'zh-CN'). Engine... |\n| `direction` | no | `enum:ltr \\| rtl` | Base text direction for the language. When omitted, engines derive it from the script: Arabic (Arab), Hebrew (Hebr), Syriac (Syrc), Thaana (Thaa), N'Ko (Nkoo), Adlam (Adlm), Samaritan (Samr), Mandaic (Mand) and Hanifi... |\n| `script` | no | `string` | ISO 15924 script code of the language's writing system. The script selects the OOXML font slot the language's text uses: East Asian scripts (Hans, Hant, Hani, Jpan, Kore, Hang, Hira, Kana, Bopo, Yiii) use the eastAsia... |\n| `fontScheme` | no | `string` | The font-scheme id a picker suggests for this language when targeting PowerPoint output. Display metadata: engines take a language's default script fonts from their own vocabulary. |\n| `googleFontScheme` | no | `string` | The font-scheme id a picker suggests for this language when targeting Google Slides output. Display metadata. |\n| `summary` | no | `string` | One-sentence note about coverage or font defaults. |\n| `description` | no | `string` | Longer prose describing the language record and any font-pairing rationale. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Layout Preview Index\n\n- File: `spec/schemas/layout-preview-index.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-layout-preview-index/v1`\n- Type: `object`\n- Required fields: `$schema`, `version`, `description`, `records`\n- Purpose: Shape of previews/layouts/index.json in @openpresentation/gallery 2: one self-contained SVG preview per v2 intent layout, keyed by the matching catalog layout id. Each record names its SVG file and exact UTF-8 byte length. The gallery package owns the files; this checkout materializes ignored copies under packages/gallery/previews/layouts/. This internal index schema is excluded from the package's published/typed schema surface.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-layout-preview-index/v1\"` | |\n| `version` | yes | `const:\"2\"` | Index format version, as a string. |\n| `description` | yes | `string` | Human-readable description of the preview gallery and its rendering conventions. |\n| `records` | yes | `array<ref:PreviewRecord>` | One entry per v2 layout and its SVG preview. |\n\n### Nested Types\n\n#### PreviewRecord\n\n- Type: `object`\n- Required fields: `id`, `file`, `bytes`\n- Purpose: One layout template preview.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | The matching layout record id, such as cover, chart or two-column. |\n| `file` | yes | `string` | SVG filename, relative to this index directory, using the matching layout id. |\n| `bytes` | yes | `integer` | Exact UTF-8 byte length of the referenced SVG file. |\n\n## Slide Layout\n\n- File: `spec/schemas/layout.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-layout/v2`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `areas`\n- Purpose: Schema for slide-layout records (OPF 0.19). A layout is a grid template: 'areas' names the cells of a grid using nested row arrays, 'columns' and 'rows' size its tracks, and 'regions' says, for every body area, which content kinds it takes and how several blocks share it. Slide content stays flat: engines bind a slide's blocks to the regions by reading order, role and kind (bindRegions in @openpresentation/opf), a block may pin itself to a region with 'region', and an empty region collapses i...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-layout/v2\"` | Identifies a published record file as a layout template. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The layout's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this fie... |\n| `name` | yes | `string` | Human-readable layout name, shown in layout pickers and as the PowerPoint layout name. |\n| `summary` | no | `string` | One-sentence positioning of the layout when to reach for it. |\n| `description` | no | `string` | Longer prose describing the layout structure and ideal use cases. |\n| `design` | no | `ref:DesignHints` | The layout's design defaults, with the same keys and values as the deck's design and a slide's design (a slide overrides exactly what its layout sets, by the same name). An absent key means the layout has no opinion.... |\n| `areas` | yes | `array<array<string>>` | The grid as nested arrays: each inner array is a row, each string is one cell, and a name repeated over adjacent cells spans them; '.' is an empty cell. Cells are lowercase names ([a-z][a-z0-9-]*) or '.', and every ro... |\n| `columns` | no | `array<number>` | Relative column widths, one per cell of a row; the columns share the content width in proportion, separated by the gap. Omitted: all 1. Columns are numbers only. |\n| `rows` | no | `array<oneOf:number / const:\"auto\">` | Relative row heights, one per string of 'areas'. 'auto' is the height the row's content needs at the starting text size (at most half the content box; areas that span several rows do not size it); the numeric rows sha... |\n| `regions` | no | `object` | One entry per body area, keyed by the area's name. Region names may not be 'title', 'subtitle', 'tag', 'auto' or one of the promoted-region words ('left', 'center', 'right', 'top', 'middle', 'bottom'), so a Markdown b... |\n| `overflowRegion` | no | `string` | The region where content that no region accepts goes, whatever its 'accepts', while it has room; content beyond every region's room is drawn there too, with the warning opf/layout-unplaced. Omitted: the first primary... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n| `composition` | no | `ref:Composition` | Only 'gap', 'padding', 'minFontSize' and 'overflow', with their 0.18 meaning. A template's regions replace composition.mode, columns and weights. |\n\n### Nested Types\n\n#### Region\n\n- Type: `object`\n- Required fields: `accepts`\n- Purpose: One body region of a layout: which content it takes and how several blocks share it.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `accepts` | yes | `array<enum:text \\| list \\| image \\| video \\| chart \\| table \\| code \\| metric \\| quote \\| timeline \\| group>` | Content kinds the region takes: the body content kinds, and 'group' for a content group of any content. A group is also accepted where every one of its leaf kinds is. |\n| `role` | no | `enum:primary \\| secondary \\| media \\| supporting` | Which content the region wants first. Binding gives a picture (image, video) to a media region first, then primary, secondary, supporting; every other kind to a primary region first, then secondary, supporting, media;... |\n| `flow` | no | `enum:none \\| grid \\| column \\| auto` | How several blocks share the region: 'none' one block fills it; 'grid' by count (1 fills, 2 side by side, 3 in a row, 4 as 2 x 2, 5 as 3 + 2, 6 as 3 x 2, 7 to 12 in four columns; rows and columns swap in a portrait re... |\n| `max` | no | `integer` | Blocks the region holds on one slide. Default 1 for flow 'none' (which requires 1), else 6. Content beyond every region's room is drawn in the overflow region with the warning opf/layout-unplaced, and paginate moves i... |\n| `bleed` | no | `boolean` | The region's outer edges that lie on the content box's edge extend to the slide edge, as a placed image does. A bled region is never drawn as a card. Default false. |\n| `listColumns` | no | `oneOf:const:\"auto\" / const:1` | 'auto': a list in the region that does not fit its cell at the starting size (a lone list, or one of several blocks) flows into up to three columns before it shrinks, as a list payload's columns: 'auto'. Default 1. |\n| `anchor` | no | `enum:top \\| middle \\| bottom` | Vertical position of the region's content when it is shorter than the region. Default 'top'. |\n| `empty` | no | `enum:collapse \\| keep` | What the region does on a slide that gives it no content: 'collapse' (default) joins its cells to a neighbouring area that shares the full length of one of its edges and is not itself empty; 'keep' leaves the space. |\n\n#### DesignHints\n\n- Type: `object`\n- Required fields: none\n- Purpose: Design hints a layout carries. The keys and lowercase values are those of the deck's design and a slide's design in opf.schema.json (Design), so a slide overrides a layout by the same name and layout.design can be copied into slide.design unchanged. An absent key means the layout has no opinion. Only the keys the layout sets are stored. scripts/check-spec-integrity.mjs verifies that every key here has the same typ...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `titleAlignment` | no | `enum:left \\| center \\| right` | Horizontal alignment of the title heading area. |\n| `contentAlignment` | no | `enum:left \\| center \\| right` | Horizontal alignment of the body regions. |\n| `contentBox` | no | `boolean` | Whether the body regions are drawn inside a visible card or surface. |\n| `contentDirection` | no | `enum:horizontal \\| vertical` | Axis along which parallel body content is arranged: 'vertical' is a column, 'horizontal' a row. On a layout slide it turns every flowing region into one row or one column; the slide's own composition.mode, columns and... |\n| `chartPrimary` | no | `enum:none \\| top \\| bottom \\| left \\| right` | Where the primary chart sits relative to supporting content on automatic slides. Template layouts express this arrangement with their regions; chartPrimary does not override a template. |\n| `imageFit` | no | `enum:cover \\| contain \\| stretch` | Default fit of the slide's image blocks: 'cover' fills the frame and crops around focus, 'contain' shows the whole picture, 'stretch' scales it to the frame. |\n| `listBullet` | no | `enum:character \\| image` | Marker style of lists: 'character' draws the glyph, 'image' draws the deck's icon logo as a picture bullet. |\n| `mirror` | no | `boolean` | Draw the layout mirrored: each row's cells and the column sizes reversed, so a 'beside' layout puts its picture, chart or table at the end. Logical: a right-to-left deck mirrors on top. Binding and reading order do no... |\n\n#### Composition\n\n- Type: `object`\n- Required fields: none\n- Purpose: A template's composition: only gap, padding, minFontSize and overflow, with their 0.18 meaning. A slide's composition overrides them; a slide's composition.mode, columns and weights arrange its first primary flowing region.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `gap` | no | `number` | Space between cells as a fraction of the container short edge (canvas at slide root). Default 0.03333333333333333. |\n| `padding` | no | `number` | Inset as a fraction of the container short edge. Default 0.08 on a slide, 0 inside a group. |\n| `minFontSize` | no | `number` | Minimum readable text size in reference pixels at a 720-pixel canvas short edge. Default 16. Overflow is diagnosed when text cannot fit at this size. |\n| `overflow` | no | `enum:warn \\| error` | warn returns diagnostics for content that does not fit; error rejects layout. Content is never silently removed. Default warn. |\n\n## Narrative Template\n\n- File: `spec/schemas/narrative.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-narrative/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`, `beats`\n- Purpose: Schema for narrative records. A narrative is a plan: a named story arc (e.g. 'problem-solution', 'scqa') as an ordered list of beats, each saying what its slide must do. A document points at one with the string 'narrative' and links its slides to beats with 'slides[].beat'; a narrative the document defines itself is a record in catalogs.custom.narratives. Core validate warns about a 'slides[].beat' id the resolved narrative does not define (opf/unknown-beat) and about a root 'duration' outsid...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-narrative/v1\"` | Identifies a published record file as a narrative record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The narrative's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this... |\n| `name` | yes | `string` | Human-readable template name, e.g. 'Problem Solution'. |\n| `summary` | no | `string` | One-sentence description of when and why to use this narrative. |\n| `description` | no | `string` | Longer prose describing the narrative arc and ideal use cases. Used by AI-driven generation to seed deck-level direction. |\n| `audienceFit` | no | `array<string>` | Audiences this narrative works well for, e.g. ['executive', 'investor', 'customer']. |\n| `duration` | no | `object` | Typical talk length this narrative suits, as a range in minutes. A deck's own target is the root 'duration' (one number); core validate warns when that target lies outside this range, and when 'min' is greater than 'm... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search, e.g. ['business', 'pitch', 'internal']. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n| `beats` | yes | `array<ref:Beat>` | Ordered list of beats that make up the narrative arc. |\n\n### Nested Types\n\n#### Beat\n\n- Type: `object`\n- Required fields: `id`, `name`\n- Purpose: A single narrative beat a labeled segment of the story arc with a specific dramatic purpose. One beat is one slide: 'type' is that slide's Slide.type and 'layout' is its Slide.layout, so a planner copies them straight into a skeleton deck. Split a heavy beat into several beats rather than giving it several slides.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `id` | yes | `string` | Stable slug used by Slide.beat to reference this beat. Lowercase kebab-case. |\n| `name` | yes | `string` | Human-readable beat name, e.g. 'The Problem'. |\n| `description` | no | `string` | Curator-written prose that explains what this beat should accomplish. |\n| `instructions` | no | `string` | Short author-facing instruction for the beat typically one phrase. Complements 'description' with a concise directive. |\n| `type` | no | `enum:text \\| list \\| image \\| video \\| chart \\| table \\| code \\| metric \\| quote \\| timeline` | Default content kind for the beat's slide: the same values as Slide.type. Helps engines and planners choose a layout when only the beat is known. |\n| `layout` | no | `string` | Suggested layout id for the beat's slide, e.g. 'section-divider', 'title-slide', 'text-left'. Resolves the same way as Slide.layout against catalogs.layouts and the default catalog at https://www.pptx.gallery/layouts. |\n| `thoughtCues` | no | `array<string>` | Optional speaker or thinking cues for the beat: questions the slide should answer. A planner or author turns them into the slide's notes; no engine copies them there on its own. |\n\n## Purpose\n\n- File: `spec/schemas/purpose.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-purpose/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for purpose records. Each record names a presentation objective such as informing, aligning, persuading, driving a decision, or selling. Documents reference a purpose from 'purpose', which also accepts free-form text and inline Purpose objects. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines never fetch a catalog. A published record file carries '$schema' a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-purpose/v1\"` | Identifies a published record file as a purpose record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The purpose's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this fi... |\n| `name` | yes | `string` | Human-readable purpose name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the purpose what this deck is trying to accomplish. |\n| `description` | no | `string` | Longer prose describing when to use this purpose and how it should shape a deck. |\n| `outcome` | no | `string` | Desired audience outcome after the presentation. |\n| `successCriteria` | no | `array<string>` | Observable signals that the deck accomplished this purpose. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog that work well for this purpose. Never resolved, embedded or validated as references. |\n| `recommendedTones` | no | `array<string>` | Soft cross-link: ids of tones in the same catalog that work well for this purpose. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n\n## Social Platform\n\n- File: `spec/schemas/social-platform.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-social-platform/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for social-platform records: display metadata for authoring UIs (name, brand color, handle example) alongside the URL patterns. The keys of a Socials object are an engine vocabulary validated directly; engines link a handle with the patterns in spec/reference/engine-vocabularies.json and never look a social-platform record up. pptx.gallery publishes these records and @openpresentation/gallery ships them as catalogDisplay.socialPlatforms.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-social-platform/v1\"` | Identifies this record as a social-platform entry in the openpresentation.org catalog. |\n| `id` | yes | `string` | The social platform's key, as written in a Socials object. Lowercase kebab-case. |\n| `name` | yes | `string` | Human-readable platform name shown in pickers and footers. |\n| `summary` | no | `string` | One-sentence positioning of the platform what it's used for and who's on it. |\n| `description` | no | `string` | Longer prose describing the platform and any rendering conventions (e.g., handle prefixes, distributed instances). |\n| `baseUrl` | no | `string` | Canonical base URL of the platform used as the prefix when normalizing handles to full URLs. |\n| `profileUrlPattern` | no | `string` | URL pattern for individual member profiles. Use '{handle}' as the placeholder for the handle (with the prefix already stripped). |\n| `companyUrlPattern` | no | `string` | Optional URL pattern for organization / company pages, when the platform distinguishes them from member profiles. Use '{handle}' as the placeholder. |\n| `handlePrefix` | no | `string` | Conventional prefix character displayed before the handle (e.g. '@' for X / Mastodon / Threads / TikTok). Empty string when no prefix is used. Renderers strip it before substituting into URL patterns. |\n| `handleExample` | no | `string` | Example handle in its conventional rendered form, used by picker UIs and validation hints. |\n| `brandColor` | no | `string` | Brand color (hex) for branded icon chips, link styling, or section accents in authoring UIs. Catalog metadata: engines do not draw it. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n## Theme\n\n- File: `spec/schemas/theme.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-theme/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for theme records. Each theme is a small, named bundle that pairs a color scheme, a font scheme, a default theme-controlled background, and a slide size. Documents reference a theme from design.theme; design.colorScheme, design.fontScheme, design.background and design.dimensions take precedence over the resolved theme. The theme's own colorScheme and fontScheme references resolve in the theme's group first, so a catalog's theme finds that catalog's schemes. A document references a reco...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-theme/v1\"` | Identifies a published record file as a theme record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The theme's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this field. |\n| `name` | yes | `string` | Human-readable theme name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the theme when to reach for it. |\n| `description` | no | `string` | Longer prose describing what the theme looks and feels like and the kinds of decks it suits. |\n| `colorScheme` | no | `string` | The theme's default colour scheme: a reference (a bare id or 'name:id') resolved like design.colorScheme, but in the theme's own catalogs group first. |\n| `fontScheme` | no | `string` | The theme's default font scheme: a reference (a bare id or 'name:id') resolved like design.fontScheme, but in the theme's own catalogs group first. |\n| `background` | no | `ref:ThemeBackground` | |\n| `dimensions` | no | `enum:16:9 \\| 4:3 \\| 16:10 \\| 1:1 \\| 4:5 \\| 9:16 \\| letter \\| a4 \\| widescreen \\| standard` | Default slide size for this theme. Accepts the same preset values as design.dimensions.preset. |\n| `design` | no | `object` | The theme's design defaults (OPF 0.19), so a house style such as boxed content or centred titles is set once: the keys and values of the deck's design for titleAlignment, contentAlignment, contentBox, contentDirection... |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional; engines fall back gracefully when previews aren't available. |\n\n### Nested Types\n\n#### ThemeBackgroundSlot\n\n- Type: `enum:light1 | light2 | dark1 | dark2`\n- Required fields: none\n- Purpose: PowerPoint theme-controlled slide background slot from the active color scheme. These are slots, not assumptions about actual colors: light1 is usually white and dark1 is usually black by convention, but the color scheme controls the real values.\n\n_No named properties._\n\n#### ThemeBackground\n\n- Type: `object`\n- Required fields: `type`, `slot`\n- Purpose: Theme-controlled PowerPoint slide background. The slot is resolved through the active color scheme and remains theme-aware.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `type` | yes | `const:\"theme\"` | Theme-controlled background fill. |\n| `slot` | yes | `ref:ThemeBackgroundSlot` | |\n\n## Tone\n\n- File: `spec/schemas/tone.schema.json`\n- Schema id: `https://openpresentation.org/schema/opf-tone/v1`\n- Type: `object`\n- Required fields: `$schema`, `id`, `name`\n- Purpose: Schema for tone records. Each record names a presentation tone (e.g. 'formal', 'casual', 'inspirational') and carries voice cues, anti-patterns, and sample phrases that AI-driven generation uses to shape output. Documents reference a tone from 'tone'. A document references a record as a bare id or 'name:id' and resolves it in its own catalogs groups first, then in the catalog the host registered for the group's source; engines never fetch a catalog. A published record file carries '$schema' a...\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `$schema` | yes | `const:\"https://openpresentation.org/schema/opf-tone/v1\"` | Identifies a published record file as a tone record. Required in a published file; an embedded copy omits it. |\n| `id` | yes | `string` | The tone's id in its catalog, lowercase kebab-case. Required in a published record file; a document references the record by it (a bare id, or 'name:id' for a named group) and embeds it keyed by it, without this field. |\n| `name` | yes | `string` | Human-readable tone name shown in pickers. |\n| `summary` | no | `string` | One-sentence positioning of the tone when to reach for it. |\n| `description` | no | `string` | Longer prose describing the tone and the kinds of decks it suits. |\n| `voiceCues` | no | `array<string>` | Short directives that shape AI generation toward this tone. Phrased as imperatives, e.g. 'use second-person', 'favor short sentences', 'lead with the recommendation'. |\n| `avoid` | no | `array<string>` | Anti-patterns that AI generation should not produce when this tone is active. |\n| `samplePhrases` | no | `array<string>` | Short example phrases that exemplify this tone. Used by picker UIs and as few-shot examples for AI generation. |\n| `recommendedNarratives` | no | `array<string>` | Soft cross-link: ids of narratives in the same catalog this tone pairs well with. Used by picker UIs to suggest narratives once a tone is chosen. Never resolved, embedded or validated as references. |\n| `tags` | no | `array<string>` | Free-form labels for filtering and search. |\n| `preview` | no | `object` | Visual previews of the record, used by picker UIs and inline rendering. All sub-fields are optional. |\n"
14
14
  },
15
15
  {
16
16
  "slug": "chart-options",
@@ -99,8 +99,8 @@ var docsData = Object.freeze([
99
99
  {
100
100
  "slug": "finding-schema-reference",
101
101
  "file": "docs/finding-schema-reference.md",
102
- "title": "OPF Finding Schema Reference",
103
- "markdown": "# OPF Finding Schema Reference\n\nA finding is one thing a checker found at one place in a presentation, and a report lists them. `validate` in `@openpresentation/opf`, the Markdown and YAML converters, the editor's Review panel and hosted reviewers such as pptx.dev's AI review all produce this shape, so a host shows and applies findings from any of them the same way. This page summarizes `spec/schemas/finding.schema.json`; the schema remains the source of truth. How core produces findings is in [validate](validate.md).\n\n## Report Contract\n\n- Schema id: `https://openpresentation.org/schema/opf-finding/v1`\n- Required top-level fields: `valid`, `findings`, `counts`\n- Additional top-level fields: allowed (`validate` adds `schemaValid`, `checks`, `template` and `unfilledVariables`)\n\n## Top-Level Fields\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `valid` | yes | `boolean` | True when no finding has severity `error`. |\n| `findings` | yes | `array<ref:Finding>` | Every finding, in report order. |\n| `counts` | yes | `object` | How many findings each severity has. |\n\n## Object And Type Reference\n\n### FindingSeverity\n\n- Type: `enum:error | warning | info`\n- Required fields: none\n- Purpose: `error` makes a report invalid. `warning` is a problem the author should fix. `info` is advisory.\n\n_No named properties._\n\n\n### FindingCategory\n\n- Type: `anyOf:enum:format | references | policy | accessibility | layout | content / string`\n- Required fields: none\n- Purpose: What kind of question the finding answers. Core uses `format` (is it well-formed OPF), `references` (does everything it points at resolve), `policy` (host house rules), `accessibility`, `layout` (will it present as authored) and `content` (is anything left unfinished). Other producers may use their own category names, such as `narrative` or `audience-fit`.\n\n_No named properties._\n\n\n### FindingLocation\n\n- Type: `object`\n- Required fields: `offset`, `length`, `line`, `column`\n- Purpose: A range in the checked text: UTF-16 offset and length from the start, and the one-based line and column of the start. Present when the finding was produced from text (JSON, Markdown or YAML).\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `offset` | yes | `integer` | |\n| `length` | yes | `integer` | |\n| `line` | yes | `integer` | |\n| `column` | yes | `integer` | |\n\n\n### JsonPatchOperation\n\n- Type: `oneOf:object / object / object`\n- Required fields: none\n- Purpose: One RFC 6902 JSON Patch operation. Paths are JSON Pointers into the checked presentation.\n\n_No named properties._\n\n\n### FindingFocus\n\n- Type: `object`\n- Required fields: `path`, `field`\n- Purpose: The field an author has to fill in, for a fix that cannot be written as a patch.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `path` | yes | `string` | JSON Pointer of the field to edit. |\n| `field` | yes | `string` | What the field is, such as `alt`, `title`, `text`, `link`, `language` or `fontSize`. |\n| `value` | no | `string \\| number \\| boolean \\| null \\| array \\| object` | The field's current value, when it has one. |\n\n\n### FindingFix\n\n- Type: `object`\n- Required fields: `title`\n- Purpose: A suggested repair. Core never applies it: the host shows it and applies it through its own undoable edit path. A `patch` fix carries JSON Patch operations; a `focus` fix only names the field the author has to fill in.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `title` | yes | `string` | What applying the fix does, in a few words. |\n| `id` | no | `string` | Stable id within one finding, such as `focus-alt` or `use-readable-color`. |\n| `kind` | no | `enum:patch \\| focus` | `patch` fixes carry `patch`; `focus` fixes carry `focus`. |\n| `safe` | no | `boolean` | True when applying the fix cannot change what the content says or how it looks beyond the repair, such as switching a failing colour to the one the theme chose. False fixes need a deliberate choice. |\n| `patch` | no | `array<ref:JsonPatchOperation>` | RFC 6902 JSON Patch operations that apply the fix to the checked presentation. |\n| `focus` | no | `ref:FindingFocus` | |\n\n\n### FindingSuggestion\n\n- Type: `object`\n- Required fields: `value`, `label`, `origin`\n- Purpose: A value the author could use instead, such as a catalog record that exists. Suggestions name records actually present in the checked context and never replace authored values silently.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `value` | yes | `string \\| number \\| boolean \\| null` | |\n| `label` | yes | `string` | |\n| `origin` | yes | `string` | Where the suggestion comes from: `schema`, `document` (a record the document embeds), `registered` (a catalog the host registered) or `contract`. |\n| `definition` | no | `string` | Where the suggested record or constraint is defined. |\n\n\n### ValidationIssue\n\n- Type: `object`\n- Required fields: `path`, `message`, `keyword`, `schemaPath`, `params`\n- Purpose: The raw schema or semantic issue behind a finding, kept with every union alternative so a host can inspect the exact constraint.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `path` | yes | `string` | |\n| `message` | yes | `string` | |\n| `keyword` | yes | `string` | The JSON Schema keyword that failed, or `opf` for a rule the schema cannot express. |\n| `schemaPath` | yes | `string` | |\n| `params` | yes | `object` | |\n\n\n### Finding\n\n- Type: `object`\n- Required fields: `ruleId`, `severity`, `category`, `path`, `message`\n- Purpose: One thing a checker found at one place in a presentation.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `ruleId` | yes | `string` | Stable `<producer>/<rule>` id, such as `opf/text-contrast` for core or `pptx.dev/narrative-gap` for a hosted reviewer. Ids do not change between releases. |\n| `source` | no | `string` | The producer: `opf` (the default) for core, or the name of another tool such as `pptx.dev/review`. |\n| `severity` | yes | `ref:FindingSeverity` | |\n| `category` | yes | `ref:FindingCategory` | |\n| `path` | yes | `string` | JSON Pointer to the value the finding is about, such as `/slides/4/title`. Empty for the whole document, or for a value in a supplied context such as a loaded catalog. |\n| `scope` | no | `enum:document \\| context` | `document` (the default) when `path` points into the checked presentation, `context` when it points into host-supplied context such as loaded catalog records. |\n| `message` | yes | `string` | What is wrong, in a sentence. |\n| `help` | no | `string` | What to do about it. |\n| `slide` | no | `integer` | Zero-based index of the slide the finding belongs to. |\n| `slideId` | no | `string` | The id of that slide, when it has one. |\n| `location` | no | `ref:FindingLocation` | |\n| `definition` | no | `string` | A link to the rule's documentation or to the schema constraint or catalog record the finding is about. |\n| `lookup` | no | `array<string>` | Arguments for the `opf schema` or `opf catalog` command that shows the definition, as an argument list rather than a shell string. |\n| `suggestions` | no | `array<ref:FindingSuggestion>` | |\n| `measured` | no | `object` | The numbers behind the finding, such as `{ \"ratio\": 2.1, \"needed\": 4.5 }`. |\n| `fixes` | no | `array<ref:FindingFix>` | |\n| `validation` | no | `ref:ValidationIssue` | |\n"
102
+ "title": "Validation results",
103
+ "markdown": "# Validation results\n\nErrors, warnings and suggested fixes.\n\nA finding is one thing a checker found at one place in a presentation, and a report lists them. `validate` in `@openpresentation/opf`, the Markdown and YAML converters, the editor's Review panel and hosted reviewers such as pptx.dev's AI review all produce this shape, so a host shows and applies findings from any of them the same way. This page summarizes `spec/schemas/finding.schema.json`; the schema remains the source of truth. How core produces findings is in [validate](validate.md).\n\n## Report Contract\n\n- Schema id: `https://openpresentation.org/schema/opf-finding/v1`\n- Required top-level fields: `valid`, `findings`, `counts`\n- Additional top-level fields: allowed (`validate` adds `schemaValid`, `checks`, `template` and `unfilledVariables`)\n\n## Top-Level Fields\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `valid` | yes | `boolean` | True when no finding has severity `error`. |\n| `findings` | yes | `array<ref:Finding>` | Every finding, in report order. |\n| `counts` | yes | `object` | How many findings each severity has. |\n\n## Object And Type Reference\n\n### FindingSeverity\n\n- Type: `enum:error | warning | info`\n- Required fields: none\n- Purpose: `error` makes a report invalid. `warning` is a problem the author should fix. `info` is advisory.\n\n_No named properties._\n\n\n### FindingCategory\n\n- Type: `anyOf:enum:format | references | policy | accessibility | layout | content / string`\n- Required fields: none\n- Purpose: What kind of question the finding answers. Core uses `format` (is it well-formed OPF), `references` (does everything it points at resolve), `policy` (host house rules), `accessibility`, `layout` (will it present as authored) and `content` (is anything left unfinished). Other producers may use their own category names, such as `narrative` or `audience-fit`.\n\n_No named properties._\n\n\n### FindingLocation\n\n- Type: `object`\n- Required fields: `offset`, `length`, `line`, `column`\n- Purpose: A range in the checked text: UTF-16 offset and length from the start, and the one-based line and column of the start. Present when the finding was produced from text (JSON, Markdown or YAML).\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `offset` | yes | `integer` | |\n| `length` | yes | `integer` | |\n| `line` | yes | `integer` | |\n| `column` | yes | `integer` | |\n\n\n### JsonPatchOperation\n\n- Type: `oneOf:object / object / object`\n- Required fields: none\n- Purpose: One RFC 6902 JSON Patch operation. Paths are JSON Pointers into the checked presentation.\n\n_No named properties._\n\n\n### FindingFocus\n\n- Type: `object`\n- Required fields: `path`, `field`\n- Purpose: The field an author has to fill in, for a fix that cannot be written as a patch.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `path` | yes | `string` | JSON Pointer of the field to edit. |\n| `field` | yes | `string` | What the field is, such as `alt`, `title`, `text`, `link`, `language` or `fontSize`. |\n| `value` | no | `string \\| number \\| boolean \\| null \\| array \\| object` | The field's current value, when it has one. |\n\n\n### FindingFix\n\n- Type: `object`\n- Required fields: `title`\n- Purpose: A suggested repair. Core never applies it: the host shows it and applies it through its own undoable edit path. A `patch` fix carries JSON Patch operations; a `focus` fix only names the field the author has to fill in.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `title` | yes | `string` | What applying the fix does, in a few words. |\n| `id` | no | `string` | Stable id within one finding, such as `focus-alt` or `use-readable-color`. |\n| `kind` | no | `enum:patch \\| focus` | `patch` fixes carry `patch`; `focus` fixes carry `focus`. |\n| `safe` | no | `boolean` | True when applying the fix cannot change what the content says or how it looks beyond the repair, such as switching a failing colour to the one the theme chose. False fixes need a deliberate choice. |\n| `patch` | no | `array<ref:JsonPatchOperation>` | RFC 6902 JSON Patch operations that apply the fix to the checked presentation. |\n| `focus` | no | `ref:FindingFocus` | |\n\n\n### FindingSuggestion\n\n- Type: `object`\n- Required fields: `value`, `label`, `origin`\n- Purpose: A value the author could use instead, such as a catalog record that exists. Suggestions name records actually present in the checked context and never replace authored values silently.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `value` | yes | `string \\| number \\| boolean \\| null` | |\n| `label` | yes | `string` | |\n| `origin` | yes | `string` | Where the suggestion comes from: `schema`, `document` (a record the document embeds), `registered` (a catalog the host registered) or `contract`. |\n| `definition` | no | `string` | Where the suggested record or constraint is defined. |\n\n\n### ValidationIssue\n\n- Type: `object`\n- Required fields: `path`, `message`, `keyword`, `schemaPath`, `params`\n- Purpose: The raw schema or semantic issue behind a finding, kept with every union alternative so a host can inspect the exact constraint.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `path` | yes | `string` | |\n| `message` | yes | `string` | |\n| `keyword` | yes | `string` | The JSON Schema keyword that failed, or `opf` for a rule the schema cannot express. |\n| `schemaPath` | yes | `string` | |\n| `params` | yes | `object` | |\n\n\n### Finding\n\n- Type: `object`\n- Required fields: `ruleId`, `severity`, `category`, `path`, `message`\n- Purpose: One thing a checker found at one place in a presentation.\n\n| Field | Required | Type | Notes |\n| --- | --- | --- | --- |\n| `ruleId` | yes | `string` | Stable `<producer>/<rule>` id, such as `opf/text-contrast` for core or `pptx.dev/narrative-gap` for a hosted reviewer. Ids do not change between releases. |\n| `source` | no | `string` | The producer: `opf` (the default) for core, or the name of another tool such as `pptx.dev/review`. |\n| `severity` | yes | `ref:FindingSeverity` | |\n| `category` | yes | `ref:FindingCategory` | |\n| `path` | yes | `string` | JSON Pointer to the value the finding is about, such as `/slides/4/title`. Empty for the whole document, or for a value in a supplied context such as a loaded catalog. |\n| `scope` | no | `enum:document \\| context` | `document` (the default) when `path` points into the checked presentation, `context` when it points into host-supplied context such as loaded catalog records. |\n| `message` | yes | `string` | What is wrong, in a sentence. |\n| `help` | no | `string` | What to do about it. |\n| `slide` | no | `integer` | Zero-based index of the slide the finding belongs to. |\n| `slideId` | no | `string` | The id of that slide, when it has one. |\n| `location` | no | `ref:FindingLocation` | |\n| `definition` | no | `string` | A link to the rule's documentation or to the schema constraint or catalog record the finding is about. |\n| `lookup` | no | `array<string>` | Arguments for the `opf schema` or `opf catalog` command that shows the definition, as an argument list rather than a shell string. |\n| `suggestions` | no | `array<ref:FindingSuggestion>` | |\n| `measured` | no | `object` | The numbers behind the finding, such as `{ \"ratio\": 2.1, \"needed\": 4.5 }`. |\n| `fixes` | no | `array<ref:FindingFix>` | |\n| `validation` | no | `ref:ValidationIssue` | |\n"
104
104
  },
105
105
  {
106
106
  "slug": "font-fidelity",
@@ -220,7 +220,7 @@ var docsData = Object.freeze([
220
220
  "slug": "markdown",
221
221
  "file": "docs/markdown.md",
222
222
  "title": "Markdown and outlines",
223
- "markdown": '# Markdown and outlines\n\n`@openpresentation/opf/markdown` (RR-30) converts between OPF and a small, documented Markdown dialect, 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.md` or `deck.opf.md` to `deck.opf.json` and back; `opf from-md` and `opf to-md` before 0.18), and every command that reads or writes a deck understands a file ending `.opf.md` ([Markdown decks in every command](#markdown-decks-in-every-command)); `parse` and `stringify` read and write a deck in JSON, YAML or Markdown with one call, and `open`, `save` and `convert` of `@openpresentation/opf` do it for files in Node.\n\n- **JSON stays canonical.** The schema, the interchange form and every package describe the JSON. A `.opf.md` file is an authoring serialization of the same data, as a `.opf.yaml` file is ([OPF as YAML](yaml.md)).\n- **Write a deck as text.** YAML front matter holds the deck, `---` separates slides, `#` is the title, `##` the subtitle, and lists, quotes, tables, images, code, charts, metrics, timelines and speaker notes have their own syntax.\n- **Read a deck as text.** `toMarkdown` writes any valid OPF document as that dialect. What the dialect has no syntax for (a design, nested groups, a styled table cell) is embedded as YAML in a fenced block, so nothing is lost; or it is left out and reported, on request.\n- **Round trip.** The Markdown the writer produces converts back to the same deck and to itself, byte for byte. Markdown written by hand converts to a deck whose canonical Markdown is the same text, apart from layout the dialect treats as equivalent (the examples in `examples/markdown/` are canonical).\n- **Errors have places.** Findings use the shared [Finding format](finding-schema-reference.md) and carry the source offset, length, line and column of the Markdown that caused them, including OPF validation errors mapped back to the Markdown that produced the field.\n\n```js\nimport { fromMarkdown, toMarkdown } from "@openpresentation/opf/markdown";\n\nconst { presentation, valid, findings } = fromMarkdown(markdown);\nconst { markdown: text, report } = toMarkdown(presentation);\n```\n\n```sh\nopf convert deck.md deck.opf.json\nopf convert deck.opf.json deck.opf.md\nopf validate deck.opf.md\nopf convert deck.opf.md slides/deck.svg\n```\n\n## An example\n\n````md\n---\nname: Q3 Business Review\nlanguage: en-US\n---\n\n<!-- slide: id=cover layout=cover section=Overview -->\n# Q3 Business Review\n\n## Operations and growth\n\nNote: Two minutes on the agenda, then straight into the numbers.\n\n---\n\n# Revenue grew every quarter\n\n```chart column\nQuarter,Revenue\nQ1,12\nQ2,18\n```\n\n> We would rather spend a week on capacity than a month on an outage.\n> \u2014 Priya Raman, Head of Platform\n````\n\nconverts to\n\n```json\n{\n "name": "Q3 Business Review",\n "language": "en-US",\n "slides": [\n { "id": "cover", "layout": "cover", "section": "Overview", "title": "Q3 Business Review", "subtitle": "Operations and growth", "notes": "Two minutes on the agenda, then straight into the numbers." },\n {\n "title": "Revenue grew every quarter",\n "blocks": [\n { "chart": { "type": "column", "data": { "columns": ["Quarter", "Revenue"], "rows": [["Q1", 12], ["Q2", 18]] } } },\n { "quote": { "text": "We would rather spend a week on capacity than a month on an outage.", "attribution": "Priya Raman, Head of Platform" } }\n ]\n }\n ]\n}\n```\n\n[`examples/markdown/quarterly-review.opf.md`](../examples/markdown/quarterly-review.opf.md) uses every block kind. [`examples/markdown/outline.md`](../examples/markdown/outline.md) is a plain outline read with `split: "headings"`.\n\n## The dialect\n\n### Document\n\n| Part | Syntax | OPF |\n| --- | --- | --- |\n| Deck | YAML between a first line `---` and a closing `---` (or `...`) | Every deck property except `slides`: `name`, `description`, `language`, `design`, `variables`, `assets`, `extensions`, ... in the order written. JSON-compatible YAML only: no anchors, tags or duplicate keys. `slides` is not allowed |\n| Slides | Lines of three or more hyphens, outside fences, HTML comments and speaker notes\' fences | One slide per segment. Empty segments are skipped (a warning when between two slides); `<!-- slide -->` keeps an empty slide |\n| Outline | `split: "auto"` (the default) or `"headings"` (`--split headings`) | With no `---` line and two or more `# ` headings outside fences, comments and notes, each `# ` heading starts a slide, so a bare outline needs no `---`. `"headings"` cuts at every `# ` heading, even inside speaker notes; `"rules"` (`--split rules`) cuts only at `---`, so a second `# ` is a second title |\n\nA deck without front matter starts with its first slide, so a file never begins with `---` unless it opens the front matter. Line endings may be LF, CRLF or CR and a leading BOM is ignored. The converter adds nothing the Markdown does not say: no `$schema`, no `name` (pass `defaults: { name }` or `--title` for a fallback that the front matter overrides).\n\n### Slide\n\n| Part | Syntax | OPF |\n| --- | --- | --- |\n| Options | `<!-- slide: id=cover layout=cover section="Part 1" tag=NEW hidden beat=a beat=b type=chart -->` | `id`, `layout`, `section`, `tag`, `hidden` (`hidden=false` too), `beat` (repeat for several), `type`. Values are bare words or JSON strings. At most one per slide, anywhere in it; written first |\n| Title | `# Title` | `title`: a string, or `TextRun[]` when the line has [inline formatting](#inline-text). One per slide; closing `#` characters are dropped |\n| Subtitle | `## Subtitle` | `subtitle`, a string or `TextRun[]` like the title. One per slide |\n| Deeper headings | `### Text` | A bold paragraph, with a `heading-demoted` warning |\n| Notes | `Note: ...` or `Notes: ...` at the start of a line | `notes`: plain text, every following line of the slide verbatim (blank lines, `#`, `-` and fences included), ending at the next `---` |\n| Content | The blocks below, top to bottom | One block with no `id`/`type`/`region`: a root field (`text`, `items`, `chart`, ...). Otherwise `blocks` |\n| Embedded | A fenced `opf-slide` block of YAML | Extra slide fields merged in (`composition`, `design`, `extensions`, ...). A field set both here and in Markdown is an error |\n\n### Blocks\n\nBlocks are separated by blank lines; a fence, heading, quote, table, image line or comment also ends a paragraph or list without one. A comment `<!-- block: ... -->` directly before a block gives it options:\n\n| Option | Meaning |\n| --- | --- |\n| `id=...`, `type=...` | The block\'s `id` and explicit `type`. Such a block is always an entry in `blocks` |\n| `as=bullets` | Store a list as `bullets` instead of `items` |\n| `as=video` | Store an image line as `video` |\n| `region=top:left` | Put the block in that promoted region of the slide (`left`, `center+right`, `top+middle`, `middle+bottom:left+center+right`, ...) instead of the content |\n\n| Markdown | OPF field | Notes |\n| --- | --- | --- |\n| A paragraph | `text` | A soft line break is a space; a backslash at the end of a line is a hard break (`\\n`). Inline formatting becomes `TextRun[]`, otherwise a string |\n| `- item` (`*` and `+` too) | `items` | Nested by deeper indentation (two spaces when written) into `level`. An indented line ` : text` after an item is its `description`. A different marker character starts a new list (the writer alternates `-` and `*` for lists in a row). Numbered markers (`1.`) are read as bullets with a `numbered-list` warning: the dialect has no syntax for the `numbering` field of [numbered lists](numbered-lists.md), so numbers and styles are not kept (add `numbering` in an `opf-slide` or `opf-block` fence) |\n| `> text` and trailing dash lines | `quote` | `> \u2014 Name, Title` is the `attribution`, a second dash line the `source`. The rules (`\u2014`, `\u2013`, `--`, `~`, `-`; one short line; not a dashed list) are those of [content conversions](conversions.md), as is the code fence rule. A quote with a `role` or a `photo` has no Markdown form (a dash line holds a name and a title in one string, and a quote has no image syntax): it is written as an `opf-block`, which reads back exactly. Put `Name, Title` in the attribution line for a plain Markdown quote |\n| ```` ```lang title="file" ```` | `code` | `{ source, language, filename }`, or just the string when there is neither. Backtick or tilde fences, longer fences for code that contains fences |\n| `\\| a \\| b \\|` rows with a `\\| --- \\| --- \\|` row | `table` | The first row is `columns`; an all-empty first row means none. Cells are text with inline formatting, as in the [data import](data-import.md) (`007` and `2024` stay text); an empty cell is `null`. `\\|` is a pipe, `<br>` a line break. Alignment colons are ignored. Short and long rows warn. A table with an `alt` has no pipe-table syntax for it, so it is written as an `opf-block` and reads back exactly |\n| `![alt](src "title")` | `image` | `{ src, alt, title }`, or the string when there is only a source. `<src with spaces>` in angle brackets |\n| ```` ```metric ```` | `metric` | `key: value` lines: `value`, `label`, `description`, `unit`, `delta`, `trend`, `sentiment`. A plain decimal `value` or `delta` is a number; `"..."` is a JSON string; `unit: %` and `delta: +3 pts` are plain text |\n| ```` ```chart column ```` | `chart` | The word after `chart` is the chart `type`; an optional `alt="..."` after it is the chart\'s text alternative (`alt=""` marks it decorative; a quoted JSON string, as on the timeline fence). The body is CSV with a header row (the first column is the category labels and always text; in the other columns an unquoted decimal is a number, `true` and `false` are booleans, an empty field is `null` and a quoted field is text) or a JSON object holding `columns` and `rows` |\n| ```` ```timeline name="Roadmap" ```` | `timeline` | One event per line: `when \u2014 what` (spaced em dash), or the date forms of the conversions (`2026 Q1: Pilot`, `Jan - Kickoff`), or just `what`. A task-list prefix sets the event\'s `status`: `[x]` done, `[>]` current ("we are here"), `[ ]` planned. An indented line is the event\'s `description`. `name` and `description` go on the fence line |\n| ```` ```opf-block ```` | any block | A YAML `ContentPayload`: `id`, `type`, any content field, nested `blocks` groups |\n\n### Inline text\n\n| Syntax | Run |\n| --- | --- |\n| `**bold**`, `__bold__` | `bold` |\n| `*italic*`, `_italic_` | `italic` (`_` only at word edges: `snake_case` is text) |\n| `~~strike~~` | `strikethrough` |\n| `` `code` ``, ``` ``a ` b`` ``` | `code` (a backtick fence of any length; one space is trimmed from each side when both sides have one; the text is literal and other formatting wraps the span) |\n| `<u>`, `<sup>`, `<sub>` | `underline`, `superscript`, `subscript` |\n| `[text](url)`, `<https://...>` | `link` (a link title is ignored) |\n| `[text]{color=#B42318 size=24 font="Open Sans"}` | `color` (a hex colour, scheme slot or `var:name`), `fontSize`, `fontFamily`, `lang` (a BCP-47 tag); `bold italic underline strike sup sub` work inside the braces too |\n| `\\*` and any backslash before ASCII punctuation | The character itself |\n\nFlanking follows CommonMark, so `2*(3+4)*5` and `a * b` are text. A backtick that has no matching fence, or that is escaped with a backslash, is text. There is no raw HTML beyond the tags above and `<br>`, no entities (`&amp;` is literal) and no setext headings or indented code. The title, the subtitle and a quote\'s text keep inline formatting (a string when nothing is formatted, `TextRun[]` otherwise); a quote with formatted text and no attribution stays `{ text: [...] }`, because the string shorthand is for plain text. A quote\'s attribution and source are plain strings and drop formatting with a `formatting-dropped` warning; backticks in them stay literal characters. `cite` and `footnote` have no inline syntax: a heading or quote that carries one is written in an `opf-slide` fence. Image alt text, code, metric values and timeline lines are read as written, without inline formatting.\n\n## Writing OPF as Markdown\n\n`toMarkdown(presentation, { unsupported })` first validates the presentation (`OPFMarkdownError`, `code: "invalid-document"`, when it is not valid OPF). Every part is written in the dialect and **read back before it is kept**: a part whose text could not be read back as the same value (a table cell that is not text, a title that ends in ` #`, a title or quote run with `cite` or `footnote`, notes with a `---` line, formatting next to punctuation that CommonMark cannot open) takes the fallback instead of being written wrongly.\n\n| `unsupported` | Behaviour |\n| --- | --- |\n| `"embed"` (default) | The part is written as YAML: a block in an `opf-block` fence, slide fields (and any region that is not one plain block) in an `opf-slide` fence. The result converts back to the same deck |\n| `"drop"` | The part is left out and listed in `report.loss`. For Markdown to read, not to convert back |\n\nThe report is `{ lossless, native, embedded, loss }`: `native` is true when the whole document is plain dialect syntax, `embedded` lists the JSON Pointer and reason of each YAML part, `loss` is empty unless `unsupported` is `drop`. The 126 example decks need YAML for fewer than 5% of their slides (36 of 805): table cells that are numbers, booleans or styled, video assets with a `description`, nested groups, `href` runs.\n\nThe writer\'s canonical form: front matter, then slides joined by a blank line, `---` and a blank line; the options comment directly above what follows it; one blank line between blocks; two lists in a row alternate `-` and `*` so they stay two blocks. A round trip may collapse equivalent forms:\n\n- `blocks` with one plain block becomes the root field; a root with several content fields becomes `blocks` in key order.\n- Shorthand and object forms collapse to the shorter one: a metric with only `value`, a code object with only `source`, an image with only `src`, a quote with only `text`, a timeline array with no name, a list item with level 0 and no description.\n- Adjacent runs with the same formatting merge and `false` flags drop.\n\n## Findings\n\n`fromMarkdown(markdown, options)` never throws for malformed content. It returns the deck with the [validate](validate.md) report of it, `{ presentation, valid, findings, counts, schemaValid, checks }` (plus `template` and `unfilledVariables` for a deck with content variables), as `fromYaml` does; `presentation` is a best effort when `valid` is false and `schemaValid` is `null` when the check did not run (`validate: false`, or no slides). Each finding is a [Finding](finding-schema-reference.md) (`ruleId`, `severity`, `category` `format`, `path`, `scope`, `message`, `help`) plus `location` (`offset`, `length` in UTF-16 units, one-based `line` and `column`). Rule ids starting `markdown/` come from the Markdown; ids starting `opf/` are the [`validate`](validate.md) findings of the converted deck, located by the Markdown of the field they name. Options: `split` (`"rules"` or `"headings"`), `defaults`, `validate` (`true`, the default, checks `format` and `references`; a `ValidateOptions` object such as `{ only: ["content"] }` picks other rules; `false` skips the check).\n\nErrors: `front-matter`, `front-matter-not-mapping`, `front-matter-unterminated`, `front-matter-slides`, `no-slides`, `options-syntax`, `options-unknown-key`, `options-value`, `options-duplicate`, `options-orphan`, `options-trailing`, `options-embedded`, `comment-unterminated`, `duplicate-title`, `duplicate-subtitle`, `empty-heading`, `empty-quote`, `image-source`, `fence-unterminated`, `code-fence`, `chart-type`, `chart-attributes`, `chart-data`, `metric-block`, `timeline-attributes`, `timeline-description`, `timeline-events`, `opf-slide`, `opf-slide-not-mapping`, `opf-block`, `opf-block-not-mapping`, `slide-property-conflict`, `span-attributes`. Warnings: `front-matter-comments`, `empty-slide`, `heading-demoted`, `numbered-list`, `table-ragged`, `chart-ragged`, `formatting-dropped`.\n\n## Command line\n\n```text\nopf convert <deck.md|deck.opf.md|-> <output.opf.json|output.opf.yaml|-> [--from md] [--to json|yaml] [--split <auto|rules|headings>] [--title <text>] [--force] [--fail-on <level>]\nopf convert <deck.opf.json|-> <output.opf.md|-> [--to md] [--drop-unsupported] [--force] [--fail-on <level>]\n```\n\nA plain `.md` input is read as OPF Markdown (only on `opf convert`\'s input side; a Markdown output is named `.opf.md`). Output `-` writes stdout (with `--to`) and the JSON report goes to stderr. Exit codes follow the other commands: 0 success, 1 a deck that is not valid OPF, an output conflict or a `--fail-on` failure, 2 usage, a Markdown syntax error (with the located `findings`), or I/O. Nothing is written on a failure. A part the dialect has no syntax for is embedded and counted as a warning (`markdown/embedded`; `markdown/dropped` with `--drop-unsupported`), so `--fail-on warning` keeps a deck inside the plain dialect. `--split` and `--title` apply to a Markdown input and `--drop-unsupported` to a Markdown output; anywhere else they are usage errors. `opf from-md` and `opf to-md` were removed in 0.18.\n\n## Markdown decks in every command\n\nEvery command that reads a deck reads Markdown, through one reader, as it reads [YAML](yaml.md#yaml-everywhere):\n\n1. A file whose name ends `.opf.md` (in any case) is a Markdown deck. **Only `.opf.md` counts**: a plain `.md` file is never taken for a deck, so a README or notes file is never converted by accident (a plain `.md` needs `--from md`, or convert it with `opf convert notes.md deck.opf.json`, or name it `.opf.md`).\n2. Anything else (stdin and every other name) is JSON when the text starts with `{` or `[`; otherwise `--from md` (`markdown` is an alias; or `--from yaml`) says what it is, and without it the command exits 2 and asks for it. `--from` wins over a name.\n3. The Markdown is converted with the CLI\'s registered catalogs (the default catalog), so the references check of `validate` and `convert` resolves `name:id` references as it does for JSON. A Markdown syntax or conversion error exits **2**, like invalid JSON, and the message carries the position: `Invalid Markdown in deck.opf.md at line 1, column 1: The front matter starting on line 1 is not closed. [markdown/front-matter-unterminated]`, with the located `findings` and `code: "invalid-markdown"` in the JSON error report. `opf validate` instead reports it as a `markdown/<rule>` finding and exits 1. A deck that is valid Markdown but not valid OPF is reported by the command as it is for JSON, and `validate` and `convert` locate every finding in the Markdown (`deck.opf.md:3:1 error opf/schema ...`). A JSON Patch is JSON or YAML, never Markdown.\n\nA deck is written as Markdown when the output name ends `.opf.md` or `--to md` (or `--to markdown`) is given; `--to json` and `--to yaml` force the others. When the output is stdout or has another name, the format of the deck that was read is used: a Markdown deck is edited, merged, filled, paginated and embedded back to Markdown, in place (`-i`) or to stdout. `create` has no deck input and defaults to JSON. In `fill` and `ingest`, `--data-format csv|tsv|json` names the data format and `--to` the output.\n\n```sh\nopf edit deck.opf.md --patch changes.json -i # the file stays Markdown\nopf format deck.opf.md # canonical Markdown: toMarkdown(fromMarkdown(text))\nopf format deck.opf.md --check # exit 1 if the file would change\nopf convert deck.opf.json deck.opf.md # convert; - --to md for stdout\nopf paginate deck.opf.md deck.paginated.opf.md\nopf validate - --from md < deck.txt\n```\n\n`opf format` rewrites a Markdown deck to the canonical form the writer produces, so the layout the dialect treats as equivalent (`*` and `-` bullets, extra blank lines, spacing in front matter) is normalized. Anything the dialect has no syntax for is carried in an `opf-slide` or `opf-block` fence, so a command that rewrites a deck can move content into a fence that was not there: for example an `edit` that adds `design` to a slide. The CLI prints `warning: <file> has N more opf-slide/opf-block fences than before: ...` on stderr when that happens (it does not fail, nothing is lost, and a deck that already had its fences does not warn). The `<!-- -->` comments of the source are not preserved beyond the slide and block options the dialect reads.\n\nIn code, `parse(text, { format, filename, catalogs })` and `stringify(presentation, { format | filename })` (the root and `@openpresentation/opf/deck`) read and write the three forms, and in Node `@openpresentation/opf` reads and writes the files (`open`, `save`, `convert("deck.opf.json", "deck.opf.md")`): see [Reading and writing a deck in any form](validate.md#reading-a-deck-in-any-form).\n\n## Decisions and limits\n\nThese choices are the defaults of the module and can be changed.\n\n- **A dialect, not all of Markdown.** The block and inline syntax is the subset above with CommonMark\'s rules where they overlap. Setext headings, indented code, nested block quotes, raw HTML, reference links, footnotes and task lists are not part of it. Text the writer produces is escaped so it reads back as text.\n- **One YAML reader.** Front matter and the `opf-slide` and `opf-block` fences follow the same rules as a whole deck written as YAML ([OPF as YAML](yaml.md)) and use the `yaml` package (ISC, no dependencies, YAML 1.2 core schema: `2026-10-01` and `yes` stay strings). Metric blocks, timelines and chart CSV have small readers of their own because hand-written values such as `unit: %` are not valid YAML.\n- **Slide options live in HTML comments**, so any other Markdown viewer shows the slide content and ignores them.\n- **The pptx.dev Markdown view** targets the pre-v1 `elements` model: it starts a slide at each `##` heading and writes `{id=...}` attribute lines. This module keeps its front matter and its escape-hatch fences (`opf-slide`, `opf-block`) and replaces the rest, because the v1 slide is a title, a subtitle and content that maps one-to-one onto `#`, `##` and blocks.\n- **Templates.** A [template](templates-and-variables.md) is an ordinary deck here: `template: true` and `variables` are front matter, and placeholders such as `{{client}}` and `var:logo` fields are plain text. Convert, then fill with `opf fill`.\n- **Not yet in the dialect.** `numbering` on lists ([numbered lists](numbered-lists.md): the schema and the renderers have it, but the dialect does not read `1.` markers as numbering; they become bullets with a warning, and a `numbering` field in a deck is written in an `opf-slide` or `opf-block` fence and round trips), footnotes, citations and captions (RR-34), video with a description, and per-slide `design`, `composition` and nested groups (all of these embed as YAML and round trip).\n'
223
+ "markdown": '# Markdown and outlines\n\n`@openpresentation/opf/markdown` (RR-30) converts between OPF and a small, documented Markdown dialect, 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.md` or `deck.opf.md` to `deck.opf.json` and back; `opf from-md` and `opf to-md` before 0.18), and every command that reads or writes a deck understands a file ending `.opf.md` ([Markdown decks in every command](#markdown-decks-in-every-command)); `parse` and `stringify` read and write a deck in JSON, YAML or Markdown with one call, and `open`, `save` and `convert` of `@openpresentation/opf` do it for files in Node.\n\n- **JSON stays canonical.** The schema, the interchange form and every package describe the JSON. A `.opf.md` file is an authoring serialization of the same data, as a `.opf.yaml` file is ([OPF as YAML](yaml.md)).\n- **Write a deck as text.** YAML front matter holds the deck, `---` separates slides, `#` is the title, `##` the subtitle, and lists, quotes, tables, images, code, charts, metrics, timelines and speaker notes have their own syntax.\n- **Read a deck as text.** `toMarkdown` writes any valid OPF document as that dialect. What the dialect has no syntax for (a design, nested groups, a styled table cell) is embedded as YAML in a fenced block, so nothing is lost; or it is left out and reported, on request.\n- **Round trip.** The Markdown the writer produces converts back to the same deck and to itself, byte for byte. Markdown written by hand converts to a deck whose canonical Markdown is the same text, apart from layout the dialect treats as equivalent (the examples in `examples/markdown/` are canonical).\n- **Errors have places.** [Validation results](finding-schema-reference.md) carry the source offset, length, line and column of the Markdown that caused them, including OPF validation errors mapped back to the Markdown that produced the field.\n\n```js\nimport { fromMarkdown, toMarkdown } from "@openpresentation/opf/markdown";\n\nconst { presentation, valid, findings } = fromMarkdown(markdown);\nconst { markdown: text, report } = toMarkdown(presentation);\n```\n\n```sh\nopf convert deck.md deck.opf.json\nopf convert deck.opf.json deck.opf.md\nopf validate deck.opf.md\nopf convert deck.opf.md slides/deck.svg\n```\n\n## An example\n\n````md\n---\nname: Q3 Business Review\nlanguage: en-US\n---\n\n<!-- slide: id=cover layout=cover section=Overview -->\n# Q3 Business Review\n\n## Operations and growth\n\nNote: Two minutes on the agenda, then straight into the numbers.\n\n---\n\n# Revenue grew every quarter\n\n```chart column\nQuarter,Revenue\nQ1,12\nQ2,18\n```\n\n> We would rather spend a week on capacity than a month on an outage.\n> \u2014 Priya Raman, Head of Platform\n````\n\nconverts to\n\n```json\n{\n "name": "Q3 Business Review",\n "language": "en-US",\n "slides": [\n { "id": "cover", "layout": "cover", "section": "Overview", "title": "Q3 Business Review", "subtitle": "Operations and growth", "notes": "Two minutes on the agenda, then straight into the numbers." },\n {\n "title": "Revenue grew every quarter",\n "blocks": [\n { "chart": { "type": "column", "data": { "columns": ["Quarter", "Revenue"], "rows": [["Q1", 12], ["Q2", 18]] } } },\n { "quote": { "text": "We would rather spend a week on capacity than a month on an outage.", "attribution": "Priya Raman, Head of Platform" } }\n ]\n }\n ]\n}\n```\n\n[`examples/markdown/quarterly-review.opf.md`](../examples/markdown/quarterly-review.opf.md) uses every block kind. [`examples/markdown/outline.md`](../examples/markdown/outline.md) is a plain outline read with `split: "headings"`.\n\n## The dialect\n\n### Document\n\n| Part | Syntax | OPF |\n| --- | --- | --- |\n| Deck | YAML between a first line `---` and a closing `---` (or `...`) | Every deck property except `slides`: `name`, `description`, `language`, `design`, `variables`, `assets`, `extensions`, ... in the order written. JSON-compatible YAML only: no anchors, tags or duplicate keys. `slides` is not allowed |\n| Slides | Lines of three or more hyphens, outside fences, HTML comments and speaker notes\' fences | One slide per segment. Empty segments are skipped (a warning when between two slides); `<!-- slide -->` keeps an empty slide |\n| Outline | `split: "auto"` (the default) or `"headings"` (`--split headings`) | With no `---` line and two or more `# ` headings outside fences, comments and notes, each `# ` heading starts a slide, so a bare outline needs no `---`. `"headings"` cuts at every `# ` heading, even inside speaker notes; `"rules"` (`--split rules`) cuts only at `---`, so a second `# ` is a second title |\n\nA deck without front matter starts with its first slide, so a file never begins with `---` unless it opens the front matter. Line endings may be LF, CRLF or CR and a leading BOM is ignored. The converter adds nothing the Markdown does not say: no `$schema`, no `name` (pass `defaults: { name }` or `--title` for a fallback that the front matter overrides).\n\n### Slide\n\n| Part | Syntax | OPF |\n| --- | --- | --- |\n| Options | `<!-- slide: id=cover layout=cover section="Part 1" tag=NEW hidden beat=a beat=b type=chart -->` | `id`, `layout`, `section`, `tag`, `hidden` (`hidden=false` too), `beat` (repeat for several), `type`. Values are bare words or JSON strings. At most one per slide, anywhere in it; written first |\n| Title | `# Title` | `title`: a string, or `TextRun[]` when the line has [inline formatting](#inline-text). One per slide; closing `#` characters are dropped |\n| Subtitle | `## Subtitle` | `subtitle`, a string or `TextRun[]` like the title. One per slide |\n| Deeper headings | `### Text` | A bold paragraph, with a `heading-demoted` warning |\n| Notes | `Note: ...` or `Notes: ...` at the start of a line | `notes`: plain text, every following line of the slide verbatim (blank lines, `#`, `-` and fences included), ending at the next `---` |\n| Content | The blocks below, top to bottom | One block with no `id`/`type`/`region`: a root field (`text`, `items`, `chart`, ...). Otherwise `blocks` |\n| Embedded | A fenced `opf-slide` block of YAML | Extra slide fields merged in (`composition`, `design`, `extensions`, ...). A field set both here and in Markdown is an error |\n\n### Blocks\n\nBlocks are separated by blank lines; a fence, heading, quote, table, image line or comment also ends a paragraph or list without one. A comment `<!-- block: ... -->` directly before a block gives it options:\n\n| Option | Meaning |\n| --- | --- |\n| `id=...`, `type=...` | The block\'s `id` and explicit `type`. Such a block is always an entry in `blocks` |\n| `as=bullets` | Store a list as `bullets` instead of `items` |\n| `as=video` | Store an image line as `video` |\n| `region=top:left` | Put the block in that promoted region of the slide (`left`, `center+right`, `top+middle`, `middle+bottom:left+center+right`, ...) instead of the content |\n\n| Markdown | OPF field | Notes |\n| --- | --- | --- |\n| A paragraph | `text` | A soft line break is a space; a backslash at the end of a line is a hard break (`\\n`). Inline formatting becomes `TextRun[]`, otherwise a string |\n| `- item` (`*` and `+` too) | `items` | Nested by deeper indentation (two spaces when written) into `level`. An indented line ` : text` after an item is its `description`. A different marker character starts a new list (the writer alternates `-` and `*` for lists in a row). Numbered markers (`1.`) are read as bullets with a `numbered-list` warning: the dialect has no syntax for the `numbering` field of [numbered lists](numbered-lists.md), so numbers and styles are not kept (add `numbering` in an `opf-slide` or `opf-block` fence) |\n| `> text` and trailing dash lines | `quote` | `> \u2014 Name, Title` is the `attribution`, a second dash line the `source`. The rules (`\u2014`, `\u2013`, `--`, `~`, `-`; one short line; not a dashed list) are those of [content conversions](conversions.md), as is the code fence rule. A quote with a `role` or a `photo` has no Markdown form (a dash line holds a name and a title in one string, and a quote has no image syntax): it is written as an `opf-block`, which reads back exactly. Put `Name, Title` in the attribution line for a plain Markdown quote |\n| ```` ```lang title="file" ```` | `code` | `{ source, language, filename }`, or just the string when there is neither. Backtick or tilde fences, longer fences for code that contains fences |\n| `\\| a \\| b \\|` rows with a `\\| --- \\| --- \\|` row | `table` | The first row is `columns`; an all-empty first row means none. Cells are text with inline formatting, as in the [data import](data-import.md) (`007` and `2024` stay text); an empty cell is `null`. `\\|` is a pipe, `<br>` a line break. Alignment colons are ignored. Short and long rows warn. A table with an `alt` has no pipe-table syntax for it, so it is written as an `opf-block` and reads back exactly |\n| `![alt](src "title")` | `image` | `{ src, alt, title }`, or the string when there is only a source. `<src with spaces>` in angle brackets |\n| ```` ```metric ```` | `metric` | `key: value` lines: `value`, `label`, `description`, `unit`, `delta`, `trend`, `sentiment`. A plain decimal `value` or `delta` is a number; `"..."` is a JSON string; `unit: %` and `delta: +3 pts` are plain text |\n| ```` ```chart column ```` | `chart` | The word after `chart` is the chart `type`; an optional `alt="..."` after it is the chart\'s text alternative (`alt=""` marks it decorative; a quoted JSON string, as on the timeline fence). The body is CSV with a header row (the first column is the category labels and always text; in the other columns an unquoted decimal is a number, `true` and `false` are booleans, an empty field is `null` and a quoted field is text) or a JSON object holding `columns` and `rows` |\n| ```` ```timeline name="Roadmap" ```` | `timeline` | One event per line: `when \u2014 what` (spaced em dash), or the date forms of the conversions (`2026 Q1: Pilot`, `Jan - Kickoff`), or just `what`. A task-list prefix sets the event\'s `status`: `[x]` done, `[>]` current ("we are here"), `[ ]` planned. An indented line is the event\'s `description`. `name` and `description` go on the fence line |\n| ```` ```opf-block ```` | any block | A YAML `ContentPayload`: `id`, `type`, any content field, nested `blocks` groups |\n\n### Inline text\n\n| Syntax | Run |\n| --- | --- |\n| `**bold**`, `__bold__` | `bold` |\n| `*italic*`, `_italic_` | `italic` (`_` only at word edges: `snake_case` is text) |\n| `~~strike~~` | `strikethrough` |\n| `` `code` ``, ``` ``a ` b`` ``` | `code` (a backtick fence of any length; one space is trimmed from each side when both sides have one; the text is literal and other formatting wraps the span) |\n| `<u>`, `<sup>`, `<sub>` | `underline`, `superscript`, `subscript` |\n| `[text](url)`, `<https://...>` | `link` (a link title is ignored) |\n| `[text]{color=#B42318 size=24 font="Open Sans"}` | `color` (a hex colour, scheme slot or `var:name`), `fontSize`, `fontFamily`, `lang` (a BCP-47 tag); `bold italic underline strike sup sub` work inside the braces too |\n| `\\*` and any backslash before ASCII punctuation | The character itself |\n\nFlanking follows CommonMark, so `2*(3+4)*5` and `a * b` are text. A backtick that has no matching fence, or that is escaped with a backslash, is text. There is no raw HTML beyond the tags above and `<br>`, no entities (`&amp;` is literal) and no setext headings or indented code. The title, the subtitle and a quote\'s text keep inline formatting (a string when nothing is formatted, `TextRun[]` otherwise); a quote with formatted text and no attribution stays `{ text: [...] }`, because the string shorthand is for plain text. A quote\'s attribution and source are plain strings and drop formatting with a `formatting-dropped` warning; backticks in them stay literal characters. `cite` and `footnote` have no inline syntax: a heading or quote that carries one is written in an `opf-slide` fence. Image alt text, code, metric values and timeline lines are read as written, without inline formatting.\n\n## Writing OPF as Markdown\n\n`toMarkdown(presentation, { unsupported })` first validates the presentation (`OPFMarkdownError`, `code: "invalid-document"`, when it is not valid OPF). Every part is written in the dialect and **read back before it is kept**: a part whose text could not be read back as the same value (a table cell that is not text, a title that ends in ` #`, a title or quote run with `cite` or `footnote`, notes with a `---` line, formatting next to punctuation that CommonMark cannot open) takes the fallback instead of being written wrongly.\n\n| `unsupported` | Behaviour |\n| --- | --- |\n| `"embed"` (default) | The part is written as YAML: a block in an `opf-block` fence, slide fields (and any region that is not one plain block) in an `opf-slide` fence. The result converts back to the same deck |\n| `"drop"` | The part is left out and listed in `report.loss`. For Markdown to read, not to convert back |\n\nThe report is `{ lossless, native, embedded, loss }`: `native` is true when the whole document is plain dialect syntax, `embedded` lists the JSON Pointer and reason of each YAML part, `loss` is empty unless `unsupported` is `drop`. The 126 example decks need YAML for fewer than 5% of their slides (36 of 805): table cells that are numbers, booleans or styled, video assets with a `description`, nested groups, `href` runs.\n\nThe writer\'s canonical form: front matter, then slides joined by a blank line, `---` and a blank line; the options comment directly above what follows it; one blank line between blocks; two lists in a row alternate `-` and `*` so they stay two blocks. A round trip may collapse equivalent forms:\n\n- `blocks` with one plain block becomes the root field; a root with several content fields becomes `blocks` in key order.\n- Shorthand and object forms collapse to the shorter one: a metric with only `value`, a code object with only `source`, an image with only `src`, a quote with only `text`, a timeline array with no name, a list item with level 0 and no description.\n- Adjacent runs with the same formatting merge and `false` flags drop.\n\n## Findings\n\n`fromMarkdown(markdown, options)` never throws for malformed content. It returns the deck with the [validate](validate.md) report of it, `{ presentation, valid, findings, counts, schemaValid, checks }` (plus `template` and `unfilledVariables` for a deck with content variables), as `fromYaml` does; `presentation` is a best effort when `valid` is false and `schemaValid` is `null` when the check did not run (`validate: false`, or no slides). Each finding is a [Finding](finding-schema-reference.md) (`ruleId`, `severity`, `category` `format`, `path`, `scope`, `message`, `help`) plus `location` (`offset`, `length` in UTF-16 units, one-based `line` and `column`). Rule ids starting `markdown/` come from the Markdown; ids starting `opf/` are the [`validate`](validate.md) findings of the converted deck, located by the Markdown of the field they name. Options: `split` (`"rules"` or `"headings"`), `defaults`, `validate` (`true`, the default, checks `format` and `references`; a `ValidateOptions` object such as `{ only: ["content"] }` picks other rules; `false` skips the check).\n\nErrors: `front-matter`, `front-matter-not-mapping`, `front-matter-unterminated`, `front-matter-slides`, `no-slides`, `options-syntax`, `options-unknown-key`, `options-value`, `options-duplicate`, `options-orphan`, `options-trailing`, `options-embedded`, `comment-unterminated`, `duplicate-title`, `duplicate-subtitle`, `empty-heading`, `empty-quote`, `image-source`, `fence-unterminated`, `code-fence`, `chart-type`, `chart-attributes`, `chart-data`, `metric-block`, `timeline-attributes`, `timeline-description`, `timeline-events`, `opf-slide`, `opf-slide-not-mapping`, `opf-block`, `opf-block-not-mapping`, `slide-property-conflict`, `span-attributes`. Warnings: `front-matter-comments`, `empty-slide`, `heading-demoted`, `numbered-list`, `table-ragged`, `chart-ragged`, `formatting-dropped`.\n\n## Command line\n\n```text\nopf convert <deck.md|deck.opf.md|-> <output.opf.json|output.opf.yaml|-> [--from md] [--to json|yaml] [--split <auto|rules|headings>] [--title <text>] [--force] [--fail-on <level>]\nopf convert <deck.opf.json|-> <output.opf.md|-> [--to md] [--drop-unsupported] [--force] [--fail-on <level>]\n```\n\nA plain `.md` input is read as OPF Markdown (only on `opf convert`\'s input side; a Markdown output is named `.opf.md`). Output `-` writes stdout (with `--to`) and the JSON report goes to stderr. Exit codes follow the other commands: 0 success, 1 a deck that is not valid OPF, an output conflict or a `--fail-on` failure, 2 usage, a Markdown syntax error (with the located `findings`), or I/O. Nothing is written on a failure. A part the dialect has no syntax for is embedded and counted as a warning (`markdown/embedded`; `markdown/dropped` with `--drop-unsupported`), so `--fail-on warning` keeps a deck inside the plain dialect. `--split` and `--title` apply to a Markdown input and `--drop-unsupported` to a Markdown output; anywhere else they are usage errors. `opf from-md` and `opf to-md` were removed in 0.18.\n\n## Markdown decks in every command\n\nEvery command that reads a deck reads Markdown, through one reader, as it reads [YAML](yaml.md#yaml-everywhere):\n\n1. A file whose name ends `.opf.md` (in any case) is a Markdown deck. **Only `.opf.md` counts**: a plain `.md` file is never taken for a deck, so a README or notes file is never converted by accident (a plain `.md` needs `--from md`, or convert it with `opf convert notes.md deck.opf.json`, or name it `.opf.md`).\n2. Anything else (stdin and every other name) is JSON when the text starts with `{` or `[`; otherwise `--from md` (`markdown` is an alias; or `--from yaml`) says what it is, and without it the command exits 2 and asks for it. `--from` wins over a name.\n3. The Markdown is converted with the CLI\'s registered catalogs (the default catalog), so the references check of `validate` and `convert` resolves `name:id` references as it does for JSON. A Markdown syntax or conversion error exits **2**, like invalid JSON, and the message carries the position: `Invalid Markdown in deck.opf.md at line 1, column 1: The front matter starting on line 1 is not closed. [markdown/front-matter-unterminated]`, with the located `findings` and `code: "invalid-markdown"` in the JSON error report. `opf validate` instead reports it as a `markdown/<rule>` finding and exits 1. A deck that is valid Markdown but not valid OPF is reported by the command as it is for JSON, and `validate` and `convert` locate every finding in the Markdown (`deck.opf.md:3:1 error opf/schema ...`). A JSON Patch is JSON or YAML, never Markdown.\n\nA deck is written as Markdown when the output name ends `.opf.md` or `--to md` (or `--to markdown`) is given; `--to json` and `--to yaml` force the others. When the output is stdout or has another name, the format of the deck that was read is used: a Markdown deck is edited, merged, filled, paginated and embedded back to Markdown, in place (`-i`) or to stdout. `create` has no deck input and defaults to JSON. In `fill` and `ingest`, `--data-format csv|tsv|json` names the data format and `--to` the output.\n\n```sh\nopf edit deck.opf.md --patch changes.json -i # the file stays Markdown\nopf format deck.opf.md # canonical Markdown: toMarkdown(fromMarkdown(text))\nopf format deck.opf.md --check # exit 1 if the file would change\nopf convert deck.opf.json deck.opf.md # convert; - --to md for stdout\nopf paginate deck.opf.md deck.paginated.opf.md\nopf validate - --from md < deck.txt\n```\n\n`opf format` rewrites a Markdown deck to the canonical form the writer produces, so the layout the dialect treats as equivalent (`*` and `-` bullets, extra blank lines, spacing in front matter) is normalized. Anything the dialect has no syntax for is carried in an `opf-slide` or `opf-block` fence, so a command that rewrites a deck can move content into a fence that was not there: for example an `edit` that adds `design` to a slide. The CLI prints `warning: <file> has N more opf-slide/opf-block fences than before: ...` on stderr when that happens (it does not fail, nothing is lost, and a deck that already had its fences does not warn). The `<!-- -->` comments of the source are not preserved beyond the slide and block options the dialect reads.\n\nIn code, `parse(text, { format, filename, catalogs })` and `stringify(presentation, { format | filename })` (the root and `@openpresentation/opf/deck`) read and write the three forms, and in Node `@openpresentation/opf` reads and writes the files (`open`, `save`, `convert("deck.opf.json", "deck.opf.md")`): see [Reading and writing a deck in any form](validate.md#reading-a-deck-in-any-form).\n\n## Decisions and limits\n\nThese choices are the defaults of the module and can be changed.\n\n- **A dialect, not all of Markdown.** The block and inline syntax is the subset above with CommonMark\'s rules where they overlap. Setext headings, indented code, nested block quotes, raw HTML, reference links, footnotes and task lists are not part of it. Text the writer produces is escaped so it reads back as text.\n- **One YAML reader.** Front matter and the `opf-slide` and `opf-block` fences follow the same rules as a whole deck written as YAML ([OPF as YAML](yaml.md)) and use the `yaml` package (ISC, no dependencies, YAML 1.2 core schema: `2026-10-01` and `yes` stay strings). Metric blocks, timelines and chart CSV have small readers of their own because hand-written values such as `unit: %` are not valid YAML.\n- **Slide options live in HTML comments**, so any other Markdown viewer shows the slide content and ignores them.\n- **The pptx.dev Markdown view** targets the pre-v1 `elements` model: it starts a slide at each `##` heading and writes `{id=...}` attribute lines. This module keeps its front matter and its escape-hatch fences (`opf-slide`, `opf-block`) and replaces the rest, because the v1 slide is a title, a subtitle and content that maps one-to-one onto `#`, `##` and blocks.\n- **Templates.** A [template](templates-and-variables.md) is an ordinary deck here: `template: true` and `variables` are front matter, and placeholders such as `{{client}}` and `var:logo` fields are plain text. Convert, then fill with `opf fill`.\n- **Not yet in the dialect.** `numbering` on lists ([numbered lists](numbered-lists.md): the schema and the renderers have it, but the dialect does not read `1.` markers as numbering; they become bullets with a warning, and a `numbering` field in a deck is written in an `opf-slide` or `opf-block` fence and round trips), footnotes, citations and captions (RR-34), video with a description, and per-slide `design`, `composition` and nested groups (all of these embed as YAML and round trip).\n'
224
224
  },
225
225
  {
226
226
  "slug": "native-content-release-2026-09-09",
@@ -346,7 +346,7 @@ var docsData = Object.freeze([
346
346
  "slug": "validate",
347
347
  "file": "docs/validate.md",
348
348
  "title": "OPF validate: one checker for format, references, accessibility, layout and content",
349
- "markdown": "# OPF validate: one checker for format, references, accessibility, layout and content\n\n`validate` in `@openpresentation/opf` and `opf validate` in the CLI check an OPF presentation for everything a tool can tell without an opinion: whether it is well-formed OPF, whether everything it points at resolves, whether a host's house rules hold, and the problems a schema cannot see, such as text that is hard to read, text that does not fit, pictures without alt text, content read out of order, fonts outside the deck's scheme, placeholder text and slides left empty. It is the only checker. Releases before 0.14 had several (a schema validator, a lint and an audit, in the library and the CLI); 0.14 replaces them all with `validate`.\n\n`validate` is read-only, local and deterministic. It fetches nothing (no images, fonts or catalogs), consults no clock and calls no model. The same document, options and fonts give byte-identical findings.\n\n```sh\nopf validate deck.opf.json # everything, JSON report\nopf validate deck.opf.json --format text # one line per finding\nopf validate deck.opf.json --only format,references --fail-on warning\nopf validate deck.opf.json --ignore layout --config house-rules.json\nopf validate --list-rules\n```\n\n```js\nimport { validate } from '@openpresentation/opf';\n\nconst report = validate(presentation); // a parsed presentation\nconst fromText = validate(fileText); // strict JSON text: also syntax, duplicate keys and line/column\nvalidate(presentation, { only: ['format'] }); // the cheap check: schema and semantic rules only\nvalidate(presentation, { ignore: ['layout'] }); // everything except the checks that build slide layouts\nvalidate(presentation, { contracts, severity: { 'opf/text-contrast': 'error' } }); // house rules\n```\n\n## Categories\n\nEvery finding has one of six categories. They say what kind of question the finding answers, and they select what runs.\n\n| Category | Asks | Errors by default | Examples |\n| --- | --- | --- | --- |\n| `format` | Is it well-formed OPF? | yes | JSON syntax, duplicate keys, schema, a citation of an unknown reference, an unknown dataset, a required variable with no value (a warning in a template), a `name:id` prefix that names no catalog group |\n| `references` | Does everything it points at resolve? | missing or circular asset, invalid catalog record; a reference that resolves nowhere and an unused dataset are warnings | `asset:` references, catalog references, citations and datasets nobody uses |\n| `policy` | Does it follow the host's house rules? | the host sets it (default `error`) | \"only these layouts\", brand fonts: the `contracts` option |\n| `accessibility` | Can everyone read it? | no: warning and info | contrast, alt text, slide titles, reading order, link text, language |\n| `layout` | Will it present as authored? | no: warning | overflow, minimum type size, image resolution, fonts outside the scheme |\n| `content` | Is anything left unfinished? | no: warning and info | placeholder text, an empty slide, a chart value that is text |\n\n`valid` keeps meaning \"correct OPF\": it is true when no finding has severity `error`, and by default only `format`, `references` and `policy` can produce one. A low-contrast deck is still a valid deck. A team promotes the rules it cares about with `severity`.\n\nEvery rule is listed, with its category, default severity, cost and the basis it rests on, in [Rules](#rules) below. A rule stays in core only when it has a measurable basis (a standard such as WCAG 2.2, a print-preflight check, or a number computed from the layout); judgments that need an audience and a purpose, such as how many fonts or words a deck should have, are not rules here.\n\n## Input and report\n\n`validate(input, options?)` takes a parsed presentation (an object) or **strict JSON text**. A string is read as JSON and nothing else: it is never sniffed for YAML or Markdown. When the text begins like YAML (`key: value`, `---`) or Markdown (`#`), the `help` of the syntax finding says to read it with `fromYaml` or `fromMarkdown` first, which return a presentation and call `validate` for you.\n\nWith text, a syntax error or a duplicate key comes back with `location` (original-source UTF-16 offset and length, one-based line and column), and so does every finding about a value in the document; a finding about a missing field is located at the object that lacks it. Text that is not JSON reports its syntax errors (`opf/json-syntax`) whatever `only` says, because nothing else can run.\n\n### Reading a deck in any form\n\n`parse(text, { format, filename, catalogs })` (the root and `@openpresentation/opf/deck`, RR-60, RR-62) reads a deck from JSON, YAML or Markdown text and returns the presentation; a syntax, schema or reference error throws `OPFValidationError`, whose `report` is the report `validate` gives the text. The format is the `format` option (`\"json\"`, `\"yaml\"` or `\"markdown\"`), else the `filename` (`.opf.md` is Markdown, `.yaml` and `.yml` are YAML, anything else is JSON), and never the content: a plain `.md` name is not a deck. Findings are located in the text that was read: `opf/json-syntax` (line and column) for JSON, `yaml/<rule>` for YAML and `markdown/<rule>` for Markdown, and every `opf/` finding at the field it names. `parse` checks format and references; warnings do not throw, so call `validate` for the full report (`validate(text)` for JSON text; `fromYaml` and `fromMarkdown` return it for the other forms). It throws a `TypeError` for a text that is not a string, an unknown format or a bad option. `stringify(presentation, { format | filename })` is the other direction: JSON as two-space JSON, YAML and Markdown in canonical form. Both are browser-safe; `open` and `save` of `@openpresentation/opf` do the same for files in Node. See [OPF as YAML](yaml.md) and [Markdown and outlines](markdown.md).\n\nThe report is a [`FindingReport`](finding-schema-reference.md) (`spec/schemas/finding.schema.json`) with a few more fields:\n\n| Field | Meaning |\n| --- | --- |\n| `valid` | No finding has severity `error` |\n| `findings` | Every finding, ordered by slide (document-level findings first), then category, then rule |\n| `counts` | `{ error, warning, info }` |\n| `schemaValid` | The document passes the schema and the semantic format checks; `null` for text that is not JSON. A required variable with no value does not make a document schema-invalid |\n| `checks` | What ran and what was not measured (below) |\n| `template`, `unfilledVariables` | Documents that declare content variables or are templates: whether they were checked as a template, and which required variables have no value |\n\nA finding has a stable `ruleId` (`opf/text-contrast`), a `severity` (`error`, `warning`, `info`), a `category`, a JSON Pointer `path`, a `message`, a `help` sentence and a `definition` link to the rule below or to the schema constraint. It may carry `slide` and `slideId` (zero-based index and id of the slide it belongs to), `location` (from text), `measured` (the numbers behind it: ratios, sizes, counts), `suggestions` (catalog records that exist, never silently applied), `lookup` (arguments for `opf schema` or `opf catalog`, not a shell string), `validation` (the raw schema issue, with every union alternative) and `fixes`.\n\n`checks` says what each part did: `syntax` (`checked` for text), `schema`, `references`, `policy`, `accessibility` and `content` are `checked` or `not-run`; `layout` is `not-run`, `estimated` (text widths are core's portable estimate) or `measured` (the host passed `fonts.textMeasurement`); background picture pixels are never read (`backgroundPixels`), image resolution reads only embedded `data:` images (`imageBytes`), and native PowerPoint rendering is not checked (`nativeExport`).\n\nA document the schema rejects is not audited: its accessibility, layout and content rules do not run, and its format errors are reported even when `only` did not ask for them, so the report says why nothing else ran (switch them off with `ignore: ['format']`).\n\n## Options\n\n```js\nvalidate(presentation, {\n only: ['format', 'opf/missing-alt-text'], // run just these rules or categories\n ignore: ['layout'], // do not run these\n severity: { 'opf/text-contrast': 'error', content: 'off' }, // per rule or category: error, warning, info or off\n ignorePaths: [{ rule: 'opf/text-contrast', path: '/slides/3' }], // an accepted exception, by JSON Pointer prefix\n catalogs: [gallery, acmeCatalog], // the catalogs the host registered (the first is the default); nothing is fetched\n contracts, // host policy (below)\n thresholds: { contrastNormal: 7 }, // see DEFAULT_VALIDATION_THRESHOLDS\n fonts, // the host's fonts: layout rules read fonts.textMeasurement\n chartPalette: ['#0072B2', '#E69F00'], // the engine's chart colours\n template: true, values: { client: 'Acme' }, // check as a template, or fill variables first\n});\n```\n\n- **Names.** `only`, `ignore`, `severity` and `ignorePaths` take full rule ids (`opf/text-contrast`), bare rule names (`text-contrast`) or category names (`accessibility`). No rule is named like a category. A rule beats its category in `severity`. An unknown rule, category, threshold or option throws a `TypeError`, so a typo cannot silently disable a check.\n- **`fonts`.** An object with a `textMeasurement` (the renderer's `loadFonts()` result has one). Without it the layout rules use core's portable estimate and `checks.layout` says `estimated`; with it, `measured`. `textMeasurement` may also be a function of the zero-based slide index, for a script-aware measurement per slide. When the measurement has a `forScripts(profile)` method (a script planner), the layout rules ask it for each slide's script-aware measurement, built from the slide's `resolveScriptFonts` profile and a run language's own profile, the way the renderer and the exporter measure, so Arabic or Japanese text is measured in its script face rather than the Latin face its style names.\n- **`template` and `values`.** A deck that declares content variables is checked as the deck it resolves to; see [templates and variables](templates-and-variables.md).\n- **Thresholds.** `DEFAULT_VALIDATION_THRESHOLDS` holds `contrastNormal`, `contrastLarge`, `minFontSizePt`, `minImagePpi` and `minSeriesColorDifference`; each rule below lists the ones it reads.\n\n`validationRules` lists every rule with its metadata (`id`, `name`, `category`, `severity`, `cost`, `summary`, `rationale`, `standard`, `approximations`, `thresholds`), and `findValidationRule(name)` finds one by id or bare name.\n\n`assertValid(value, options?)` throws `OPFValidationError`, carrying the `report` and its error `findings`, unless `validate(value, options)` is valid, and tells TypeScript that `value` is a `Presentation`. It takes a parsed value, not text.\n\n`validateCatalogRecord(kind, record)` and `assertValidCatalogRecord(kind, record)` check one catalog record (an audience, theme, layout, font scheme and so on) against its schema. The report has the same shape; its findings are `format`, and a record that links to an unknown id only warns.\n\n## Pass `fonts`, otherwise the estimate\n\nThe layout rules (`opf/text-overflow`, `opf/min-font-size`, reading order, image resolution) depend on how wide the text is. `validate` measures it with `options.fonts`, the handle that opf-render's `loadFonts()` returns:\n\n```js\nconst fonts = await loadFonts({ pack: 'office' });\nconst report = validate(deck, { fonts }); // report.checks.layout === 'measured'\n```\n\nPass the same `fonts` handle to preview (`toSvg`), pagination (`paginate`) and export (`toPng`, `toPdf`, `toPptx`), so that validation measures with the faces the preview draws and a finding means what the slide shows. The same handle gives identical geometry across engines; opf-pptx's `layout-parity` test checks that the exported PowerPoint paragraphs agree with the SVG preview and with core's composed items, with and without `fonts`. See [Pass `fonts`, otherwise the estimate](font-fidelity.md#pass-fonts-otherwise-the-estimate) for the full list.\n\nWithout `fonts` the rules use core's built-in estimate and `checks.layout` says `estimated`: base widths are 0.54 em per character, 0.62 em for capitals and digits, 0.32 em for a space and 1 em for CJK characters. Spacing combining marks outside Latin/CJK contribute; nonspacing/enclosing marks do not. Robust script factors calibrated against the FF-44 corpus reduce underestimation for other scripts; see [script width evidence](evidence/script-width-estimates/README.md). It needs no font files and is stable from run to run, but an estimate can be wider or narrower than a particular face, so an estimated overflow finding (or its absence) is a first pass. `opf validate` on the command line loads no fonts, so its layout checks are estimated too. Node `convert` and the CLI's `opf convert` and `opf paginate` prepare an office-pack handle for you; a library call to `validate` never loads fonts itself.\n\n## Cost\n\n`validate` is lazy: it builds only what the rules you ask for need. Measured with Node 24 on three bundled example decks (milliseconds per call, warm; your machine will differ, the ratios hold):\n\n| Call | `minimal` (3 slides) | `renewal-save-plan` (6 slides, 34 KB) | `broadband-public-hearing-deck` (10 slides, 41 KB) |\n| --- | --- | --- | --- |\n| `only: ['format']` | 0.06 | 0.23 | 0.64 |\n| `only: ['format', 'references']` | 0.7 | 5.2 | 7.6 |\n| everything (the default) | 1.7 | 24 | 32 |\n\n`only: ['format']` costs what the schema check alone costs; it builds no layout. The references walk reads the schema alongside the document and costs about ten times as much. The rules of `layout` (and `opf/text-contrast`, `opf/text-on-image` and `opf/reading-order`) compose every slide and are most of the cost of a full run. Each rule's `cost` says which it needs: `syntax` reads the JSON text, `structure` reads the document alone, `composition` builds slide layouts.\n\nThe default runs everything, so an agent or CI job never gets \"valid\" for a deck that overflows, and the hot paths say what they need: pagination, conversion, patch validation and the editor's per-edit check use `{ only: ['format'] }`; the CLI's write, render, export and import commands use `{ only: ['format', 'references'] }`, so a contrast warning never blocks an export.\n\n## Quick fixes\n\nA fix is a suggestion that core never applies. A host such as the editor's Review panel shows it and applies it through its own undoable edit path.\n\n- A `patch` fix (`kind: \"patch\"`, the default) carries RFC 6902 JSON Patch operations in `patch` (`add`, `remove`, `replace`, `move`, `copy`, `test`, with JSON Pointer paths). `safe: true` means the change cannot alter what the content says: for example switching a failing text colour to the colour the theme chose for that background.\n- A `focus` fix (`kind: \"focus\"`) names the field the author must fill in (`focus.path`, `focus.field`: `alt`, `title`, `text`, `link`, `language`, `fontSize`).\n- `safe: false` patches change meaning or appearance and need a deliberate click, for example marking a picture decorative with an empty `alt`.\n\n`title` says what applying the fix does.\n\n## Catalog context and contracts\n\nContent references resolve the way every engine resolves them ([catalogs](default-catalog.md)): `catalogs.custom`, then the records embedded under `catalogs.default`, then the catalog the host registered (the `catalogs` option) for its source; `name:id` in `catalogs.<name>`, then the catalog registered for its source. A reference that resolves nowhere is a warning (`opf/unresolved-reference`), never an error; a prefix that names no group is a format error (`opf/undeclared-catalog`): it can never resolve, so engines reject the document at their format check (`validate(document, { only: [\"format\"] })`) instead of drawing a fallback. A `catalogs` option that is not an array of registered catalogs throws `OPFCatalogsOptionError` (`code: \"invalid-catalogs\"`) at every entry point. Nothing is fetched, and without the `catalogs` option only the records the document embeds resolve. Free-form audience and purpose descriptions (any string that is not a bare id or `name:id`) and arbitrary `extensions` data are never references; the `language` tag, `chart.type` and the `socials` keys are engine vocabularies the schema checks. Embedded records, and registered records the document uses, are validated against their companion schema (`opf/catalog-record`). A record under `catalogs.default` or a named group that the catalog registered for the group's source does not publish is `opf/catalog-record-not-in-source` (move it to `catalogs.custom`); with no catalog registered for that source the check is silent. Suggestions name records actually present in the checked context (`origin` `document` or `registered`).\n\nAn `asset:` reference that names no entry of the document's `assets` registry is an error (`opf/asset-reference`), and so is a cycle of `asset:` sources (`opf/asset-cycle`); resource bytes are never fetched. Chart and table data findings keep their code as the rule id (`opf/dataset-unknown`, `opf/chart-value-not-numeric`, ...), and a `opf/chart-value-not-numeric` cell whose column is written in one display style (`\"12%\"`, `\"$1,234\"`) carries `fixes` ([chart and table data](chart-table-data.md#migration-help)). Citation and caption errors keep theirs too (`opf/cite-unknown-reference`, `opf/reference-id-duplicate`, `opf/cite-unsupported-location`, `opf/caption-unsupported-payload`).\n\nA narrative the document defines is a record in `catalogs.custom.narratives`, and `narrative` is always a reference string. When the narrative resolves, a `slides[].beat` id it does not define is `opf/unknown-beat`, a root `duration` outside the record's `duration` range is `opf/duration-outside-narrative`, an embedded record whose `duration.min` exceeds `duration.max` is `opf/narrative-duration-range`, and a beat no slide references is `opf/unused-beat` (only once some slide names a beat). A slide-level `design.theme` whose resolved dimensions differ from the deck's is `opf/slide-theme-dimensions`: a PPTX has one slide size, and a slide's `design` cannot set `dimensions` at all (a schema error). An undeclared `var:<id>` in a solid, gradient-stop or pattern background colour warns like any other colour reference (`opf/variable-reference-unknown`).\n\nHost policy comes from explicit options, never from the document: fields under `extensions` are data and cannot install policy. `contracts` and `catalogs` may live in a local JSON file for the CLI (`--config`).\n\n```json\n{\n \"catalogs\": [\n {\n \"source\": \"pkg:@partner/opf-catalog\",\n \"layouts\": {\n \"partner-title\": {\"name\":\"Partner title\",\"areas\":[[\"title\"]]}\n }\n }\n ],\n \"contracts\": [\n {\n \"path\":\"/slides/*/layout\",\n \"allowedValues\":[\"partner-title\",\"text\"],\n \"message\":\"Use the brand layouts {{allowed}} at {{path}}. See {{file}}.\",\n \"documentation\":\"brand-guide.md#layouts\",\n \"severity\":\"error\"\n }\n ]\n}\n```\n\nContract paths are JSON Pointer patterns: `~0` escapes `~`, `~1` escapes `/`, and a whole `*` segment matches one property or array index. Contracts check existing fields; they do not require an omitted field or insert defaults. Allowed values are JSON primitives. Optional message placeholders are `{{path}}`, `{{value}}`, `{{allowed}}`, and `{{file}}`. A contract's severity is `error` unless it says otherwise. Invalid or misspelled configuration keys fail instead of being ignored. Messages and catalog labels are data, not executable instructions.\n\n## The CLI\n\n`opf validate <file|-> [--config <file>] [--only <list>] [--ignore <list>] [--fail-on <error|warning|info>] [--format <json|text>] [--list-rules]`\n\nThe JSON report (the default) is the `validate` report plus the source's SHA-256 (`sha256`), the installed core version (`opfVersion`) and, with `--config`, the configuration file's path and hash (`context`). `--format text` prints one line per finding (`file:line:column severity rule message`), the JSON Pointer path, a hint and any fix, then a summary. `--list-rules` prints every rule (`--format json` with its full metadata).\n\nA file ending `.yaml` or `.yml` (or stdin with `--from yaml`) is read as YAML through `fromYaml`, so the report is the same and every finding is located in the YAML; a YAML syntax or dialect error is a `yaml/<rule>` finding (`format`, exit 1). A file ending `.opf.md` (or stdin with `--from md`) is read as a [Markdown deck](markdown.md#markdown-decks-in-every-command) the same way, and a Markdown error is a `markdown/<rule>` finding. A plain `.md` file, and any other text that does not start with `{` or `[`, needs `--from`.\n\n`--only` and `--ignore` take comma-separated rule ids, bare names or category names. `--config` takes an explicit local JSON file, never `-`, holding `{ catalogs, contracts, severity, only, ignore, ignorePaths, thresholds, chartPalette }`; `--only` replaces the file's `only` and `--ignore` adds to its `ignore`.\n\n| Exit | Meaning |\n| --- | --- |\n| 0 | No finding at or above `--fail-on` (default `error`) |\n| 1 | At least one finding at or above `--fail-on`, or a document that is not valid JSON (or YAML) |\n| 2 | Usage, configuration or I/O error |\n\n`--fail-on` is the one way to say \"fail on warnings\" on every command that checks a document: `--fail-on warning`. Other commands (`create`, `edit`, `fill`, `paginate`, `bundle`, `ingest`, `render`, `export`, `import`, `from-md`, `from-yaml`) check only `format` and `references` of what they write and take `--fail-on` the same way.\n\n## How contrast is computed\n\n`opf/text-contrast` reasons about the colours the preview draws, because a check against a different colour than the one on screen is worse than none.\n\n1. **Background.** Design resolves per field: slide design, deck design, theme, engine default. The background is a theme slot or colour (`solid`), the card surface for content on a `contentBox` card, a table cell's fill, a gradient or a pattern. Solid, gradient-stop and pattern colours are read as literal hex colours, as the preview reads them (anything else falls back as it does there). Translucent backgrounds are composited over white.\n2. **Text colour.** Titles, body, lists and tags use the scheme's `dark1`, or `light1` when the background is dark (luminance below 0.179). Furniture and quote attributions use the muted colour, metric values the primary colour, and explicit run and table colours the author's `ColorRef` resolved the same way the renderer resolves it. As the preview does, a gradient background has no single colour and counts as light, so a dark gradient behind default text is reported.\n3. **Gradients.** The gradient is sampled on a 5 by 5 grid over the area the text covers (the lines' measured ink, aligned as the text is), using the preview's gradient geometry, and the worst colour decides. A pattern contributes both of its colours.\n4. **Pictures.** Pixels are never read. A background picture is bounded by a grey ramp from black to white, composited through the picture opacity and any full-frame overlay of the image background (`design.background.overlay`). Text passes only if every step passes; otherwise `opf/text-on-image` says the result cannot be guaranteed. An overlay limited to an edge band is not counted.\n5. **Ratio and size.** The WCAG 2.x relative-luminance contrast ratio is compared with 4.5:1, or 3:1 for large text: at least 18 pt, or 14 pt and bold. Sizes are the fitted sizes, expressed at the 13.33 by 7.5 in reference slide (96 px per inch). The default body size, 18.75 pt, is large text under WCAG, so default body text passes at 3:1; set `contrastLarge` to 4.5 for a stricter policy.\n\nApproximations: anti-aliasing, text shadows, font weight and the exact glyph coverage are not modelled; chart and code text are not checked (charts pick label colours against their surface; code uses its own panel colours). See each rule below for what it cannot see.\n\n## Layout rules use composition\n\nOverflow, minimum type size, reading order and image resolution use `composeSlide` at the deck's slide size with the resolved font scheme, the same composition the preview and the PPTX export consume. Without `fonts.textMeasurement` the text widths are core's portable estimate and can differ from the real fonts by a few percent; hosts that load fonts (the editor, the renderer) pass their measurement for font-exact results. A composition with `overflow: \"error\"` fails at composition time; `validate` still computes the geometry, relaxes the setting, and reports the overflow as a **warning**, so that `valid` never depends on font metrics. Rendering and export still refuse such a slide when it does not fit, and a host that wants the finding to fail promotes it with `severity: { 'opf/text-overflow': 'error' }`.\n\nPromoted region keys (`left`, `center`, `right`, `top:left` and the rest) are composed in visual reading order (rows from top to bottom, then along the row), whatever order the keys are written in. The PPTX export writes shapes in that order, which is the reading order of assistive technology; `opf/reading-order` checks that the composed order still matches the geometry (the same `visualReadingOrder` that composes the regions). Authoring with `blocks` (an ordered list) keeps the reading order equal to the visual order.\n\n## Templates and variables\n\n`opf/variable-unfilled` is one rule for a required variable that has no value. In a deck that is not a template it is an error (the deck is incomplete), so `valid` is false; in a template (`\"template\": true` in the document, or `template: true` in the options) it is a warning, because a template is incomplete on purpose; `unfilledVariables` lists the ids either way. In a document that declares no content variables, a `{{id}}` token left in a string (outside `variables`, `extensions`, `catalogs` and `assets`) is a warning. A `var:` colour that names no declared variable is `opf/variable-reference-unknown`. It is a `format` rule, so `only: ['format']` (what pagination, patching, conversion and every write command run) rejects a deck with an unfilled required variable. Fill a template with `opf fill` or `resolveVariables`, or pass `values`, to clear the findings.\n\n## Rules\n\nThe reference below is generated from the rule registry (`validationRules`); `node scripts/build-validate-docs.mjs --check` (and a core test) fails when it drifts.\n\n<!-- validate-rules:start (generated by scripts/build-validate-docs.mjs; do not edit by hand) -->\n\n| Rule | Category | Default | Cost | What it reports |\n| --- | --- | --- | --- | --- |\n| [`opf/json-syntax`](#opfjson-syntax) | Format | error | syntax | The text is not strict JSON. |\n| [`opf/duplicate-key`](#opfduplicate-key) | Format | error | syntax | A JSON object repeats a key. |\n| [`opf/schema`](#opfschema) | Format | error | structure | The document breaks the OPF schema or a structural rule the schema cannot express. |\n| [`opf/reference-id-duplicate`](#opfreference-id-duplicate) | Format | error | structure | Two references share an id. |\n| [`opf/cite-unknown-reference`](#opfcite-unknown-reference) | Format | error | structure | A citation names a reference that does not exist. |\n| [`opf/cite-unsupported-location`](#opfcite-unsupported-location) | Format | error | structure | A citation or footnote sits where no engine draws it. |\n| [`opf/caption-unsupported-payload`](#opfcaption-unsupported-payload) | Format | error | structure | A caption is on a payload that cannot carry one. |\n| [`opf/image-option-unsupported-payload`](#opfimage-option-unsupported-payload) | Format | error | structure | An image option is on a payload that is not an image. |\n| [`opf/image-placement-invalid`](#opfimage-placement-invalid) | Format | error | structure | An image placement cannot be honoured. |\n| [`opf/layout-placeholder-group`](#opflayout-placeholder-group) | Format | error | structure | A layout record's placeholder group breaks a nesting rule. |\n| [`opf/dataset-unknown`](#opfdataset-unknown) | Format | error | structure | A chart or table names a dataset that does not exist. |\n| [`opf/dataset-field-unknown`](#opfdataset-field-unknown) | Format | error | structure | A table asks a dataset for a column it does not have. |\n| [`opf/data-column-duplicate`](#opfdata-column-duplicate) | Format | error | structure | A dataset or table has two columns with the same name. |\n| [`opf/chart-mapping-unknown-column`](#opfchart-mapping-unknown-column) | Format | error | structure | A chart mapping names a column the data does not have. |\n| [`opf/number-format-invalid`](#opfnumber-format-invalid) | Format | error | structure | A number format cannot be read. |\n| [`opf/variable-invalid-value`](#opfvariable-invalid-value) | Format | error | structure | A variable value does not match its declared type. |\n| [`opf/variable-unknown-builtin`](#opfvariable-unknown-builtin) | Format | error | structure | A token names a built-in variable that does not exist. |\n| [`opf/chart-highlight-unknown-name`](#opfchart-highlight-unknown-name) | Format | error | structure | A chart highlight names a series or category the chart does not have. |\n| [`opf/variable-format`](#opfvariable-format) | Format | error | structure | A variable cannot be formatted. |\n| [`opf/variable-unfilled`](#opfvariable-unfilled) | Format | error | structure | A required template variable has no value. |\n| [`opf/run-color-unrecognized`](#opfrun-color-unrecognized) | Format | warning | structure | A text run colour is none of the documented forms. |\n| [`opf/numbering-start-ignored`](#opfnumbering-start-ignored) | Format | warning | structure | A list entry sets `start` where nothing is numbered. |\n| [`opf/slide-size-out-of-range`](#opfslide-size-out-of-range) | Format | warning | structure | The slide size is outside PowerPoint's 1 to 56 inch range. |\n| [`opf/undeclared-catalog`](#opfundeclared-catalog) | Format | error | structure | A reference names a catalog group the document does not declare. |\n| [`opf/asset-reference`](#opfasset-reference) | References | error | structure | An `asset:` reference names an asset that is not in the registry. |\n| [`opf/asset-cycle`](#opfasset-cycle) | References | error | structure | Asset references form a cycle. |\n| [`opf/catalog-record`](#opfcatalog-record) | References | error | structure | An embedded or registered catalog record is invalid. |\n| [`opf/layout-template`](#opflayout-template) | References | error | structure | A layout template's grid is malformed. |\n| [`opf/layout-region`](#opflayout-region) | References | error | structure | A layout template's regions do not match its areas. |\n| [`opf/unresolved-reference`](#opfunresolved-reference) | References | warning | structure | A content reference resolves nowhere. |\n| [`opf/catalog-record-not-in-source`](#opfcatalog-record-not-in-source) | References | warning | structure | A record embedded under a catalog group is not in that catalog. |\n| [`opf/unused-reference`](#opfunused-reference) | References | warning | structure | A reference is never cited. |\n| [`opf/unused-dataset`](#opfunused-dataset) | References | warning | structure | A dataset is never used. |\n| [`opf/unknown-beat`](#opfunknown-beat) | References | warning | structure | A slide names a beat its narrative does not define. |\n| [`opf/slide-theme-dimensions`](#opfslide-theme-dimensions) | References | warning | structure | A slide theme sets a slide size that differs from the deck. |\n| [`opf/variable-unknown`](#opfvariable-unknown) | References | warning | structure | A `{{token}}` names a variable that is not declared. |\n| [`opf/variable-unknown-value`](#opfvariable-unknown-value) | References | warning | structure | A value was supplied for a variable that is not declared. |\n| [`opf/variable-unused`](#opfvariable-unused) | References | warning | structure | A variable is declared but never used. |\n| [`opf/variable-reference-unknown`](#opfvariable-reference-unknown) | References | warning | structure | A `var:` colour names a variable that is not declared. |\n| [`opf/contract`](#opfcontract) | Policy | error | structure | A field is outside the values a host policy allows. |\n| [`opf/text-contrast`](#opftext-contrast) | Accessibility | warning | composition | Text colour has too little contrast against the background it sits on. |\n| [`opf/text-on-image`](#opftext-on-image) | Accessibility | info | composition | Text sits on a background picture whose pixels cannot be measured. |\n| [`opf/missing-alt-text`](#opfmissing-alt-text) | Accessibility | warning | structure | A picture has no alt text and is not marked decorative. |\n| [`opf/poor-alt-text`](#opfpoor-alt-text) | Accessibility | info | structure | Alt text is a file name, a URL, a generic word or very long. |\n| [`opf/missing-slide-title`](#opfmissing-slide-title) | Accessibility | warning | structure | A slide has no title. |\n| [`opf/duplicate-slide-title`](#opfduplicate-slide-title) | Accessibility | info | structure | Two slides have the same title. |\n| [`opf/reading-order`](#opfreading-order) | Accessibility | warning | composition | The order content is read differs from the order it appears on the slide. |\n| [`opf/link-text`](#opflink-text) | Accessibility | warning | structure | Link text does not say where the link goes. |\n| [`opf/chart-color-only`](#opfchart-color-only) | Accessibility | info | structure | Chart series may be indistinguishable without colour vision. |\n| [`opf/chart-text-alternative`](#opfchart-text-alternative) | Accessibility | info | structure | A chart has no text alternative, or is marked decorative. |\n| [`opf/missing-language`](#opfmissing-language) | Accessibility | info | structure | The presentation does not declare its language. |\n| [`opf/chart-option-adapted`](#opfchart-option-adapted) | Layout | warning | structure | A chart option cannot be shown by the chart type. |\n| [`opf/text-overflow`](#opftext-overflow) | Layout | warning | composition | Text or a table does not fit its space at the smallest allowed size. |\n| [`opf/unresolved-content`](#opfunresolved-content) | Layout | warning | composition | Content cannot be drawn as authored. |\n| [`opf/layout-failed`](#opflayout-failed) | Layout | warning | composition | The slide layout could not be computed. |\n| [`opf/min-font-size`](#opfmin-font-size) | Layout | warning | composition | Text is drawn smaller than the readable minimum. |\n| [`opf/region-unknown`](#opfregion-unknown) | Layout | warning | composition | A block is pinned to a region the layout does not have. |\n| [`opf/region-kind`](#opfregion-kind) | Layout | warning | composition | A block is pinned to a region that does not accept its kind. |\n| [`opf/region-full`](#opfregion-full) | Layout | warning | composition | A block is pinned to a region that is already full. |\n| [`opf/layout-unplaced`](#opflayout-unplaced) | Layout | warning | composition | No region of the layout has room for a block. |\n| [`opf/font-outside-scheme`](#opffont-outside-scheme) | Layout | warning | structure | Text uses a font family that is not in the deck's font scheme. |\n| [`opf/image-resolution`](#opfimage-resolution) | Layout | warning | composition | An image has too few pixels for the size it is shown at. |\n| [`opf/chart-value-not-numeric`](#opfchart-value-not-numeric) | Content | warning | structure | A chart value is text, not a number. |\n| [`opf/chart-mapping-adapted`](#opfchart-mapping-adapted) | Content | warning | structure | A chart mapping entry is ignored. |\n| [`opf/chart-highlight-adapted`](#opfchart-highlight-adapted) | Content | warning | structure | A chart highlight is adapted by the chart type. |\n| [`opf/code-highlight-out-of-range`](#opfcode-highlight-out-of-range) | Content | warning | structure | A code highlight marks a line the code does not have. |\n| [`opf/code-highlight-range-reversed`](#opfcode-highlight-range-reversed) | Content | warning | structure | A code highlight range ends before it starts. |\n| [`opf/variable-builtin-missing`](#opfvariable-builtin-missing) | Content | warning | structure | A built-in variable has no value in this document. |\n| [`opf/duration-outside-narrative`](#opfduration-outside-narrative) | Content | warning | structure | The target duration is outside the range the narrative suits. |\n| [`opf/narrative-duration-range`](#opfnarrative-duration-range) | Content | warning | structure | An inline narrative duration range has min greater than max. |\n| [`opf/placeholder-text`](#opfplaceholder-text) | Content | warning | structure | Placeholder text was left in the deck. |\n| [`opf/empty-text`](#opfempty-text) | Content | info | structure | A text field is present but empty. |\n| [`opf/unused-beat`](#opfunused-beat) | Content | info | structure | A beat of the narrative has no slide that references it. |\n| [`opf/empty-slide`](#opfempty-slide) | Content | warning | structure | A slide has no content at all. |\n| [`opf/language-tag`](#opflanguage-tag) | Content | warning | structure | A language tag is malformed, uses an unassigned region or is not in canonical case. |\n\n## Format rules\n\n*Is it well-formed OPF?*\n\n### `opf/json-syntax`\n\nDefault severity: **error**. Cost: syntax. The text is not strict JSON.\n\n**Why.** Every OPF tool reads JSON. A misplaced comma, quote, comment or trailing comma makes the file unreadable, and the range says where.\n\n**Basis.** RFC 8259 (strict JSON: no comments, no trailing commas)\n\n**Approximations.** Only text input is checked. A string is always read as JSON; a text that looks like YAML or Markdown gets a hint to use fromYaml or fromMarkdown first.\n\n### `opf/duplicate-key`\n\nDefault severity: **error**. Cost: syntax. A JSON object repeats a key.\n\n**Why.** JSON.parse keeps only the last value of a repeated key, so the earlier one disappears without a trace. The check compares escaped and literal spellings of the same key.\n\n**Basis.** RFC 8259 section 4 (names within an object should be unique)\n\n### `opf/schema`\n\nDefault severity: **error**. Cost: structure. The document breaks the OPF schema or a structural rule the schema cannot express.\n\n**Why.** The schema is the contract every engine, exporter and editor relies on. A field of the wrong type, a missing required field, an unknown property or content fields that cannot be mixed make the document ambiguous or undrawable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** All alternatives of a union are reported, with the failing schema location in `validation`. A required variable with no value is reported by `opf/variable-unfilled`, in the same category.\n\n### `opf/reference-id-duplicate`\n\nDefault severity: **error**. Cost: structure. Two references share an id.\n\n**Why.** A citation names a reference by id; a repeated id makes it ambiguous which entry is cited.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unknown-reference`\n\nDefault severity: **error**. Cost: structure. A citation names a reference that does not exist.\n\n**Why.** A cited id with no entry in `references` has nothing to show in the footnote area.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unsupported-location`\n\nDefault severity: **error**. Cost: structure. A citation or footnote sits where no engine draws it.\n\n**Why.** `cite` and `footnote` apply to runs in text, bullets and list items; table cells, captions, and reference and footnote texts have no marker.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/caption-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. A caption is on a payload that cannot carry one.\n\n**Why.** A caption belongs to exactly one image, chart, table or video; a group, text or code block has no place to draw it.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-option-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. An image option is on a payload that is not an image.\n\n**Why.** `fit`, `focus`, the image treatments (`shape`, `cornerRadius`, `border`, `opacity`, `recolor`, `overlay`, `aspectRatio`) and `placement` describe how a picture is drawn; a group, text or chart block has no picture for them to apply to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-placement-invalid`\n\nDefault severity: **error**. Cost: structure. An image placement cannot be honoured.\n\n**Why.** A placed image bleeds to a slide edge and the rest of the slide composes beside it. Only a top-level block (slides.N.blocks.I) can do that, and a slide edge holds one placed image; a block in a group or a promoted region, or a second block on the same edge, has nowhere to go.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/layout-placeholder-group`\n\nDefault severity: **error**. Cost: structure. A layout record's placeholder group breaks a nesting rule.\n\n**Why.** A placeholder group arranges body regions inside one cell of its layout record. Groups nest at most three levels inside a record, and a group holds body regions and groups only: the headings (title, subtitle, tag) stay at the record's top level, where the engine places them. Composition cannot use a record that breaks either rule.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-unknown`\n\nDefault severity: **error**. Cost: structure. A chart or table names a dataset that does not exist.\n\n**Why.** The data cannot be drawn without the dataset it refers to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-field-unknown`\n\nDefault severity: **error**. Cost: structure. A table asks a dataset for a column it does not have.\n\n**Why.** The column cannot be shown when the dataset has no column of that name.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/data-column-duplicate`\n\nDefault severity: **error**. Cost: structure. A dataset or table has two columns with the same name.\n\n**Why.** Series, mappings and fields address columns by name, so a repeated name makes them ambiguous.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-unknown-column`\n\nDefault severity: **error**. Cost: structure. A chart mapping names a column the data does not have.\n\n**Why.** The series cannot be built from a column that is not there.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/number-format-invalid`\n\nDefault severity: **error**. Cost: structure. A number format cannot be read.\n\n**Why.** A format code that no engine can parse is drawn as the plain number or fails to export.\n\n**Basis.** Excel number format codes, as far as OPF supports them\n\n### `opf/variable-invalid-value`\n\nDefault severity: **error**. Cost: structure. A variable value does not match its declared type.\n\n**Why.** A number variable given text, or a date given a non-date, cannot be filled into the deck.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-builtin`\n\nDefault severity: **error**. Cost: structure. A token names a built-in variable that does not exist.\n\n**Why.** The `deck.`, `speaker.`, `organization.` and `slide.` names (and `speakers`) are reserved for built-in variables; a name under them that is not a built-in can never be filled, so the token would show literally. The slide-scoped `slide.number`, `slide.section` and `deck.slideCount` are inline tokens only, so `var:slide.*` and `var:deck.slideCount` are reported too; organization logos are whole-field references only (`var:organization.logo.icon`), so a `{{organization.logo}}` token, an unknown logo shape or an unknown organization id is reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-unknown-name`\n\nDefault severity: **error**. Cost: structure. A chart highlight names a series or category the chart does not have.\n\n**Why.** `chart.highlight` emphasises marks by name; a name that matches no series or category in the chart data highlights nothing, so the chart is not drawn as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-format`\n\nDefault severity: **error**. Cost: structure. A variable cannot be formatted.\n\n**Why.** A number or date format that does not apply to the value leaves the token unreadable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unfilled`\n\nDefault severity: **error**. Cost: structure. A required template variable has no value.\n\n**Why.** A variable a deck declares as required, with no value, is template scaffolding: `{{name}}` tokens show up literally and colour variables fall back to a default. A normal deck must be filled, so this is an error and the deck is not valid; a template (`template: true`) is incomplete on purpose, so it is a warning.\n\n**Approximations.** Required variables are those the variable machinery reports as unfilled for the document (declared, no value, not `required: false`, not given by the `values` option). For a document that declares no content variables, `{{id}}` / `{{id|format}}` tokens (`{{` escapes) left in any string outside `variables`, `extensions`, `catalogs` and `assets` are reported as warnings. `var:` colour references with no declaration are reported as `opf/variable-reference-unknown`. Fill a template with `opf fill` or `resolveVariables` to clear the findings.\n\n### `opf/run-color-unrecognized`\n\nDefault severity: **warning**. Cost: structure. A text run colour is none of the documented forms.\n\n**Why.** Run colours are open strings so imported decks stay valid, but a value that is not a hex colour, a colour-scheme name or a `var:` reference falls back to the theme colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/numbering-start-ignored`\n\nDefault severity: **warning**. Cost: structure. A list entry sets `start` where nothing is numbered.\n\n**Why.** A start value only restarts an auto-number; without `numbering` on the payload it has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/slide-size-out-of-range`\n\nDefault severity: **warning**. Cost: structure. The slide size is outside PowerPoint's 1 to 56 inch range.\n\n**Why.** The schema accepts any positive custom `widthInches` and `heightInches`, but a PPTX stores the size as `p:sldSz` with each side between 914400 and 51206400 EMU, which is 1 to 56 inches. A custom size outside that range exports a file that does not conform to the standard, which PowerPoint may reject or clamp. It is a warning because the document is still correct OPF and the preview draws it.\n\n**Basis.** ECMA-376 / ISO/IEC 29500-1 section 19.2.1.39 (p:sldSz) and section 19.7.17 (ST_SlideSizeCoordinate): 914400 to 51206400 EMU\n\n**Approximations.** Checks the deck's resolved size: `design.dimensions`, else the resolved theme's `dimensions`. Inches are converted to EMU as the PPTX exporter does (`Math.round(inches * 914400)`), so a side of exactly 1 in or 56 in passes. A slide-level theme with another size is `opf/slide-theme-dimensions`. Presets are always in range. PowerPoint's own behaviour at the limits was not measured.\n\n### `opf/undeclared-catalog`\n\nDefault severity: **error**. Cost: structure. A reference names a catalog group the document does not declare.\n\n**Why.** The prefix of a `name:id` reference names the group of `catalogs` it resolves in. A prefix with no group can never resolve, whatever the host registers, so it is a format error: engines reject the document at their format check instead of drawing a fallback.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## References rules\n\n*Does everything it points at resolve?*\n\n### `opf/asset-reference`\n\nDefault severity: **error**. Cost: structure. An `asset:` reference names an asset that is not in the registry.\n\n**Why.** An image, logo or video that points at a missing asset cannot be drawn or exported. The check reads the document's own registry only; no file or URL is fetched.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/asset-cycle`\n\nDefault severity: **error**. Cost: structure. Asset references form a cycle.\n\n**Why.** An asset whose source points back at itself never resolves to a file.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/catalog-record`\n\nDefault severity: **error**. Cost: structure. An embedded or registered catalog record is invalid.\n\n**Why.** A record that fails its companion schema cannot be resolved. Invalid records are left out of the lookup, so the references to them are reported too.\n\n**Basis.** spec/schemas/<kind>.schema.json\n\n### `opf/layout-template`\n\nDefault severity: **error**. Cost: structure. A layout template's grid is malformed.\n\n**Why.** A layout (OPF 0.19) is a grid of named areas. Engines cannot compose a grid whose rows have different lengths, whose names do not form one filled rectangle each, whose column or row sizes do not match the grid, that is larger than 12 x 12, or that holds a word that is not a name or \".\". composeSlide refuses such a record, so a slide that names it composes automatically.\n\n**Basis.** spec/schemas/layout.schema.json and the area rules of OPF 0.19 (docs/layout-templates.md)\n\n**Approximations.** Checked on every layout record the document embeds and every registered record it uses, once the record passes its schema.\n\n### `opf/layout-region`\n\nDefault severity: **error**. Cost: structure. A layout template's regions do not match its areas.\n\n**Why.** Every body area of a layout needs a region that says what it accepts, and every region needs an area. Region names may not be title, subtitle, tag, auto or a promoted-region word (left, center, right, top, middle, bottom), so a Markdown block's region= stays unambiguous; a subtitle area needs a title area; a region with flow 'none' holds one block; overflowRegion must name a region. composeSlide refuses such a record.\n\n**Basis.** spec/schemas/layout.schema.json and the region rules of OPF 0.19 (docs/layout-templates.md)\n\n### `opf/unresolved-reference`\n\nDefault severity: **warning**. Cost: structure. A content reference resolves nowhere.\n\n**Why.** A layout, theme, colour scheme, font scheme, narrative, audience, purpose or tone reference that neither the document embeds nor a registered catalog defines falls back: a slide composes automatically, a design uses the engine default. A strict export fails instead. A slide with no layout is automatic composition and is never reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Resolved like every engine resolves it: catalogs.custom, then the records embedded under catalogs.default, then the catalog registered for its source (the first registered catalog when `default` is omitted); `name:id` in catalogs.<name>, then the catalog registered for its source. Nothing is fetched. Free-form audience and purpose text is not a reference.\n\n### `opf/catalog-record-not-in-source`\n\nDefault severity: **warning**. Cost: structure. A record embedded under a catalog group is not in that catalog.\n\n**Why.** A record under catalogs.default or a named group says it came from the catalog its source names. When the catalog the host registered for that source has no record of that kind and id, the record is the document's own and belongs in catalogs.custom, where an update from the catalog never looks for it. Rendering is unchanged: the embedded record still wins.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Checked only against a catalog the host registered for the group's source (the first registered catalog for an omitted default.source); with none registered for it the check is silent. A record the catalog has with different content is an update difference (updateFromCatalog), not this finding. catalogs.custom is never checked.\n\n### `opf/unused-reference`\n\nDefault severity: **warning**. Cost: structure. A reference is never cited.\n\n**Why.** A listed reference that no run cites is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unused-dataset`\n\nDefault severity: **warning**. Cost: structure. A dataset is never used.\n\n**Why.** A dataset no chart or table references is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unknown-beat`\n\nDefault severity: **warning**. Cost: structure. A slide names a beat its narrative does not define.\n\n**Why.** `slides[].beat` links a slide to a step of the narrative plan. A beat id the narrative does not have links to nothing; nothing is drawn from a beat.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** The narrative resolves like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked.\n\n### `opf/slide-theme-dimensions`\n\nDefault severity: **warning**. Cost: structure. A slide theme sets a slide size that differs from the deck.\n\n**Why.** A PPTX has one slide size. The size a slide-level theme carries is never used: design.dimensions of the deck (or of its theme) wins in every engine, and without them the exporter stops with mixed slide sizes.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown`\n\nDefault severity: **warning**. Cost: structure. A `{{token}}` names a variable that is not declared.\n\n**Why.** An undeclared token is shown literally.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-value`\n\nDefault severity: **warning**. Cost: structure. A value was supplied for a variable that is not declared.\n\n**Why.** The value is never used.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unused`\n\nDefault severity: **warning**. Cost: structure. A variable is declared but never used.\n\n**Why.** A declaration nothing refers to has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-reference-unknown`\n\nDefault severity: **warning**. Cost: structure. A `var:` colour names a variable that is not declared.\n\n**Why.** An undeclared colour variable falls back to a default colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## Policy rules\n\n*Does it follow the host's house rules?*\n\n### `opf/contract`\n\nDefault severity: **error**. Cost: structure. A field is outside the values a host policy allows.\n\n**Why.** A host that needs brand layouts, fonts or other fixed choices states them as contracts. The default severity is error; a contract can set its own. Document metadata never installs policy.\n\n**Approximations.** Only the `contracts` option supplies policy, and it checks existing fields: a contract does not require an omitted field or insert a default.\n\n\n## Accessibility rules\n\n*Can everyone read it?*\n\n### `opf/text-contrast`\n\nDefault severity: **warning**. Cost: composition. Text colour has too little contrast against the background it sits on.\n\n**Why.** Low-contrast text is hard or impossible to read for people with low vision, colour-vision differences, glare on a projector or a poor display. WCAG sets 4.5:1 for normal text and 3:1 for large text.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Computed on sRGB colours with the WCAG relative-luminance formula, against the background the preview draws: a solid or theme colour, the card surface, a table cell fill, every colour a gradient takes under the text box (sampled on a 5x5 grid, angle respected), or both colours of a pattern. Anti-aliasing, text shadows and font weight are not modelled. Text colour is the preview's (the scheme's text role or dark1 on a light background and light1 on a dark one, chosen from the background luminance, where a gradient or picture background uses the scheme's default background colour), so a default can fail on a dark gradient. A link with no colour of its own is measured in the scheme's hyperlink colour.\n\n### `opf/text-on-image`\n\nDefault severity: **info**. Cost: composition. Text sits on a background picture whose pixels cannot be measured.\n\n**Why.** Contrast over a photograph depends on the photograph. Without a full-frame overlay that guarantees readability for every possible image, the result cannot be certified from the document.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Core never reads picture pixels. The picture is bounded by a grey ramp from black to white, composited through the image opacity and a full-frame design.background overlay; the text passes only when every step of that ramp passes. An edge-banded overlay is not counted.\n\n### `opf/missing-alt-text`\n\nDefault severity: **warning**. Cost: structure. A picture has no alt text and is not marked decorative.\n\n**Why.** People using a screen reader get nothing for a picture without alternative text. A picture that is purely decorative should say so with an empty alt (alt: \"\"), which is an explicit, reviewed choice instead of an omission.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Checks the alt field of images (image blocks, Slide.image and region images, placed or not), video, organization logos (each shape and background), header/footer images (a logo reference such as var:organization.logo.icon is checked on the organization), quote photos and speaker photos, following asset: references to the assets registry. Whether the text describes the picture well is not judged here (see opf/poor-alt-text). Charts carry `chart.alt` and are checked by opf/chart-text-alternative. Picture backgrounds are decorative unless they carry their own alt, and watermarks are decorative, so neither is checked.\n\n### `opf/poor-alt-text`\n\nDefault severity: **info**. Cost: structure. Alt text is a file name, a URL, a generic word or very long.\n\n**Why.** Alt text such as \"image\", \"IMG_2041.png\" or a 400-character paragraph does not do the job of describing a picture: it is read out and adds noise without information.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Pattern checks only: file extensions and camera-style names, a bare generic word, a URL, a leading \"image of\", and more than 250 characters. Chart alt text (chart.alt) is checked too: a bare chart word, a URL, a leading \"chart of\" or more than 250 characters. It cannot tell whether a plausible sentence is accurate.\n\n### `opf/missing-slide-title`\n\nDefault severity: **warning**. Cost: structure. A slide has no title.\n\n**Why.** Slide titles are how people using a screen reader, an outline view or keyboard navigation find and tell slides apart; PowerPoint's own accessibility checker reports a missing title too.\n\n**Basis.** WCAG 2.2 SC 2.4.2 Page Titled and SC 2.4.6 Headings and Labels, level AA (PowerPoint: \"Missing slide title\")\n\n**Approximations.** Only the slide-level title field counts. Text that merely looks like a heading inside a block does not.\n\n### `opf/duplicate-slide-title`\n\nDefault severity: **info**. Cost: structure. Two slides have the same title.\n\n**Why.** Identical titles make slides indistinguishable in an outline or a screen reader's slide list.\n\n**Approximations.** Titles are compared case-insensitively with whitespace collapsed. Slides that continue one another are not exempt; give a continuation a distinct title such as \"(continued)\".\n\n### `opf/reading-order`\n\nDefault severity: **warning**. Cost: composition. The order content is read differs from the order it appears on the slide.\n\n**Why.** Screen readers, keyboard focus and PowerPoint's selection pane follow the composed order. When it differs from the visual order (top to bottom, then start to end of the reading direction), the slide is read out of sequence.\n\n**Basis.** WCAG 2.2 SC 1.3.2 Meaningful Sequence, level A (PowerPoint: \"Check reading order\")\n\n**Approximations.** Compares the composed content order with a visual order recomputed from the composed boxes: items whose vertical centres fall in the same row are ordered along the reading direction (a right-to-left deck is checked by rows only), rows from top to bottom. Headings are expected first. Free-form overlap is not analysed. On a slide with a template layout (OPF 0.19) the expected order is the binding order instead (the regions in reading order, then the blocks of each region), and items outside a region (placed images, the row below the grid) are not compared.\n\n### `opf/link-text`\n\nDefault severity: **warning**. Cost: structure. Link text does not say where the link goes.\n\n**Why.** People who scan links out of context (a screen reader's links list, a link tab order) hear only the text. \"click here\", \"read more\" or a long raw URL tells them nothing about the destination.\n\n**Basis.** WCAG 2.2 SC 2.4.4 Link Purpose (In Context), level A; PowerPoint: \"Hyperlink text is not meaningful\"\n\n**Approximations.** Matches a short list of generic phrases in English (after lower-casing and removing punctuation), blank link text, and raw URLs longer than 40 characters. Adjacent runs sharing one link are read as one link.\n\n### `opf/chart-color-only`\n\nDefault severity: **info**. Cost: structure. Chart series may be indistinguishable without colour vision.\n\n**Why.** OPF charts have no data labels or patterns, so series are told apart by colour alone (and legend order). Colours that look alike to someone with colour-vision deficiency, or when printed in greyscale, make series impossible to tell apart.\n\n**Basis.** WCAG 2.2 SC 1.4.1 Use of Color, level A\n\n**Thresholds.** `minSeriesColorDifference` (default 10)\n\n**Approximations.** Uses the engine's series palette (ValidateOptions.chartPalette; default the opf-render/opf-pptx palette, adjusted for the card surface like the preview does) in series order: series i takes colour i, pie/doughnut/treemap/funnel slices take colours per category. Pairs are compared by CIE76 distance after simulating protanopia, deuteranopia, tritanopia (Machado 2009, severity 1) and greyscale. Single-series charts and chart types without a series legend are skipped.\n\n### `opf/chart-text-alternative`\n\nDefault severity: **info**. Cost: structure. A chart has no text alternative, or is marked decorative.\n\n**Why.** A chart conveys a message; people who cannot see it need the message and ideally the numbers in text. The chart's alt field is that text alternative (the preview exposes it as the chart's accessible name and the PowerPoint export writes it as the frame's alternative text); a sentence or table beside the chart also serves. An empty alt marks a chart decorative, which is reported as info so the choice is reviewed: a chart rarely carries no message.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** A chart passes when chart.alt has text. Without alt, it passes when the slide has any other text, list, table, quote or metric content besides title and tag, or a subtitle. It does not judge whether alt or that text states the chart's point (see opf/poor-alt-text for generic alt text). alt: \"\" is reported as a decorative chart, whatever else is on the slide.\n\n### `opf/missing-language`\n\nDefault severity: **info**. Cost: structure. The presentation does not declare its language.\n\n**Why.** Screen readers and text-to-speech choose pronunciation and hyphenation from the declared language; spell checkers and translation tools use it too.\n\n**Basis.** WCAG 2.2 SC 3.1.1 Language of Page, level A\n\n**Approximations.** Only the presentation-level `language` is checked, not the language of individual runs (`TextRun.lang`; opf/language-tag checks how those tags are written).\n\n\n## Layout rules\n\n*Will it present as authored?*\n\n### `opf/chart-option-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart option cannot be shown by the chart type.\n\n**Why.** An axis title, legend or data label option the chart type does not support is adapted by every engine, so the chart is not drawn exactly as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/text-overflow`\n\nDefault severity: **warning**. Cost: composition. Text or a table does not fit its space at the smallest allowed size.\n\n**Why.** Core composition shrinks text to the readable minimum and then reports what still does not fit. Overflowing text is clipped or runs over other content in the preview and in PowerPoint.\n\n**Approximations.** Uses composeSlide at the deck's slide size with the fonts of the resolved font scheme. Without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimate, which can differ from the real font by a few percent; pass the renderer's measurement for font-exact results. A composition that sets overflow: \"error\" is reported as a warning here, so that `valid` never depends on font metrics; rendering and export still refuse such a slide when it does not fit.\n\n### `opf/unresolved-content`\n\nDefault severity: **warning**. Cost: composition. Content cannot be drawn as authored.\n\n**Why.** composeSlide reports content it cannot place or an effect it does not support (for example a picture bullet without a logo, a date field without a date, or an unsupported image treatment). The preview and the export fall back.\n\n### `opf/layout-failed`\n\nDefault severity: **warning**. Cost: composition. The slide layout could not be computed.\n\n**Why.** Composition threw for a slide that passed schema validation, so geometry-based rules (contrast, overflow, reading order, resolution) were skipped for it.\n\n### `opf/min-font-size`\n\nDefault severity: **warning**. Cost: composition. Text is drawn smaller than the readable minimum.\n\n**Why.** Small type is unreadable from the back of a room and on a phone. The engine's own default floor is 12 pt (16 px); anything much below that was lowered on purpose or by a composition that could not fit the text.\n\n**Basis.** Common presentation guidance (12 pt minimum for body text; 18 pt or more is easier to read from a distance).\n\n**Thresholds.** `minFontSizePt` (default 11)\n\n**Approximations.** Sizes are those composeSlide fitted, expressed on the 13.33 x 7.5 in reference slide (96 px per inch, so 1 px is 0.75 pt; a smaller canvas scales type down with it) and explicit run fontSize values in points. Per payload, the smallest part (a metric label or a quote attribution) is reported. Table cell text (default 11.25 pt), code and header/footer furniture are not checked.\n\n### `opf/region-unknown`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region the layout does not have.\n\n**Why.** A region pin (`region` on a root block) names a region of the slide's layout. A name the layout does not have is ignored: the block is placed by its kind, which may not be where the author meant.\n\n### `opf/region-kind`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that does not accept its kind.\n\n**Why.** A region says which content kinds it takes. A pin to a region that does not take the block's kind is ignored and the block is placed by its kind.\n\n### `opf/region-full`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that is already full.\n\n**Why.** A region holds at most `max` blocks on one slide. A pin to a full region is ignored and the block is placed by its kind.\n\n### `opf/layout-unplaced`\n\nDefault severity: **warning**. Cost: composition. No region of the layout has room for a block.\n\n**Why.** Content is never dropped: a block no region has room for is drawn in the layout's overflow region beyond its max (or below the grid), where it crowds the others. Paginate moves it to a continuation slide with the same layout.\n\n### `opf/font-outside-scheme`\n\nDefault severity: **warning**. Cost: structure. Text uses a font family that is not in the deck's font scheme.\n\n**Why.** The font scheme is the deck's font choice. A run that names another family will not follow a font-scheme change, may be missing on the viewer's machine, and breaks the deck's typographic consistency.\n\n**Approximations.** Compares the run's fontFamily (case-insensitively) with the heading, body, code and accent families of the slide's resolved font scheme and the scheme's major/minor fonts. Families that the host substitutes are still different names here.\n\n### `opf/image-resolution`\n\nDefault severity: **warning**. Cost: composition. An image has too few pixels for the size it is shown at.\n\n**Why.** An image stretched beyond its pixel size looks blurry or blocky on a projector or a high-density screen.\n\n**Thresholds.** `minImagePpi` (default 96)\n\n**Approximations.** Only embedded data: images (and asset: references to them) have readable pixel sizes; URLs and files are never fetched, so they are not checked. The displayed size is the composed frame (cover and stretch are measured as the cover scale, contain as the contain scale). Effective ppi is the image pixels per inch of the 96 px/inch reference slide. SVG is vector and exempt.\n\n\n## Content rules\n\n*Is anything left unfinished?*\n\n### `opf/chart-value-not-numeric`\n\nDefault severity: **warning**. Cost: structure. A chart value is text, not a number.\n\n**Why.** A chart plots numbers. A text cell such as \"12%\" is plotted as a gap. When every text value of a column is written in one display style the finding carries the fix that stores the numbers and gives the column the matching format.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart mapping entry is ignored.\n\n**Why.** A mapping that names a column the chart type cannot use is left out of the chart.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart highlight is adapted by the chart type.\n\n**Why.** A highlight the chart type cannot show as authored (a category highlight on a chart without categories, for example) is adapted by every engine, so the emphasis is not exactly what was written.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-out-of-range`\n\nDefault severity: **warning**. Cost: structure. A code highlight marks a line the code does not have.\n\n**Why.** Marked lines count from 1 by line break in code.source; an entry past the last line marks nothing.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-range-reversed`\n\nDefault severity: **warning**. Cost: structure. A code highlight range ends before it starts.\n\n**Why.** A range written end before start marks nothing; write the smaller line number first.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-builtin-missing`\n\nDefault severity: **warning**. Cost: structure. A built-in variable has no value in this document.\n\n**Why.** A `{{deck.*}}`, `{{speaker.*}}` or `{{organization.*}}` token whose field the document does not set resolves to nothing, so the text shows a gap; a `var:organization.logo` reference to an organization without a logo draws no image. `{{slide.section}}` is checked per slide: a slide without a `section` that uses it, in its own text or in the header or footer it inherits, is reported at that slide.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/duration-outside-narrative`\n\nDefault severity: **warning**. Cost: structure. The target duration is outside the range the narrative suits.\n\n**Why.** A narrative records the talk lengths it suits. A deck whose duration is outside that range probably needs a different narrative or a different length; nothing is drawn or exported differently.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/narrative-duration-range`\n\nDefault severity: **warning**. Cost: structure. An inline narrative duration range has min greater than max.\n\n**Why.** A range with its bounds swapped matches no duration, so the duration check cannot help.\n\n**Basis.** spec/schemas/narrative.schema.json\n\n### `opf/placeholder-text`\n\nDefault severity: **warning**. Cost: structure. Placeholder text was left in the deck.\n\n**Why.** \"Lorem ipsum\", \"Click to add title\", \"TBD\" and bracketed prompts are scaffolding. Shipping them reads as unfinished work.\n\n**Approximations.** Case-insensitive patterns for lorem ipsum, template prompts (\"Click to add...\", \"Your title here\", \"[Insert ...]\", a bare \"Title\" or \"Text\"), and the markers TBD, TBC, TODO, FIXME and XXX in capitals. A deliberate use of the words is flagged too; disable the rule for that slide with ignorePaths.\n\n### `opf/empty-text`\n\nDefault severity: **info**. Cost: structure. A text field is present but empty.\n\n**Why.** An empty title, text block or list item draws nothing, and leaves a hole in the outline and for assistive technology.\n\n### `opf/unused-beat`\n\nDefault severity: **info**. Cost: structure. A beat of the narrative has no slide that references it.\n\n**Why.** The narrative is the plan and the slides are the product. When some slides name a beat (slides[].beat) and a beat of the plan has none, the deck skips a step of the story or the plan is out of date.\n\n**Approximations.** Resolved like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked. A deck in which no slide references any beat is not checked either, because it has not linked its slides to the plan. A slide that lists several beats covers each of them.\n\n### `opf/empty-slide`\n\nDefault severity: **warning**. Cost: structure. A slide has no content at all.\n\n**Why.** A slide with no title, no content and no picture is almost always an accident of editing. (A deliberately blank slide can use the blank layout.)\n\n### `opf/language-tag`\n\nDefault severity: **warning**. Cost: structure. A language tag is malformed, uses an unassigned region or is not in canonical case.\n\n**Why.** Engines read the deck language and each run's `lang` as BCP 47 tags to choose script fonts, text direction, proofing language and the PowerPoint `lang` attribute. A tag such as `en-a`, or a region that does not exist (`en-UK`; the United Kingdom is `GB`), is guessed at or dropped, and a wrong-case tag (`EN-us`) is not what a locale lookup expects.\n\n**Basis.** BCP 47 (RFC 5646, section 2.1) and the platform locale data (Intl.getCanonicalLocales, Intl.DisplayNames)\n\n**Approximations.** Checks the root `language` (a tag string, or the `bcp47` of an inline Language object) and `lang` on the TextRun objects of titles, subtitles, tags, text, lists, tables, quotes, metrics and timelines. A region is assigned when the runtime's CLDR data names it (ISO 3166-1 codes and UN M.49 areas such as 419); private-use regions (`AA`, `QM`-`QZ`, `XA`-`XZ`) and `ZZ` are accepted, as are private-use (`x-...`) tags. Only the spelling of a tag is checked, not whether the language subtag is assigned or whether the language suits the text. The schema rejects tags with characters other than letters, digits and hyphens (`en_US` is a schema error, with no suggestion), and `en-UK` as the deck language is already an error of the schema rule, so those are not reported twice.\n\n<!-- validate-rules:end -->\n"
349
+ "markdown": "# OPF validate: one checker for format, references, accessibility, layout and content\n\n`validate` in `@openpresentation/opf` and `opf validate` in the CLI check an OPF presentation for everything a tool can tell without an opinion: whether it is well-formed OPF, whether everything it points at resolves, whether a host's house rules hold, and the problems a schema cannot see, such as text that is hard to read, text that does not fit, pictures without alt text, content read out of order, fonts outside the deck's scheme, placeholder text and slides left empty. It is the only checker. Releases before 0.14 had several (a schema validator, a lint and an audit, in the library and the CLI); 0.14 replaces them all with `validate`.\n\n`validate` is read-only, local and deterministic. It fetches nothing (no images, fonts or catalogs), consults no clock and calls no model. The same document, options and fonts give byte-identical findings.\n\n```sh\nopf validate deck.opf.json # everything, JSON report\nopf validate deck.opf.json --format text # one line per finding\nopf validate deck.opf.json --only format,references --fail-on warning\nopf validate deck.opf.json --ignore layout --config house-rules.json\nopf validate --list-rules\n```\n\n```js\nimport { validate } from '@openpresentation/opf';\n\nconst report = validate(presentation); // a parsed presentation\nconst fromText = validate(fileText); // strict JSON text: also syntax, duplicate keys and line/column\nvalidate(presentation, { only: ['format'] }); // the cheap check: schema and semantic rules only\nvalidate(presentation, { ignore: ['layout'] }); // everything except the checks that build slide layouts\nvalidate(presentation, { contracts, severity: { 'opf/text-contrast': 'error' } }); // house rules\n```\n\n## Categories\n\nEvery finding has one of six categories. They say what kind of question the finding answers, and they select what runs.\n\n| Category | Asks | Errors by default | Examples |\n| --- | --- | --- | --- |\n| `format` | Is it well-formed OPF? | yes | JSON syntax, duplicate keys, schema, a citation of an unknown reference, an unknown dataset, a required variable with no value (a warning in a template), a `name:id` prefix that names no catalog group |\n| `references` | Does everything it points at resolve? | missing or circular asset, invalid catalog record; a reference that resolves nowhere and an unused dataset are warnings | `asset:` references, catalog references, citations and datasets nobody uses |\n| `policy` | Does it follow the host's house rules? | the host sets it (default `error`) | \"only these layouts\", brand fonts: the `contracts` option |\n| `accessibility` | Can everyone read it? | no: warning and info | contrast, alt text, slide titles, reading order, link text, language |\n| `layout` | Will it present as authored? | no: warning | overflow, minimum type size, image resolution, fonts outside the scheme |\n| `content` | Is anything left unfinished? | no: warning and info | placeholder text, an empty slide, a chart value that is text |\n\n`valid` keeps meaning \"correct OPF\": it is true when no finding has severity `error`, and by default only `format`, `references` and `policy` can produce one. A low-contrast deck is still a valid deck. A team promotes the rules it cares about with `severity`.\n\nEvery rule is listed, with its category, default severity, cost and the basis it rests on, in [Rules](#rules) below. A rule stays in core only when it has a measurable basis (a standard such as WCAG 2.2, a print-preflight check, or a number computed from the layout); judgments that need an audience and a purpose, such as how many fonts or words a deck should have, are not rules here.\n\n## Input and report\n\n`validate(input, options?)` takes a parsed presentation (an object) or **strict JSON text**. A string is read as JSON and nothing else: it is never sniffed for YAML or Markdown. When the text begins like YAML (`key: value`, `---`) or Markdown (`#`), the `help` of the syntax finding says to read it with `fromYaml` or `fromMarkdown` first, which return a presentation and call `validate` for you.\n\nWith text, a syntax error or a duplicate key comes back with `location` (original-source UTF-16 offset and length, one-based line and column), and so does every finding about a value in the document; a finding about a missing field is located at the object that lacks it. Text that is not JSON reports its syntax errors (`opf/json-syntax`) whatever `only` says, because nothing else can run.\n\n### Reading a deck in any form\n\n`parse(text, { format, filename, catalogs })` (the root and `@openpresentation/opf/deck`, RR-60, RR-62) reads a deck from JSON, YAML or Markdown text and returns the presentation; a syntax, schema or reference error throws `OPFValidationError`, whose `report` is the report `validate` gives the text. The format is the `format` option (`\"json\"`, `\"yaml\"` or `\"markdown\"`), else the `filename` (`.opf.md` is Markdown, `.yaml` and `.yml` are YAML, anything else is JSON), and never the content: a plain `.md` name is not a deck. Findings are located in the text that was read: `opf/json-syntax` (line and column) for JSON, `yaml/<rule>` for YAML and `markdown/<rule>` for Markdown, and every `opf/` finding at the field it names. `parse` checks format and references; warnings do not throw, so call `validate` for the full report (`validate(text)` for JSON text; `fromYaml` and `fromMarkdown` return it for the other forms). It throws a `TypeError` for a text that is not a string, an unknown format or a bad option. `stringify(presentation, { format | filename })` is the other direction: JSON as two-space JSON, YAML and Markdown in canonical form. Both are browser-safe; `open` and `save` of `@openpresentation/opf` do the same for files in Node. See [OPF as YAML](yaml.md) and [Markdown and outlines](markdown.md).\n\n[Validation results](finding-schema-reference.md) use the `FindingReport` contract (`spec/schemas/finding.schema.json`). Core's `ValidationReport` adds a few more fields:\n\n| Field | Meaning |\n| --- | --- |\n| `valid` | No finding has severity `error` |\n| `findings` | Every finding, ordered by slide (document-level findings first), then category, then rule |\n| `counts` | `{ error, warning, info }` |\n| `schemaValid` | The document passes the schema and the semantic format checks; `null` for text that is not JSON. A required variable with no value does not make a document schema-invalid |\n| `checks` | What ran and what was not measured (below) |\n| `template`, `unfilledVariables` | Documents that declare content variables or are templates: whether they were checked as a template, and which required variables have no value |\n\nA finding has a stable `ruleId` (`opf/text-contrast`), a `severity` (`error`, `warning`, `info`), a `category`, a JSON Pointer `path`, a `message`, a `help` sentence and a `definition` link to the rule below or to the schema constraint. It may carry `slide` and `slideId` (zero-based index and id of the slide it belongs to), `location` (from text), `measured` (the numbers behind it: ratios, sizes, counts), `suggestions` (catalog records that exist, never silently applied), `lookup` (arguments for `opf schema` or `opf catalog`, not a shell string), `validation` (the raw schema issue, with every union alternative) and `fixes`.\n\n`checks` says what each part did: `syntax` (`checked` for text), `schema`, `references`, `policy`, `accessibility` and `content` are `checked` or `not-run`; `layout` is `not-run`, `estimated` (text widths are core's portable estimate) or `measured` (the host passed `fonts.textMeasurement`); background picture pixels are never read (`backgroundPixels`), image resolution reads only embedded `data:` images (`imageBytes`), and native PowerPoint rendering is not checked (`nativeExport`).\n\nA document the schema rejects is not audited: its accessibility, layout and content rules do not run, and its format errors are reported even when `only` did not ask for them, so the report says why nothing else ran (switch them off with `ignore: ['format']`).\n\n## Options\n\n```js\nvalidate(presentation, {\n only: ['format', 'opf/missing-alt-text'], // run just these rules or categories\n ignore: ['layout'], // do not run these\n severity: { 'opf/text-contrast': 'error', content: 'off' }, // per rule or category: error, warning, info or off\n ignorePaths: [{ rule: 'opf/text-contrast', path: '/slides/3' }], // an accepted exception, by JSON Pointer prefix\n catalogs: [gallery, acmeCatalog], // the catalogs the host registered (the first is the default); nothing is fetched\n contracts, // host policy (below)\n thresholds: { contrastNormal: 7 }, // see DEFAULT_VALIDATION_THRESHOLDS\n fonts, // the host's fonts: layout rules read fonts.textMeasurement\n chartPalette: ['#0072B2', '#E69F00'], // the engine's chart colours\n template: true, values: { client: 'Acme' }, // check as a template, or fill variables first\n});\n```\n\n- **Names.** `only`, `ignore`, `severity` and `ignorePaths` take full rule ids (`opf/text-contrast`), bare rule names (`text-contrast`) or category names (`accessibility`). No rule is named like a category. A rule beats its category in `severity`. An unknown rule, category, threshold or option throws a `TypeError`, so a typo cannot silently disable a check.\n- **`fonts`.** An object with a `textMeasurement` (the renderer's `loadFonts()` result has one). Without it the layout rules use core's portable estimate and `checks.layout` says `estimated`; with it, `measured`. `textMeasurement` may also be a function of the zero-based slide index, for a script-aware measurement per slide. When the measurement has a `forScripts(profile)` method (a script planner), the layout rules ask it for each slide's script-aware measurement, built from the slide's `resolveScriptFonts` profile and a run language's own profile, the way the renderer and the exporter measure, so Arabic or Japanese text is measured in its script face rather than the Latin face its style names.\n- **`template` and `values`.** A deck that declares content variables is checked as the deck it resolves to; see [templates and variables](templates-and-variables.md).\n- **Thresholds.** `DEFAULT_VALIDATION_THRESHOLDS` holds `contrastNormal`, `contrastLarge`, `minFontSizePt`, `minImagePpi` and `minSeriesColorDifference`; each rule below lists the ones it reads.\n\n`validationRules` lists every rule with its metadata (`id`, `name`, `category`, `severity`, `cost`, `summary`, `rationale`, `standard`, `approximations`, `thresholds`), and `findValidationRule(name)` finds one by id or bare name.\n\n`assertValid(value, options?)` throws `OPFValidationError`, carrying the `report` and its error `findings`, unless `validate(value, options)` is valid, and tells TypeScript that `value` is a `Presentation`. It takes a parsed value, not text.\n\n`validateCatalogRecord(kind, record)` and `assertValidCatalogRecord(kind, record)` check one catalog record (an audience, theme, layout, font scheme and so on) against its schema. The report has the same shape; its findings are `format`, and a record that links to an unknown id only warns.\n\n## Pass `fonts`, otherwise the estimate\n\nThe layout rules (`opf/text-overflow`, `opf/min-font-size`, reading order, image resolution) depend on how wide the text is. `validate` measures it with `options.fonts`, the handle that opf-render's `loadFonts()` returns:\n\n```js\nconst fonts = await loadFonts({ pack: 'office' });\nconst report = validate(deck, { fonts }); // report.checks.layout === 'measured'\n```\n\nPass the same `fonts` handle to preview (`toSvg`), pagination (`paginate`) and export (`toPng`, `toPdf`, `toPptx`), so that validation measures with the faces the preview draws and a finding means what the slide shows. The same handle gives identical geometry across engines; opf-pptx's `layout-parity` test checks that the exported PowerPoint paragraphs agree with the SVG preview and with core's composed items, with and without `fonts`. See [Pass `fonts`, otherwise the estimate](font-fidelity.md#pass-fonts-otherwise-the-estimate) for the full list.\n\nWithout `fonts` the rules use core's built-in estimate and `checks.layout` says `estimated`: base widths are 0.54 em per character, 0.62 em for capitals and digits, 0.32 em for a space and 1 em for CJK characters. Spacing combining marks outside Latin/CJK contribute; nonspacing/enclosing marks do not. Robust script factors calibrated against the FF-44 corpus reduce underestimation for other scripts; see [script width evidence](evidence/script-width-estimates/README.md). It needs no font files and is stable from run to run, but an estimate can be wider or narrower than a particular face, so an estimated overflow finding (or its absence) is a first pass. `opf validate` on the command line loads no fonts, so its layout checks are estimated too. Node `convert` and the CLI's `opf convert` and `opf paginate` prepare an office-pack handle for you; a library call to `validate` never loads fonts itself.\n\n## Cost\n\n`validate` is lazy: it builds only what the rules you ask for need. Measured with Node 24 on three bundled example decks (milliseconds per call, warm; your machine will differ, the ratios hold):\n\n| Call | `minimal` (3 slides) | `renewal-save-plan` (6 slides, 34 KB) | `broadband-public-hearing-deck` (10 slides, 41 KB) |\n| --- | --- | --- | --- |\n| `only: ['format']` | 0.06 | 0.23 | 0.64 |\n| `only: ['format', 'references']` | 0.7 | 5.2 | 7.6 |\n| everything (the default) | 1.7 | 24 | 32 |\n\n`only: ['format']` costs what the schema check alone costs; it builds no layout. The references walk reads the schema alongside the document and costs about ten times as much. The rules of `layout` (and `opf/text-contrast`, `opf/text-on-image` and `opf/reading-order`) compose every slide and are most of the cost of a full run. Each rule's `cost` says which it needs: `syntax` reads the JSON text, `structure` reads the document alone, `composition` builds slide layouts.\n\nThe default runs everything, so an agent or CI job never gets \"valid\" for a deck that overflows, and the hot paths say what they need: pagination, conversion, patch validation and the editor's per-edit check use `{ only: ['format'] }`; the CLI's write, render, export and import commands use `{ only: ['format', 'references'] }`, so a contrast warning never blocks an export.\n\n## Quick fixes\n\nA fix is a suggestion that core never applies. A host such as the editor's Review panel shows it and applies it through its own undoable edit path.\n\n- A `patch` fix (`kind: \"patch\"`, the default) carries RFC 6902 JSON Patch operations in `patch` (`add`, `remove`, `replace`, `move`, `copy`, `test`, with JSON Pointer paths). `safe: true` means the change cannot alter what the content says: for example switching a failing text colour to the colour the theme chose for that background.\n- A `focus` fix (`kind: \"focus\"`) names the field the author must fill in (`focus.path`, `focus.field`: `alt`, `title`, `text`, `link`, `language`, `fontSize`).\n- `safe: false` patches change meaning or appearance and need a deliberate click, for example marking a picture decorative with an empty `alt`.\n\n`title` says what applying the fix does.\n\n## Catalog context and contracts\n\nContent references resolve the way every engine resolves them ([catalogs](default-catalog.md)): `catalogs.custom`, then the records embedded under `catalogs.default`, then the catalog the host registered (the `catalogs` option) for its source; `name:id` in `catalogs.<name>`, then the catalog registered for its source. A reference that resolves nowhere is a warning (`opf/unresolved-reference`), never an error; a prefix that names no group is a format error (`opf/undeclared-catalog`): it can never resolve, so engines reject the document at their format check (`validate(document, { only: [\"format\"] })`) instead of drawing a fallback. A `catalogs` option that is not an array of registered catalogs throws `OPFCatalogsOptionError` (`code: \"invalid-catalogs\"`) at every entry point. Nothing is fetched, and without the `catalogs` option only the records the document embeds resolve. Free-form audience and purpose descriptions (any string that is not a bare id or `name:id`) and arbitrary `extensions` data are never references; the `language` tag, `chart.type` and the `socials` keys are engine vocabularies the schema checks. Embedded records, and registered records the document uses, are validated against their companion schema (`opf/catalog-record`). A record under `catalogs.default` or a named group that the catalog registered for the group's source does not publish is `opf/catalog-record-not-in-source` (move it to `catalogs.custom`); with no catalog registered for that source the check is silent. Suggestions name records actually present in the checked context (`origin` `document` or `registered`).\n\nAn `asset:` reference that names no entry of the document's `assets` registry is an error (`opf/asset-reference`), and so is a cycle of `asset:` sources (`opf/asset-cycle`); resource bytes are never fetched. Chart and table data findings keep their code as the rule id (`opf/dataset-unknown`, `opf/chart-value-not-numeric`, ...), and a `opf/chart-value-not-numeric` cell whose column is written in one display style (`\"12%\"`, `\"$1,234\"`) carries `fixes` ([chart and table data](chart-table-data.md#migration-help)). Citation and caption errors keep theirs too (`opf/cite-unknown-reference`, `opf/reference-id-duplicate`, `opf/cite-unsupported-location`, `opf/caption-unsupported-payload`).\n\nA narrative the document defines is a record in `catalogs.custom.narratives`, and `narrative` is always a reference string. When the narrative resolves, a `slides[].beat` id it does not define is `opf/unknown-beat`, a root `duration` outside the record's `duration` range is `opf/duration-outside-narrative`, an embedded record whose `duration.min` exceeds `duration.max` is `opf/narrative-duration-range`, and a beat no slide references is `opf/unused-beat` (only once some slide names a beat). A slide-level `design.theme` whose resolved dimensions differ from the deck's is `opf/slide-theme-dimensions`: a PPTX has one slide size, and a slide's `design` cannot set `dimensions` at all (a schema error). An undeclared `var:<id>` in a solid, gradient-stop or pattern background colour warns like any other colour reference (`opf/variable-reference-unknown`).\n\nHost policy comes from explicit options, never from the document: fields under `extensions` are data and cannot install policy. `contracts` and `catalogs` may live in a local JSON file for the CLI (`--config`).\n\n```json\n{\n \"catalogs\": [\n {\n \"source\": \"pkg:@partner/opf-catalog\",\n \"layouts\": {\n \"partner-title\": {\"name\":\"Partner title\",\"areas\":[[\"title\"]]}\n }\n }\n ],\n \"contracts\": [\n {\n \"path\":\"/slides/*/layout\",\n \"allowedValues\":[\"partner-title\",\"text\"],\n \"message\":\"Use the brand layouts {{allowed}} at {{path}}. See {{file}}.\",\n \"documentation\":\"brand-guide.md#layouts\",\n \"severity\":\"error\"\n }\n ]\n}\n```\n\nContract paths are JSON Pointer patterns: `~0` escapes `~`, `~1` escapes `/`, and a whole `*` segment matches one property or array index. Contracts check existing fields; they do not require an omitted field or insert defaults. Allowed values are JSON primitives. Optional message placeholders are `{{path}}`, `{{value}}`, `{{allowed}}`, and `{{file}}`. A contract's severity is `error` unless it says otherwise. Invalid or misspelled configuration keys fail instead of being ignored. Messages and catalog labels are data, not executable instructions.\n\n## The CLI\n\n`opf validate <file|-> [--config <file>] [--only <list>] [--ignore <list>] [--fail-on <error|warning|info>] [--format <json|text>] [--list-rules]`\n\nThe JSON report (the default) is the `validate` report plus the source's SHA-256 (`sha256`), the installed core version (`opfVersion`) and, with `--config`, the configuration file's path and hash (`context`). `--format text` prints one line per finding (`file:line:column severity rule message`), the JSON Pointer path, a hint and any fix, then a summary. `--list-rules` prints every rule (`--format json` with its full metadata).\n\nA file ending `.yaml` or `.yml` (or stdin with `--from yaml`) is read as YAML through `fromYaml`, so the report is the same and every finding is located in the YAML; a YAML syntax or dialect error is a `yaml/<rule>` finding (`format`, exit 1). A file ending `.opf.md` (or stdin with `--from md`) is read as a [Markdown deck](markdown.md#markdown-decks-in-every-command) the same way, and a Markdown error is a `markdown/<rule>` finding. A plain `.md` file, and any other text that does not start with `{` or `[`, needs `--from`.\n\n`--only` and `--ignore` take comma-separated rule ids, bare names or category names. `--config` takes an explicit local JSON file, never `-`, holding `{ catalogs, contracts, severity, only, ignore, ignorePaths, thresholds, chartPalette }`; `--only` replaces the file's `only` and `--ignore` adds to its `ignore`.\n\n| Exit | Meaning |\n| --- | --- |\n| 0 | No finding at or above `--fail-on` (default `error`) |\n| 1 | At least one finding at or above `--fail-on`, or a document that is not valid JSON (or YAML) |\n| 2 | Usage, configuration or I/O error |\n\n`--fail-on` is the one way to say \"fail on warnings\" on every command that checks a document: `--fail-on warning`. Other commands (`create`, `edit`, `fill`, `paginate`, `bundle`, `ingest`, `render`, `export`, `import`, `from-md`, `from-yaml`) check only `format` and `references` of what they write and take `--fail-on` the same way.\n\n## How contrast is computed\n\n`opf/text-contrast` reasons about the colours the preview draws, because a check against a different colour than the one on screen is worse than none.\n\n1. **Background.** Design resolves per field: slide design, deck design, theme, engine default. The background is a theme slot or colour (`solid`), the card surface for content on a `contentBox` card, a table cell's fill, a gradient or a pattern. Solid, gradient-stop and pattern colours are read as literal hex colours, as the preview reads them (anything else falls back as it does there). Translucent backgrounds are composited over white.\n2. **Text colour.** Titles, body, lists and tags use the scheme's `dark1`, or `light1` when the background is dark (luminance below 0.179). Furniture and quote attributions use the muted colour, metric values the primary colour, and explicit run and table colours the author's `ColorRef` resolved the same way the renderer resolves it. As the preview does, a gradient background has no single colour and counts as light, so a dark gradient behind default text is reported.\n3. **Gradients.** The gradient is sampled on a 5 by 5 grid over the area the text covers (the lines' measured ink, aligned as the text is), using the preview's gradient geometry, and the worst colour decides. A pattern contributes both of its colours.\n4. **Pictures.** Pixels are never read. A background picture is bounded by a grey ramp from black to white, composited through the picture opacity and any full-frame overlay of the image background (`design.background.overlay`). Text passes only if every step passes; otherwise `opf/text-on-image` says the result cannot be guaranteed. An overlay limited to an edge band is not counted.\n5. **Ratio and size.** The WCAG 2.x relative-luminance contrast ratio is compared with 4.5:1, or 3:1 for large text: at least 18 pt, or 14 pt and bold. Sizes are the fitted sizes, expressed at the 13.33 by 7.5 in reference slide (96 px per inch). The default body size, 18.75 pt, is large text under WCAG, so default body text passes at 3:1; set `contrastLarge` to 4.5 for a stricter policy.\n\nApproximations: anti-aliasing, text shadows, font weight and the exact glyph coverage are not modelled; chart and code text are not checked (charts pick label colours against their surface; code uses its own panel colours). See each rule below for what it cannot see.\n\n## Layout rules use composition\n\nOverflow, minimum type size, reading order and image resolution use `composeSlide` at the deck's slide size with the resolved font scheme, the same composition the preview and the PPTX export consume. Without `fonts.textMeasurement` the text widths are core's portable estimate and can differ from the real fonts by a few percent; hosts that load fonts (the editor, the renderer) pass their measurement for font-exact results. A composition with `overflow: \"error\"` fails at composition time; `validate` still computes the geometry, relaxes the setting, and reports the overflow as a **warning**, so that `valid` never depends on font metrics. Rendering and export still refuse such a slide when it does not fit, and a host that wants the finding to fail promotes it with `severity: { 'opf/text-overflow': 'error' }`.\n\nPromoted region keys (`left`, `center`, `right`, `top:left` and the rest) are composed in visual reading order (rows from top to bottom, then along the row), whatever order the keys are written in. The PPTX export writes shapes in that order, which is the reading order of assistive technology; `opf/reading-order` checks that the composed order still matches the geometry (the same `visualReadingOrder` that composes the regions). Authoring with `blocks` (an ordered list) keeps the reading order equal to the visual order.\n\n## Templates and variables\n\n`opf/variable-unfilled` is one rule for a required variable that has no value. In a deck that is not a template it is an error (the deck is incomplete), so `valid` is false; in a template (`\"template\": true` in the document, or `template: true` in the options) it is a warning, because a template is incomplete on purpose; `unfilledVariables` lists the ids either way. In a document that declares no content variables, a `{{id}}` token left in a string (outside `variables`, `extensions`, `catalogs` and `assets`) is a warning. A `var:` colour that names no declared variable is `opf/variable-reference-unknown`. It is a `format` rule, so `only: ['format']` (what pagination, patching, conversion and every write command run) rejects a deck with an unfilled required variable. Fill a template with `opf fill` or `resolveVariables`, or pass `values`, to clear the findings.\n\n## Rules\n\nThe reference below is generated from the rule registry (`validationRules`); `node scripts/build-validate-docs.mjs --check` (and a core test) fails when it drifts.\n\n<!-- validate-rules:start (generated by scripts/build-validate-docs.mjs; do not edit by hand) -->\n\n| Rule | Category | Default | Cost | What it reports |\n| --- | --- | --- | --- | --- |\n| [`opf/json-syntax`](#opfjson-syntax) | Format | error | syntax | The text is not strict JSON. |\n| [`opf/duplicate-key`](#opfduplicate-key) | Format | error | syntax | A JSON object repeats a key. |\n| [`opf/schema`](#opfschema) | Format | error | structure | The document breaks the OPF schema or a structural rule the schema cannot express. |\n| [`opf/reference-id-duplicate`](#opfreference-id-duplicate) | Format | error | structure | Two references share an id. |\n| [`opf/cite-unknown-reference`](#opfcite-unknown-reference) | Format | error | structure | A citation names a reference that does not exist. |\n| [`opf/cite-unsupported-location`](#opfcite-unsupported-location) | Format | error | structure | A citation or footnote sits where no engine draws it. |\n| [`opf/caption-unsupported-payload`](#opfcaption-unsupported-payload) | Format | error | structure | A caption is on a payload that cannot carry one. |\n| [`opf/image-option-unsupported-payload`](#opfimage-option-unsupported-payload) | Format | error | structure | An image option is on a payload that is not an image. |\n| [`opf/image-placement-invalid`](#opfimage-placement-invalid) | Format | error | structure | An image placement cannot be honoured. |\n| [`opf/layout-placeholder-group`](#opflayout-placeholder-group) | Format | error | structure | A layout record's placeholder group breaks a nesting rule. |\n| [`opf/dataset-unknown`](#opfdataset-unknown) | Format | error | structure | A chart or table names a dataset that does not exist. |\n| [`opf/dataset-field-unknown`](#opfdataset-field-unknown) | Format | error | structure | A table asks a dataset for a column it does not have. |\n| [`opf/data-column-duplicate`](#opfdata-column-duplicate) | Format | error | structure | A dataset or table has two columns with the same name. |\n| [`opf/chart-mapping-unknown-column`](#opfchart-mapping-unknown-column) | Format | error | structure | A chart mapping names a column the data does not have. |\n| [`opf/number-format-invalid`](#opfnumber-format-invalid) | Format | error | structure | A number format cannot be read. |\n| [`opf/variable-invalid-value`](#opfvariable-invalid-value) | Format | error | structure | A variable value does not match its declared type. |\n| [`opf/variable-unknown-builtin`](#opfvariable-unknown-builtin) | Format | error | structure | A token names a built-in variable that does not exist. |\n| [`opf/chart-highlight-unknown-name`](#opfchart-highlight-unknown-name) | Format | error | structure | A chart highlight names a series or category the chart does not have. |\n| [`opf/variable-format`](#opfvariable-format) | Format | error | structure | A variable cannot be formatted. |\n| [`opf/variable-unfilled`](#opfvariable-unfilled) | Format | error | structure | A required template variable has no value. |\n| [`opf/run-color-unrecognized`](#opfrun-color-unrecognized) | Format | warning | structure | A text run colour is none of the documented forms. |\n| [`opf/numbering-start-ignored`](#opfnumbering-start-ignored) | Format | warning | structure | A list entry sets `start` where nothing is numbered. |\n| [`opf/slide-size-out-of-range`](#opfslide-size-out-of-range) | Format | warning | structure | The slide size is outside PowerPoint's 1 to 56 inch range. |\n| [`opf/undeclared-catalog`](#opfundeclared-catalog) | Format | error | structure | A reference names a catalog group the document does not declare. |\n| [`opf/asset-reference`](#opfasset-reference) | References | error | structure | An `asset:` reference names an asset that is not in the registry. |\n| [`opf/asset-cycle`](#opfasset-cycle) | References | error | structure | Asset references form a cycle. |\n| [`opf/catalog-record`](#opfcatalog-record) | References | error | structure | An embedded or registered catalog record is invalid. |\n| [`opf/layout-template`](#opflayout-template) | References | error | structure | A layout template's grid is malformed. |\n| [`opf/layout-region`](#opflayout-region) | References | error | structure | A layout template's regions do not match its areas. |\n| [`opf/unresolved-reference`](#opfunresolved-reference) | References | warning | structure | A content reference resolves nowhere. |\n| [`opf/catalog-record-not-in-source`](#opfcatalog-record-not-in-source) | References | warning | structure | A record embedded under a catalog group is not in that catalog. |\n| [`opf/unused-reference`](#opfunused-reference) | References | warning | structure | A reference is never cited. |\n| [`opf/unused-dataset`](#opfunused-dataset) | References | warning | structure | A dataset is never used. |\n| [`opf/unknown-beat`](#opfunknown-beat) | References | warning | structure | A slide names a beat its narrative does not define. |\n| [`opf/slide-theme-dimensions`](#opfslide-theme-dimensions) | References | warning | structure | A slide theme sets a slide size that differs from the deck. |\n| [`opf/variable-unknown`](#opfvariable-unknown) | References | warning | structure | A `{{token}}` names a variable that is not declared. |\n| [`opf/variable-unknown-value`](#opfvariable-unknown-value) | References | warning | structure | A value was supplied for a variable that is not declared. |\n| [`opf/variable-unused`](#opfvariable-unused) | References | warning | structure | A variable is declared but never used. |\n| [`opf/variable-reference-unknown`](#opfvariable-reference-unknown) | References | warning | structure | A `var:` colour names a variable that is not declared. |\n| [`opf/contract`](#opfcontract) | Policy | error | structure | A field is outside the values a host policy allows. |\n| [`opf/text-contrast`](#opftext-contrast) | Accessibility | warning | composition | Text colour has too little contrast against the background it sits on. |\n| [`opf/text-on-image`](#opftext-on-image) | Accessibility | info | composition | Text sits on a background picture whose pixels cannot be measured. |\n| [`opf/missing-alt-text`](#opfmissing-alt-text) | Accessibility | warning | structure | A picture has no alt text and is not marked decorative. |\n| [`opf/poor-alt-text`](#opfpoor-alt-text) | Accessibility | info | structure | Alt text is a file name, a URL, a generic word or very long. |\n| [`opf/missing-slide-title`](#opfmissing-slide-title) | Accessibility | warning | structure | A slide has no title. |\n| [`opf/duplicate-slide-title`](#opfduplicate-slide-title) | Accessibility | info | structure | Two slides have the same title. |\n| [`opf/reading-order`](#opfreading-order) | Accessibility | warning | composition | The order content is read differs from the order it appears on the slide. |\n| [`opf/link-text`](#opflink-text) | Accessibility | warning | structure | Link text does not say where the link goes. |\n| [`opf/chart-color-only`](#opfchart-color-only) | Accessibility | info | structure | Chart series may be indistinguishable without colour vision. |\n| [`opf/chart-text-alternative`](#opfchart-text-alternative) | Accessibility | info | structure | A chart has no text alternative, or is marked decorative. |\n| [`opf/missing-language`](#opfmissing-language) | Accessibility | info | structure | The presentation does not declare its language. |\n| [`opf/chart-option-adapted`](#opfchart-option-adapted) | Layout | warning | structure | A chart option cannot be shown by the chart type. |\n| [`opf/text-overflow`](#opftext-overflow) | Layout | warning | composition | Text or a table does not fit its space at the smallest allowed size. |\n| [`opf/unresolved-content`](#opfunresolved-content) | Layout | warning | composition | Content cannot be drawn as authored. |\n| [`opf/layout-failed`](#opflayout-failed) | Layout | warning | composition | The slide layout could not be computed. |\n| [`opf/min-font-size`](#opfmin-font-size) | Layout | warning | composition | Text is drawn smaller than the readable minimum. |\n| [`opf/region-unknown`](#opfregion-unknown) | Layout | warning | composition | A block is pinned to a region the layout does not have. |\n| [`opf/region-kind`](#opfregion-kind) | Layout | warning | composition | A block is pinned to a region that does not accept its kind. |\n| [`opf/region-full`](#opfregion-full) | Layout | warning | composition | A block is pinned to a region that is already full. |\n| [`opf/layout-unplaced`](#opflayout-unplaced) | Layout | warning | composition | No region of the layout has room for a block. |\n| [`opf/font-outside-scheme`](#opffont-outside-scheme) | Layout | warning | structure | Text uses a font family that is not in the deck's font scheme. |\n| [`opf/image-resolution`](#opfimage-resolution) | Layout | warning | composition | An image has too few pixels for the size it is shown at. |\n| [`opf/chart-value-not-numeric`](#opfchart-value-not-numeric) | Content | warning | structure | A chart value is text, not a number. |\n| [`opf/chart-mapping-adapted`](#opfchart-mapping-adapted) | Content | warning | structure | A chart mapping entry is ignored. |\n| [`opf/chart-highlight-adapted`](#opfchart-highlight-adapted) | Content | warning | structure | A chart highlight is adapted by the chart type. |\n| [`opf/code-highlight-out-of-range`](#opfcode-highlight-out-of-range) | Content | warning | structure | A code highlight marks a line the code does not have. |\n| [`opf/code-highlight-range-reversed`](#opfcode-highlight-range-reversed) | Content | warning | structure | A code highlight range ends before it starts. |\n| [`opf/variable-builtin-missing`](#opfvariable-builtin-missing) | Content | warning | structure | A built-in variable has no value in this document. |\n| [`opf/duration-outside-narrative`](#opfduration-outside-narrative) | Content | warning | structure | The target duration is outside the range the narrative suits. |\n| [`opf/narrative-duration-range`](#opfnarrative-duration-range) | Content | warning | structure | An inline narrative duration range has min greater than max. |\n| [`opf/placeholder-text`](#opfplaceholder-text) | Content | warning | structure | Placeholder text was left in the deck. |\n| [`opf/empty-text`](#opfempty-text) | Content | info | structure | A text field is present but empty. |\n| [`opf/unused-beat`](#opfunused-beat) | Content | info | structure | A beat of the narrative has no slide that references it. |\n| [`opf/empty-slide`](#opfempty-slide) | Content | warning | structure | A slide has no content at all. |\n| [`opf/language-tag`](#opflanguage-tag) | Content | warning | structure | A language tag is malformed, uses an unassigned region or is not in canonical case. |\n\n## Format rules\n\n*Is it well-formed OPF?*\n\n### `opf/json-syntax`\n\nDefault severity: **error**. Cost: syntax. The text is not strict JSON.\n\n**Why.** Every OPF tool reads JSON. A misplaced comma, quote, comment or trailing comma makes the file unreadable, and the range says where.\n\n**Basis.** RFC 8259 (strict JSON: no comments, no trailing commas)\n\n**Approximations.** Only text input is checked. A string is always read as JSON; a text that looks like YAML or Markdown gets a hint to use fromYaml or fromMarkdown first.\n\n### `opf/duplicate-key`\n\nDefault severity: **error**. Cost: syntax. A JSON object repeats a key.\n\n**Why.** JSON.parse keeps only the last value of a repeated key, so the earlier one disappears without a trace. The check compares escaped and literal spellings of the same key.\n\n**Basis.** RFC 8259 section 4 (names within an object should be unique)\n\n### `opf/schema`\n\nDefault severity: **error**. Cost: structure. The document breaks the OPF schema or a structural rule the schema cannot express.\n\n**Why.** The schema is the contract every engine, exporter and editor relies on. A field of the wrong type, a missing required field, an unknown property or content fields that cannot be mixed make the document ambiguous or undrawable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** All alternatives of a union are reported, with the failing schema location in `validation`. A required variable with no value is reported by `opf/variable-unfilled`, in the same category.\n\n### `opf/reference-id-duplicate`\n\nDefault severity: **error**. Cost: structure. Two references share an id.\n\n**Why.** A citation names a reference by id; a repeated id makes it ambiguous which entry is cited.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unknown-reference`\n\nDefault severity: **error**. Cost: structure. A citation names a reference that does not exist.\n\n**Why.** A cited id with no entry in `references` has nothing to show in the footnote area.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/cite-unsupported-location`\n\nDefault severity: **error**. Cost: structure. A citation or footnote sits where no engine draws it.\n\n**Why.** `cite` and `footnote` apply to runs in text, bullets and list items; table cells, captions, and reference and footnote texts have no marker.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/caption-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. A caption is on a payload that cannot carry one.\n\n**Why.** A caption belongs to exactly one image, chart, table or video; a group, text or code block has no place to draw it.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-option-unsupported-payload`\n\nDefault severity: **error**. Cost: structure. An image option is on a payload that is not an image.\n\n**Why.** `fit`, `focus`, the image treatments (`shape`, `cornerRadius`, `border`, `opacity`, `recolor`, `overlay`, `aspectRatio`) and `placement` describe how a picture is drawn; a group, text or chart block has no picture for them to apply to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/image-placement-invalid`\n\nDefault severity: **error**. Cost: structure. An image placement cannot be honoured.\n\n**Why.** A placed image bleeds to a slide edge and the rest of the slide composes beside it. Only a top-level block (slides.N.blocks.I) can do that, and a slide edge holds one placed image; a block in a group or a promoted region, or a second block on the same edge, has nowhere to go.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/layout-placeholder-group`\n\nDefault severity: **error**. Cost: structure. A layout record's placeholder group breaks a nesting rule.\n\n**Why.** A placeholder group arranges body regions inside one cell of its layout record. Groups nest at most three levels inside a record, and a group holds body regions and groups only: the headings (title, subtitle, tag) stay at the record's top level, where the engine places them. Composition cannot use a record that breaks either rule.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-unknown`\n\nDefault severity: **error**. Cost: structure. A chart or table names a dataset that does not exist.\n\n**Why.** The data cannot be drawn without the dataset it refers to.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/dataset-field-unknown`\n\nDefault severity: **error**. Cost: structure. A table asks a dataset for a column it does not have.\n\n**Why.** The column cannot be shown when the dataset has no column of that name.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/data-column-duplicate`\n\nDefault severity: **error**. Cost: structure. A dataset or table has two columns with the same name.\n\n**Why.** Series, mappings and fields address columns by name, so a repeated name makes them ambiguous.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-unknown-column`\n\nDefault severity: **error**. Cost: structure. A chart mapping names a column the data does not have.\n\n**Why.** The series cannot be built from a column that is not there.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/number-format-invalid`\n\nDefault severity: **error**. Cost: structure. A number format cannot be read.\n\n**Why.** A format code that no engine can parse is drawn as the plain number or fails to export.\n\n**Basis.** Excel number format codes, as far as OPF supports them\n\n### `opf/variable-invalid-value`\n\nDefault severity: **error**. Cost: structure. A variable value does not match its declared type.\n\n**Why.** A number variable given text, or a date given a non-date, cannot be filled into the deck.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-builtin`\n\nDefault severity: **error**. Cost: structure. A token names a built-in variable that does not exist.\n\n**Why.** The `deck.`, `speaker.`, `organization.` and `slide.` names (and `speakers`) are reserved for built-in variables; a name under them that is not a built-in can never be filled, so the token would show literally. The slide-scoped `slide.number`, `slide.section` and `deck.slideCount` are inline tokens only, so `var:slide.*` and `var:deck.slideCount` are reported too; organization logos are whole-field references only (`var:organization.logo.icon`), so a `{{organization.logo}}` token, an unknown logo shape or an unknown organization id is reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-unknown-name`\n\nDefault severity: **error**. Cost: structure. A chart highlight names a series or category the chart does not have.\n\n**Why.** `chart.highlight` emphasises marks by name; a name that matches no series or category in the chart data highlights nothing, so the chart is not drawn as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-format`\n\nDefault severity: **error**. Cost: structure. A variable cannot be formatted.\n\n**Why.** A number or date format that does not apply to the value leaves the token unreadable.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unfilled`\n\nDefault severity: **error**. Cost: structure. A required template variable has no value.\n\n**Why.** A variable a deck declares as required, with no value, is template scaffolding: `{{name}}` tokens show up literally and colour variables fall back to a default. A normal deck must be filled, so this is an error and the deck is not valid; a template (`template: true`) is incomplete on purpose, so it is a warning.\n\n**Approximations.** Required variables are those the variable machinery reports as unfilled for the document (declared, no value, not `required: false`, not given by the `values` option). For a document that declares no content variables, `{{id}}` / `{{id|format}}` tokens (`{{` escapes) left in any string outside `variables`, `extensions`, `catalogs` and `assets` are reported as warnings. `var:` colour references with no declaration are reported as `opf/variable-reference-unknown`. Fill a template with `opf fill` or `resolveVariables` to clear the findings.\n\n### `opf/run-color-unrecognized`\n\nDefault severity: **warning**. Cost: structure. A text run colour is none of the documented forms.\n\n**Why.** Run colours are open strings so imported decks stay valid, but a value that is not a hex colour, a colour-scheme name or a `var:` reference falls back to the theme colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/numbering-start-ignored`\n\nDefault severity: **warning**. Cost: structure. A list entry sets `start` where nothing is numbered.\n\n**Why.** A start value only restarts an auto-number; without `numbering` on the payload it has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/slide-size-out-of-range`\n\nDefault severity: **warning**. Cost: structure. The slide size is outside PowerPoint's 1 to 56 inch range.\n\n**Why.** The schema accepts any positive custom `widthInches` and `heightInches`, but a PPTX stores the size as `p:sldSz` with each side between 914400 and 51206400 EMU, which is 1 to 56 inches. A custom size outside that range exports a file that does not conform to the standard, which PowerPoint may reject or clamp. It is a warning because the document is still correct OPF and the preview draws it.\n\n**Basis.** ECMA-376 / ISO/IEC 29500-1 section 19.2.1.39 (p:sldSz) and section 19.7.17 (ST_SlideSizeCoordinate): 914400 to 51206400 EMU\n\n**Approximations.** Checks the deck's resolved size: `design.dimensions`, else the resolved theme's `dimensions`. Inches are converted to EMU as the PPTX exporter does (`Math.round(inches * 914400)`), so a side of exactly 1 in or 56 in passes. A slide-level theme with another size is `opf/slide-theme-dimensions`. Presets are always in range. PowerPoint's own behaviour at the limits was not measured.\n\n### `opf/undeclared-catalog`\n\nDefault severity: **error**. Cost: structure. A reference names a catalog group the document does not declare.\n\n**Why.** The prefix of a `name:id` reference names the group of `catalogs` it resolves in. A prefix with no group can never resolve, whatever the host registers, so it is a format error: engines reject the document at their format check instead of drawing a fallback.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## References rules\n\n*Does everything it points at resolve?*\n\n### `opf/asset-reference`\n\nDefault severity: **error**. Cost: structure. An `asset:` reference names an asset that is not in the registry.\n\n**Why.** An image, logo or video that points at a missing asset cannot be drawn or exported. The check reads the document's own registry only; no file or URL is fetched.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/asset-cycle`\n\nDefault severity: **error**. Cost: structure. Asset references form a cycle.\n\n**Why.** An asset whose source points back at itself never resolves to a file.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/catalog-record`\n\nDefault severity: **error**. Cost: structure. An embedded or registered catalog record is invalid.\n\n**Why.** A record that fails its companion schema cannot be resolved. Invalid records are left out of the lookup, so the references to them are reported too.\n\n**Basis.** spec/schemas/<kind>.schema.json\n\n### `opf/layout-template`\n\nDefault severity: **error**. Cost: structure. A layout template's grid is malformed.\n\n**Why.** A layout (OPF 0.19) is a grid of named areas. Engines cannot compose a grid whose rows have different lengths, whose names do not form one filled rectangle each, whose column or row sizes do not match the grid, that is larger than 12 x 12, or that holds a word that is not a name or \".\". composeSlide refuses such a record, so a slide that names it composes automatically.\n\n**Basis.** spec/schemas/layout.schema.json and the area rules of OPF 0.19 (docs/layout-templates.md)\n\n**Approximations.** Checked on every layout record the document embeds and every registered record it uses, once the record passes its schema.\n\n### `opf/layout-region`\n\nDefault severity: **error**. Cost: structure. A layout template's regions do not match its areas.\n\n**Why.** Every body area of a layout needs a region that says what it accepts, and every region needs an area. Region names may not be title, subtitle, tag, auto or a promoted-region word (left, center, right, top, middle, bottom), so a Markdown block's region= stays unambiguous; a subtitle area needs a title area; a region with flow 'none' holds one block; overflowRegion must name a region. composeSlide refuses such a record.\n\n**Basis.** spec/schemas/layout.schema.json and the region rules of OPF 0.19 (docs/layout-templates.md)\n\n### `opf/unresolved-reference`\n\nDefault severity: **warning**. Cost: structure. A content reference resolves nowhere.\n\n**Why.** A layout, theme, colour scheme, font scheme, narrative, audience, purpose or tone reference that neither the document embeds nor a registered catalog defines falls back: a slide composes automatically, a design uses the engine default. A strict export fails instead. A slide with no layout is automatic composition and is never reported.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Resolved like every engine resolves it: catalogs.custom, then the records embedded under catalogs.default, then the catalog registered for its source (the first registered catalog when `default` is omitted); `name:id` in catalogs.<name>, then the catalog registered for its source. Nothing is fetched. Free-form audience and purpose text is not a reference.\n\n### `opf/catalog-record-not-in-source`\n\nDefault severity: **warning**. Cost: structure. A record embedded under a catalog group is not in that catalog.\n\n**Why.** A record under catalogs.default or a named group says it came from the catalog its source names. When the catalog the host registered for that source has no record of that kind and id, the record is the document's own and belongs in catalogs.custom, where an update from the catalog never looks for it. Rendering is unchanged: the embedded record still wins.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** Checked only against a catalog the host registered for the group's source (the first registered catalog for an omitted default.source); with none registered for it the check is silent. A record the catalog has with different content is an update difference (updateFromCatalog), not this finding. catalogs.custom is never checked.\n\n### `opf/unused-reference`\n\nDefault severity: **warning**. Cost: structure. A reference is never cited.\n\n**Why.** A listed reference that no run cites is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unused-dataset`\n\nDefault severity: **warning**. Cost: structure. A dataset is never used.\n\n**Why.** A dataset no chart or table references is not drawn anywhere.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/unknown-beat`\n\nDefault severity: **warning**. Cost: structure. A slide names a beat its narrative does not define.\n\n**Why.** `slides[].beat` links a slide to a step of the narrative plan. A beat id the narrative does not have links to nothing; nothing is drawn from a beat.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n**Approximations.** The narrative resolves like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked.\n\n### `opf/slide-theme-dimensions`\n\nDefault severity: **warning**. Cost: structure. A slide theme sets a slide size that differs from the deck.\n\n**Why.** A PPTX has one slide size. The size a slide-level theme carries is never used: design.dimensions of the deck (or of its theme) wins in every engine, and without them the exporter stops with mixed slide sizes.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown`\n\nDefault severity: **warning**. Cost: structure. A `{{token}}` names a variable that is not declared.\n\n**Why.** An undeclared token is shown literally.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unknown-value`\n\nDefault severity: **warning**. Cost: structure. A value was supplied for a variable that is not declared.\n\n**Why.** The value is never used.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-unused`\n\nDefault severity: **warning**. Cost: structure. A variable is declared but never used.\n\n**Why.** A declaration nothing refers to has no effect.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-reference-unknown`\n\nDefault severity: **warning**. Cost: structure. A `var:` colour names a variable that is not declared.\n\n**Why.** An undeclared colour variable falls back to a default colour.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n\n## Policy rules\n\n*Does it follow the host's house rules?*\n\n### `opf/contract`\n\nDefault severity: **error**. Cost: structure. A field is outside the values a host policy allows.\n\n**Why.** A host that needs brand layouts, fonts or other fixed choices states them as contracts. The default severity is error; a contract can set its own. Document metadata never installs policy.\n\n**Approximations.** Only the `contracts` option supplies policy, and it checks existing fields: a contract does not require an omitted field or insert a default.\n\n\n## Accessibility rules\n\n*Can everyone read it?*\n\n### `opf/text-contrast`\n\nDefault severity: **warning**. Cost: composition. Text colour has too little contrast against the background it sits on.\n\n**Why.** Low-contrast text is hard or impossible to read for people with low vision, colour-vision differences, glare on a projector or a poor display. WCAG sets 4.5:1 for normal text and 3:1 for large text.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Computed on sRGB colours with the WCAG relative-luminance formula, against the background the preview draws: a solid or theme colour, the card surface, a table cell fill, every colour a gradient takes under the text box (sampled on a 5x5 grid, angle respected), or both colours of a pattern. Anti-aliasing, text shadows and font weight are not modelled. Text colour is the preview's (the scheme's text role or dark1 on a light background and light1 on a dark one, chosen from the background luminance, where a gradient or picture background uses the scheme's default background colour), so a default can fail on a dark gradient. A link with no colour of its own is measured in the scheme's hyperlink colour.\n\n### `opf/text-on-image`\n\nDefault severity: **info**. Cost: composition. Text sits on a background picture whose pixels cannot be measured.\n\n**Why.** Contrast over a photograph depends on the photograph. Without a full-frame overlay that guarantees readability for every possible image, the result cannot be certified from the document.\n\n**Basis.** WCAG 2.2 SC 1.4.3 Contrast (Minimum), level AA\n\n**Thresholds.** `contrastNormal` (default 4.5), `contrastLarge` (default 3)\n\n**Approximations.** Core never reads picture pixels. The picture is bounded by a grey ramp from black to white, composited through the image opacity and a full-frame design.background overlay; the text passes only when every step of that ramp passes. An edge-banded overlay is not counted.\n\n### `opf/missing-alt-text`\n\nDefault severity: **warning**. Cost: structure. A picture has no alt text and is not marked decorative.\n\n**Why.** People using a screen reader get nothing for a picture without alternative text. A picture that is purely decorative should say so with an empty alt (alt: \"\"), which is an explicit, reviewed choice instead of an omission.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Checks the alt field of images (image blocks, Slide.image and region images, placed or not), video, organization logos (each shape and background), header/footer images (a logo reference such as var:organization.logo.icon is checked on the organization), quote photos and speaker photos, following asset: references to the assets registry. Whether the text describes the picture well is not judged here (see opf/poor-alt-text). Charts carry `chart.alt` and are checked by opf/chart-text-alternative. Picture backgrounds are decorative unless they carry their own alt, and watermarks are decorative, so neither is checked.\n\n### `opf/poor-alt-text`\n\nDefault severity: **info**. Cost: structure. Alt text is a file name, a URL, a generic word or very long.\n\n**Why.** Alt text such as \"image\", \"IMG_2041.png\" or a 400-character paragraph does not do the job of describing a picture: it is read out and adds noise without information.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** Pattern checks only: file extensions and camera-style names, a bare generic word, a URL, a leading \"image of\", and more than 250 characters. Chart alt text (chart.alt) is checked too: a bare chart word, a URL, a leading \"chart of\" or more than 250 characters. It cannot tell whether a plausible sentence is accurate.\n\n### `opf/missing-slide-title`\n\nDefault severity: **warning**. Cost: structure. A slide has no title.\n\n**Why.** Slide titles are how people using a screen reader, an outline view or keyboard navigation find and tell slides apart; PowerPoint's own accessibility checker reports a missing title too.\n\n**Basis.** WCAG 2.2 SC 2.4.2 Page Titled and SC 2.4.6 Headings and Labels, level AA (PowerPoint: \"Missing slide title\")\n\n**Approximations.** Only the slide-level title field counts. Text that merely looks like a heading inside a block does not.\n\n### `opf/duplicate-slide-title`\n\nDefault severity: **info**. Cost: structure. Two slides have the same title.\n\n**Why.** Identical titles make slides indistinguishable in an outline or a screen reader's slide list.\n\n**Approximations.** Titles are compared case-insensitively with whitespace collapsed. Slides that continue one another are not exempt; give a continuation a distinct title such as \"(continued)\".\n\n### `opf/reading-order`\n\nDefault severity: **warning**. Cost: composition. The order content is read differs from the order it appears on the slide.\n\n**Why.** Screen readers, keyboard focus and PowerPoint's selection pane follow the composed order. When it differs from the visual order (top to bottom, then start to end of the reading direction), the slide is read out of sequence.\n\n**Basis.** WCAG 2.2 SC 1.3.2 Meaningful Sequence, level A (PowerPoint: \"Check reading order\")\n\n**Approximations.** Compares the composed content order with a visual order recomputed from the composed boxes: items whose vertical centres fall in the same row are ordered along the reading direction (a right-to-left deck is checked by rows only), rows from top to bottom. Headings are expected first. Free-form overlap is not analysed. On a slide with a template layout (OPF 0.19) the expected order is the binding order instead (the regions in reading order, then the blocks of each region), and items outside a region (placed images, the row below the grid) are not compared.\n\n### `opf/link-text`\n\nDefault severity: **warning**. Cost: structure. Link text does not say where the link goes.\n\n**Why.** People who scan links out of context (a screen reader's links list, a link tab order) hear only the text. \"click here\", \"read more\" or a long raw URL tells them nothing about the destination.\n\n**Basis.** WCAG 2.2 SC 2.4.4 Link Purpose (In Context), level A; PowerPoint: \"Hyperlink text is not meaningful\"\n\n**Approximations.** Matches a short list of generic phrases in English (after lower-casing and removing punctuation), blank link text, and raw URLs longer than 40 characters. Adjacent runs sharing one link are read as one link.\n\n### `opf/chart-color-only`\n\nDefault severity: **info**. Cost: structure. Chart series may be indistinguishable without colour vision.\n\n**Why.** OPF charts have no data labels or patterns, so series are told apart by colour alone (and legend order). Colours that look alike to someone with colour-vision deficiency, or when printed in greyscale, make series impossible to tell apart.\n\n**Basis.** WCAG 2.2 SC 1.4.1 Use of Color, level A\n\n**Thresholds.** `minSeriesColorDifference` (default 10)\n\n**Approximations.** Uses the engine's series palette (ValidateOptions.chartPalette; default the opf-render/opf-pptx palette, adjusted for the card surface like the preview does) in series order: series i takes colour i, pie/doughnut/treemap/funnel slices take colours per category. Pairs are compared by CIE76 distance after simulating protanopia, deuteranopia, tritanopia (Machado 2009, severity 1) and greyscale. Single-series charts and chart types without a series legend are skipped.\n\n### `opf/chart-text-alternative`\n\nDefault severity: **info**. Cost: structure. A chart has no text alternative, or is marked decorative.\n\n**Why.** A chart conveys a message; people who cannot see it need the message and ideally the numbers in text. The chart's alt field is that text alternative (the preview exposes it as the chart's accessible name and the PowerPoint export writes it as the frame's alternative text); a sentence or table beside the chart also serves. An empty alt marks a chart decorative, which is reported as info so the choice is reviewed: a chart rarely carries no message.\n\n**Basis.** WCAG 2.2 SC 1.1.1 Non-text Content, level A\n\n**Approximations.** A chart passes when chart.alt has text. Without alt, it passes when the slide has any other text, list, table, quote or metric content besides title and tag, or a subtitle. It does not judge whether alt or that text states the chart's point (see opf/poor-alt-text for generic alt text). alt: \"\" is reported as a decorative chart, whatever else is on the slide.\n\n### `opf/missing-language`\n\nDefault severity: **info**. Cost: structure. The presentation does not declare its language.\n\n**Why.** Screen readers and text-to-speech choose pronunciation and hyphenation from the declared language; spell checkers and translation tools use it too.\n\n**Basis.** WCAG 2.2 SC 3.1.1 Language of Page, level A\n\n**Approximations.** Only the presentation-level `language` is checked, not the language of individual runs (`TextRun.lang`; opf/language-tag checks how those tags are written).\n\n\n## Layout rules\n\n*Will it present as authored?*\n\n### `opf/chart-option-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart option cannot be shown by the chart type.\n\n**Why.** An axis title, legend or data label option the chart type does not support is adapted by every engine, so the chart is not drawn exactly as authored.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/text-overflow`\n\nDefault severity: **warning**. Cost: composition. Text or a table does not fit its space at the smallest allowed size.\n\n**Why.** Core composition shrinks text to the readable minimum and then reports what still does not fit. Overflowing text is clipped or runs over other content in the preview and in PowerPoint.\n\n**Approximations.** Uses composeSlide at the deck's slide size with the fonts of the resolved font scheme. Without a host-supplied text measurement (ValidateOptions.fonts.textMeasurement) widths are core's portable estimate, which can differ from the real font by a few percent; pass the renderer's measurement for font-exact results. A composition that sets overflow: \"error\" is reported as a warning here, so that `valid` never depends on font metrics; rendering and export still refuse such a slide when it does not fit.\n\n### `opf/unresolved-content`\n\nDefault severity: **warning**. Cost: composition. Content cannot be drawn as authored.\n\n**Why.** composeSlide reports content it cannot place or an effect it does not support (for example a picture bullet without a logo, a date field without a date, or an unsupported image treatment). The preview and the export fall back.\n\n### `opf/layout-failed`\n\nDefault severity: **warning**. Cost: composition. The slide layout could not be computed.\n\n**Why.** Composition threw for a slide that passed schema validation, so geometry-based rules (contrast, overflow, reading order, resolution) were skipped for it.\n\n### `opf/min-font-size`\n\nDefault severity: **warning**. Cost: composition. Text is drawn smaller than the readable minimum.\n\n**Why.** Small type is unreadable from the back of a room and on a phone. The engine's own default floor is 12 pt (16 px); anything much below that was lowered on purpose or by a composition that could not fit the text.\n\n**Basis.** Common presentation guidance (12 pt minimum for body text; 18 pt or more is easier to read from a distance).\n\n**Thresholds.** `minFontSizePt` (default 11)\n\n**Approximations.** Sizes are those composeSlide fitted, expressed on the 13.33 x 7.5 in reference slide (96 px per inch, so 1 px is 0.75 pt; a smaller canvas scales type down with it) and explicit run fontSize values in points. Per payload, the smallest part (a metric label or a quote attribution) is reported. Table cell text (default 11.25 pt), code and header/footer furniture are not checked.\n\n### `opf/region-unknown`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region the layout does not have.\n\n**Why.** A region pin (`region` on a root block) names a region of the slide's layout. A name the layout does not have is ignored: the block is placed by its kind, which may not be where the author meant.\n\n### `opf/region-kind`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that does not accept its kind.\n\n**Why.** A region says which content kinds it takes. A pin to a region that does not take the block's kind is ignored and the block is placed by its kind.\n\n### `opf/region-full`\n\nDefault severity: **warning**. Cost: composition. A block is pinned to a region that is already full.\n\n**Why.** A region holds at most `max` blocks on one slide. A pin to a full region is ignored and the block is placed by its kind.\n\n### `opf/layout-unplaced`\n\nDefault severity: **warning**. Cost: composition. No region of the layout has room for a block.\n\n**Why.** Content is never dropped: a block no region has room for is drawn in the layout's overflow region beyond its max (or below the grid), where it crowds the others. Paginate moves it to a continuation slide with the same layout.\n\n### `opf/font-outside-scheme`\n\nDefault severity: **warning**. Cost: structure. Text uses a font family that is not in the deck's font scheme.\n\n**Why.** The font scheme is the deck's font choice. A run that names another family will not follow a font-scheme change, may be missing on the viewer's machine, and breaks the deck's typographic consistency.\n\n**Approximations.** Compares the run's fontFamily (case-insensitively) with the heading, body, code and accent families of the slide's resolved font scheme and the scheme's major/minor fonts. Families that the host substitutes are still different names here.\n\n### `opf/image-resolution`\n\nDefault severity: **warning**. Cost: composition. An image has too few pixels for the size it is shown at.\n\n**Why.** An image stretched beyond its pixel size looks blurry or blocky on a projector or a high-density screen.\n\n**Thresholds.** `minImagePpi` (default 96)\n\n**Approximations.** Only embedded data: images (and asset: references to them) have readable pixel sizes; URLs and files are never fetched, so they are not checked. The displayed size is the composed frame (cover and stretch are measured as the cover scale, contain as the contain scale). Effective ppi is the image pixels per inch of the 96 px/inch reference slide. SVG is vector and exempt.\n\n\n## Content rules\n\n*Is anything left unfinished?*\n\n### `opf/chart-value-not-numeric`\n\nDefault severity: **warning**. Cost: structure. A chart value is text, not a number.\n\n**Why.** A chart plots numbers. A text cell such as \"12%\" is plotted as a gap. When every text value of a column is written in one display style the finding carries the fix that stores the numbers and gives the column the matching format.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-mapping-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart mapping entry is ignored.\n\n**Why.** A mapping that names a column the chart type cannot use is left out of the chart.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/chart-highlight-adapted`\n\nDefault severity: **warning**. Cost: structure. A chart highlight is adapted by the chart type.\n\n**Why.** A highlight the chart type cannot show as authored (a category highlight on a chart without categories, for example) is adapted by every engine, so the emphasis is not exactly what was written.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-out-of-range`\n\nDefault severity: **warning**. Cost: structure. A code highlight marks a line the code does not have.\n\n**Why.** Marked lines count from 1 by line break in code.source; an entry past the last line marks nothing.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/code-highlight-range-reversed`\n\nDefault severity: **warning**. Cost: structure. A code highlight range ends before it starts.\n\n**Why.** A range written end before start marks nothing; write the smaller line number first.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/variable-builtin-missing`\n\nDefault severity: **warning**. Cost: structure. A built-in variable has no value in this document.\n\n**Why.** A `{{deck.*}}`, `{{speaker.*}}` or `{{organization.*}}` token whose field the document does not set resolves to nothing, so the text shows a gap; a `var:organization.logo` reference to an organization without a logo draws no image. `{{slide.section}}` is checked per slide: a slide without a `section` that uses it, in its own text or in the header or footer it inherits, is reported at that slide.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/duration-outside-narrative`\n\nDefault severity: **warning**. Cost: structure. The target duration is outside the range the narrative suits.\n\n**Why.** A narrative records the talk lengths it suits. A deck whose duration is outside that range probably needs a different narrative or a different length; nothing is drawn or exported differently.\n\n**Basis.** spec/schemas/opf.schema.json (JSON Schema 2020-12) and the semantic rules of OPF\n\n### `opf/narrative-duration-range`\n\nDefault severity: **warning**. Cost: structure. An inline narrative duration range has min greater than max.\n\n**Why.** A range with its bounds swapped matches no duration, so the duration check cannot help.\n\n**Basis.** spec/schemas/narrative.schema.json\n\n### `opf/placeholder-text`\n\nDefault severity: **warning**. Cost: structure. Placeholder text was left in the deck.\n\n**Why.** \"Lorem ipsum\", \"Click to add title\", \"TBD\" and bracketed prompts are scaffolding. Shipping them reads as unfinished work.\n\n**Approximations.** Case-insensitive patterns for lorem ipsum, template prompts (\"Click to add...\", \"Your title here\", \"[Insert ...]\", a bare \"Title\" or \"Text\"), and the markers TBD, TBC, TODO, FIXME and XXX in capitals. A deliberate use of the words is flagged too; disable the rule for that slide with ignorePaths.\n\n### `opf/empty-text`\n\nDefault severity: **info**. Cost: structure. A text field is present but empty.\n\n**Why.** An empty title, text block or list item draws nothing, and leaves a hole in the outline and for assistive technology.\n\n### `opf/unused-beat`\n\nDefault severity: **info**. Cost: structure. A beat of the narrative has no slide that references it.\n\n**Why.** The narrative is the plan and the slides are the product. When some slides name a beat (slides[].beat) and a beat of the plan has none, the deck skips a step of the story or the plan is out of date.\n\n**Approximations.** Resolved like every content reference: the document's catalogs groups, then the registered catalogs. A narrative that resolves nowhere is not checked. A deck in which no slide references any beat is not checked either, because it has not linked its slides to the plan. A slide that lists several beats covers each of them.\n\n### `opf/empty-slide`\n\nDefault severity: **warning**. Cost: structure. A slide has no content at all.\n\n**Why.** A slide with no title, no content and no picture is almost always an accident of editing. (A deliberately blank slide can use the blank layout.)\n\n### `opf/language-tag`\n\nDefault severity: **warning**. Cost: structure. A language tag is malformed, uses an unassigned region or is not in canonical case.\n\n**Why.** Engines read the deck language and each run's `lang` as BCP 47 tags to choose script fonts, text direction, proofing language and the PowerPoint `lang` attribute. A tag such as `en-a`, or a region that does not exist (`en-UK`; the United Kingdom is `GB`), is guessed at or dropped, and a wrong-case tag (`EN-us`) is not what a locale lookup expects.\n\n**Basis.** BCP 47 (RFC 5646, section 2.1) and the platform locale data (Intl.getCanonicalLocales, Intl.DisplayNames)\n\n**Approximations.** Checks the root `language` (a tag string, or the `bcp47` of an inline Language object) and `lang` on the TextRun objects of titles, subtitles, tags, text, lists, tables, quotes, metrics and timelines. A region is assigned when the runtime's CLDR data names it (ISO 3166-1 codes and UN M.49 areas such as 419); private-use regions (`AA`, `QM`-`QZ`, `XA`-`XZ`) and `ZZ` are accepted, as are private-use (`x-...`) tags. Only the spelling of a tag is checked, not whether the language subtag is assigned or whether the language suits the text. The schema rejects tags with characters other than letters, digits and hyphens (`en_US` is a schema error, with no suggestion), and `en-UK` as the deck language is already an error of the schema rule, so those are not reported twice.\n\n<!-- validate-rules:end -->\n"
350
350
  },
351
351
  {
352
352
  "slug": "yaml",