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
| Prop | Syntax | Category | Description |
|---|---|---|---|
maxLinks | maxLinks=<option> | body | Cap the section body at N markdown links, to keep a field from turning into a link dump. |
minParagraphs | minParagraphs=<option> | body | Require at least N distinct paragraphs in the section body — counts are a proxy for structural completeness (WARN by default). |
maxParagraphs | maxParagraphs=<option> | body | Cap the section body at N distinct paragraphs — counts are a proxy for structural completeness (WARN by default). |
minWords | minWords=<option> | body | Require at least N words of prose in the section body, so a field can't be filled with a token gesture. |
maxWords | maxWords=<option> | body | Cap the section body at N words, to keep summaries tight. |
distinctFrom | distinctFrom=<option> | body | Fail when this section's text is identical or near-identical to another named section in the same document. |
forbid.lexicon | forbid.lexicon=<option> | body | 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.mask | forbid.mask=<option> | body | Fail when the section body matches a mask (a small token vocabulary compiling to a safe regex), e.g. a stray issue reference. |
forbid.scanFences | forbid.scanFences=<option> | body | Opt the sibling forbid.mask into scanning fenced code blocks and inline code spans (default: excluded). |
forbid.excludePlaceholder | forbid.excludePlaceholder=<option> | body | 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.scanComments | forbid.scanComments=<option> | body | Opt the sibling forbid.mask into scanning HTML-comment content (default: hidden). |
forbid.maxHits | forbid.maxHits=<option> | body | Cap the sibling forbid.mask at the first N reported occurrences, by document position (default: every occurrence). |
forbid | forbid=<option> | body | Fail when the section body matches a forbidden regular expression. |
noPlaceholder | noPlaceholder | body | Reject leftover template or placeholder text (TODO, TBD, lorem ipsum, the original prompt) left in the body. |
contains | contains=<option> | body | Require the section to contain specific block types — any of image, table, code, list. |
requiredSet | requiredSet=<option> | content | 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.when | requiredSet.when=<option> | content | Enforce the sibling requiredSet only when this case-insensitive trigger pattern matches the section's text. |
sentence.all | sentence.all=<option> | body | 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.target | sentence.target=<option> | body | 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.gate | sentence.gate=<option> | body | The mask each sentence.target occurrence must co-occur with, in the same sentence. |
sentence.gate.direction | sentence.gate.direction=<option> | body | before | after | any (default any): where the gate must sit relative to the target occurrence, within the sentence. |
sentence.scope | sentence.scope=<option> | body | 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.nodes | sentence.nodes=<option> | body | Which block nodes sentence rules read, an array literal drawn from the closed set paragraph, heading, blockquote, listItem. Default [paragraph, listItem]. |
lead.mask | lead.mask=<option> | body | Require the section's first prose line (the first paragraph under the heading, skipping fences/tables/lists) to match a mask. |
pattern.mask.anyOf | pattern.mask.anyOf=<option> | body | Require the section body to match ANY of several masks; the miss diagnostic names the closest alternative. |
pattern.mask | pattern.mask=<option> | body | Require the section body to match a mask (a small token vocabulary compiling to a safe regex), e.g. dates or issue refs. |
pattern | pattern=<option> | body | Require the section body to match a regular expression (e.g. an issue reference must be present). |
entitiesIn | entitiesIn=<option> | body | Fail when the section names an entity that a declared source file does not contain. |
groundedIn | groundedIn=<option> | body | Fail when a claim-shaped token (amount, date, id by default) in the section does not appear, canonicalized, in a declared source file. |
groundedIn.kinds | groundedIn.kinds=<option> | body | Restrict groundedIn to an array literal naming a subset of claim kinds (amount, number, date, id, mention, url, quote). |
matches | matches=<option> | body | Names the source file verbatim compares the section's exact text against. |
quotesMatch | quotesMatch=<option> | body | Fail when a double-quoted claim in the section does not verbatim-match (after mechanical folding) a span in a declared source file. |
verbatim | verbatim | body | Fail when the section's text drifts from a declared source's exact wording, after folding cosmetic quote/dash/whitespace differences. |
heading.level | heading.level=<option> | heading | Require the block's matched heading to be an ATX heading of the given level 1-6 (heading.level=1 = a # title). |
heading.mask | heading.mask=<option> | heading | Require the block's matched heading to match a mask (a small token vocabulary compiling to a safe, anchored regex). |
heading.noPlaceholder | heading.noPlaceholder | heading | Reject a placeholder heading (TODO, TBD, an unfilled imperative stub). |
heading.pattern | heading.pattern=<option> | heading | Require the block's matched heading to match a regular expression (e.g. an ADR id prefix). |
heading.minWords | heading.minWords=<option> | heading | Require the block's matched heading to have at least N words, so it isn't a single bare noun. |
heading.maxWords | heading.maxWords=<option> | heading | Cap the block's matched heading at N words, to keep headings tight. |
containsContext | containsContext=<option> | integration | 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. |
issueKeyword | issueKeyword=<option> | integration | Require a linked issue introduced by one of these keywords (Fixes, Closes, Resolves, ...). |
issueState | issueState=<option> | integration | 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. |
maxAge | maxAge=<option> | integration | 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). |
desc | desc=<option> | meta | An author description / generation hint for the construct — desc="…". Metadata only; it never validates the output. |
help | help=<option> | meta | Author guidance surfaced on this field's validation errors — like a form field's help text (help="…"). Metadata only; it never validates the output. |
match.heading | match.heading=<option> | meta | Deliberately structural: overrides which heading this section binds to, instead of its own visible heading. Unlike name=, this changes matching, not just display. |
name | name=<option> | meta | 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. |
allowEmpty | allowEmpty | presence | 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). |
optional | optional | presence | Never require the section; validate its content only if the author included it. |
recommended ⚗️ | recommended | presence | Warn (rather than error) when the section is missing, but still validate it when present. |
requiredOrNA | requiredOrNA | presence | Require real content OR an explicit N/A — for sections like visual evidence that sometimes genuinely don't apply. |
required | required | presence | Require the section to exist and to have real content (an empty heading fails). |
Prop details
maxLinks
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 -->recommended
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 -->