document
The document root. Wraps the whole file and is where document-scope rules (`title`, `title.pattern`, …) live. A structural marker — it takes no `end<name>`.
Marker · open <!-- mdv: document … --> · structural marker — no end tag
<!-- mdv: document title title.pattern=/^(LOG|ALERT|REPORT):\s.+/ -->
# LOG: Stardate 47988 — derelict freighter recoveredProps
| Prop | Syntax | Category | Description |
|---|---|---|---|
headings.level | headings.level=<option> | structure | Select which ATX heading level (1-6) the directive's heading collection draws from; a selector atom for headings.mask / headings.mask.anyOf / headings.order on the same directive. |
headings.select.mask | headings.select.mask=<option> | structure | Narrow the directive's heading collection to headings whose whole text matches this mask (a predicate selector — non-matching headings fall outside the collection, they are not violations). |
headings.clean | headings.clean | structure | Validate the cleanHeading-normalized heading text instead of the raw source text (raw is the default — masks usually target exactly what the author wrote, brackets included). |
headings.mask | headings.mask=<option> | structure | Require EVERY heading in the directive's collection (all headings at headings.level and/or matching headings.select.mask) to match this anchored mask; one diagnostic per failing heading, on that heading's line. |
headings.mask.anyOf | headings.mask.anyOf=<option> | structure | Require every heading in the directive's collection to match ANY of several anchored masks; the per-heading miss diagnostic names the closest alternative. |
headings.order | headings.order=<option> | structure | Require the keys extracted from the directive's heading collection to strictly descend down the file (headings.order.compare picks the comparator: semver, date, or number; unkeyed headings are skipped; equal keys are violations). |
headings.order.compare | headings.order.compare=<option> | structure | Name the comparator headings.order sorts by: semver (SemVer 2.0.0 precedence, lenient extraction), date (first ISO YYYY-MM-DD run), or number (first numeric run); a modifier atom for headings.order on the same directive. |
headings.order.extract.mask | headings.order.extract.mask=<option> | structure | Narrow where headings.order reads its key: the mask's first match in the heading is handed to the comparator's extractor; without it the extractor scans the whole heading. A modifier atom. |
links.anchors | links.anchors | content | Require every intra-document #fragment link to match a real heading's GitHub slug (repeat headings get GitHub's -1/-2 suffixes). Near-misses earn a did-you-mean with the nearest real slug. Fenced code is never scanned. |
lexicon | lexicon=<option> | content | Check document prose against a glossary source: report occurrences of Deprecated: spellings (with the canonical term) and of Casing: terms written with non-canonical casing. Headings are exempt from casing; code never reports. |
lexicon.undefined | lexicon.undefined=<option> | content | Opt-in: report entity-shaped mentions (capitalized runs, acronyms) absent from the glossary source, once per name, with a decision-fork suggestion (use the near-match, add with a definition, or stop and flag a redefinition for human review). |
markerPairs | markerPairs | document | Warn when paired BEGIN/END HTML-comment generator markers are orphaned (no partner) or crossed (overlapping instead of nested) — a common sign a generator half-ran. |
pairedDisposition.open.mask | pairedDisposition.open.mask=<option> | content | FIFO declare/resolve pairing within heading-bounded sections: every open-mask line must be closed by a close-mask line in the same section, oldest first; leftovers report. |
pairedDisposition.close.mask | pairedDisposition.close.mask=<option> | content | A close mask for pairedDisposition.open.mask — a line matching it closes the oldest open entry in the section. |
pairedDisposition.close.mask.anyOf | pairedDisposition.close.mask.anyOf=<option> | content | Alternative close masks for pairedDisposition.open.mask — a line matching ANY alternative closes the oldest open entry. |
document.placement.absent | document.placement.absent=<option> | document | Warn when a target exists but no marker (mask/lexicon match) exists anywhere in the document. |
document.placement.guarded | document.placement.guarded=<option> | document | Warn when the nearest preceding marker (a mask/lexicon match) sits more than proximity= blocks before the target link — present, but not close enough to read as placed there. |
document.placement.unguardedRegion | document.placement.unguardedRegion=<option> | document | Warn when an inline marker sits inside the trailing regionFraction= of the document, after the target — buried, not merely late. |
document.placement.unguarded | document.placement.unguarded=<option> | document | Warn when every marker (if any) sits at or after the target — nothing guards it beforehand. anchor=firstAfter (default) or anchor=pivot picks the reported line. |
document.placement.weakGuarded | document.placement.weakGuarded=<option> | document | Warn when the only marker preceding the target is a link whose LABEL matches, not text stated directly in the block's own prose. |
firstLine.mask.anyOf | firstLine.mask.anyOf=<option> | document | Require the document's subject — its first heading or paragraph, AST-selected — to match ANY of several masks; the miss diagnostic names the closest alternative. |
firstLine.mask | firstLine.mask=<option> | document | Require the document's subject — its first heading or paragraph, AST-selected — to match a mask. Leading HTML is skipped; a heading is read without its # markers; a document opening with a list, table, code block, or nothing recognizable stays silent. |
scan.mask | scan.mask=<option> | content | Scan document prose for every occurrence of a mask and report captured values absent from a contract-relative |in: source, once at first occurrence. |
mentions.shape | mentions.shape=<option> | content | The shape of a checked mention (a quoted mask; the whole match is the mention). Carrier for mentions.in, which performs the membership scan. |
mentions.in | mentions.in=<option> | content | Check every mention matching the sibling mentions.shape against this source: an unknown mention reports once, at its first occurrence, with a did-you-mean suggestion. |
maxSeverity | maxSeverity=<option> | document | Cap every doc-side error at "warn" so a failing check exits 0 - a severity floor the contract author sets deliberately, not a per-rule dial. autofix still treats the document as passing (validateSyntax only reports schema-author errors), so clamped issues are not auto-repaired; they are left for a human to read and decide. Only "warn" is accepted - "error" would be a no-op, since error is already the ceiling. |
noExemplarResidue | noExemplarResidue | content | Flag worked-example (exemplar) values a contract declares in its own manifest when they survive into an ordinary target document. Self-activated by the manifest's presence. |
noFenceWrapper | noFenceWrapper | document | Reject a document whose entire body is wrapped in a single code fence (a common LLM failure where the model fenced the whole answer). |
ciContains | ciContains | content | Every x (95% CI a–b) interval in prose or a table cell must contain its point estimate and be ordered (a ≤ x ≤ b). Skips mixed-unit or one-sided intervals; never fires inside code fences. Warns only. |
percent | percent | content | Recompute self-contained n/N (p%) percentages: warn when p ≠ round(100·n/N) at the displayed precision. Skips when N=0 or a part is unparseable. Warns only. |
sumToTotal | sumToTotal | content | Within one scope (a line/item), parse label: value parts plus a declared total and warn when the parts do not sum to the total. Skips when there is no total, <2 parts, or an unparseable value. Warns only. |
noLLMResidue | noLLMResidue | document | Warn when leftover assistant chatter is present (e.g. "As an AI language model", "Sure, here's", "I hope this helps"). |
noTruncation | noTruncation | document | Warn when the document appears to end mid-sentence or with an obvious cut marker - a common sign of a truncated model response. |
citations.resolve.local | citations.resolve.local | document | Require every relative file link in the document to point to a file that actually exists on disk, resolved relative to the document's own directory. Absolute URLs, anchors, and absolute paths are ignored. Only runs when the caller supplies a filesystem capability (the CLI always does); library callers validating an in-memory string get no diagnostics from this rule. |
citations.resolve | citations.resolve | document | Require every citation in the document to resolve — an inline text with a real URL, a [text][id] reference, or a [^id] footnote whose definition exists. Dangling citations are flagged; lines with no citation are fine. When the caller supplies a resolver capability, every reference is additionally checked against it — one that doesn't confirm as real is flagged, even if it already resolves on syntax alone. |
title.noPlaceholder | title.noPlaceholder | document | Reject placeholder titles such as "Untitled" or "TODO". |
title.pattern | title.pattern=<option> | document | Require the title to match a regular expression — e.g. a Conventional-Commit-shaped title. |
title.minWords | title.minWords=<option> | document | Require the title to have at least N words, so it's specific rather than a single noun. |
title.maxWords | title.maxWords=<option> | document | Cap the title at N words. |
title | title | document | Require the document to have a title — the PR or issue title, supplied via --title or --context. |
Prop details
headings.level
Select which ATX heading level (1-6) the directive's heading collection draws from; a selector atom for headings.mask / headings.mask.anyOf / headings.order on the same directive.
headings.level=<option> · category: structure · status: stable · src/pipeline/rules/document/headings/collection.ts
Selects which ATX level the directive's heading collection draws from (1-6). A selector atom:
carries no diagnostic of its own; headings.mask, headings.mask.anyOf, and headings.order
on the same directive validate the selected collection.
<!-- mdv: document headings.level=2 headings.mask="\[<semver>\] - <date>" -->headings.select.mask
Narrow the directive's heading collection to headings whose whole text matches this mask (a predicate selector — non-matching headings fall outside the collection, they are not violations).
headings.select.mask=<option> · category: structure · status: stable · src/pipeline/rules/document/headings/collection.ts
<!-- mdv: document headings.level=2 headings.select.mask="\[<text>\][ - <date>]" headings.order=desc headings.order.compare=semver -->headings.clean
Validate the cleanHeading-normalized heading text instead of the raw source text (raw is the default — masks usually target exactly what the author wrote, brackets included).
headings.clean · category: structure · status: stable · src/pipeline/rules/document/headings/collection.ts
Switches the directive's heading collection to the cleanHeading-normalized view (links resolved to their text, brackets stripped, leading emoji/numbering removed). The default is the RAW trimmed heading text, because collection masks usually validate exactly what the author wrote.
<!-- mdv: document headings.level=3 headings.clean headings.mask="<word|oneOf: [Added,Changed,Fixed,Removed]>" -->headings.mask
Require EVERY heading in the directive's collection (all headings at headings.level and/or matching headings.select.mask) to match this anchored mask; one diagnostic per failing heading, on that heading's line.
headings.mask=<option> · category: structure · status: stable · src/pipeline/rules/document/headings/collection.ts
<!-- mdv: document headings.level=2 headings.mask="\[<semver>\] - <date>" -->
## [1.4.0] - 2026-06-21headings.mask.anyOf
Require every heading in the directive's collection to match ANY of several anchored masks; the per-heading miss diagnostic names the closest alternative.
headings.mask.anyOf=<option> · category: structure · status: stable · src/pipeline/rules/document/headings/collection.ts
Every heading in the directive's collection must match ANY of the alternatives; the miss diagnostic names the closest alternative (longest shared prefix, ties to first listed).
<!-- mdv: document headings.level=2 headings.mask.anyOf=["\[Unreleased\]", "\[<semver>\] - <date>"] -->headings.order
Require the keys extracted from the directive's heading collection to strictly descend down the file (headings.order.compare picks the comparator: semver, date, or number; unkeyed headings are skipped; equal keys are violations).
headings.order=<option> · category: structure · status: stable · src/pipeline/rules/document/headings/collection.ts
The keys extracted from the directive's heading collection must strictly DESCEND down the file
(newest/highest first). Headings that yield no key are skipped — they neither break nor continue
the run (the [Unreleased] posture). Equal keys are violations (strict descent).
<!-- mdv: document headings.level=2 headings.order=desc headings.order.compare=semver -->
## [1.4.0] - 2026-06-21
## [1.3.2] - 2026-05-30headings.order.compare
Name the comparator headings.order sorts by: semver (SemVer 2.0.0 precedence, lenient extraction), date (first ISO YYYY-MM-DD run), or number (first numeric run); a modifier atom for headings.order on the same directive.
headings.order.compare=<option> · category: structure · status: stable · src/pipeline/rules/document/headings/collection.ts
<!-- mdv: document headings.level=2 headings.order=desc headings.order.compare=date -->headings.order.extract.mask
Narrow where headings.order reads its key: the mask's first match in the heading is handed to the comparator's extractor; without it the extractor scans the whole heading. A modifier atom.
headings.order.extract.mask=<option> · category: structure · status: stable · src/pipeline/rules/document/headings/collection.ts
Narrows WHERE headings.order reads its key from: the mask's first match in the heading text is
handed to the comparator's extractor instead of the whole heading. Optional — without it the
comparator's lenient extractor scans the whole heading.
<!-- mdv: document headings.level=2 headings.order=desc headings.order.compare=number headings.order.extract.mask="v<num>" -->links.anchors
Require every intra-document #fragment link to match a real heading's GitHub slug (repeat headings get GitHub's -1/-2 suffixes). Near-misses earn a did-you-mean with the nearest real slug. Fenced code is never scanned.
links.anchors · category: content · status: stable · src/pipeline/rules/document/headings/link-anchors.ts
Require every intra-document anchor link to point at a real heading. The fragment is compared
against each heading's GitHub slug (lower-case, spaces to hyphens, punctuation dropped, with
GitHub's -1 repeat disambiguation); a near-miss earns a did-you-mean naming the nearest
real slug. Links inside fenced code never count.
<!-- mdv: document links.anchors -->
# Guide
[Quick Start](#quick-start)
## Quick Startlexicon
Check document prose against a glossary source: report occurrences of Deprecated: spellings (with the canonical term) and of Casing: terms written with non-canonical casing. Headings are exempt from casing; code never reports.
lexicon=<option> · category: content · status: stable · src/pipeline/rules/document/lexicon.ts
Bind the document to a glossary source and enforce its data-driven terminology laws: every
Deprecated: spelling reports with the canonical term as the fix, and every Casing: term
whose occurrence differs from the declared casing reports the canonical form. Headings are
exempt from the casing check (title case is legitimate there); a capitalized first letter of an
otherwise all-lowercase term is tolerated anywhere (sentence position), so brand names that must
stay lowercase even at sentence start belong in Deprecated: explicitly (e.g. Mediva).
Occurrences inside fenced or inline code never report. Stays silent without a sources
capability — same honest-null posture as entitiesIn.
<!-- mdv: document lexicon=./glossary.json -->lexicon.undefined
Opt-in: report entity-shaped mentions (capitalized runs, acronyms) absent from the glossary source, once per name, with a decision-fork suggestion (use the near-match, add with a definition, or stop and flag a redefinition for human review).
lexicon.undefined=<option> · category: content · status: stable · src/pipeline/rules/document/lexicon.ts
Opt-in named-entity gate: every entity-shaped mention in the document's prose (capitalized runs,
acronyms, punctuated brand names — the same deterministic extraction entitiesIn uses) must be
present in the glossary source as a term, alias, deprecated form, or compound. A miss reports
once with a decision fork rather than a bare "add it": the near-match to prefer, the
add-with-definition path for genuinely new terms, and an explicit stop-and-ask-a-human path for
redefinitions — copy designed so an LLM author cannot satisfy the diagnostic by blindly
appending to the glossary. Lowercase plain-English words are never candidates, and neither are
lone sentence-case words or filename-shaped tokens (precision over recall: acronyms, CamelCase,
dotted brands, and multi-word runs are the entity shapes that gate); this rule checks named
entities, not vocabulary at large. Stays silent without a sources capability.
<!-- mdv: document lexicon.undefined=./glossary.json -->markerPairs
Warn when paired BEGIN/END HTML-comment generator markers are orphaned (no partner) or crossed (overlapping instead of nested) — a common sign a generator half-ran.
markerPairs · category: document · status: stable · src/pipeline/rules/document/markers/marker-pairs.ts
Flags unbalanced BEGIN/END generator-comment markers: an END with no matching BEGIN (or a BEGIN
with no matching END) is an ORPHAN; a pair that overlaps another open pair instead of nesting is a
CROSS. Orphaned markers are the tell-tale of a generator that half-ran. Warns by default (harden
with error=markerPairs); a document-scope rule. Recognises BEGIN key/END key, start:key/
end:key, and key:start/key:end. Markers inside fenced code blocks are ignored.
<!-- mdv: document markerPairs -->
<!-- BEGIN generated:cli-flags -->
| Flag | Description |
<!-- END generated:cli-flags -->pairedDisposition.open.mask
FIFO declare/resolve pairing within heading-bounded sections: every open-mask line must be closed by a close-mask line in the same section, oldest first; leftovers report.
pairedDisposition.open.mask=<option> · category: content · status: stable · src/pipeline/rules/document/markers/paired-disposition.ts
P17 (template-packs dissolution): identifier-free FIFO declare/resolve pairing within
heading-bounded sections. Every line matching pairedDisposition.open.mask opens an entry;
every line matching any close mask (pairedDisposition.close.mask /
pairedDisposition.close.mask.anyOf) closes the OLDEST still-open entry in the current section
(first-in, first-out — matching how such logs read, top to bottom). A heading is a scope
boundary: entries still open when a heading arrives are reported there and then — an entry in
one section can never be closed from another. Anything open at document end reports too.
Matching is line-based over prose-bearing blocks (paragraph/blockquote per physical line, list
items; headings are boundaries, code nodes structurally invisible) and case-insensitive by
default — the convention every ported /^Open.../i source pattern carries. Masks share the
contract's token environment; regex-flavored fragments (character classes, quantifiers) belong
in file-local tokens, not inline mask text.
{found} is the opening line with the open mask's leading literal stripped, trimmed, truncated
at 80 chars with … — "the detail after the label," the text a human names the entry by.
<!-- mdv: fragment anomaly-detail=".*" -->
<!-- mdv: fragment resolution-detail=".*" -->
<!-- mdv: document pairedDisposition.open.mask="Anomaly: <anomaly-detail>" pairedDisposition.close.mask="Resolution:<resolution-detail>" -->pairedDisposition.close.mask
A close mask for pairedDisposition.open.mask — a line matching it closes the oldest open entry in the section.
pairedDisposition.close.mask=<option> · category: content · status: stable · src/pipeline/rules/document/markers/paired-disposition.ts
Modifier: a single close mask. Consumed by pairedDisposition.open.mask via the siblings channel (parsed option, no raw re-parse). Empty diagnostics + diagnosticHost:false keep it out of the code=/message= one-host binding count.
pairedDisposition.close.mask.anyOf
Alternative close masks for pairedDisposition.open.mask — a line matching ANY alternative closes the oldest open entry.
pairedDisposition.close.mask.anyOf=<option> · category: content · status: stable · src/pipeline/rules/document/markers/paired-disposition.ts
Modifier: alternative close masks (mask.anyOf fold form — quoted alternatives joined by bare
|). Any alternative closes the oldest open entry. Same siblings-channel consumption.
document.placement.absent
Warn when a target exists but no marker (mask/lexicon match) exists anywhere in the document.
document.placement.absent=<option> · category: document · status: stable · src/pipeline/rules/document/markers/placement/absent.ts
P8 (placement.verdict "absent" outcome; matrix row's disclosure-absent concrete referent): the
strongest claim this primitive can make — fires when a target exists but NO marker of any kind
exists anywhere in the document. Distinct from document.placement.unguarded (a marker exists,
just not before the target) — conflating the two loses a real distinction the underlying message
text always carried (must-fix per the source row). Anchored at the target's own line.
<!-- mdv: document document.placement.absent=pivot="amzn.to","/go/";marker="affiliate","commission" -->document.placement.guarded
Warn when the nearest preceding marker (a mask/lexicon match) sits more than proximity= blocks before the target link — present, but not close enough to read as placed there.
document.placement.guarded=<option> · category: document · status: stable · src/pipeline/rules/document/markers/placement/guarded.ts
P8 (placement.verdict "guarded" outcome, matrix row's preceding-inline concrete referent):
fires when the nearest preceding marker of the highest-precedence kind (inline) exists, but sits
more than proximity= blocks before the target — "guarded, but too far to read as placed there."
Silent when the marker is within the proximity window, or when no marker precedes at all (see
document.placement.weakGuarded / document.placement.unguarded / document.placement.absent
for those outcomes). Anchored at the MARKER's own line, not the target's — the reader needs to
move the marker, not the target (P8 spec, "Diagnostic anchoring").
<!-- mdv: document document.placement.guarded=pivot="amzn.to","/go/";marker="affiliate","commission";proximity=2 -->document.placement.unguardedRegion
Warn when an inline marker sits inside the trailing regionFraction= of the document, after the target — buried, not merely late.
document.placement.unguardedRegion=<option> · category: document · status: stable · src/pipeline/rules/document/markers/placement/unguarded-region.ts
P8 (placement.verdict "unguarded" outcome, regionMatch refinement; matrix row's
disclosure-footer-only concrete referent): a STRICT refinement of document.placement.unguarded
— fires only when the verdict is unguarded AND a marker of the highest-precedence kind
(inline) sits inside the trailing regionFraction= of the document. Region membership is
restricted to that SAME precedence kind as guarded/weakGuarded's inline — a link-only
marker in the region does NOT count (must-fix per the source row; easy to drop by accident).
Every firing of this rule implies document.placement.unguarded also holds on the same document —
both are expected to co-fire when both are enabled, which is why they are separate rule names
(not merged with unguarded's own engine-internal mutual exclusion, which only dedupes repeated
applications of the SAME rule name).
Anchored at the region-matched marker's own line — a THIRD distinct line from unguarded's
pivot-line/first-after-line pair.
<!-- mdv: document document.placement.unguardedRegion=pivot="amzn.to","/go/";marker="affiliate","commission";regionFraction=0.75 -->document.placement.unguarded
Warn when every marker (if any) sits at or after the target — nothing guards it beforehand. anchor=firstAfter (default) or anchor=pivot picks the reported line.
document.placement.unguarded=<option> · category: document · status: stable · src/pipeline/rules/document/markers/placement/unguarded.ts
P8 (placement.verdict "unguarded" outcome, matrix row's after-only concrete referent): fires
when every marker (if any) sits at or after the target — nothing guards it beforehand. The SAME
underlying fact supports two framings a template author picks between with anchor= (P8 spec,
"Diagnostic anchoring": "P8 must expose BOTH anchor choices as available payload fields on the
verdict object... so a template author's choice of code/message can pick either"):
anchor=firstAfter(default) — "here's where the visible marker actually is" (weaker claim, anchored at the first marker found after the target).anchor=pivot— "here's where a marker should have already been" (stronger claim, anchored at the target itself).{first-after-line}and{pivot-line}are both always available inmessage=/suggestion=overrides regardless of which one is the reported anchor line.
Applying this SAME rule name twice with different anchor=/code= on an identical pivot/marker
config (e.g. one soft, one hard framing of the same fact) reports only the FIRST application —
engine-internal mutual exclusion (ADJUDICATION.md row ruling), not per-template convention.
<!-- mdv: document document.placement.unguarded=pivot="amzn.to","/go/";marker="affiliate","commission" -->document.placement.weakGuarded
Warn when the only marker preceding the target is a link whose LABEL matches, not text stated directly in the block's own prose.
document.placement.weakGuarded=<option> · category: document · status: stable · src/pipeline/rules/document/markers/placement/weak-guarded.ts
P8 (placement.verdict "weak-guarded" outcome, matrix row's link-only-preceding concrete
referent): fires when the nearest preceding marker exists ONLY as a link's label (not stated in
the block's own prose) — no inline marker precedes the target at all. Unlike the other four
document.placement.* rules, this one makes no claim about ABSENCE (a marker IS visibly present),
so it is not gated by any external fact — it always fires when the verdict kind matches. Anchored
at the marker's own line.
<!-- mdv: document document.placement.weakGuarded=pivot="amzn.to","/go/";marker="affiliate","commission" -->firstLine.mask.anyOf
Require the document's subject — its first heading or paragraph, AST-selected — to match ANY of several masks; the miss diagnostic names the closest alternative.
firstLine.mask.anyOf=<option> · category: document · status: stable · src/pipeline/rules/document/masks/first-line-mask-any-of.ts
The list-valued mask slot (GRAMMAR-SPEC §1 "mask.anyOf") for the document's subject line: same G1
AST selection as firstLine.mask, but the subject may match ANY of several masks — alternatives
tried in order, a miss names the closest one (longest shared prefix, ties to first listed).
|in: membership captures are per-alternative: only the MATCHED alternative's captures are
checked, same honest-null sources contract as firstLine.mask.
<!-- mdv: document firstLine.mask.anyOf=["<word|oneOf: [feat,fix]>: <text>", "Unreleased"] -->firstLine.mask
Require the document's subject — its first heading or paragraph, AST-selected — to match a mask. Leading HTML is skipped; a heading is read without its # markers; a document opening with a list, table, code block, or nothing recognizable stays silent.
firstLine.mask=<option> · category: document · status: stable · src/pipeline/rules/document/masks/first-line-mask.ts
Require the document's "subject" line — its first heading or paragraph, in document order — to
match a mask. The generic primitive behind retired rules such as git-commit/subjectFormat
(template-packs matrix plugin-git-commit, behavior G1): selection is AST-aware (leading HTML is
skipped, a heading is read without its # markers, and a document opening with a list/table/code
block — or an empty document — is silent rather than guessed at). The slot threads the shared
token environment (GRAMMAR-SPEC §3) exactly like pattern.mask.
<!-- mdv: document firstLine.mask="<word|oneOf: [feat,fix,docs]>: <text>" -->
feat: tune orbital beaconscan.mask
Scan document prose for every occurrence of a mask and report captured values absent from a contract-relative |in: source, once at first occurrence.
scan.mask=<option> · category: content · status: stable · src/pipeline/rules/document/masks/scan-mask.ts
Scan prose-bearing document nodes for every occurrence of a mask and check each |in: capture
against its contract-relative source. Unknown captured values report once, case-insensitively,
at their first real line. Headings, paragraphs, blockquotes, and list items are scanned; fenced
code, HTML, tables, definitions, and thematic breaks are structurally excluded.
This is the document-occurrence counterpart to field-scoped pattern.mask: it is intentionally
silent when a mask has no occurrence, when every captured value is known, or when an optional
source is absent. At least one |in: capture is required because a shape-only document scan has
no mismatch polarity; use forbid.mask when finding the shape itself is the failure.
<!-- mdv: fragment person = (?:Captain|Ensign)\s+[A-Z][A-Za-z'-]+ -->
<!-- mdv: document scan.mask="<person|in: ./roster.txt?>" -->mentions.shape
The shape of a checked mention (a quoted mask; the whole match is the mention). Carrier for mentions.in, which performs the membership scan.
mentions.shape=<option> · category: content · status: stable · src/pipeline/rules/document/masks/scan-mask.ts
mentions.in
Check every mention matching the sibling mentions.shape against this source: an unknown mention reports once, at its first occurrence, with a did-you-mean suggestion.
mentions.in=<option> · category: content · status: stable · src/pipeline/rules/document/masks/scan-mask.ts
maxSeverity
Cap every doc-side error at "warn" so a failing check exits 0 - a severity floor the contract author sets deliberately, not a per-rule dial. autofix still treats the document as passing (validateSyntax only reports schema-author errors), so clamped issues are not auto-repaired; they are left for a human to read and decide. Only "warn" is accepted - "error" would be a no-op, since error is already the ceiling.
maxSeverity=<option> · category: document · status: stable · src/pipeline/rules/document/max-severity.ts
A contract-wide severity floor: every doc-side diagnostic that would be error is reported as
warn instead, so a failing check exits 0. The floor always wins - a per-rule error=<rule>
override cannot escape it. Only doc-side output is clamped; a broken CONTRACT (a malformed
directive, an invalid option) still errors, because that is the schema author's own bug, not the
document's. This is an advisory-contract escape hatch, not a rule you tune per-diagnostic: to
promote a specific rule back to blocking, delete this atom, not the rule.
<!-- mdv: document maxSeverity=warn -->
<!-- mdv: section required -->
## Summary
A short note that would otherwise fail as an error, but only warns here.
<!-- mdv: endsection -->noExemplarResidue
Flag worked-example (exemplar) values a contract declares in its own manifest when they survive into an ordinary target document. Self-activated by the manifest's presence.
noExemplarResidue · category: content · status: stable · src/pipeline/rules/document/no-exemplar-residue.ts
M3 exemplar-safety (scratchpad/out-sol-mock-data.md): flag worked-example ("exemplar") values a
contract declares in its own <!-- mdv: exemplars ... --> manifest (compiled at schema-compile
time, src/compile/exemplars.ts) when they survive into an ordinary target document. SELF-
ACTIVATING — this rule is never authored by hand; a present, valid manifest synthesizes this
rule's own document-scope application (origin "manifest") at error severity, so the copied
contract is self-contained. Config's normal rules: severity override still applies by name,
same as any other rule — only the APPLICATION itself is synthesized, not the severity.
Scans structured AST domains, never raw bytes, and NEVER the manifest's own HTML comment (HTML
nodes are never a scanned domain here) — the manifest cannot catch itself. Matching is exact,
case-sensitive, NFC-normalized; no stemming or fuzzing. word requires identifier boundaries on
both sides; phrase tolerates whitespace-run differences but is otherwise a literal substring;
exact requires the WHOLE selected unit (a heading's text, a list item, a table cell, a whole
code block, or a frontmatter value) to equal the declared value. Every declared value reports
once, at its first occurrence, with the total occurrence count across every declared location -
never one diagnostic per occurrence (a single pasted fictional world must not flood the terminal).
Never autofixed: the honest replacement is a real project fact the validator cannot invent.
<!-- mdv: exemplars
{"version": 1, "values": [{ "value": "Lanternway", "match": "word", "in": ["heading", "prose"] }] }
-->
# LanternwaynoFenceWrapper
Reject a document whose entire body is wrapped in a single code fence (a common LLM failure where the model fenced the whole answer).
noFenceWrapper · category: document · status: stable · src/pipeline/rules/document/no-fence-wrapper.ts
<!-- mdv: document noFenceWrapper -->
# Mission notes
Real Markdown, not the whole captain's log inside one ``` fence.ciContains
Every x (95% CI a–b) interval in prose or a table cell must contain its point estimate and be ordered (a ≤ x ≤ b). Skips mixed-unit or one-sided intervals; never fires inside code fences. Warns only.
ciContains · category: content · status: stable · src/pipeline/rules/numeric/ci-outside.ts
Every x (95% CI a–b) interval in prose or a table cell must contain its point estimate and be
ordered: warn when the point is below the lower bound, above the upper bound, or the interval is
inverted (a > b). Walks ctx.ast, so a CI inside a code fence never fires. Skips silently when the
figures mix % and bare units, when the interval is one-sided, or when anything is not confidently
parsed. Warns only.
<!-- mdv: document ciContains -->
The hazard ratio was 0.82 (95% CI 0.71–0.95).percent
Recompute self-contained n/N (p%) percentages: warn when p ≠ round(100·n/N) at the displayed precision. Skips when N=0 or a part is unparseable. Warns only.
percent · category: content · status: stable · src/pipeline/rules/numeric/percent.ts
Recompute self-contained n/N (p%) figures: warn when the stated percent p does not equal
100·n/N rounded to the precision p was displayed at. Skips (never warns) when N is zero or
any part is unparseable. Document-scope so it catches the figure wherever it appears in prose;
warns only.
<!-- mdv: document percent -->
37/120 (42.1%) of participants reported nausea.sumToTotal
Within one scope (a line/item), parse label: value parts plus a declared total and warn when the parts do not sum to the total. Skips when there is no total, <2 parts, or an unparseable value. Warns only.
sumToTotal · category: content · status: stable · src/pipeline/rules/numeric/sum-to-total.ts
Sum-to-total within one scope: parse the label: value parts and the declared total on a line
(train: 8000, validation: 1000, test: 500 (total: 10000)) and warn when the parts do not add up
to the total. Skips (never warns) a scope with no total, fewer than two parts, or any unparseable
value. Warns only.
<!-- mdv: document sumToTotal -->
train: 8000, validation: 1000, test: 500 (total: 10000)noLLMResidue
Warn when leftover assistant chatter is present (e.g. "As an AI language model", "Sure, here's", "I hope this helps").
noLLMResidue · category: document · status: stable · src/pipeline/rules/plugins/agent-hygiene/no-llm-residue.ts
<!-- mdv: document noLLMResidue -->
# Mission notes
Straight to the airlock report, with no "Certainly! Here's the…" preamble.noTruncation
Warn when the document appears to end mid-sentence or with an obvious cut marker - a common sign of a truncated model response.
noTruncation · category: document · status: stable · src/pipeline/rules/plugins/agent-hygiene/no-truncation.ts
<!-- mdv: document noTruncation -->
# Mission notes
The jump completed and every navigation shard reported healthy.citations.resolve.local
Require every relative file link in the document to point to a file that actually exists on disk, resolved relative to the document's own directory. Absolute URLs, anchors, and absolute paths are ignored. Only runs when the caller supplies a filesystem capability (the CLI always does); library callers validating an in-memory string get no diagnostics from this rule.
citations.resolve.local · category: document · status: stable · src/pipeline/rules/plugins/evidence/citations-resolve-local.ts
Checks that every relative file link in the document - [text](./docs/foo.md),
[text](../x/y.png), [text](docs/foo.md) - points to a file that actually exists on disk,
resolved relative to the document's own directory. Absolute URLs, anchors, and absolute paths are
ignored; any #fragment/?query suffix is stripped before the existence check. There is no
network access, ever - this only ever probes the local filesystem via the capability the caller
supplies. Library callers validating an in-memory string (no document path/exists) get no
diagnostics from this rule at all - there is no disk to check against, so it stays silent rather
than complaining about missing context.
<!-- mdv: document citations.resolve.local -->
See [the setup guide](./docs/setup.md) for details.citations.resolve
Require every citation in the document to resolve — an inline text with a real URL, a [text][id] reference, or a [^id] footnote whose definition exists. Dangling citations are flagged; lines with no citation are fine. When the caller supplies a resolver capability, every reference is additionally checked against it — one that doesn't confirm as real is flagged, even if it already resolves on syntax alone.
citations.resolve · category: document · status: stable · src/pipeline/rules/plugins/evidence/citations-resolve.ts
Checks that every citation in the document resolves — an inline text with a real URL, a [text][id] reference whose definition exists, or a [^id] footnote whose definition exists. A dangling citation (placeholder URL, or a ref/footnote with no matching definition) is flagged. Lines with no citation are fine; this only catches the ones that point at nothing.
When the caller supplies a resolve capability (ValidationOptions.document.resolve, built from
a ResolverSnapshot via resolveRefs/snapshotResolver in src/api/resolve-refs.ts), every
syntactically-valid link and footnote citation is ALSO checked against it: a reference the
resolver cannot confirm exists is flagged unresolved-reference, even though it already resolves
on syntax alone. Library callers with no resolver capability get none of these extra diagnostics —
only the syntax-only unresolved-citation check above ever runs for them.
<!-- mdv: document citations.resolve -->
The probe battery draw dropped after hibernation cycling [1].
[1]: https://example.com/benchmarktitle.noPlaceholder
Reject placeholder titles such as "Untitled" or "TODO".
title.noPlaceholder · category: document · status: stable · src/pipeline/rules/title/title-no-placeholder.ts
Checks that the document heading is not placeholder text; a document-scope rule.
<!-- mdv: document title.noPlaceholder -->
# Confirm rover uplinktitle.pattern
Require the title to match a regular expression — e.g. a Conventional-Commit-shaped title.
title.pattern=<option> · category: document · status: stable · src/pipeline/rules/title/title-pattern.ts
<!-- mdv: document title.pattern=/^(feat|fix|docs):\s.+/ -->
# feat: tune orbital beacontitle.minWords
Require the title to have at least N words, so it's specific rather than a single noun.
title.minWords=<option> · category: document · status: stable · src/pipeline/rules/title/title-words.ts
<!-- mdv: document title.minWords=3 -->
# Align Europa relaytitle.maxWords
Cap the title at N words.
title.maxWords=<option> · category: document · status: stable · src/pipeline/rules/title/title-words.ts
Checks that the document heading stays under a maximum word count; a document-scope rule.
<!-- mdv: document title.maxWords=8 -->
# Patch the beacon after docking tests passtitle
Require the document to have a title — the PR or issue title, supplied via --title or --context.
title · category: document · status: stable · src/pipeline/rules/title/title.ts
<!-- mdv: document title -->
# Calibrate starship sensorsmedia
A slot for visual evidence — a screenshot or video. `requiredOrNA` accepts an explicit N/A for changes with nothing visual to show. Close with `endmedia`.
front
A frontmatter key, written `front <key>`. Attaches presence/value rules to a single YAML frontmatter field. A structural marker — it takes no `end<name>`.