Skip to content

LFM Diagnostic Protocol

Status: Draft · Version: 1.1 · Date: 2026-10-08 · Owner: @saden1 Companion to: LFM-1.0.md (conformance level L2)

A conformant LFM document is always valid Markdown (§ LFM L0), so LFM diagnostics are never parse errors. They are authoring findings: constructs that are valid Markdown but, on this Lattice layout, will not render the way the author intends. This document specifies the stable shape of a finding, the rule registry, severities, and the contract for machine-applicable fixes.

This is the surface a tooling vendor implements to give LFM authors inline findings and quick-fixes — in an editor, a CI check, a PR bot, or a host like GitHub. This protocol is the foundation of a future LFM Language Server (§6).

The reference implementation is lib/authoring/lint-core.js (pure, fs-free, shared by the CLI tools/lint-deck.js, the engine’s validate(), and the browser panels in the Studio and the Playground). This document publishes that implementation’s contract; the contract, not the implementation, is the standard.


A diagnostic is a JSON object:

{
"slide": 3, // 1-based slide number; 0 = deck-wide (front matter)
"rule": "card-style-inline-title",// stable rule ID from the registry (§3)
"severity": "error", // "error" | "warning" | "info" | "suggestion" (§2)
"classToken": "cards-grid", // the component/modifier token the finding is about
"line": "- **Title.** body", // the offending source line (trimmed), or the directive
"message": "inline \"- **Title.** body\" on a card-style slide — the body inherits the parent li bold",
"fix": "Use the nested-list shape:\n - Title\n - body text",
"autofixable": true // optional; present and true when an automatic fix exists (§4)
}
FieldTypeRequiredMeaning
slideintegeryes1-based slide number, matching the preview’s “Slide N” and edit markers. 0 for a deck-wide finding sourced from front matter.
rulestringyesA stable ID from the registry (§3). Tools key suppression and docs links off this.
severitystringyeserror, warning, info or suggestion (§2).
classTokenstringyesThe _class component or modifier token, or front-matter value, the finding concerns.
linestringyesThe offending source line (trimmed) or the _class/front-matter directive line, for locating and display.
messagestringyesHuman-readable explanation of what and why it matters.
fixstringyesAuthor-facing guidance — the canonical correct shape.
autofixablebooleannotrue when a deterministic automatic fix exists (§4). Absent means no automatic fix; apply fix by hand.

Location note (v1): v1 locates a finding by slide + line (the text of the offending line, scoped to the slide). A future minor version MAY add a precise range ({ start, end } line/column) for editors that want character-level squiggles; tools MUST treat an absent range as “highlight the matching line within slide.”

  • error — the slide will render visibly wrong (a slot inherits the wrong weight, a grid row splits, a required slot renders empty). Authors SHOULD resolve every error before publishing.

  • warning — the slide will render, but likely not as intended; for example, a typo silently selected a fallback (unknown class, unknown finish, unresolved map region). Warnings are surfaced but do not block rendering.

  • info (1.1) — nothing the author must fix, but something they may not expect: a retired name still accepted, a code line whose end is cut off, text that may clip under a bare scale-* class.

  • suggestion (1.1) — advice. The slide renders whole and as intended; the finding says it could read better (a crowded layout, a long heading). A tool SHOULD show suggestions apart from errors and warnings, and MUST NOT count them as failures.

A rule may emit more than one severity depending on context (§3 lists each one); the severity on a finding is the one that applies to that finding.

The rule IDs below are stable for LFM 1.x: a tool may rely on them, and they will not be renamed or repurposed within the major version. New rules are added in minor versions; a rule is never silently removed (it may be deprecated).

The published namespace convention going forward is lfm/<rule>; the bare IDs below are the canonical identifiers the reference implementation emits, and lfm/<rule> is the equivalent qualified form a host MAY present.

The registry is complete. Every rule ID the reference implementation can emit is listed here, and every ID listed here is one it emits. test/unit/authoring/diagnostics-registry.test.js reads the reference implementation and fails when the code emits an ID the registry does not list, the registry lists one the code does not emit, or a row’s Severity or Autofix cell disagrees with what the code can produce. So a new rule cannot ship without a row. (The test reads the code statically; a rule ID set through a variable is allowed only where the test’s allowlist says where it really comes from.) (Version 1.0 listed 13 IDs while the implementation emitted 178; §7 records the catch-up.)

Families. A row whose ID contains <…> stands for a family: one ID per member, generated at run time (for example, unknown-<axis register> covers unknown-spark and unknown-icon). A tool MUST treat every member as a distinct, stable ID.

Rule IDSeverityAutofixWhat it catches
unknown-classwarning✓A _class token that is not a known component or modifier, or one that was renamed. Carries a did-you-mean.
conflicting-variantswarning—Two tokens on one slide from an axis that takes one value at a time (two variants, two finishes); only one wins.
block-unsupportedwarning—An editorial modifier (insight-*, no-note) on a layout that has nothing for it to act on.
list-modifier-inertwarning—On a list slide, a counter modifier its register never reads: numbered without takeaway, or lettered/roman/bullet without principles. Also principles over - bullets (it numbers an ordered list only), and takeaway over a 1. list without numbered (the numbers are hidden).
card-style-inline-titleerror✓- **Title.** body (or ordered 1. **Title.** body) on a card-style layout — the body inherits the parent li bold. Fix: nested - Title / - body.
ledger-inline-titleerror✓The unordered inline-bold shape on a ledger/numbered layout — autofixes to the ordered 1. Name / - body shape the layout wants.
chart-list-split-quotewarning✓A > at the left margin between two items of a chart’s list. It ends the list, so every row after it drops out of the chart. On a chart that reads a row’s > (flowchart, state-chart and the 13 detail charts) the fix indents a one-line quote under the item above, where it becomes that row’s description (a quote of several lines gets no machine fix: indent each line by hand); on a list chart that reads no detail under its rows there is no machine fix.
chart-row-quote-unreadwarning—A > under a row with no mark of its own: a radar series (its marks are its axes) or a quadrant group (its marks are its points). The chart lifts it out, so it is shown nowhere; move it under an axis or a point, where it is that mark’s description.
chart-value-malformedwarning—A value in a chart’s rows or cells that is shaped like a number but is not one: two signs (--5, (-5)), an unpaired parenthesis ((5, 5)), or digits that are not one number (1,2,3, 1.2.3.4x). The chart reads it as no value (a point chart draws no mark, a pie wedge or funnel stage reads as zero, a bullet prints it as typed); before, each read as a different number with no warning. Only charts that read numbers check it, so a dotted date on a timeline is not flagged. Which number was meant is the author’s call, so there is no machine fix.
chart-row-record-unreadwarning—A chart’s data row written as one {…} record (- Uptime `{4.2M, target=5.0M}` ) that the chart cannot draw: the record holds a field the chart cannot draw (none on a native chart, bullet or progress, which read every field; a value no single-backtick span can hold on another), it is followed on its row by a quote the chart does not read or another paragraph (a native chart reads a record only when it ends the row’s first line, as old spans must), it follows another code span on the row (`1` `{2}`), it sits in a later paragraph of its row, or on the line after a heading item (the old spans draw a value there, the record is read only from the row’s first line, so it stays a pill), it is a plain number under a row that has a value (stacked-bar’s segment under a total: the old form reads it as a segment, the record as a note), or the chart’s row schema refuses it and it is not a pill either ({3, targt=5}: a typo that used to drop the row from the chart), it sits in a chart list inside a > quote (the old spans draw there, a record does not), or a badge before the row’s label holds a value too, as a record or a plain number (`{1}` North `{40}`, `1` North `{40}`: the chart reads neither; {1, circle} reads as a badge). A record in a > quote that is a row’s description is not checked: neither form draws a row there. A record with an old form draws, because the chart family rewrites it to the spans each chart reads. Scatter and quadrant read a record directly and are not checked; a nested bullet under a row that has a value is detail on a chart that reads it so, and is not checked, except bullet’s keyword rows (Target, Plan, Floor, Band and the rest of its keywords) written as records, which the bullet reads as a note. There is no machine fix, because the model’s codemod writes the record form.
chart-table-unescaped-pipewarning—A `
chart-series-name-inconsistentwarning✓A series name spelled two ways across the groups of a nested bar, line, stacked-bar, slope or radar list (Licenses under one group, Licences under the next). The chart matches series by their text, so the odd spelling draws a second series. The two names differ only in case or spacing, or by one small edit, and never share a group, and the groups show a shared shape (each group holding the odd spelling also holds another series, and each of those appears under the intended spelling’s groups; a case or spacing difference needs no such evidence). Only rows the chart draws as a series point (a number last) in the slide’s first list count; a note row is never renamed. A difference only in digits (Q1, Q2), or in a one- or two-letter last word (Plan A, Plan B, UK, US), is a different series, because a series missing from a group is a supported shape. The fix rewrites the spelling used under fewer groups to the one used under more (the first group’s, on a tie).
chart-description-unreadwarning✓A {> prose} description where the chart does not read one. In a list row it is not read, and written after the row’s record it leaves the row with no value (- North {40} {> grew fast}“ draws no bar); a list row’s description is a > quote indented under it. On a chart whose rows take a quote the fix moves the prose into that quote; elsewhere, and for a description in a table cell of a chart that does not read one (only heatmap does), the finding has no fix On a heatmap table it also warns on a header description with no column name before it (the whole span is then the column’s name), a {>} that shares its header cell, a second {>} column (dropped), a description in the label column or in the {>} column (read as its own text).
statement-ol-bolderror—A **bold** span inside an ordered-list statement, which splits the counter-grid row (e.g. principles).
split-bodyless-itemerror✓A right-panel item with no nested body on a split layout — the title won’t lift to bold.
split-missing-headlinewarning—An h2-anchored split slide (split-panel/split-compare) with no ## headline — the left panel renders empty.
split-statement-missing-quotewarning—A split-panel pullquote with no > blockquote — the pull-quote (the variant’s point) renders empty.
split-compare-option-countwarning—split-compare without exactly two top-level options — the layout assumes a two-up and highlights the 2nd as preferred.
number-slot-bodyless-itemwarning—A kpi/stats number item with no nested label — the number won’t render in display type.
big-number-hero-headingwarning—A big-number slide whose number is written as a heading, so the required number slot (the first list item) is empty.
claim-bleed-unsafewarning—A bleeding claim (a per-slide claim-* or the deck-wide claim:) on a component that excludes it (a prose-dense or table layout), whose content is cropped at the true edge.
qr-empty-payloaderror—A qr payload bullet with no value.
qr-missing-payloaderror—A qr slide with no scannable payload bullet.
qr-duplicate-payloaderror—A qr slide with more than one payload bullet; it renders only one.
collage-countwarning—A collage with fewer than four or more than six photos; past six the tiles are too small to read and the layout falls back to an even grid.
collage-altwarning—A collage photo with no alt text, which is the screen-reader label and the label in the enlarged view.
collage-bgwarning—A collage that uses ![bg](https://github.com/Laticent/lattice/blob/main/spec/…) background syntax, which belongs to image; a collage lists inline photos one per bullet.
collage-hintwarning—A trailing hint span after a collage photo that is not hero, one focal word (top, bottom, left, right, center) and a ratio such as 3:2; the whole span is ignored and nothing is drawn for it.
collage-tailwarning—Text after a collage photo that is not a clean hint span; a collage draws no per-photo text, and any hint on that line is ignored.
collage-hero-dupwarning—More than one collage photo marked hero; only the first counts.
track-directivewarning—A bare track: (deck-wide from that slide on) where _track: was meant, _track on a slide that is not topic or on topic fact, a track with fewer than two labels (none is drawn), or a track with no current topic.
track-listwarning—A list on a topic slide, which shows as plain content; the track is built from the section’s headings.
focus-specwarning—A malformed _focus directive; it silently does nothing at render.
focus-stylewarning—A _focusStyle value that is not spotlight, ring, list-fill, blur or highlight.
focus-stepswarning—A _focusSteps step that is not a valid _focus spec (only the first bad step is reported).
unknown-map-regionwarning—A map list item whose lead name the basemap can’t resolve. Carries a did-you-mean against the basemap vocabulary.
map-highlight-impliedinfo—A map highlight slide whose values are labels (or absent), so the data already fills it by category and the class says nothing.
trail-budgetwarning—A word in an authority-chain trail column too long for the column, which squeezes the others.
tag-budgetwarning—On decision and compare-prose (not axis), a card tag long enough to wrap, or a band label long enough to run past two lines, which grows every tag on the slide and pushes the cards down.
tag-aliasinfo—banner-tag, the old name for tag-band.
bg-side-unreadwarning—A ![bg left] / ![bg right] (or a ratio) on a slide that is not image: only the image layout places a picture beside the text, so it fills the whole slide behind it instead.
bg-ratio-unreadwarning—A ratio such as ![bg left:40%] on an image slide: the ratio is not read, so the slide falls back to a full-bleed background and loses its panel.
label-set-above-bodywarning—A label list naming key entries, placed above the chart: there it names the axes instead of relabeling the key, or shows as text if it has more items than the chart has axes.
label-set-unboundwarning—A bracketed label list that relabels nothing: on a component with no key, an entry that is not one of the component’s key members, or a key named twice (the last wins).
shell-fence-is-scriptinfo—A shell (or similar) fence holding a script; the language only colors terminal prompts, so it renders nearly plain.
code-recordwarning—An @code{…} record above a fence that does not read: an unknown word, a bad line range, a width out of range. The render ignores what it cannot read.
code-record-orphanwarning—An @code{…} record line with no fence after it, so it configures nothing and renders as inline code.
code-line-rangewarning—A line reference past the end of its code block: focus= or mark= in a record, or a slide’s _focus: line against its first fence. It marks nothing. Not checked on a formatted block, whose lines the formatter decides.
code-format-unsupportedwarning—A block whose own @code{…} record asks for format in a language no formatter reads; it keeps its text.
Rule IDSeverityAutofixWhat it catches
capacity-overflowwarning—More elements than the layout’s capacity, on a wide (16:9) deck or under fit: report, where nothing splits the slide; the extra ones may be cut off. (At other sizes the slide splits and nothing is reported.)
capacity-crowdsuggestion—More elements than the layout reads well with; it still renders whole.
venue-prosewarning / info—A component that is one block of prose (a big-number caption, a redline clause, an image paragraph) carries more words than it holds at the deck’s venue, so the end is cut off. A warning at a named venue, info under a bare scale-*.
capacity-scalewarning / info—More elements, longer text, or a longer or wider code block than the layout fits at the deck’s type size (a venue:, a scale-* class, or, on a 16:9 deck with neither, the laptop size itself), so some may be cut off. A warning at a named venue, at the laptop size, or for a code block too long even at laptop size; otherwise info under a bare scale-*.
spot-scalewarning / info—A scale-* or venue-* class on some slides only, so type size jumps between slides.
code-line-clippedinfo—A code line wider than the pane it renders into, so its end is cut off.
pane-layoutwarning—A pane layout that does not lay out as written: set deck-wide or for a run of slides, fewer than two or more than two panes, a ratio that is not a class name, off the 25–75 grid in 5% steps, or more than one ratio, both columns and rows, a ### above the title, or a class token, slide layout or modifier a pane cannot use.
pane-syntaxwarning—The old pane syntax (<!-- panes: -->, <!-- pane: -->), which still works but may be removed.
pane-titlesuggestion—A pill or label above a pane title that joins the title on one line.
pane-insightwarning—More than one Key Insight on a panes slide; they all show together below the panes.
pane-arrangesuggestion—Panes that will render reoriented, or split into one slide per pane, rather than as written.
pane-overflowwarning—A pane with more elements than fit: in its share of the slide as laid out (after a pane title and the slide’s eyebrow, subtitle, Key Insight or note take their height), or, when the panes split into one slide per pane, on the slide it becomes.
pane-crowdsuggestion—A pane with more elements than it reads well with.

3.3 Inline notation (pills, sparks, marks, plugin kinds)

Section titled “3.3 Inline notation (pills, sparks, marks, plugin kinds)”
Rule IDSeverityAutofixWhat it catches
pill-literalwarning—A span that opens like a pill ({…}) but does not parse, so it renders as code.
legend-spanwarning—A @callout{…} or @legend{…} span that does not read, has a callout with no target, reads {1, lg} as a styled label, or sits on a slide where no component reads one.
callout-spellinginfo—A @legend{…} span that pins a mark: a callout written the way it was before callouts had a plugin of their own. It still draws; @callout{…} is the spelling now.
legend-offinfo—A callout on a slide where the author switched callouts off (callout: off, callout-off): it is hidden, and its detail still reaches the speaker note.
bare-row-pillwarning—A bare code span where a component used to draw a pill from one (cards, kpi, actors, a register, a roadmap header and the others): it is plain inline code now, and the pills plugin’s {…} draws the pill.
spark-literalwarning—A span that opens like a spark (~{…}) but does not parse, so it renders as code.
<inline kind>-literalwarning—A family: one ID per plugin inline kind (for example icon-literal). A span that opens like the kind but renders as code; the plugin supplies the reason.
unknown-inline-kindwarning✓A named span @name{…} whose name no kind has: neither the host’s, a shipped plugin’s, nor a listed plugin’s. It renders as code; the autofix takes the nearest kind name when one is close.
video-bulleterror✓A video slide that names its clip as a bare bullet (with optional caption and poster bullets). The bullet form is gone: the address comes only from the author’s own @video{…} record. A slide with no record is an error whatever bullet it tried. The autofix writes the record for a lone address bullet; a clip with caption or poster bullets is left to the author (a one-line fix cannot span lines).
video-not-alonewarning—An @video{…} record inside running text. A video is a figure, so only a record on a line of its own is drawn; the span stays code.
video-slide-classwarning—A lone @video{…} record on a slide that is not a video slide. The layout and the poster picture are both scoped to the video class, so the clip draws only as a plain text link; the finding names the class to write.
video-extra-recordwarning—A second lone @video{…} record on one video slide. The layouts seat one clip, so the extra record is drawn below the first and runs off the slide; move it to a slide of its own.
spark-too-bigwarning✓A spark too tall or too wide for its line; the autofix resizes it. Reported by the Studio from a measured render (sparkFitFindings), not by lintTextWith or the CLI.
pill-shape-crowdedwarning—A shaped pill ({X, circle}) holding more than the shape can, so it stretches out of shape.
bracket-list-closed-earlywarning—An extra ] that closes a bracketed list early, so it shows as plain text.
mixed-spellingwarning✓A word spelled two ways in one deck’s inline notation; the autofix rewrites it to the spelling the deck uses most.
typed-shape-glyphwarning—A typed glyph (✓, →, ●) doing the job of a drawn shape (HARD RULE #29); the fix names the modifier or mark that draws it.

3.4 Front matter: unknown or retired values

Section titled “3.4 Front matter: unknown or retired values”
Rule IDSeverityAutofixWhat it catches
unknown-finishwarning✓A front-matter finish: value that isn’t a known register — the deck ignores it and draws no backdrop.
unknown-pluginwarning✓A name in the front-matter plugins: import list that no plugin has — the list only adds, so the deck would silently load nothing for it.
plugin-fence-attributeswarning—Words after a zip plugin’s fence name (```gantt title="Q3"). A plugin’s fence is handed its text only, so they are ignored today and may take on a meaning later.
unknown-modewarning✓A mode: value the engine does not know; the deck uses boardroom.
unknown-presetwarning✓A preset: name the engine does not know; no preset applies.
unknown-fitwarning—A fit: value the engine does not know; the deck uses heal.
unknown-splitwarning✓A split: value the engine does not know; the deck uses headings.
unknown-claimwarning✓A claim: value the engine does not know; the deck uses framed.
unknown-stampwarning✓A stamp: value the engine does not know; markers use the tab shape.
unknown-tonewarning✓A tone: value the engine does not know; markers use the rail shape.
unknown-spectrumwarning✓A spectrum: value the engine does not know; the deck uses the rainbow default.
unknown-spectrum-edgewarning✓A spectrum-edge: value the engine does not know; the bar sits on top.
unknown-spectrum-cardwarning✓A spectrum-card: value the engine does not know; no card rail is drawn.
unknown-spectrum-card-edgewarning✓A spectrum-card-edge: value the engine does not know; the rail sits on the left.
unknown-spectrum-trimwarning✓A spectrum-trim: value the engine does not know; the accents stay plain.
unknown-rulewarning✓A rule: value the engine does not know; headings keep the default underline.
unknown-inline-codewarning✓An inline-code: value the engine does not know; pills and marks keep rendering.
unknown-eyebrowwarning✓An eyebrow: value the engine does not know; eyebrows render as the bare label.
unknown-headlinewarning✓A headline: value the engine does not know; each layout keeps its own alignment.
unknown-cornerswarning—A corners: value the engine does not know; corners render square.
unknown-liftwarning✓A lift: value the engine does not know; cards render flat.
unknown-venuewarning✓A venue: value the engine does not know; the deck renders at the designed size.
unknown-markswarning✓A marks: value the engine does not know (including the retired off and etching); the deck wears the default finish.
unknown-cardswarning✓A cards: value the engine does not know; each layout keeps its own card spacing.
unknown-backdropwarning✓A backdrop: word that is neither a strength nor a mask, or a second word for an axis already set.
unknown-tagwarning✓A tag: word the engine does not know, or a second word for an axis already set.
unknown-<axis register>warning✓A family: one ID per axis register (unknown-spark, and one per register a plugin declares, such as unknown-icon). An unknown word, or a second word for an axis already set.
unknown-<setting>warning✓A family: one ID per key whose value list the deck-settings table holds (lib/core/deck-settings.js, generic: true): unknown-theme, unknown-size, unknown-motion, unknown-motion-style, unknown-motion-speed, unknown-player-motion, unknown-glossary, unknown-logo-finish (an axis register: a word outside the vocabulary, or two words on one axis). A value the key’s reader does not accept; the deck uses the default (for theme:, the command line refuses it).
unknown-keywarning✓A top-level front-matter key that no reader reads and no plugin declares; the deck ignores the whole line. Carries a did-you-mean.
unknown-treatmentwarning✓A tint-* or mark-* class token that lib/base/base.treatments.css does not paint.
treatment-placementwarning—tint-corner or tint-edge without a placement it accepts (at-tl … for a corner, at-top … for an edge), so it paints nothing.
thin-card-no-inkwarning—An image spotlight card under 60% opaque with no ink-light or ink-dark on the slide or the deck’s image-card: line, so the theme’s own ink is read against the photo and can vanish. Raise the backing or name an ink; the Studio’s Card backing slider picks one from the photo.
unknown-color-modewarning✓A color-mode: value the engine does not know; the deck uses the theme’s default colors.
unknown-deliverywarning✓A delivery: value the engine does not know; the deck uses restrained.
unknown-pacewarning✓A pace: value the engine does not know; playback uses each viewer’s own setting.
unknown-debug-facetwarning—A word in a front-matter debug: value or a <!-- _debug: --> directive that the engine does not know; the overlay falls back to on-hover.
deck-wide-componentwarning—A component named in the deck-wide class: register (every slide would become it), or a class color-mode: overrides. Ignored.
bad-render-target-valuewarning—A render-target setting that is neither on nor off; the export treats it as off.
nested-render-target-keywarning—An indented render-target key that the export still reads as the deck setting.
marks-renamedinfo—chart-finish: or a chart-finish-* slide class, the old spelling of marks: / marks-*. It still renders; when a deck sets both keys, marks: wins.
guards-renamedwarning / info—guards:, the old name of fit:. A warning when its value is not valid and no fit: is set (the deck uses heal); otherwise info, including when fit: is also set and wins.
retired-backdrop-keywarning—The retired multi-line backdrop: block, which is no longer read.
retired-logo-keywarning✓logo-style: (now logo-finish:), logo-on:, logo-x:, logo-y: or logo-scale:, which no longer render. The fix folds them into one logo-finish: line (title, a size word, the nearest of six placements) and deletes them.
retired-form-keywarning—form: off, which no longer works; slides now show the title band and progress bar.
retired-form-tokenwarning—The retired no-form class token.
autosplit-retirederror / suggestion—autosplit:, which no longer controls splitting. An error for off at a size that splits anyway; otherwise a suggestion (the line is stale).
asset-missingwarning—An asset:<name> picture reference that names no file in assets/ beside the deck (or whose file is a symlink out of the deck folder), so the CLI draws the missing-picture stand-in. Checked by tools/lint-deck.js, which has the file system; the linter core does not.
paginate-unsupported-valuesuggestion—paginate: skip or paginate: hold in front matter, Marp values Lattice treats as false.
paginate-comment-retiredwarning✓A paginate: or _paginate: slide comment, which does nothing: front matter, the no-pagination / with-pagination classes and the component manifest set page numbers. Autofixable only where deleting it changes nothing.
stray-overflow-markerwarning—overflow-marker: in the deck; it is an export setting, so the line does nothing.
stray-export-settingswarning—An export-settings block copied in from another export.
Rule IDSeverityAutofixWhat it catches
caption-key-retirederror / warning—Front-matter captions: (renamed say:; ignored, so an error) or a caption: comment (now a speaker note: an error in lowercase, a warning when capitalized).
say-key-casewarning—A capitalized Say:, which is a speaker note; only lowercase say: is spoken.
lexicon-single-letter-keywarning—A single-letter/digit lexicon: key (e:, 2:) — read-aloud rewrites every embedded occurrence, not just the standalone token, garbling the deck’s narration. A single glyph (→, ×) is safe.
greeting-hardcoded-periodwarning—A greeting that names a time of day; {greeting} adapts to the listener’s clock.
greeting-not-englishwarning—{greeting} (always English) in a deck whose lang: is not English.
unknown-bookend-placeholderwarning—A {placeholder} in a greeting or closing that the voice would read aloud as written.
unterminated-commenterror—A <!-- that is never closed: everything after it disappears, and its text ships inside any export that carries the source.
author-script-deferswarning—A <script> that runs later; PDF, PPTX and PNG exports capture before it runs.
Rule IDSeverityAutofixWhat it catches
gantt-retired-delimitererror✓A gantt span using a retired delimiter; the only delimiter is ... Autofixes.
gantt-bad-spanerror—A gantt span whose sides are not dates, quarters or months.
gantt-unknown-tokenwarning—A gantt token that is not a span, a status, after= or milestone.
gantt-mixed-timewarning—A gantt mixing dates with quarters or months.
gantt-dangling-aftererror—A gantt after= naming no task on the slide.
gantt-inverted-dependencywarning—A gantt task that starts before the task it depends on.
journey-actor-shortcutwarning✓A journey actor in the retired @sales shortcut, which renders as code and drops the actor. Autofixes to {who=sales}.
ep-curve-duplicatewarning—An EP curve point whose return period is already in its curve; the chart keeps the first.
ep-curve-pointwarning—An EP curve record the chart cannot read as {return period, loss}, such as one with a thousands separator; the point is left out.
ep-curve-too-manywarning—An EP curve slide with more than four curves; the chart draws the first four.
histogram-bands-joinwarning—Neighboring histogram bands that leave a gap or overlap.
histogram-dash-rangewarning—A histogram band written with a dash instead of ..; the row is left out.
histogram-log-zerowarning—log on a histogram axis whose domain reaches zero; the chart draws a linear axis.
histogram-negativewarning—A histogram value below zero; the band draws with no height.
quadrant-axis-partwarning—A quadrant axis list with the wrong number of members, or a member part the chart ignores.
quadrant-retired-axiserror✓The retired quadrant axis format. Autofixes to the bracketed list.
Rule IDSeverityAutofixWhat it catches
flowchart-list-splitwarning—A numbered list past 9 whose sub-items are not indented far enough, so later items drop from the chart.
state-chart-v1-transitionwarning—The retired v1 state-chart transition or self-loop form.
state-chart-v1-tintwarning—The retired v1 :::token tint, which is read as part of the name.
flowchart-duplicate-iderror—An #id already used by another shape.
flowchart-conflicting-styleerror—A shape styled twice with different values.
flowchart-unknown-modifierwarning—A span that is not a style; it is ignored, or read as part of the shape’s name.
flowchart-orphan-connectionerror—A connection row with no shape above it.
flowchart-nested-under-connectionerror—A row nested under a connection, which is neither a member nor a connection.
flowchart-empty-nameerror—A row with no usable shape name.
flowchart-two-groupserror—A shape placed in two groups.
flowchart-line-word-on-shapeerror—A line style written after a shape rather than a connection target.
flowchart-shape-word-on-lineerror—A shape id or style written after a connection.
flowchart-missing-targeterror—An arrow with no target.
flowchart-mermaid-arrowinfo—A Mermaid-style arrow, read as the house form.
flowchart-comparison-arrowwarning—A comparison (<=) that reads as a connection to a shape.
flowchart-near-duplicatewarning—A new shape whose name is one or two letters from an existing one.
flowchart-group-self-edgeerror—A group connected to itself or to its own member.
flowchart-icon-on-groupwarning—An icon on a group, whose title draws none.
flowchart-unknown-iconwarning—An icon name the icon set does not have; the shape shows its text alone.
flowchart-icon-only-without-iconwarning—icon-only on a shape that names no icon.
flowchart-key-shapewarning—A key line that is not a key set.
flowchart-key-unboundwarning—A key entry naming a word the chart does not use.
Rule IDSeverityAutofixWhat it catches
hub-spoke-emptywarning—A hub with no satellites; nothing is drawn.
hub-spoke-extra-hubwarning—A second top-level item; hub-spoke draws one hub.
hub-spoke-hub-statuswarning—A status on the hub, which is not drawn.
hub-spoke-too-fewwarning—One satellite, which reads as a caption, not a structure.
hub-spoke-too-manywarning—More satellites than are drawn; the rest are dropped.
hub-spoke-tiered-brancheswarning—More branches than tiered draws.
hub-spoke-tiered-leaveswarning—More leaves than tiered draws.
hub-spoke-tiered-channelwarning—sized or flow-* on a tiered figure, which prints values but does not scale them.
hub-spoke-two-flowswarning—More than one flow-* class; only one applies.
hub-spoke-two-channelswarning—sized and flow-* together; connector weight wins.
hub-spoke-extra-valuewarning—A second value pill on a satellite; it is dropped.
hub-spoke-numeric-groupwarning—A group pill that looks like a figure but is not a readable value.
hub-spoke-extra-statuswarning—A second status on a satellite; one is drawn.
hub-spoke-extra-groupwarning—A second group on a satellite.
hub-spoke-unknown-iconwarning—An icon name the icon set does not have.
hub-spoke-icon-only-without-iconwarning—icon-only on an item that names no icon.
hub-spoke-icon-only-empty-namewarning—An icon-only item with no name for a screen reader.
hub-spoke-bad-recordwarning—A record that is not an icon record; it is dropped.
hub-spoke-inline-kindwarning—A mark, spark or icon kind on a hub-spoke row, which draws nothing there.
hub-spoke-extra-recordwarning—A second icon record on an item.
hub-spoke-status-groupwarning—Every satellite carrying a status, which most likely meant groups, or a status word on one satellite where the others carry groups.
hub-spoke-too-many-groupswarning—More groups than the hue cap; satellites stay neutral and no key is drawn.
hub-spoke-missing-valuewarning—Some satellites with a value and some without, or a scaling modifier with no values.
hub-spoke-nonpositive-valuewarning—A zero or negative value, which no area or weight can show; scaling is off.
hub-spoke-mixed-unitswarning—Satellite values in different units; scaling is off.
hub-spoke-sized-manywarning—sized across more satellites than the eye can rank; sizing is off.
hub-spoke-sized-floorwarning—Several satellites too small to draw to scale, so their areas no longer compare.
hub-spoke-flow-stepwarning—Two values within a few percent that draw a weight step apart.
hub-spoke-sumwarning—Satellites that add up to more than the hub.
hub-spoke-hub-overflowerror—Hub text that does not fit the hub at the smallest type.
hub-spoke-crowdedwarning—A figure outside the envelope the layout is certified for (names, pills or spokes past their limits).

These come from reviewText in lib/authoring/review-core.js, which lint:deck runs by default on named files (skipped under --all or --no-review) and the Studio shows in its review panel. They judge how well a slide communicates, not whether it renders, so every one is a suggestion.

Rule IDSeverityAutofixWhat it catches
label-titlesuggestion—A heading that is a category label, not the takeaway.
chart-no-takeawaysuggestion—A data slide whose heading names the topic, not the “so what”.
wall-of-textsuggestion—A slide past the deck profile’s word or bullet budget.
long-headingsuggestion—A heading past its word budget.
divider-numbered-headingsuggestion—A numbered divider heading long enough to run off the frame.
density-overflowsuggestion—An element run well past its word budget.
density-crowdsuggestion—An element crowding its word budget.
verbose-<chrome slot>suggestion—A family: one ID per chrome slot whose name is one word (verbose-eyebrow, verbose-subtitle). The slot is past its word budget.
verbose-key-insightsuggestion—The Key Insight past its word budget (the verbose-* family’s member with a two-word slot name).
pill-not-a-checkboxsuggestion—{x} written as a checkbox; braces make a pill, brackets make a mark.
stub-slidesuggestion—A heading with no body.
metric-no-referentsuggestion—A hero number with nothing to compare it to.
image-no-altsuggestion—An image with empty alt text.
possessive-stackingsuggestion—Stacked possessives that stumble read aloud.
title-incompletesuggestion—A title slide with placeholder text, or no subtitle.
duplicate-headingsuggestion—A heading that repeats an earlier slide’s.
monotone-openingssuggestion—Many headings that open the same way.
no-asksuggestion—A deck of four or more content slides with no ask or recommendation.
agenda-missingsuggestion—A long deck with no agenda.
length-vs-timesuggestion—More slides than the stated talk length allows.

Emitted by tools/lint-deck.js itself (--discover), not by lint-core, because they need the narration normalizer.

Rule IDSeverityAutofixWhat it catches
narration-acronymssuggestion—All-caps tokens that narration will read letter by letter.
narration-passthroughsuggestion—Slash, ratio and identifier tokens that narration will read as glyphs.

A record comment opens with { or @meta{ (<!-- {layout=content} -->). These rules report a record in a position no reader will take it from, and the old directive form the record replaces (engineering/decisions/2026-10-17-meta-record.md §5.3, §5.5). A record inside a code fence is literal text and is never reported.

Rule IDSeverityAutofixWhat it catches
meta-opener-bareerror—A bare <!-- with the record on the next line: the comment is a note, not a record.
meta-not-own-lineerror—A record inside a list item or blockquote.
meta-in-html-blockerror—A record directly under an HTML line with no blank line between, so both become one HTML block.
meta-block-in-containererror—A multi-line record holding a blank line inside a list or blockquote, where the comment ends early.
legacy-directive-commentwarning—An old-form directive comment (<!-- _class: x -->). Off unless the host opts in (metaCut) until the engine stops reading the form.

Autofix is per-finding, not per-rule. The ✓ marks rules that can offer a deterministic autofix; whether a given finding carries one depends on its source line. card-style-inline-title, ledger-inline-title, and split-bodyless-item autofix the bold inline shape (- **Title.** body); the gantt span rule gantt-retired-delimiter autofixes the retired delimiter to ..; quadrant-retired-axis rewrites the old axis format; spark-too-big resizes the spark; mixed-spelling rewrites to the deck’s majority spelling. A rule with a did-you-mean (unknown-class, unknown-plugin and the unknown-* rules marked ✓; not unknown-fit, unknown-corners or unknown-debug-facet) autofixes only when a valid name is close enough to suggest, and never for a second word on an axis already set; the finding then carries replace: { from, to }. unknown-map-region names its did-you-mean in the message only. A bare-title or otherwise ambiguous finding emits autofixable: false and relies on the fix guidance (§4).

The machine-readable companion to this table is the per-component grammar in dist/docs/grammar.json, which records, per component, the slot/shape contract these rules enforce.

When autofixable is true, exactly one correct rewrite exists, so a tool MAY apply it without prompting the author. The reference implementation exposes:

  • autofixNestedTitle(line) — converts the unordered inline shape - **Title.** body → - Title / - body (card-style and split rules). It returns null for shapes that are not uniquely fixable (a bare title, an ambiguous non-bold split), which emit autofixable: false.
  • autofixOrderedNestedTitle(line) — the ledger variant: - **Title.** body → 1. Title / - body (Markdown auto-numbers, so the literal 1. is fine).
  • autofixGanttDelimiter(line) — swaps a retired gantt span delimiter (→ / – / — / ->) for .., only inside the line’s inline-code spans.
  • applyFix(source, finding) — applies an autofixable finding to the document, scoped to the finding’s slide so an identical line elsewhere is untouched. The rewrite is computed from the located source line, so its indentation is kept.
  • applyAllFixes(source, vocab) — applies every autofixable finding in one pass loop (re-linting between fixes, since each shifts line numbers), returning the fully-fixed source. Backs the Studio’s “Fix all issues” and an editor command.

A conformant L2 tool MAY implement its own fixer; it MUST NOT mark a finding autofixable unless the rewrite is deterministic and unique.

The reference engine is lintTextWith(source, vocab) in lint-core.js:

  • Pure — no fs, no network, no model call. Every finding (including did-you-mean) is computed deterministically. This is deliberate: diagnostics must be reproducible and runnable in a browser, a CI gate, and an editor alike.
  • Vocabulary-injected — the set of valid names/modifiers/map-regions/finish names is passed in, built from the component manifests (lib/authoring/lint.js on Node; a build-time precomputed vocab in the browser). The same manifests generate grammar.json, so diagnostics and the published grammar can never disagree.
  • A Language Server. This protocol is its foundation — a server wraps lintTextWith + applyFix behind LSP textDocument/publishDiagnostics and textDocument/codeAction. Not built here.
  • Character-precise ranges. v1 locates by slide + line; ranges are a forward-compatible addition (§1).
  • Cross-document / deck-level findings beyond front matter (e.g. duplicate slide IDs, asset existence). Out of scope for 1.x.
  • The optional fields the reference implementation already emits. Some findings carry fields §1 does not define yet: didYouMean and replace (a did-you-mean rewrite), span and col (a span inside the line), chunkLine (which line an autofix starts on), and shapeChange (the fix changes how an existing deck renders). Some findings also omit classToken or line, which §1 lists as required: the review tier’s slide- and deck-level findings and the CLI’s --discover findings carry no line. And three front-matter findings (autosplit-retired, caption-key-retired, paginate-unsupported-value) report slide: 1, where §1 says front matter is slide: 0. Specifying these is the next minor version’s work.
  • Unreleased. §3.11 adds five rules for slide-metadata comments: meta-opener-bare, meta-not-own-line, meta-in-html-block, meta-block-in-container and legacy-directive-comment (off by default).
  • 1.1 (2026-10-08). The registry catches up with the reference implementation: 13 rows became 181: all 178 literal rule IDs plus three families, grouped by what they check (§3), and a test now pins the registry to the code. Two severities join error and warning: info and suggestion (§2). Rows for rules with a did-you-mean (unknown-class, unknown-plugin and most unknown-* front-matter rules) now mark autofix ✓, because the reference implementation offers the rewrite; the test checks each row’s Severity and Autofix against the code. No rule ID was renamed or removed. The spec gains an owner. focus-spec, focus-style, focus-steps and track-directive now run on every slide, a slide with no class included. Source: engineering/decisions/2026-10-08-spec-audit.md §4.1.
  • 1.0 (2026-06-13). First draft: the finding shape, two severities, and a registry of 13 rules.