DocsReference

section

The one heading-owning, recursive container tag. Wraps its ATX heading plus any own-prose or nested tags (child sections and leaves), and is the thing presence/body rules (`required`, `minWords`, `noPlaceholder`, …) and `occurs=` target. Sections nest, so a schema is an AST of sections — close every one with its `endsection` end tag. Opens directly before its ATX heading.

Container · open <!-- mdv: section … --> · close <!-- mdv: endsection -->

<!-- mdv: section required minWords=20 -->
## Away Mission Summary
The landing party reached the derelict freighter, recovered the flight recorder,
and confirmed no survivors aboard. Returning to ship with the data core intact.
<!-- mdv: endsection -->

Props

PropSyntaxCategoryDescription
maxLinksmaxLinks=<option>bodyCap the section body at N markdown links, to keep a field from turning into a link dump.
minParagraphsminParagraphs=<option>bodyRequire at least N distinct paragraphs in the section body — counts are a proxy for structural completeness (WARN by default).
maxParagraphsmaxParagraphs=<option>bodyCap the section body at N distinct paragraphs — counts are a proxy for structural completeness (WARN by default).
minWordsminWords=<option>bodyRequire at least N words of prose in the section body, so a field can't be filled with a token gesture.
maxWordsmaxWords=<option>bodyCap the section body at N words, to keep summaries tight.
distinctFromdistinctFrom=<option>bodyFail when this section's text is identical or near-identical to another named section in the same document.
forbid.lexiconforbid.lexicon=<option>bodyFail when the section body contains any phrase from a lexicon file. The lexicon must be plain newline-delimited text (.txt), one phrase per line; blank lines and #-prefixed comment lines are ignored. A .md lexicon path is loaded as markdown and its member extraction yields different results, so always use a .txt file here.
forbid.maskforbid.mask=<option>bodyFail when the section body matches a mask (a small token vocabulary compiling to a safe regex), e.g. a stray issue reference.
forbid.scanFencesforbid.scanFences=<option>bodyOpt the sibling forbid.mask into scanning fenced code blocks and inline code spans (default: excluded).
forbid.excludePlaceholderforbid.excludePlaceholder=<option>bodyOpt the sibling forbid.mask out of reporting hits whose matched text is an obvious documented placeholder (EXAMPLE/REDACTED word, x-run, <stub>, trailing ellipsis).
forbid.scanCommentsforbid.scanComments=<option>bodyOpt the sibling forbid.mask into scanning HTML-comment content (default: hidden).
forbid.maxHitsforbid.maxHits=<option>bodyCap the sibling forbid.mask at the first N reported occurrences, by document position (default: every occurrence).
forbidforbid=<option>bodyFail when the section body matches a forbidden regular expression.
noPlaceholdernoPlaceholderbodyReject leftover template or placeholder text (TODO, TBD, lorem ipsum, the original prompt) left in the body.
containscontains=<option>bodyRequire the section to contain specific block types — any of image, table, code, list.
requiredSetrequiredSet=<option>contentRequire every member of a contract-relative pattern-set file (label = regex per line, case-insensitive) to match somewhere in the section, reporting each missing member separately. Compose with requiredSet.when= to enforce only when a trigger pattern matches. Silent without the sources capability.
requiredSet.whenrequiredSet.when=<option>contentEnforce the sibling requiredSet only when this case-insensitive trigger pattern matches the section's text.
sentence.allsentence.all=<option>bodySome one sentence in the section must match every listed mask — co-occurrence in a single clause, not presence scattered across the body. Use for commitments that only count when their parts sit together (a response verb and a time window).
sentence.targetsentence.target=<option>bodyEvery match of the target mask must co-occur, in the same sentence, with the sibling sentence.gate= mask — optionally ordered with sentence.gate.direction=before|after. Each ungated occurrence reports on its own line.
sentence.gatesentence.gate=<option>bodyThe mask each sentence.target occurrence must co-occur with, in the same sentence.
sentence.gate.directionsentence.gate.direction=<option>bodybefore | after | any (default any): where the gate must sit relative to the target occurrence, within the sentence.
sentence.scopesentence.scope=<option>bodysentence | paragraph (default sentence): the unit sentence.all / sentence.target evaluate co-occurrence within. Use paragraph as the honest escape when genuinely separate events belong in separate sentences of one paragraph.
sentence.nodessentence.nodes=<option>bodyWhich block nodes sentence rules read, an array literal drawn from the closed set paragraph, heading, blockquote, listItem. Default [paragraph, listItem].
lead.masklead.mask=<option>bodyRequire the section's first prose line (the first paragraph under the heading, skipping fences/tables/lists) to match a mask.
pattern.mask.anyOfpattern.mask.anyOf=<option>bodyRequire the section body to match ANY of several masks; the miss diagnostic names the closest alternative.
pattern.maskpattern.mask=<option>bodyRequire the section body to match a mask (a small token vocabulary compiling to a safe regex), e.g. dates or issue refs.
patternpattern=<option>bodyRequire the section body to match a regular expression (e.g. an issue reference must be present).
entitiesInentitiesIn=<option>bodyFail when the section names an entity that a declared source file does not contain.
groundedIngroundedIn=<option>bodyFail when a claim-shaped token (amount, date, id by default) in the section does not appear, canonicalized, in a declared source file.
groundedIn.kindsgroundedIn.kinds=<option>bodyRestrict groundedIn to an array literal naming a subset of claim kinds (amount, number, date, id, mention, url, quote).
matchesmatches=<option>bodyNames the source file verbatim compares the section's exact text against.
quotesMatchquotesMatch=<option>bodyFail when a double-quoted claim in the section does not verbatim-match (after mechanical folding) a span in a declared source file.
verbatimverbatimbodyFail when the section's text drifts from a declared source's exact wording, after folding cosmetic quote/dash/whitespace differences.
heading.levelheading.level=<option>headingRequire the block's matched heading to be an ATX heading of the given level 1-6 (heading.level=1 = a # title).
heading.maskheading.mask=<option>headingRequire the block's matched heading to match a mask (a small token vocabulary compiling to a safe, anchored regex).
heading.noPlaceholderheading.noPlaceholderheadingReject a placeholder heading (TODO, TBD, an unfilled imperative stub).
heading.patternheading.pattern=<option>headingRequire the block's matched heading to match a regular expression (e.g. an ADR id prefix).
heading.minWordsheading.minWords=<option>headingRequire the block's matched heading to have at least N words, so it isn't a single bare noun.
heading.maxWordsheading.maxWords=<option>headingCap the block's matched heading at N words, to keep headings tight.
containsContextcontainsContext=<option>integrationRequire the section body to contain the string value of context.values.<key>, supplied via --context. Without --context (or without that key), the rule reports a missing-context-value warning rather than passing silently.
issueKeywordissueKeyword=<option>integrationRequire a linked issue introduced by one of these keywords (Fixes, Closes, Resolves, ...).
issueStateissueState=<option>integrationRequire the linked issue to be in a given state (open / closed / any). The state comes from --context; without it the rule reports a missing-context warning rather than passing silently. The scaffolded CI does not pass --context, so issueState is for local or custom-CI use.
maxAgemaxAge=<option>integrationFail when the LAST ISO date found in the section's text is older than a maximum age, checked against an injected clock (--now, or the real clock by default).
descdesc=<option>metaAn author description / generation hint for the construct — desc="…". Metadata only; it never validates the output.
helphelp=<option>metaAuthor guidance surfaced on this field's validation errors — like a form field's help text (help="…"). Metadata only; it never validates the output.
match.headingmatch.heading=<option>metaDeliberately structural: overrides which heading this section binds to, instead of its own visible heading. Unlike name=, this changes matching, not just display.
namename=<option>metaAn author label for the construct, used in docs and diagnostics — name="Release summary". Display only; it never validates the output or changes which heading a section binds to.
allowEmptyallowEmptypresencewith required/requiredOrNA, the section must still exist but an empty body passes — for sections that are legitimately empty at times (a changelog's Unreleased).
optionaloptionalpresenceNever require the section; validate its content only if the author included it.
recommended ⚗️recommendedpresenceWarn (rather than error) when the section is missing, but still validate it when present.
requiredOrNArequiredOrNApresenceRequire real content OR an explicit N/A — for sections like visual evidence that sometimes genuinely don't apply.
requiredrequiredpresenceRequire the section to exist and to have real content (an empty heading fails).

Prop details

Cap the section body at N markdown links, to keep a field from turning into a link dump.

maxLinks=<option> · category: body · status: stable · src/pipeline/rules/content/counts/links.ts

Checks that a field's body contains at most N markdown links; targets a block body. Counts inline [t](url), reference [t][id]/bracket shortcut [t] (with a matching definition), and autolinks <https://…> - not plain pasted URLs with no link syntax at all.

<!-- mdv: section maxLinks=1 -->
## Outreach
One paragraph of relevance. [Book 15 minutes](cal.acme.com/demo)
<!-- mdv: endsection -->

minParagraphs

Require at least N distinct paragraphs in the section body — counts are a proxy for structural completeness (WARN by default).

minParagraphs=<option> · category: body · status: stable · src/pipeline/rules/content/counts/paragraphs.ts

F3 (sol triage S-4, count family): the section body must contain at least N distinct AST paragraph nodes — "2-5 paragraphs" (ADR trial), a property maxWords/minWords cannot express (word budgets are the wrong unit for "how many distinct points did you make"). Per the ratified severity doctrine, a paragraph COUNT is a proxy for structural completeness, never a constitutive fact, so this defaults to WARN (a deliberate deviation from the pre-doctrine board text, which predates the doctrine).

<!-- mdv: section required minParagraphs=2 -->
## Context
The first paragraph states the problem.

The second paragraph states the constraint.
<!-- mdv: endsection -->

maxParagraphs

Cap the section body at N distinct paragraphs — counts are a proxy for structural completeness (WARN by default).

maxParagraphs=<option> · category: body · status: stable · src/pipeline/rules/content/counts/paragraphs.ts

Mirrors minParagraphs: caps the section body at N distinct AST paragraph nodes. Same WARN default (counts are proxies, per the ratified severity doctrine).

<!-- mdv: section required maxParagraphs=5 -->
## Context
One paragraph.
<!-- mdv: endsection -->

minWords

Require at least N words of prose in the section body, so a field can't be filled with a token gesture.

minWords=<option> · category: body · status: stable · src/pipeline/rules/content/counts/words.ts

<!-- mdv: section minWords=6 -->
## Rover Update
The rover confirms soil samples before uplink.
<!-- mdv: endsection -->

maxWords

Cap the section body at N words, to keep summaries tight.

maxWords=<option> · category: body · status: stable · src/pipeline/rules/content/counts/words.ts

Checks that prose stays within a maximum word count; targets a block body.

<!-- mdv: section maxWords=12 -->
## Flight Note
Launch the repair drone after beacon checks pass.
<!-- mdv: endsection -->

distinctFrom

Fail when this section's text is identical or near-identical to another named section in the same document.

distinctFrom=<option> · category: body · status: stable · src/pipeline/rules/content/distinct-from.ts

Checks that this section's body genuinely differs from another named section in the same document — built for the bug-report Actual-vs-Expected pair, where copy-pasting one into the other silently satisfies every other rule (both are non-empty, both pass minWords). Normalizes both sides (lowercase, collapse whitespace, strip trailing punctuation) and flags an exact match OR a word-set Jaccard overlap at or above 0.9. Silent when the referenced heading doesn't exist in the document, or either side is empty (those are missing-section/empty-section's job) — honest-null, never guess.

<!-- mdv: section distinctFrom="Expected Behavior" -->
## Actual Behavior
The export crashes with a null pointer exception instead of saving the file.
<!-- mdv: endsection -->

forbid.lexicon

Fail when the section body contains any phrase from a lexicon file. The lexicon must be plain newline-delimited text (.txt), one phrase per line; blank lines and #-prefixed comment lines are ignored. A .md lexicon path is loaded as markdown and its member extraction yields different results, so always use a .txt file here.

forbid.lexicon=<option> · category: body · status: stable · src/pipeline/rules/content/forbid/forbid-lexicon.ts

The data-driven sibling of forbid=: the value names a newline-delimited phrase file (blank lines and # comments ignored), loaded before validation into ctx.document.sources exactly like entitiesIn's source (same path law: relative to the contract, no absolute paths, no ..). The lexicon file must be plain text — .txt — one phrase per line; a .md path is loaded through markdown member extraction instead and silently yields different (often zero) phrases. A phrase hits when it appears in the field's prose case-insensitively at Unicode-aware word boundaries. Stays silent when the caller supplied no sources capability (library validation of an in-memory string) — same honest-null posture as entitiesIn.

<!-- mdv: section forbid.lexicon=./banned-phrases.txt -->
## Summary
The exporter now streams results instead of buffering them.
<!-- mdv: endsection -->

forbid.mask

Fail when the section body matches a mask (a small token vocabulary compiling to a safe regex), e.g. a stray issue reference.

forbid.mask=<option> · category: body · status: stable · src/pipeline/rules/content/forbid/forbid-mask.ts

Sugar for forbid=/re/: fails when the section body matches a small closed token vocabulary (<semver>, <date>, <issue>, …) instead of a hand-written regex. Matches anywhere in the body, same as forbid=.

Occurrence-aware and AST-aware (template-packs DESIGN, sol steelman #5): the scan walks the field's cached block nodes — one diagnostic PER match, anchored to the real document line and naming the matched text — instead of one bodywide test anchored to the directive. Top-level fenced code blocks are skipped structurally (code nodes are never visited), a fence QUOTED inside a blockquote/list item is recognized using the same char+run-length rules as parse-fences.ts (see fenceSkipLines), and inline code spans are blanked across the node's full text using a linear, exact-run-aware scanner (see maskInlineCode) before matching — mirroring plugin-email's scan-templates.ts, so a forbidden shape shown as a code example never fires. html nodes ARE scanned so visible HTML prose is checked like any other text.

HTML-comment hiding is a RAW-REGION model, not per-node text reconstruction: the block-summary AST DISCARDS raw content a closer can hide in — a fence's info string lives in lang/meta, not value (parse-fences.ts), and parseDefinition drops everything after a definition's URL (parse-blocks.ts) — so reconstructing "the text of each node" and stitching a carried boolean across it (the old design) loses closers that sit in that discarded content, and everything after fails open. Instead: findOpenerLine walks the nodes, in document order, for the FIRST <!-- that sits in a node whose text this rule actually examines (paragraph/heading/blockquote/ listItem/list/table/html) — code/definition/thematicBreak content can never open a comment (so a directive or fenced example is never mistaken for a real comment opener). The moment that first opener's line is known, computeHiddenIntervals switches to the field's own TRUE raw source lines (ctx.target.sourceLines(), real line numbers, live <!-- mdv: ... --> directive lines included — see the accessor's own doc comment in define-rule.ts for why ctx.target.text() is unusable here: it omits directive lines and .trim()s the join, so it is neither contiguous nor reliably line-addressable) and toggles on every subsequent -->/<!-- occurrence in THAT text, regardless of which AST node (or no node at all, for a directive line) it landed in, producing a Map<line, hidden (colStart, colEnd) intervals> through to the end of the field. A fence's info-string closer, a definition's trailing-text closer, a closer sitting inside a LIVE directive line, and a close-then-reopen chain are all just raw text at that point — nothing is lost.

maskOccurrences then runs its ordinary node-based scan for candidate hits with no comment awareness at all — and no reliable column either: a hit inside a blockquote/listItem carries a column relative to the node's OWN marker-stripped text, and a table-cell hit carries a column relative to the cell's own content, neither of which lines up with the raw line's real columns. run() therefore never compares a hit's column against a hidden interval's column at all — see the hidden-hit filter: it computes the hidden line's VISIBLE SEGMENTS (the substrings of the real raw line outside every hidden interval, in raw coordinates) and keeps the hit iff its matched TEXT occurs, via plain substring search, within a single visible segment — one coordinate system throughout, no cross-coordinate column math anywhere. A match spanning a hidden/visible boundary can never be a substring of any one segment, so it is correctly dropped without ever computing a column for it. code/definition never contribute candidate hits regardless of hidden/visible state (unless forbid.scanFences=true opts code in — P7, below) — they still have no checkable prose of their own by default — so a forbidden shape shown only inside one never fires even while a comment's hidden state carries straight through it.

A mask here may still use a :in(ref) token (it compiles fine — compileMask doesn't know which rule is asking), but forbid.mask never runs the membership check pattern.mask/heading.mask do: forbid is a NEGATIVE match — the diagnostic fires when the mask IS found, so "found and its captured value is a known member" isn't a coherent question to ask. compiled.captures is intentionally dropped here (this rule's CompiledMask type never carries it).

P7 scan-scope options (ADJUDICATION.md D-T4 "P7 scan-scope options"): three sibling modifier atoms defined below this rule — forbid.scanFences=, forbid.scanComments=, forbid.maxHits= — are read here at runtime via ctx.field.rules (the same raw-field-map pattern groundedIn.kinds uses to modify groundedIn, see grounded-in.ts) rather than through the code=/message=/suggestion= contract-local-diagnostics machinery, so all three work identically under mdv-syntax 1 and 2 — this rule and its options carry no v2 coupling. Every default (fences/inline-code excluded, comments hidden, every occurrence reported) reproduces the behavior described above unchanged.

<!-- mdv: section forbid.mask="TODO by <date>" -->
## Notes
The release notes are ready for review.
<!-- mdv: endsection -->

forbid.scanFences

Opt the sibling forbid.mask into scanning fenced code blocks and inline code spans (default: excluded).

forbid.scanFences=<option> · category: body · status: stable · src/pipeline/rules/content/forbid/forbid-mask.ts

Modifier atom for forbid.mask: forbid.scanFences=true opts the mask INTO scanning fenced code blocks and inline code spans — the opposite of forbid.mask's default (P7, ADJUDICATION.md D-T4 "P7 scan-scope options"; driver: plugin-safety's secret scanning must catch a key pasted inside a fence or a `like this` inline span, not just one sitting in plain prose — matrix/plugin-safety.md L1). false (the default, unset behaves as false) reproduces every fence/inline-code exclusion this file's module doc comment describes, byte-for-byte. true disables it: a top-level fenced block's value is scanned line-for-line verbatim (see the case "code" branch in scanBlockNode), a blockquote/list-item's own quoted-fence toggle (fenceSkipLines) is skipped so its "hidden" lines are scanned like ordinary text, and maskInlineCode's backtick-span blanking is skipped everywhere else too.

Consumed by forbid.mask's own run() via the field's raw rule map (ctx.field.rules["forbid.scanFences"]) — the same pattern groundedIn.kinds uses to modify groundedIn (grounded-in.ts) — so this rule itself never reports a diagnostic; written with no forbid.mask= on the same directive, it simply has no effect. Legal under both grammar modes: forbid.mask is a v1 rule and this modifier carries no v2 coupling.

<!-- mdv: section forbid.mask="<awskey>" forbid.scanFences=true -->

forbid.excludePlaceholder

Opt the sibling forbid.mask out of reporting hits whose matched text is an obvious documented placeholder (EXAMPLE/REDACTED word, x-run, <stub>, trailing ellipsis).

forbid.excludePlaceholder=<option> · category: body · status: stable · src/pipeline/rules/content/forbid/forbid-mask.ts

Modifier atom for forbid.mask: forbid.excludePlaceholder=true opts the mask's hits into the shared placeholder exclusion (P7, driver: matrix/plugin-safety.md — documentation is full of deliberately fake values, and "sk-live-EXAMPLE" firing as a leak teaches authors to ignore the rule). A hit whose WHOLE matched text contains an obvious stand-in (EXAMPLE/REDACTED/ PLACEHOLDER word, an x{3,} run, an <angle-bracket> stub, a trailing ellipsis — see placeholder-token.ts) is silently skipped. Default false: forbid.mask reports every match. Consumed the same way as forbid.scanFences (raw field-rule lookup; reports nothing itself).

<!-- mdv: section forbid.mask="<awskey>" forbid.excludePlaceholder=true -->

forbid.scanComments

Opt the sibling forbid.mask into scanning HTML-comment content (default: hidden).

forbid.scanComments=<option> · category: body · status: stable · src/pipeline/rules/content/forbid/forbid-mask.ts

Modifier atom for forbid.mask: forbid.scanComments=true opts the mask INTO scanning HTML-comment content (P7, ADJUDICATION.md D-T4; driver: matrix/plugin-safety.md L2 — a secret hidden inside a <!-- ... --> comment is still a committed secret). false (the default, unset behaves as false) reproduces forbid.mask's existing comment-hiding exactly (computeHiddenIntervals/the hidden-hit filter, see the module doc comment). true skips that hiding pass entirely: a forbidden shape sitting inside a comment — including a LIVE <!-- mdv: ... --> directive line's own text — is scanned and reported like any other raw text.

Consumed the same way as forbid.scanFences (raw field-rule lookup in forbid.mask's run(); this rule reports no diagnostic of its own). Legal under both grammar modes.

<!-- mdv: section forbid.mask="<awskey>" forbid.scanComments=true -->

forbid.maxHits

Cap the sibling forbid.mask at the first N reported occurrences, by document position (default: every occurrence).

forbid.maxHits=<option> · category: body · status: stable · src/pipeline/rules/content/forbid/forbid-mask.ts

Modifier atom for forbid.mask: forbid.maxHits=N caps the occurrences forbid.mask reports per application to the first N (P7, ADJUDICATION.md D-T4), selected by SOURCE-ORDER document position — ADJUDICATION.md A5 (Round-2 amendments): "first document occurrence wins, alternative list order irrelevant." See matrix/plugin-templates-github.md's noPromptInjection row for the counterexample this policy exists to satisfy: the LEGACY selection there iterates alternatives in pattern-declaration order and stops at the first PATTERN that matches anywhere in the document — "system prompt" (pattern #6) on line 1 loses to "ignore previous instructions" (pattern #1) on line 3 purely because pattern #1 is checked first. Source-order instead takes the first MATCH by position regardless of which alternative produced it, so the SAME two hits would select line 1 — a documented, deliberate behavior delta, not a bug. forbid.mask compiles only one regex, so source-order and pattern-order coincide for it today; the cap is still written as "stop after N hits off a document-ordered iterator" (see forbid.mask's run()) so it already satisfies A5 unchanged if a forbid.mask.anyOf sibling with multiple alternatives lands later and merges its hits into that same ordered stream.

Unset (the default) means "report every occurrence" — forbid.mask's existing behavior. Must be a positive whole number; 0, a negative number, or anything non-numeric is rejected at compile time under the standard invalid-option path (the same reporting shape forbid.lexicon's own value validation uses — see parseSourcePath in grounded-in.ts). Consumed via the raw field-rule map like the other two P7 modifiers; this rule itself never reports.

<!-- mdv: section forbid.mask="<awskey>" forbid.maxHits=1 -->

forbid

Fail when the section body matches a forbidden regular expression.

forbid=<option> · category: body · status: stable · src/pipeline/rules/content/forbid/forbid.ts

Checks that forbidden text or patterns are absent from a block body. The value is a /regex/, a "quoted literal", or an array of terms — so forbid=/tbd|tba/, forbid="not applicable", and forbid=[TODO, TBD, FIXME] all work.

<!-- mdv: section forbid=/tbd|tba/ -->
## Rebel Briefing
The shield generator repair is ready for squad review.
<!-- mdv: endsection -->

noPlaceholder

Reject leftover template or placeholder text (TODO, TBD, lorem ipsum, the original prompt) left in the body.

noPlaceholder · category: body · status: stable · src/pipeline/rules/content/forbid/no-placeholder.ts

<!-- mdv: section noPlaceholder -->
## Docking Check
Guidance telemetry now locks before final approach.
<!-- mdv: endsection -->

contains

Require the section to contain specific block types — any of image, table, code, list.

contains=<option> · category: body · status: stable · src/pipeline/rules/content/occurrence/contains.ts

Checks that required content appears in a block body. The value is one or more of: a block-type name — paragraph, code, table, image, list, checklist, blockquote, html (a heading is not a block type) — a /regex/, or a "quoted literal", in one array literal. So contains=table requires a table, contains=/because/ requires that word, and contains=[code, image] requires both.

<!-- mdv: section contains=[table, /because/] -->
## Mars Relay Decision
We rerouted telemetry because the relay window closes at 14:30.

| Window | Status |
| ------ | ------ |
| 14:30  | closing|
<!-- mdv: endsection -->

requiredSet

Require every member of a contract-relative pattern-set file (label = regex per line, case-insensitive) to match somewhere in the section, reporting each missing member separately. Compose with requiredSet.when= to enforce only when a trigger pattern matches. Silent without the sources capability.

requiredSet=<option> · category: content · status: stable · src/pipeline/rules/content/occurrence/required-set.ts

Require every member of a pattern set to be present in this section's text, reporting each missing member as its own diagnostic. The set is a contract-relative P10 file of label = regex lines (case-insensitive), owned by the copied template — the personalization surface. Compose with requiredSet.when= to enforce the set only when a trigger pattern matches the section (a "when you say X you must also state Y" contract). Honest-null: with no sources capability the rule is silent — never guess at a set it cannot read.

<!-- mdv: section requiredSet=./effect-stat-set.txt -->
## Primary outcome
The hazard ratio was 0.82 (95% CI 0.70–0.96), p=0.013.
<!-- mdv: endsection -->

requiredSet.when

Enforce the sibling requiredSet only when this case-insensitive trigger pattern matches the section's text.

requiredSet.when=<option> · category: content · status: stable · src/pipeline/rules/content/occurrence/required-set.ts

Modifier for requiredSet: a trigger pattern (case-insensitive regex) — the set is enforced only in sections where the trigger matches. "When you state X, you must also state Y."

<!-- mdv: section requiredSet=./var-quote-set.txt requiredSet.when="\bVaR\b" -->

sentence.all

Some one sentence in the section must match every listed mask — co-occurrence in a single clause, not presence scattered across the body. Use for commitments that only count when their parts sit together (a response verb and a time window).

sentence.all=<option> · category: body · status: stable · src/pipeline/rules/content/occurrence/sentence.ts

Requires some single sentence in the section to match EVERY listed mask. Alternatives that appear only in different sentences do not satisfy it — the point is co-occurrence in one clause ("we acknowledge reports within 48 hours"), not mere presence anywhere in the body. Reported once, at the directive line. Sentences never cross a line break; inline code is blanked; fenced code is structurally invisible.

<!-- mdv: fragment verb = \b(acknowledge|respond|reply)\b -->
<!-- mdv: fragment window = \b\d+\s*(?:hours?|days?)\b -->
<!-- mdv: section sentence.all=["<verb>", "<window>"] -->
## Reporting
We acknowledge new reports within 48 hours.
<!-- mdv: endsection -->

sentence.target

Every match of the target mask must co-occur, in the same sentence, with the sibling sentence.gate= mask — optionally ordered with sentence.gate.direction=before|after. Each ungated occurrence reports on its own line.

sentence.target=<option> · category: body · status: stable · src/pipeline/rules/content/occurrence/sentence.ts

Every occurrence of the target mask must share its sentence with an occurrence of the sibling sentence.gate= mask — optionally constrained to sit before or after the target with sentence.gate.direction= (default any). Each ungated occurrence reports on its own line.

<!-- mdv: section sentence.target="Exhibit <num>" sentence.gate="attached hereto" sentence.gate.direction=after -->
## Exhibits
Exhibit 3, attached hereto as Annex A.
<!-- mdv: endsection -->

sentence.gate

The mask each sentence.target occurrence must co-occur with, in the same sentence.

sentence.gate=<option> · category: body · status: stable · src/pipeline/rules/content/occurrence/sentence.ts

Names the gate mask sentence.target checks each occurrence against. Pure modifier — consumed via the field's raw rule map, never reports itself.

sentence.gate.direction

before | after | any (default any): where the gate must sit relative to the target occurrence, within the sentence.

sentence.gate.direction=<option> · category: body · status: stable · src/pipeline/rules/content/occurrence/sentence.ts

sentence.scope

sentence | paragraph (default sentence): the unit sentence.all / sentence.target evaluate co-occurrence within. Use paragraph as the honest escape when genuinely separate events belong in separate sentences of one paragraph.

sentence.scope=<option> · category: body · status: stable · src/pipeline/rules/content/occurrence/sentence.ts

P5 split-event escape (#1049 escape audit item 4): widens the co-occurrence unit both sentence hosts evaluate, from one sentence (default) to one paragraph/list item. Declare it where the honest write-up of genuinely separate events is naturally split across sentences — the rule should not pressure fusing them into a single clause that reads more confident than what happened. Pure modifier — consumed via the field's raw rule map, never reports itself.

sentence.nodes

Which block nodes sentence rules read, an array literal drawn from the closed set paragraph, heading, blockquote, listItem. Default [paragraph, listItem].

sentence.nodes=<option> · category: body · status: stable · src/pipeline/rules/content/occurrence/sentence.ts

lead.mask

Require the section's first prose line (the first paragraph under the heading, skipping fences/tables/lists) to match a mask.

lead.mask=<option> · category: body · status: stable · src/pipeline/rules/content/shape/lead-mask.ts

F6 (sol triage S-9): bind the first physical line of a field's own first prose paragraph — the field-scoped reach firstLine.mask cannot provide (that rule is document-subject only). A NEW name, deliberately not a reuse of firstLine.mask: a field-scoped rule of that name would shadow the document rule wherever both could apply (registry field-lookup precedence, src/compile/classify.ts). Mask grammar is identical to heading.mask — v1 or v2, including the :in(ref)/|in: membership modifier — and works the same way in both grammars.

<!-- mdv: section required lead.mask="<word|oneOf: [GET,POST,PATCH,DELETE]> <text>" -->
### Create Envelope

POST /v1/envelopes
<!-- mdv: endsection -->

pattern.mask.anyOf

Require the section body to match ANY of several masks; the miss diagnostic names the closest alternative.

pattern.mask.anyOf=<option> · category: body · status: stable · src/pipeline/rules/content/shape/pattern-mask-any-of.ts

The list-valued mask slot (SPEC §4): pattern.mask.anyOf=["a", "b"] — ONE array literal of quoted whole masks (the lexer keeps the whole […] as one atom value). Alternatives are tried in order; the section passes when ANY matches. On a miss the diagnostic names the closest alternative (longest shared prefix with the found text, ties to first listed). |in: membership captures are per-alternative: only the MATCHED alternative's captures are checked, same honest-null sources contract as pattern.mask.

<!-- mdv: section pattern.mask.anyOf=["Released <date>", "Unreleased"] -->

pattern.mask

Require the section body to match a mask (a small token vocabulary compiling to a safe regex), e.g. dates or issue refs.

pattern.mask=<option> · category: body · status: stable · src/pipeline/rules/content/shape/pattern-mask.ts

Sugar for pattern=/re/: checks that prose matches a small closed token vocabulary (<semver>, <date>, <issue>, …) instead of a hand-written regex. Everything outside <token> is a literal. Matches anywhere in the body, same as pattern=.

A token may carry a :in(ref) membership modifier (<mention:in(./team.json)>): once the shape matches, the captured value for each such token is checked against ctx.document.sources(ref) (the same capability entitiesIn reads) and reported as unknown-entity on a miss — shape and membership are separate concerns, so a shape mismatch alone never triggers a membership check, and a run with no sources capability supplied degrades to a silent no-op, same as entitiesIn.

<!-- mdv: section pattern.mask="Released <date>" -->
## Release Notes
Released 2026-07-02
<!-- mdv: endsection -->

pattern

Require the section body to match a regular expression (e.g. an issue reference must be present).

pattern=<option> · category: body · status: stable · src/pipeline/rules/content/shape/pattern.ts

<!-- mdv: section pattern=/^Status: (Draft|Final)$/m -->
## Mission Packet
Status: Final
<!-- mdv: endsection -->

entitiesIn

Fail when the section names an entity that a declared source file does not contain.

entitiesIn=<option> · category: body · status: stable · src/pipeline/rules/content/source/entities-in.ts

Checks every entity-shaped token in the section (capitalized candidate runs, @handle mentions, and /-anchored route tokens like POST /v2/exports) against a contract-declared source of truth — a local file named by the attribute, loaded before validation into ctx.document.sources (see src/api/resolve-sources.ts). A token the source does not contain is a hallucinated or drifted name; the diagnostic suggests the nearest member when one is close. Stays silent when the caller supplied no sources capability (library validation of an in-memory string) — same honest-null posture as citations.resolve.

<!-- mdv: section entitiesIn=./story-bible.json -->
## Chapter Twelve
Mira crossed the Ashgate before dawn.
<!-- mdv: endsection -->

groundedIn

Fail when a claim-shaped token (amount, date, id by default) in the section does not appear, canonicalized, in a declared source file.

groundedIn=<option> · category: body · status: stable · src/pipeline/rules/content/source/grounded-in.ts

Checks every claim-shaped token in the section — amounts, dates, ids by default — against a contract-declared source of truth, loaded before validation into ctx.document.sources (see src/api/resolve-sources.ts). Prose words are never checked (the false-positive firewall, docs/archive/plans/grounding/PLAN.md §3.3); only tokens the claim extractors recognize and canonicalize can miss. Narrow or widen the checked kinds with the sibling groundedIn.kinds= modifier. Stays silent without the sources capability, and equally silent on a view without token sets (a pre-token snapshot replay) — honest-null, never guess.

<!-- mdv: section groundedIn=./ticket.json -->
## Reply
We have refunded £380 to your card.
<!-- mdv: endsection -->

groundedIn.kinds

Restrict groundedIn to an array literal naming a subset of claim kinds (amount, number, date, id, mention, url, quote).

groundedIn.kinds=<option> · category: body · status: stable · src/pipeline/rules/content/source/grounded-in.ts

Narrows (or widens) which claim kinds the sibling groundedIn= checks. Validated at compile time; consumed by groundedIn's run via the field's raw rule map — this rule itself never reports. Without it, groundedIn checks the default kinds (amount, date, id).

<!-- mdv: section groundedIn=./ticket.json groundedIn.kinds=[amount, date] -->
## Refund
We refunded £380 on 2026-07-01.
<!-- mdv: endsection -->

matches

Names the source file verbatim compares the section's exact text against.

matches=<option> · category: body · status: stable · src/pipeline/rules/content/source/matches.ts

Names the contract-declared source file the sibling verbatim flag compares the section's text against, loaded before validation into ctx.document.sources (see src/api/resolve-sources.ts). A pure modifier — like groundedIn.kinds, it validates the path shape at compile time and never reports itself; verbatim's run reads the value back off the field's raw rule map.

<!-- mdv: section verbatim matches=./risk-wording.txt -->
## Risk
Past performance is not a guide to future returns.
<!-- mdv: endsection -->

quotesMatch

Fail when a double-quoted claim in the section does not verbatim-match (after mechanical folding) a span in a declared source file.

quotesMatch=<option> · category: body · status: stable · src/pipeline/rules/content/source/quotes-match.ts

Checks every quote-shaped claim token (§3.3 — a scare-quote-exempt, ≥3-word double-quoted span) in the section against a contract-declared source of truth, loaded before validation into ctx.document.sources (see src/api/resolve-sources.ts). Unlike verbatim (which compares the WHOLE section against the WHOLE source), this checks only the draft's actual quotations — a deterministic folded-substring check either finds the quote or it does not, never an edit distance or semantic match (§8.4). Two escape hatches keep it honest about known verbatim quirks: a [sic] marker is stripped before comparing (it flags the ORIGINAL's own quirk, not the draft's drift), and a bracketed ellipsis ([...]/[…]) splits the quote into segments that must each appear in the source in ascending order but need not be contiguous — a real elision, not a license to quote out of order. Stays silent without the sources capability, and equally silent on a view with neither raw text NOR texts (a pre-text snapshot replay) — honest-null, never guess.

quotesMatch=./sources/ (a trailing /, issue #750) names a bounded DIRECTORY instead of one file — every file directly inside loads (non-recursive, capped), and a quote is grounded when its segment chain lands within ANY ONE file's folded text (never a concatenation across files). A miss anchors its drift message on the file with the longest matching prefix.

<!-- mdv: section quotesMatch=./interview.md -->
## Statement
The spokesperson said "we expect the migration to complete by Q3."
<!-- mdv: endsection -->

verbatim

Fail when the section's text drifts from a declared source's exact wording, after folding cosmetic quote/dash/whitespace differences.

verbatim · category: body · status: stable · src/pipeline/rules/content/source/verbatim.ts

Checks the section's exact text against a contract-declared source of truth (the sibling matches=<path> attribute), loaded before validation into ctx.document.sources (see src/api/resolve-sources.ts). Unlike groundedIn (which checks claim-shaped tokens only), this compares the WHOLE section against the WHOLE source, after mechanically folding both sides (foldQuote — NFC, smart-quote/apostrophe/dash straightening, whitespace collapse, edge- punctuation trim, lowercase; §8.4) so cosmetic differences (curly vs. straight quotes, an em dash vs. a hyphen, trailing whitespace) never trip it. A real wording drift reports ONE diagnostic naming the first divergent character (in the folded strings) and a short window of both readings. Without a sibling matches= this does nothing at run time — pairing verbatim alone is a schema-author question (a future warning), out of scope here. Stays silent when the caller supplied no sources capability, or when the loaded view carries no raw text (a pre-text snapshot replay) — honest-null, never guess.

<!-- mdv: section verbatim matches=./risk-wording.txt -->
## Risk
Past performance is not a guide to future returns.
<!-- mdv: endsection -->

heading.level

Require the block's matched heading to be an ATX heading of the given level 1-6 (heading.level=1 = a # title).

heading.level=<option> · category: heading · status: stable · src/pipeline/rules/heading/heading-level.ts

heading.mask

Require the block's matched heading to match a mask (a small token vocabulary compiling to a safe, anchored regex).

heading.mask=<option> · category: heading · status: stable · src/pipeline/rules/heading/heading-mask.ts

Sugar for heading=/re/: checks that the block's matched heading matches a small closed token vocabulary (<semver>, <date>, <issue>, …) instead of a hand-written regex. Anchored — the mask must match the whole heading, not just part of it.

A token may carry a :in(ref) membership modifier (<mention:in(./team.json)>): once the heading matches the shape, the captured value for each such token is checked against ctx.document.sources(ref) and reported as unknown-entity on a miss — same posture as pattern.mask: shape and membership are separate concerns, and no sources capability means a silent no-op, never a guess.

block
<!-- mdv: section required heading.mask="[<semver>] - <date>" -->
## [0.2.0] - 2026-07-02
<!-- mdv: endsection -->

heading.noPlaceholder

Reject a placeholder heading (TODO, TBD, an unfilled imperative stub).

heading.noPlaceholder · category: heading · status: stable · src/pipeline/rules/heading/heading-no-placeholder.ts

Checks that the block's matched heading is not leftover placeholder text (TODO, TBD, an unfilled imperative stub). Validates the heading itself, not the body.

block
<!-- mdv: section required heading.noPlaceholder -->
## Sensor handoff
<!-- mdv: endsection -->

heading.pattern

Require the block's matched heading to match a regular expression (e.g. an ADR id prefix).

heading.pattern=<option> · category: heading · status: stable · src/pipeline/rules/heading/heading-pattern.ts

Checks that the block's matched heading matches a required regular expression — the heading itself, not the body. One spelling: heading.pattern=/re/. (The undotted heading=/re/ spelling was a second declaration of this same atom and was removed; there is no compatibility path.)

block
<!-- mdv: section required heading.pattern=/^ADR-[0-9]{4}:.+/ -->
## ADR-0007: Adopt beacon validation
<!-- mdv: endsection -->

heading.minWords

Require the block's matched heading to have at least N words, so it isn't a single bare noun.

heading.minWords=<option> · category: heading · status: stable · src/pipeline/rules/heading/heading-words.ts

Checks that the block's matched heading has at least N words, so a heading is specific rather than a single bare noun. Validates the heading itself, not the body.

block
<!-- mdv: section required heading.minWords=2 -->
## Rover contingency
<!-- mdv: endsection -->

heading.maxWords

Cap the block's matched heading at N words, to keep headings tight.

heading.maxWords=<option> · category: heading · status: stable · src/pipeline/rules/heading/heading-words.ts

Checks that the block's matched heading stays within N words, to keep headings tight. Validates the heading itself, not the body.

block
<!-- mdv: section required heading.maxWords=8 -->
## Airlock repair owner
<!-- mdv: endsection -->

containsContext

Require the section body to contain the string value of context.values.<key>, supplied via --context. Without --context (or without that key), the rule reports a missing-context-value warning rather than passing silently.

containsContext=<option> · category: integration · status: stable · src/pipeline/rules/integrations/contains-context.ts

<!-- mdv: section containsContext=packageVersion -->
## Changelog
Released 0.2.0.
<!-- mdv: endsection -->

issueKeyword

Require a linked issue introduced by one of these keywords (Fixes, Closes, Resolves, ...).

issueKeyword=<option> · category: integration · status: stable · src/pipeline/rules/integrations/issue-keyword.ts

<!-- mdv: section issueKeyword=[Fixes, Closes] -->
## Hangar Tracking
Fixes #123
<!-- mdv: endsection -->

issueState

Require the linked issue to be in a given state (open / closed / any). The state comes from --context; without it the rule reports a missing-context warning rather than passing silently. The scaffolded CI does not pass --context, so issueState is for local or custom-CI use.

issueState=<option> · category: integration · status: stable · src/pipeline/rules/integrations/issue-state.ts

<!-- mdv: section issueKeyword=Fixes issueState=open -->
## Droid Bay Tracking
Fixes #123
<!-- mdv: endsection -->

maxAge

Fail when the LAST ISO date found in the section's text is older than a maximum age, checked against an injected clock (--now, or the real clock by default).

maxAge=<option> · category: integration · status: stable · src/pipeline/rules/integrations/max-age.ts

Checks a field's recorded "last verified" date against an injected clock: fails when more than maxAge has elapsed since the LAST ISO date found in the section's text. Silent (honest-null) whenever the caller supplied no clock (context.values.now, stamped by the CLI's --now or its real-clock default - see src/cli/main.ts/src/cli/commands/check.ts) or the field names no date at all - the same degrade-to-"don't know" posture as entitiesIn/citations.resolve. The mask= attribute (or any other shape rule) owns the field's shape separately; this rule only reads whichever date is actually there.

<!-- mdv: section mask="Verified {date:iso}" maxAge=90d -->
Verified: 2026-06-01
<!-- mdv: endsection -->

desc

An author description / generation hint for the construct — desc="…". Metadata only; it never validates the output.

desc=<option> · category: meta · status: stable · src/pipeline/rules/meta/desc.ts

An author description / generation hint for the construct — desc="One paragraph for the changelog". Like name, it is metadata: it validates nothing about the output and emits no diagnostics. Quoted values (with spaces) are one atom, so the description survives intact.

block
<!-- mdv: section required desc="A summary a release manager can paste into the changelog" -->
## Mission Summary
One paragraph for the station changelog.
<!-- mdv: endsection -->

help

Author guidance surfaced on this field's validation errors — like a form field's help text (help="…"). Metadata only; it never validates the output.

help=<option> · category: meta · status: stable · src/pipeline/rules/meta/help.ts

Author guidance surfaced on this field's validation errors — like a form field's help text. Metadata: it never validates the output. The value is stamped onto every diagnostic the field emits (CLI note: line, --json help, the PR-comment "What's needed" cell, and the autofix repair context), so the schema author can say HOW to satisfy the field — e.g. when it is honest to tick a checkbox — right where the failure is reported.

<!-- mdv: taskList required allChecked help="Tick only after the release CI run is green on the merge commit." -->
- [ ] CI green
<!-- mdv: endtaskList -->

match.heading

Deliberately structural: overrides which heading this section binds to, instead of its own visible heading. Unlike name=, this changes matching, not just display.

match.heading=<option> · category: meta · status: stable · src/pipeline/rules/meta/match-heading.ts

The explicit, deliberately structural binding-anchor override — match.heading="Plan for Next Week" makes a section bind to a heading with that exact text instead of its own visible heading. Unlike name= (display-only metadata that never validates or binds anything), match.heading= DOES change which heading this section matches: the schema's visible heading becomes documentation only, and the quoted value is the real anchor checked documents must carry. Reach for this only when the schema's own heading is intentionally a placeholder or example and the real target heading differs — the rare case, not the default.

block
<!-- mdv: section required match.heading="Plan for Next Week" -->
## Plan for the Week of <Month> <Day>
<!-- mdv: endsection -->

name

An author label for the construct, used in docs and diagnostics — name="Release summary". Display only; it never validates the output or changes which heading a section binds to.

name=<option> · category: meta · status: stable · src/pipeline/rules/meta/name.ts

An author label for the construct, used in docs and diagnostics — name="Release summary". It is metadata, never a check: it validates nothing about the output, emits no diagnostics, and — since this is display-only — never changes which heading a section binds to. A section always matches its own visible heading; name= only relabels how that section is referred to in diagnostic text. For the rare case where the binding target must differ from the visible heading, use the explicit match.heading="..." atom instead. Quoted values (with spaces) are one atom, so the label survives intact.

block
<!-- mdv: section required name="Release summary" minWords=20 -->
## Mission Summary
The orbital release notes confirm that navigation, docking, telemetry, and crew handoff checks all cleared before the morning launch window.
<!-- mdv: endsection -->

allowEmpty

with required/requiredOrNA, the section must still exist but an empty body passes — for sections that are legitimately empty at times (a changelog's Unreleased).

allowEmpty · category: presence · status: stable · src/pipeline/rules/presence/allow-empty.ts

<!-- mdv: section required allowEmpty -->
## [Unreleased]
<!-- mdv: endsection -->

optional

Never require the section; validate its content only if the author included it.

optional · category: presence · status: stable · src/pipeline/rules/presence/optional.ts

Checks that a block may be absent but validates normally when present; targets a block.

<!-- mdv: section optional -->
## Away Team Notes
Extra context for bridge reviewers.
<!-- mdv: endsection -->

Warn (rather than error) when the section is missing, but still validate it when present.

recommended · category: presence · status: experimental · src/pipeline/rules/presence/recommended.ts

<!-- mdv: section recommended -->
## Flight Background
Prior slingshot attempts and tradeoffs.
<!-- mdv: endsection -->

requiredOrNA

Require real content OR an explicit N/A — for sections like visual evidence that sometimes genuinely don't apply.

requiredOrNA · category: presence · status: stable · also on: media · src/pipeline/rules/presence/required-or-na.ts

block
<!-- mdv: section requiredOrNA -->
## Viewscreen Captures
N/A: no cockpit display changed.
<!-- mdv: endsection -->

required

Require the section to exist and to have real content (an empty heading fails).

required · category: presence · status: stable · src/pipeline/rules/presence/required.ts

<!-- mdv: section required -->
## Mission Context
The change validates generated flight notes.
<!-- mdv: endsection -->

On this page