DocsReference

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 recovered

Props

PropSyntaxCategoryDescription
headings.levelheadings.level=<option>structureSelect 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.maskheadings.select.mask=<option>structureNarrow 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.cleanheadings.cleanstructureValidate 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.maskheadings.mask=<option>structureRequire 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.anyOfheadings.mask.anyOf=<option>structureRequire every heading in the directive's collection to match ANY of several anchored masks; the per-heading miss diagnostic names the closest alternative.
headings.orderheadings.order=<option>structureRequire 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.compareheadings.order.compare=<option>structureName 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.maskheadings.order.extract.mask=<option>structureNarrow 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.anchorslinks.anchorscontentRequire 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.
lexiconlexicon=<option>contentCheck 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.undefinedlexicon.undefined=<option>contentOpt-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).
markerPairsmarkerPairsdocumentWarn 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.maskpairedDisposition.open.mask=<option>contentFIFO 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.maskpairedDisposition.close.mask=<option>contentA close mask for pairedDisposition.open.mask — a line matching it closes the oldest open entry in the section.
pairedDisposition.close.mask.anyOfpairedDisposition.close.mask.anyOf=<option>contentAlternative close masks for pairedDisposition.open.mask — a line matching ANY alternative closes the oldest open entry.
document.placement.absentdocument.placement.absent=<option>documentWarn when a target exists but no marker (mask/lexicon match) exists anywhere in the document.
document.placement.guardeddocument.placement.guarded=<option>documentWarn 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.unguardedRegiondocument.placement.unguardedRegion=<option>documentWarn when an inline marker sits inside the trailing regionFraction= of the document, after the target — buried, not merely late.
document.placement.unguardeddocument.placement.unguarded=<option>documentWarn 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.weakGuardeddocument.placement.weakGuarded=<option>documentWarn 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.anyOffirstLine.mask.anyOf=<option>documentRequire 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.maskfirstLine.mask=<option>documentRequire 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.maskscan.mask=<option>contentScan document prose for every occurrence of a mask and report captured values absent from a contract-relative |in: source, once at first occurrence.
mentions.shapementions.shape=<option>contentThe shape of a checked mention (a quoted mask; the whole match is the mention). Carrier for mentions.in, which performs the membership scan.
mentions.inmentions.in=<option>contentCheck 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.
maxSeveritymaxSeverity=<option>documentCap 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.
noExemplarResiduenoExemplarResiduecontentFlag 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.
noFenceWrappernoFenceWrapperdocumentReject a document whose entire body is wrapped in a single code fence (a common LLM failure where the model fenced the whole answer).
ciContainsciContainscontentEvery 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.
percentpercentcontentRecompute 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.
sumToTotalsumToTotalcontentWithin 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.
noLLMResiduenoLLMResiduedocumentWarn when leftover assistant chatter is present (e.g. "As an AI language model", "Sure, here's", "I hope this helps").
noTruncationnoTruncationdocumentWarn when the document appears to end mid-sentence or with an obvious cut marker - a common sign of a truncated model response.
citations.resolve.localcitations.resolve.localdocumentRequire 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.resolvecitations.resolvedocumentRequire 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.noPlaceholdertitle.noPlaceholderdocumentReject placeholder titles such as "Untitled" or "TODO".
title.patterntitle.pattern=<option>documentRequire the title to match a regular expression — e.g. a Conventional-Commit-shaped title.
title.minWordstitle.minWords=<option>documentRequire the title to have at least N words, so it's specific rather than a single noun.
title.maxWordstitle.maxWords=<option>documentCap the title at N words.
titletitledocumentRequire 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-21

headings.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-30

headings.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 Start

lexicon

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 in message=/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 beacon

scan.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"] }] }
-->
# Lanternway

noFenceWrapper

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/benchmark

title.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 uplink

title.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 beacon

title.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 relay

title.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 pass

title

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 sensors

On this page