Core specification

The normative mediva core: data model, directives, targets, props, sources, and diagnostics.

Status: DRAFT 4 (2026-10-01). What changed since draft 3 is in Appendix D, what changed since draft 2 is in Appendix C, and what changed since draft 1 is in Appendix B.

Scope and overview

This is the normative specification of mediva: a standard for contracts over Markdown documents. A contract is itself a Markdown file whose <!-- mdv: … --> comments state what a conforming document must contain; an implementation checks a document against its contract and reports every violation as a diagnostic. The standard covers:

  • What a contract says — the directive grammar and block kinds (§3), the targets a directive exposes (§4), the props that constrain them (§5), and the definitions those props use (§6).
  • What a checker reports — diagnostics, their codes, and one-cause-one-diagnostic (§8), plus contract composition with extends (§9).
  • Optional modules — Grounding (§4.1, §7: claims checked against external sources), Repair (§9.1: normative fixes for shape defects), and Protocol (§8.1, §8.5: JSON diagnostics, exit codes, the repair loop).

It does not cover prose style, grammar, spelling, or how a host produces the data it supplies.

Conformance. An implementation claims mediva Core — all of Core, with no partial Core — and optionally any of the three modules: "mediva Core + Grounding", and so on (§0). The conformance suite is the executable check of a claim.

Where to start.

  • Writing contracts: §3 (directives), §5 (props), then Appendix A (the spelling table). Props (content.words.min=20), named checks (@tables/total), and named rule sets (§5.4) are what a contract says.
  • Implementing a checker: §0 (levels), §2 (principles), §3–§6 in order, §8, then the conformance suite.
  • Integrating a checker into a tool or CI: §8.1 and §8.5 (Protocol).
  • Open issues: §10.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be read as described in RFC 2119.

0. Conformance levels

mediva has one mandatory level, Core, and three optional modules. An implementation states its conformance as "mediva Core" or "mediva Core + <Module> [+ <Module> …]" — never a module alone, since every module is defined in terms of Core's data model.

LevelCoversRequired for the name "mediva Core"
Core§1–§6, §8, §9: directive syntax and block kinds (§3), structural flags and presence (§3.3), targets and the Item shape including disposition (§4), props (§5), the definitions they use (§6), diagnostics and their codes (§8), extends (§9), optional closers (§3.1.1), named rule sets (§5.4), and include (§9.2).An implementation MUST implement all of it. There is no partial Core.
Grounding§4.1 (claim kinds), §7 (sources, vocabularies, change sets, entity kinds path/ref).An implementation MAY omit Grounding entirely; a contract that uses a Grounding-only construct (claims, a plural grounding target such as content.paths.in, in=<source>, @changes) against an implementation that doesn't claim Grounding is the implementation's own compile error to report (unknown target or unknown source), not a silent no-op.
Repair§9.1 (R.1–R.5): the three normative shape repairs and their pattern-publication rule.An implementation MAY omit Repair. A Core-only implementation MUST still compile a contract that writes content.none=@agent/repair (it is an ordinary prop referencing a vocabulary name); it simply never satisfies the repairs the name requests, the same as any other vocabulary it doesn't ship.
Protocol§8.1, §8.5: the JSON diagnostic shape, exit codes, and the idempotent-check / repair-loop contracts.An implementation MAY omit Protocol (for example a library-only implementation with no process boundary and no JSON encoding at all). A surface that runs as a separate process and reports a result to its caller MUST claim Protocol if it wants that boundary depended on.

Dispositions (draft 1's "Implementation status" note under items) are Core, not a separate module or folded into Grounding: they apply to every list block regardless of whether the contract grounds anything, they are small (two derived fields and one prop), and the fabrication pattern they exist to catch (§4, disposition) is a contract's ordinary business, which Core already owns (a list's targets, like checked and labels, are Core today).

An implementation MAY implement Grounding, Repair, or Protocol individually; there is no ordering dependency between them. All are defined only in terms of Core's targets and props (§4, §5).

The conformance suite tags each case with the level(s) it exercises, so an implementation runs only the cases for the levels it claims. Not yet applied: the suite's cases do not carry level tags yet.

0.1 Stability tiers

Every feature a contract can use (a tag, a declaration keyword such as include, an atom) sits in one of three tiers:

TierPromiseHow a contract gets it
StableChanges only in a breaking release (a major release; before 1.0, a minor one) whose changelog names the change. A renamed or removed spelling is from then on an unknown name (§3.4); Appendix A gives the current one.Always on.
ExperimentalMay change or be removed in any release.Always on.
Official pluginVersioned by its own package; its names are namespaced (@ns/…).Shipped with the implementation and on by default, or loaded through configuration.

A feature is stable unless the implementation marks it experimental. An implementation MUST say which features are experimental wherever it documents them (its reference and its explanations). A contract needs no opt-in to use one.

1. Terms

  • Contract: a Markdown file (conventionally *.mdv.md) whose HTML comments carry mdv directives. Its visible Markdown is the template a document is expected to resemble.
  • Document: the Markdown being checked.
  • Block: one declared construct in the contract (§3), bound to zero or more spans of the document.
  • Target: a named value a block exposes about its bound span (§4), such as words or items.
  • Constraint: a prop (§5) on a block, evaluated once per bound block instance.
  • Host: whatever runs an implementation and supplies data it cannot produce itself — the current time (§6.2), a document's title, a change set (§7.1), a source's contents. An implementation MUST treat host-supplied data as an opaque input; normative text never assumes how the host obtained it.
  • Implementation: software that compiles contracts and checks documents. A conforming implementation MUST produce the diagnostics this spec defines for every case in the conformance suite tagged with a level it claims (§0).

2. Principles

  1. One meaning, one definition. Every prop has one definition (§5.3). Diagnostics SHOULD be reported in the spelling the author used.
  2. Deterministic. Checking MUST be a pure function of the contract, the document, the resolved sources, and the explicit context (§6.2). No network, clock, randomness, or model call may influence a result unless it is supplied as context or a source snapshot by the host.
  3. Surrounding content is allowed. A contract declares the blocks it cares about. Content it doesn't declare passes unless strict closes a boundary.
  4. Honest incompleteness is well-formed. A short honest answer (No blockers., Not established.) satisfies required (§3.3). Whether reported work is finished is a separate gate (state, §8.2) — and whether a checked item is backed by anything checkable is a narrower question within it (disposition, §4).
  5. A contract that fails to compile checks nothing. An implementation MUST NOT validate a document against a contract that has compile errors; it MUST report only the contract's errors. A constraint that compiles but cannot run (a column constraint's malformed pattern) is a contract error too, found while checking; the result is the same.
  6. A contract says what a document must contain and mean; whether the Markdown is well formed is a linter's job. No atom asks for a Markdown lint rule. The lint gate (§8.2) runs a Markdown linter's rules, chosen by the host's configuration, never by a contract, a document, a command-line flag or a plugin, and adds only what an implementation needs to read a document against its contract (§8.6). A lint finding is always an error: no setting anywhere gives one a level.

3. Data model and directives — Core

3.1 Directive syntax

A directive is an HTML comment whose content starts with mdv::

directive  = "<!--" ws "mdv:" ws kind *(ws atom) ws "-->"
closer     = "<!--" ws "mdv:" ws "end" kind ws "-->"
definition = "<!--" ws "mdv:" ws "define" ws setname *(ws atom) ws "-->"  ; §5.4
inclusion  = "<!--" ws "mdv:" ws "include" ws incref ws "-->"            ; §9.2
atom       = flag / assignment / use / check
flag       = name                         ; e.g. required, strict
assignment = key "=" value                ; e.g. content.words.min=20
use        = "use=" setname *("," setname)   ; local sets (§5.4): use=prose,titled
check      = checkname [ "=" value ] / checkname "." name "=" value ; @tables/total=Amount
key        = name *("." name)            ; e.g. content.paths.in
value      = integer / duration / bare / quoted / array / ref / slot
duration   = integer ("d" / "h" / "m")    ; 90d
quoted     = DQUOTE *(char / "\\" DQUOTE / "\\\\") DQUOTE
array      = "[" [ item *("," ws item) ] "]"
ref        = "./" path [ "#" json-pointer ] / "@" name *("/" name)
setname    = lowercase *(lowercase / digit / "-") ; a local rule set (§5.4)
checkname  = "@" name 1*("/" name)        ; a vocabulary check (§7.2.1)
incref     = ("./" / "../") path [ "#" anchor ]   ; §9.2
anchor     = 1*(lowercase / digit / "-")  ; a heading slug

define (§5.4) and include (§9.2) are declarations: they open no block.

A block kind's closer is its name prefixed with end (endsection, endlist, …). document has no closer. Every other closer is optional: §3.1.1 defines the scope of a block with and without one. (Draft 2 required every closer. Each draft 2 contract keeps its exact meaning.)

3.1.1 Block scope and optional closers

Every contract that compiles under draft 2 means exactly the same under this subsection. An explicit closer is always valid, and explicit closers still pair the way they did before. (Design record: SYNTAX-SIMPLE.md, item B1; issue #1432.)

Scope. Each block directive has a scope: the run of contract lines it owns. Scope decides two things. First, which blocks nest in which: a section's child sections and leaves. Second, which template content a block carries, such as a leaf's example list or the items labels.eq=@template reads. Scope is a property of the contract. The nesting it produces, the contract's block tree, is also the tree a document is matched against: a nested section binds only inside its parent's span (Binding, below). §3.2 still applies: a section's span in the document runs from its heading to the next heading of the same or higher level. The one exception is a section that covers only its H1's lead (Lead, below).

What counts. Scoping reads the contract with front matter removed. A line inside a fenced code block is content, never a directive or a heading. A heading is an ATX heading or a setext heading. Only an ATX heading can be bound by a section directive (§3.2), so a setext heading is always unbound: it takes part in scoping only by ending scopes, exactly like an unbound ATX heading of the same level. Insignificant lines are blank lines and HTML comments that are not directives. A declaration is any directive that opens no block. That covers document, front, define (§5.4), include (§9.2), and an implementation's own non-block markers. A declaration never ends a section's scope.

Step 1: pair the explicit closers. Read the directives in order and ignore headings for now. Keep a stack of open section directives.

  1. A section directive is pushed onto the stack.
  2. A leaf directive (list, table, code, line, media) is never pushed. For a leaf closer end<K>, find the nearest preceding block directive or closer, skipping declarations. If that is an unpaired leaf directive of kind K, the closer pairs with it. If it is a block directive of a different kind, the closer is mismatched-tag. Otherwise the closer is unexpected-closer.
  3. endsection pairs with the section on top of the stack, then pops it. On an empty stack it is unexpected-closer.
  4. Any section still on the stack at the end of the contract is closerless. So is any leaf that no closer paired with.

Step 2: scope the sections. A section directive binds the heading that follows it. A closerless section's directive MUST be followed by its heading, with only insignificant lines in between. Otherwise the compile error is section-without-heading. A section with an explicit closer binds the first heading inside its scope, exactly as in draft 2. A section's level is the level of the heading it binds.

Scopes come from one pass over the directives and headings, in contract order:

stack ← []                                     ; open sections, innermost last
for each item x in contract order:
  x is a section directive binding a heading of level n:
      close-down(n, the line before x); push x
  x is a section directive binding no heading (explicit closer only):
      push x
  x is a heading no directive binds, level n:
      close-down(n, the line before x)
  x is an endsection paired with section S:
      pop every section above S, then S; each scope ends at x
at the end of the contract: pop everything; each scope ends at the last line

close-down(n, end):
  while the top of the stack is closerless and (it is a lead section or its level ≥ n):
      pop it; its scope ends at end

A section with an explicit closer is never popped by close-down. It also stops the loop, which makes its scope opaque: a heading inside it never ends a section that encloses it. That is how draft 2's contracts keep their meaning. In those contracts every section has a closer, so close-down never pops anything. Nesting then follows headings: a closerless section contains the sections after it whose headings are deeper than its own, up to the next heading at its own level or higher. That is the same outline a Markdown reader sees.

Lead. Most documents have exactly one H1, their title, and a contract that constrains the title means its opening lines, not the whole document. So when the contract has exactly one H1 (an ATX or setext heading of level 1 outside front matter, fences and HTML comments), a closerless section that binds it is a lead section: close-down pops it at the next heading of any level, and its scope is the H1's lead, the content up to that heading. The ## sections after it are its siblings, not its children. Rules meant for the whole document go on the document directive. A lead section has no child sections, so strict or ordered on it checks nothing and is flag-on-title-lead (warn), whose help moves the flag to the document directive. The rule is narrow on purpose:

  • An explicit closer still wins. A section paired with an endsection keeps the scope Step 1 gives it, so a closer at the end of the contract (paired with the H1's section, so every section inside it has its own closer) wraps everything, as before.
  • A contract with two or more H1s has no lead section. Each closerless H1 section runs to the next H1.

In the document, a section on an H1 whose contract scope holds no other heading (a lead section, or an H1 section whose explicit closer sits before the next heading) spans only the H1's lead when the document has exactly one H1: no heading after it is its child, so its strict and ordered see no child sections. In a document with two or more H1s, its span runs to the next H1 (§3.2). Content rules already read a section's own prose up to its first subheading, so the words, blocks and links of a ## section never count toward the title's rules either way. A heading followed directly by a subheading is still present and not empty (empty-section), as for any section.

Step 3: scope the leaves. A leaf with a paired closer owns the lines from its directive through its closer, as in draft 2. A closerless leaf owns less:

  • A closerless list, table, code, or media leaf owns the lines from its directive through the last line of the first construct of its family that starts after the directive. The family constructs are a top-level list, a pipe table, a fenced code block, and a paragraph that contains an image or a video. The construct counts only if it starts before the next directive, the next heading, and the end of the enclosing section's scope. Anything between the directive and the construct belongs to the leaf, including introductory prose and a construct of another family. That is the same scope as a closer placed right after the construct.
  • If no such construct exists, the leaf is bare: it owns only its directive line. A bare leaf binds a document exactly as an opener followed directly by its closer does.
  • A closerless line leaf is always bare. It binds lines by label=, never by contract content.

A leaf belongs to the innermost section whose scope contains its directive. §3.2's placement rules still apply: a leaf outside every section is a compile error, and a block directive between a section directive and that section's heading is block-before-anchor.

Binding. The contract's block tree is the matching tree, in every lane: the same rule binds a plain section and a section inside an occurs= instance (§3.2), whose instance is its parent.

  • A section's identity is its heading text, compared as §3.2 normalizes it (its match.heading= text when it has one), and its heading level. A document heading at another level never binds it.
  • A section whose parent is the document binds a heading with its identity anywhere in the document. The ## sections after a lead section are such sections (Lead, above).
  • A nested section binds only a heading inside the span of the heading its parent bound. A heading with its text under another section neither satisfies it nor is checked by its rules. An enclosing section at the same level or deeper (a draft 2 closer wrapping a sibling, or a match.heading= directive written directly before the section it checks) cannot contain its heading in any document, so matching looks through it to the nearest enclosing section at a shallower level, or to the document.
  • When the parent bound nothing, the nested section and everything inside it are not checked. The parent's own presence rule reports the absence (§8.4).
  • When two headings in one parent's span match, the last one binds and the contract reports duplicate-heading, naming the parent. The same heading under two different parents is not a duplicate.
  • A leaf with no heading of its own reads the section its enclosing block bound.
  • A diagnostic names a nested section by its path, the matched headings from the outermost ancestor down, joined by > as in a block path (§8.1): Missing section "Options > Pros".
  • A heading in scope with the section's text at another level (a near miss) does not bind the section, and it is reported once, with the fix that sets its level (§8.4). For a section that is required or required na, or requiredWhen with its condition met, it is missing-section at that heading, naming the near miss (a requiredWhen section with no such heading at all is the state finding missing-conditional-section: what the document says requires it); for a recommended section it is missing-recommended-section there. Any other section gets heading-wrong-level (§8.6). The fix is offered only when that heading is the one such heading in scope. The section stays unbound, so its rules do not run, until the level is corrected.

Diagnostics.

CodeSeverityWhen
unexpected-closererrorA closer pairs with nothing (Step 1).
mismatched-tagerrorThe nearest block directive before a leaf closer is a block of another kind (Step 1).
section-without-headingerrorA closerless section directive is not followed by its heading (Step 2). This also covers a section directive with no heading after it at all.
block-before-anchorerrorA block directive sits between a section directive and that section's heading (unchanged).
unclosed-tagerrorOnly for an implementation-defined block kind that does not opt into optional closers. The core kinds never report it.
flag-on-title-leadwarnstrict or ordered on a section that covers only its H1's lead (Lead). The flag has no child sections to check; the help names the document directive, where it constrains the document's sections.
ambiguous-closerwarnAn endsection pairs with a section S whose parent P is closerless, and after the closer, a leaf directive or a significant content line sits directly in P's scope (inside no child section of P). The closer still closes S, the innermost open section, and the contract still compiles. The warning exists because a reader can take the closer for P's. Its message MUST say how the implementation read the structure (which section the closer closed, and that the following content belongs to P), and its suggestion MUST give the one-line fix: where to add P's own endsection.

Redundant closers. A closer is redundant when it occupies a whole line and removing it leaves the contract's block tree unchanged: the same blocks, the same nesting, and the same significant lines in every block's scope. A redundant closer is valid and is not a diagnostic. A closer that carries meaning, such as the one in Example 3 below, is not redundant.

Example 1: the same contract with and without its redundant closers. With every closer, as draft 2 required:

<!-- mdv: section required content.words.min=8 -->
## Summary

Describe the problem in one paragraph.
<!-- mdv: endsection -->

<!-- mdv: section -->
## Reproduction

<!-- mdv: list required items.min=1 items.all=@evidence/specific -->
- Step:
  Expected result:
<!-- mdv: endlist -->
<!-- mdv: endsection -->

<!-- mdv: section required content.words.min=4 -->
## Impact
<!-- mdv: endsection -->

Without its redundant closers, it compiles to the same blocks:

<!-- mdv: section required content.words.min=8 -->
## Summary

Describe the problem in one paragraph.

<!-- mdv: section -->
## Reproduction

<!-- mdv: list required items.min=1 items.all=@evidence/specific -->
- Step:
  Expected result:

<!-- mdv: section required content.words.min=4 -->
## Impact

Summary ends before Reproduction's directive. The list leaf owns its directive and the two-line list. Reproduction ends before Impact's directive, and Impact runs to the end of the contract.

Example 2: nesting, prose before a list, and a line leaf.

<!-- mdv: section required -->
## Release
<!-- mdv: line label="Owner:" mask="<mention>" -->

Owner: @alice

<!-- mdv: section required -->
### Checks

<!-- mdv: list required items.all=done-or-na -->
The checks, in order:

- [ ] Tests pass
- [ ] Changelog updated

<!-- mdv: section required allowEmpty -->
### Notes

## Appendix

Release is level 2. It contains Checks and Notes, both level 3, and ends at the bare heading ## Appendix. That heading is template content at the top level, not a block. The line leaf is bare. The list leaf owns the sentence The checks, in order: and the two-item list. Checks ends before Notes' directive, and Notes ends before ## Appendix.

Example 3: a closer that carries meaning. Removing the closer below changes the block tree:

<!-- mdv: section required content.words.min=20 -->
# Team roster

Current members of this team, one heading per person.

<!-- mdv: section required occurs=1.. heading.mask="@<word> — <text>" -->
## @lira-frost — Lira Frost

Status: Active

## @dax-oduya — Dax Oduya

Status: Active
<!-- mdv: endsection -->

Without the closer, the example heading ## @dax-oduya — Dax Oduya would end the repeated section early. The title needs none: it is the contract's only H1, so its section covers only the lead (Lead, above), and a closer after the lead would be redundant.

Example 4: an ambiguous closer (warning).

<!-- mdv: section -->
## Rollout
<!-- mdv: section -->
### Canary
<!-- mdv: endsection -->
<!-- mdv: list required -->

The closer pairs with Canary. The list therefore belongs to Rollout, and ambiguous-closer points out that a reader may have meant the closer for Rollout.

3.2 Block kinds

KindBinds toNestsNotes
documentthe whole file—Holds document-wide constraints. No closer.
sectionan ATX heading and its bodysections and leavesThe only heading-owning block. Closer optional: scope follows heading levels (§3.1.1).
listone top-level Markdown listnoBulleted, numbered or checkbox.
tableone pipe tableno
codeone fenced code blockno
linelines selected by label=no
mediaan image or video spanno

A leaf's closer is optional. A closerless leaf owns the next construct of its family in the contract, skipping prose and constructs of other families on the way (§3.1.1).

line. A line leaf checks each prose line opening with its label= against mask=. With no label=, the mask's literal text before its first <token> is the label (mask="Docs: <word>" reads lines starting Docs:); a mask that opens with a token still needs label=. mask= MAY be an array of alternatives, as content.mask is. A value of not done: <reason> (any case) is the state finding not-done (§5.3): the line keeps its label, counts as present, and is not checked against the mask; one whose reason is missing or a stub gives no reason (value-not-given, §8.2). Any other value is read by the capture rule (§8.2): nothing after the label, a placeholder, or no value of a token's type is the state finding value-not-given; a value outside a <token|oneOf:…> list is the state finding value-not-allowed; a value of its type in another form (an allowed one in another case) is the shape finding value-wrong-form; and the mask's literal parts not there while the values are is the shape finding label-line-mismatch. A oneOf list names the values the line may claim, not its grammar. The same holds wherever a mask checks a value (items.mask, content.lead.mask, content.firstLine.mask, heading.mask, headings.mask, a column's mask, a section's content.mask). A repeated section's selector picks its instances by the selection rule (§8.2), and each instance's heading.mask then reads its heading by the capture rule.

Leaves MUST be declared inside a section. A leaf binds to the construct of its family (list, table, code, line, media) inside its section: same-family leaves bind in declaration order to same-family constructs in document order. Two same-family leaves where an earlier one is not required is a compile error (the binding would be ambiguous). An extra construct of a declared family is an error. A section with occurs=<min>..<max> binds every heading its selector picks as an instance; zero parent instances means no child presence failures. A section with heading.mask and no occurs= is a set of one (occurs=1 when required, else occurs=0..1): it binds the heading its mask picks, never the contract's worked heading.

Entity checks on the document directive. An entity check reads the names in prose: content.entities.in (§4.1), and an implementation's own gazetteer checks (the reference implementation's match/* rules). A contract usually wants the same names checked everywhere, so an entity check MAY be written once on the document directive instead of on every section. There it reads all the document's prose: every line after the front matter except headings, directives and HTML comments. A finding reports at the line where the name first appears. A directive holds one code= (§3.3), so two customized entity checks take a document directive each; on a directive that also carries document rules, the code=, message=, suggestion= and severity overrides stay with the document rules. The directive's other rules, and the section form of each entity check, are unchanged. Any other section rule on the document directive is still unknown-rule.

3.3 Structural flags

FlagMeaning
requiredThe block MUST be present and non-empty (a heading with nothing under it fails with empty-section). Any honest content counts: ## Blockers followed by No blockers. passes. An HTML comment is not content: a block holding only comments is empty.
allowEmptyWith required: the block MUST be present and MAY be empty.
recommendedA missing block is a warning (missing-recommended-section), never an error.
optionalThe default presence: the block MAY be absent. Written explicitly it says so; next to use= it switches a set's presence off (§5.4).
naWith required: the block's na empty state (§3.5) MAY stand in for the content. Equivalent to empty=@empty/na.
empty=<marker>The block's empty state (§3.5): @empty/<name> or an inline quoted pattern. Content that is exactly the marker satisfies presence; the block's other constraints are not evaluated.
strictThe block's boundary is closed: undeclared child sections, list items not in the template, extra table columns, and extra front-matter keys are errors. It says nothing about order: strict ordered asks for exactly the declared children, in declared order. On a list, the items are exactly the template's (labels.eq=@template); on a table, a column outside its columns.has=[…] is an error, and a table that declares no columns has nothing to close, which SHOULD be a warning.
orderedDeclared children MUST appear in declared order. It means nothing else; list numbering is numbered (§4).
freeTextThe block's child sections stay open: strict on the same block (written there or brought in by use=, §5.4) does not close it, and an undeclared child section is free text. ordered still applies.
exhaustiveOn a repeated section whose heading is a mask (§8.2): every heading in its parent's scope at that section's level MUST be one of its instances or a declared sibling; any other is unmatched-instance-heading. Headings at other levels are not checked, so the parent's scope stays open apart from that one level. Independent of strict and freeText.
occurs=<range>Section cardinality, in the range grammar of §5.3: N, N..M, N.. (at least N), ..M (at most M).
code=<name>Diagnostic code for this block's constraint failures (§8.1).
error=<code> / warn=<code> / off=<code>Severity override for a code raised in this block. Several codes at once: warn=a,b or warn=[a, b].

With no presence flag a block is optional.

Front matter. <!-- mdv: front <key> <atoms…> --> constrains one top-level front-matter key (front status required); a bare front <key> declares the key as allowed but optional. An empty value or a bare YAML null (null, ~) does not satisfy required; false does, as it is a value. Front matter has no body, so a key's value predicates are written on the key itself, with no scope: front title pattern=/…/, front owner none=@placeholders, front date date=iso. front strict, with nothing after it, is the strict flag for the front matter: a key the contract does not declare is an error (unexpected-frontmatter-key), reported at that key's line. A bare front word that reads as a flag (front required, front strcit) SHOULD be a warning, never silently a key name.

3.4 Spellings

The spelling of every prop is fixed by this spec, and each prop has exactly one. An implementation MUST NOT accept another spelling for a prop, and MUST NOT keep an earlier draft's spelling alive behind a warning: a spelling this spec does not define is an unknown name (unknown-rule, §8.3), reported like any other typo. Appendix A lists the spellings earlier drafts used, beside the current one, for rewriting an old contract by hand.

An implementation MAY reserve words for a later release. Writing a reserved word MUST be an error, so a contract never silently checks less than it says. When this spec already spells what the reserved word suggests (a reference implementation's allowNA is na, its minChars is content.words.min), the error MUST name that spelling.

An array value means "any of" wherever an operator takes one value (items.mask=["<a>", "<b>"]); there is no separate .anyOf spelling.

Configuration files are not contracts, but they use the same spellings: an implementation's config that keys rule settings by rule writes the prop as a contract does ("content.none=@placeholders"), and one that keys severities by diagnostic code writes the code ("missing-section").

3.5 Empty states

An empty state is a named marker declared by the contract's author — never by this spec — that records "this block (or this checklist item) does not apply here, and honestly holds nothing". It is never the form for work that applies but was not done: that is a not-done item (§4.2). An implementation MUST NOT require or suggest any particular wording beyond the resolved marker's own pattern, and MUST NOT suggest the marker as the way to clear an open item.

Marker. A marker is a pattern and a minReasonWords (a non-negative integer). A pattern is literal text with at most one {reason} slot; it MUST contain literal text besides the slot, and { … } names other than reason are compile errors. minReasonWords defaults to 2 when the pattern has a slot and 0 when it has none; a positive value on a slotless pattern is a compile error.

References. A block names its marker with empty=@empty/<name> (<name>: a lowercase letter, then lowercase letters, digits, or -; words is reserved) or inline with empty="<pattern>" (reason floor: empty.words.min=N). The @empty/ family is reserved for this purpose; it is not a source (§7.1). na is sugar for empty=@empty/na.

Resolution. A reference resolves by the first of these that defines the name:

  1. the directive itself (an inline pattern);
  2. the contract's document directive: empty.<name>="<pattern>", optionally empty.<name>.words.min=N;
  3. project configuration (implementation-defined key; the reference implementation uses emptyStates in mediva.config.*);
  4. an implementation's programmatic default;
  5. the built-in marker na (below).

An unresolvable reference is a compile error (invalid-option). Resolution is per contract and independent of directive order. Every list item is additionally judged by an item marker: the block's empty= marker when it has one, otherwise the resolved na.

Built-in na. Pattern (N/A: {reason}), minReasonWords 2, matched with the historical tolerances: N/A in any case; :, ,, or a dash as the separator; a reason without ); optional terminal ., !, or ?. For a whole block the parentheses are optional. A contract that declares no empty state therefore behaves exactly as before this section existed.

Matching. A declared marker matches literally and case-sensitively, except that each run of whitespace in the pattern matches one or more whitespace characters, and the pattern is trimmed. The {reason} slot matches one or more characters; the captured reason meets minReasonWords N when it has at least N whitespace-separated tokens, at least min(N, 2) of which contain a Unicode letter, and — when N ≥ 2 — the letter-bearing tokens, case-folded with non-alphanumerics removed, are not all identical. A marker matches:

  • an item when it ends the item's text, preceded by whitespace or nothing;
  • a block when it is the block's entire prose. A leaf whose construct is absent also matches when some paragraph of its section is entirely the marker. A leaf that declares no empty= inherits its enclosing section's explicitly declared marker.

Semantics. An item whose text carries its item marker has disposition == "na" (§4). A block whose content is written in its marker's form is empty-but-disposed whatever its reason says (the built-in na also in the bare form (N/A)): its shape holds, and no other constraint of that block is evaluated. Whether the reason meets the marker's floor is a claim, judged in the state gate: a reason that is none, too short or a placeholder ((N/A), (N/A: x), (N/A: TBD)) is needs-content-or-na under na (empty-section under required), and the readout gives the block disposition: "na" and its reason (null when it gives none). Otherwise required (with or without allowEmpty) and na are satisfied, and no other constraint of that block is evaluated (count props such as content.words.min, items.min, rows.min never see the marker as content). For media, content that is neither media nor the block marker fails with missing-media; the built-in na additionally accepts any mention of "N/A", "not applicable", or "no user-visible" (legacy tolerance). A block that is empty and declares a marker SHOULD have its presence diagnostic's hint quote the resolved pattern.

Conformance. Level EMPTY-1: the built-in na for items and blocks, empty=@empty/na, and na. Level EMPTY-2: document-level and inline markers, the full cascade minus configuration, leaf inheritance. Configuration (level 3) is host-defined and outside the language-neutral suite.

4. Targets — Core

A block exposes the targets that apply to its kind. A target is the left-hand side of a prop, where a target that measures the body is written under content. (below the table).

TargetTypeBlocksValue
textstringallThe block's own prose, without HTML comments. For a section, child section and leaf spans are excluded, except under none (§5.3).
wordsintallWord count of text (word defined below the table).
headingstringsectionHeading text without the # marker.
itemslist(Item)listTop-level items in order (§4.2 defines Item).
checkedlist(Item)listThe items whose box is ticked.
labelslist(string)listEach item's label: its text up to the first —, :, or end.
numberedboollistTrue for an ordered (1.) list.
rowslist(map(string, string))tableBody rows keyed by header cell text.
columnslist(string)tableHeader cells in order.
prosestringalltext with fenced code removed.
blockslist(string)allThe kinds of block present (paragraph, list, table, code, …).
leadLine or nullallThe first line of the first non-empty paragraph.
fenceslist(Fence)allFenced code blocks. Fence = {lang, line}.
linkslist(Link)allInline links, resolvable reference links, and autolinks. Link = {text, line}.
claimslist(Claim)allGrounding module. Claim tokens in text (§4.1). Claim = {kind, text, value, index}.
refslist(Ref)allGrounding module. Reference links and footnote references. content.refs.all=defined requires each to have a definition (§6.3).
quoteslist(Quote)allGrounding module. Blockquote paragraphs and inline "…" quotations. Not yet a target in the reference implementation.
dateslist(timestamp)allGrounding module. ISO-8601 dates in text. Not yet a target in the reference implementation.
langstringcodeFence info string's first word.
titlestringdocumentThe document title, supplied by the host. A title. prop with no title supplied fails with missing-title; it never passes unchecked.
frontmap(string, dyn)documentParsed front matter.
headingslist(Heading)documentThe document's headings. Heading = {text, depth, line}, where text is the heading as a section binds it (§4, heading.).

Each claim kind also reads as a plural target, amounts, numbers, dates, durations, percents, ids and urls: amounts is the claims of kind amount, and so on (§4.1, Grounding module).

Word. text is split into tokens on whitespace and punctuation, exactly as draft 2 always did: a token is a run of letters, digits, or _, where an internal ' or - does not split it ("don't" and "well-known" are one token each). For a space-delimited script (English and any other script that separates words with whitespace, Hangul included) each token is one word, unchanged. Within a token, every Han ideograph (Unicode Script=Han, so both Chinese hanzi and Japanese kanji) and every Hiragana or Katakana character counts as its own word, the way common word processors count CJK prose; any run of other characters in the same token still counts as a single word (API的设计 is a single token that counts as 4: API plus three ideographs). CJK punctuation (,。「」) and other non-letter, non-digit characters (including emoji) are never part of a token, the same as ASCII punctuation and whitespace. Known limitation: Thai, Lao, Khmer, and Myanmar text has no spaces and no per-character word convention; an implementation MUST NOT attempt dictionary-based segmentation for them (segmentation dictionaries are not fixed by the Unicode Character Database and vary by ICU version, which would make word counts non-deterministic across runtimes), so a run of such characters counts as a single word regardless of length.

Three scopes. A prop's target reads as one of three scopes, or as the block's own structure:

  • title. is the document's title (title.words.min=3, title.none=@placeholders).
  • heading. is the section's heading (heading.mask="…", heading.none=@placeholders), read as the section binds it: a link [text](url) and a bare [text] read as text, and a closing # run and any leading characters that are not letters or digits are dropped, so ## [1.2.0] - 2026-07-02 reads 1.2.0 - 2026-07-02. A heading mask (heading.mask, which picks its section's heading (§3.2), and the document's headings.mask) reads the heading the same way but keeps its leading characters other than an emoji: ## @lira — Lira reads @lira — Lira for heading.mask="<mention> — <text>". heading.raw.mask= and headings.raw.mask= match the heading as written, brackets included.
  • content. is the body of the section or of the document. Every target that measures the body is written under it: content.words, content.links, content.sentences, content.blocks, content.dates, content.prose, content.fences, content.claims, content.quotes, content.entities, the plural grounding targets (content.paths, content.issues, content.mentions, content.amounts, …; §4.1) and content.lead. The body text itself is content, so the text predicates are content.none, content.mask, content.eq, content.pattern, content.scan and content.unlike. On the document directive the body is the whole document: content.lead is then the document's first line, its first heading or paragraph.
  • A block's own structure takes no scope: items, checked, labels, numbered, rows, columns, lang, headings, and front matter keys.

4.1 Claims and entities — Grounding module

claims lists every claim token in the block's text. Recognition is closed and conservative: a shape either unambiguously canonicalizes, or it is not a claim token at all. An unrecognized or ambiguous span is invisible to grounding, never a guessed value — a false negative (grounding says nothing about a real claim) is acceptable; a false positive (grounding fabricates a canonical value for something that wasn't really that kind) is not.

KindRecognized asCanonical form (value)Equivalence
amountA currency symbol or ISO 4217 code adjacent to a number (£412, $1,200.50, 412.00 GBP, GBP 412)CODE:magnitude when a currency is determinable, else :magnitude (magnitude = comma-stripped, trailing-zero-dropped decimal)Same code and magnitude; a currencyless claim (:412) also accepts any source amount of the same magnitude under some currency (never the reverse — a currency-qualified claim never silently accepts a different currency).
numberA plain number with no unit, currency, or sign contextComma-stripped, trailing-zero-dropped decimalExact string equality.
dateOne of three unambiguous surface forms: YYYY-MM-DD, D Month YYYY, Month D, YYYYISO 8601 calendar date YYYY-MM-DDExact string equality.
durationA numeral (digits, or one of a closed set of number words: one–twelve, twenty, thirty, sixty, ninety) directly followed by a duration unit word, space- or hyphen-separated (90 days, two weeks, 48 hours, 48-hour). Recognized units: day(s), week(s), month(s), year(s), hour(s)/hr(s), minute(s)/min(s). A hyphenated compound number (twenty-five) MUST NOT extract from its last word alone.ISO 8601 duration: P{n}D / P{n}W (folded to P{7n}D at canonicalization) / P{n}M / P{n}Y (date part), or PT{n}H / PT{n}M (time part)Exact string equality on the folded canonical form — weeks fold into days, so P2W and P14D both canonicalize to P14D and compare equal for free. Months and years are never converted to days.
percentA number directly followed by %, percent, or per cent (90%, 12.5 percent)The bare decimal magnitude, comma-stripped and trailing-zero-dropped ("90%" → "90")Exact string equality. A percentage never also produces a number claim for the same span.
idA short uppercase-alnum prefix, a dash, and ≥3 trailing digits (INV-2041, INC-2044); a source-side /route/path also grounds as idTrimmed verbatimExact string equality.
mention@handle: letters, digits, _, and hyphens inside the handle, as in a GitHub handle (@payments-oncall); a trailing hyphen is not part of itLowercased, @ strippedExact string equality.
urlAn explicit-scheme URL, or a bare domain with a pathScheme and trailing slash stripped, lowercasedExact string equality.
quoteA quoted span of ≥3 words (straight or smart quotes)Lowercased, whitespace-collapsed, smart-quote/dash-straightened, edge-punctuation-trimmedExact string equality (mechanical folding only, never semantic).

Plugins MAY contribute namespaced kinds (clinical/tlf); they are opaque to this table and to the default grounded-kind set, amount, date, duration, percent, id.

Decisions normative to this table:

  • Currency ambiguity. ISO 4217 does not uniquely map $ to a currency. An implementation MUST resolve $ → USD, £ → GBP, € → EUR, ¥ → JPY, deterministically and without per-instance guessing. A contract needing a different $-symbol currency (CAD, AUD, …) spells it with the ISO code (CAD 50).
  • Duration recognition is closed, not full numeral parsing. Compounds (twenty-five, one hundred) are out of scope; a recognizer MUST refuse to start a numeral match immediately after a letter or hyphen, so -five in twenty-five hours never matches on its own. Fractional durations (1.5 days) do not extract. hr(s) is recognized; no other unit abbreviation is.
  • Week/day folding is the one unit conversion. Weeks convert to days (exact, unconditional); months and years never do (their day count is not fixed).
  • Source matching for duration/percent (§7.1) accepts three deterministic forms: an ISO 8601 string already; a number under a closed, exact-suffix key name (_days, _weeks, _hours, _months, _years for duration; _percent, _pct for percent — the match is an exact suffix, never "contains", and the suffix MUST NOT be the whole key); or a percentage string leaf ("90%"), handled by the same tokenizer prose uses. Key-name inference produces the duration/percent token in addition to, never instead of, the number/amount token a JSON number leaf already produces.
  • A version string (v1.2) or a clock time (14:30) MUST NOT become a duration or percent claim. Both recognizers require an explicit unit word or %/percent directly following the numeral, which neither shape has.

Extraction is non-overlapping, and the longest span wins. Every recognizer above, plus the path recognizer and slash routes (/v2/invoices/INV-2042, §7.1), proposes spans over the text; an implementation MUST keep the longest of any overlapping spans, and between two of the same length the earlier kind in this order: date, amount, duration, percent, id, url, number. So a URL is one url claim, never also the id or date in its path; a date or id inside a file path or route (reports/2026-07-02.md, src/INV-2042.md) is part of that path and no claim at all, since a path and a route are spans that compete but are not claim kinds; and £412 is one amount, never also the number 412. A mention stands unless a longer span holds it; a quote contains other claims and is extracted independently. A ref (#123, ABC-123, §7.1) inside a URL, a path or a route is likewise part of that span. A source value is read both this way and in recognizer order, so a source never grounds less than it did before this rule.

One spelling: a plural target per thing extracted. Extract-then-check grounding writes content.<plural>.in=<ref>, with no parentheses:

TargetChecksCode
content.amounts.in, content.numbers.in, content.dates.in, content.durations.in, content.percents.in, content.ids.in, content.urls.inEvery claim of that one kind (table above) is in the source, canonicalized. Two kinds may name two sources.ungrounded
content.claims.inUmbrella over typed values: every claim of the default kinds amount, date, duration, percent, id.ungrounded
content.entities.inUmbrella over named things: capitalized names, @mentions and slash routes (§3.2). Not yet a claim kind; see §10.unknown-entity
content.paths.in, content.issues.inEvery file path / issue ref (§7.1). issues, not refs: content.refs already names citation refs (§4).not-in-change-set
content.mentions.mask + content.mentions.inEvery span shaped by the mask is a member of the source (§7.1).unknown-entity
content.quotes.inEvery quotation reproduces the source (§7.1).quote-drift

The mention and quote claim kinds are read by content.mentions.in and content.quotes.in; they have no separate plural claim target. A grounding atom whose required source (no trailing ?) the host does not supply means the check could not run (source-unavailable, §8.2), never a silent pass (§7.1). To ground only some kinds, write one plural target per kind: content.amounts.in=X content.dates.in=X.

The path and ref entity kinds used with change sets (§7.1) are a separate, standalone recognizer family, not part of the claim-kind table; see §7.1 for why.

4.2 The Item shape — Core

Item = {
  text: string,
  checked: bool | null,        // null for a plain bullet (not a checkbox)
  label: string,                // text up to the first " — ", ":", or end
  disposition: string,          // "done" | "claimed" | "na" | "not-done" | "open"
  evidence: string,              // "" when the item carries none
  line: int,
}

checked and label are unchanged from an implementation that has no notion of disposition: a contract that only ever reads checked, items.filter(...), or labels sees no behavior difference from adding disposition/evidence.

disposition is one of five strings, in this classification order (first match wins):

  1. The item's text ends in its block's item marker (§3.5; with no declared marker, the built-in (N/A: <reason>), whose reason has at least two whitespace-separated tokens, at least two of which carry a Unicode letter, not all the same token, case/punctuation insensitive) → "na", regardless of checked. N/A means the item does not apply here; it never means "could not do it". A ticked N/A (- [x] Screenshots (N/A: CLI-only change)) is still "na", not "claimed", but it contradicts itself: the tick records the work as done and the marker says the item does not apply. items.all=done-or-na and items.none=bare-tick report it as contradictory-disposition (§5.3).

  2. The item gives a not-done reason → "not-done". The item applies, its work was not done, and the text says why. Two ways:

    • The explicit form, regardless of checked: (not done: <reason>) ending the text, preceded by whitespace or nothing, or not done with its reason right after a rule 1 separator of the evidence split (Tests run — not done: <reason>, Tests run: not done, <reason>); not done in any case, joined to the reason by :, ,, or a dash. A ticked item that carries it contradicts itself (§5.3).
    • On an unticked checkbox item (checked == false), any author text after the label: after the declared template label when one matches (rule 0's split), otherwise after a rule 1 separator or in a trailing parenthetical (Tests run (no runner in this sandbox)). With nothing after the label on its own line, the first non-empty nested note (rule 4) is the reason. So - [ ] Tests run: not run, no test runner was available is "not-done" with that reason.

    A reason MUST contain at least one Unicode letter, and is not a reason when it is a bare not done or when the text uses not-applicable wording that is not a well-formed marker (N/A, not applicable, does not apply, or the declared marker's literal text with a reason too short): that text claims the item does not apply, so the item stays "open". A not-done reason is stripped before label comparison, exactly as a well-formed N/A marker is, so - [ ] Tests run (not done: no runner here) still matches a declared Tests run.

  3. checked == true → "done" if evidence != "", else "claimed".

  4. checked == false → "open".

  5. checked == null (a plain bullet) → "done" if evidence != "", else "open". A plain bullet can never be "claimed": nothing was ticked, so there is no false attestation to catch.

evidence is the verifiable remainder of an item's text — something a reader could check against reality, not a semantic judgement about whether the claim is true:

  1. Template label. If text starts with a label the block's contract list itself declares (its - [ ] … items, compared as labels.eq=@template compares them: case-insensitive, whitespace collapsed, a leading emoji ignored, the longest match winning) and the declared label is followed by the end of the text or by joining punctuation (a rule 1 separator, ;, ,, -, (, [, or a sentence-ending .), that declared label is the item's label. A declared label ending in terminal punctuation (., ;, :, !) is matched without it: the author may keep that punctuation and continue after whitespace, or insert text before it with any join, and the label alone, with or without its final punctuation, is still the label alone. The author's text after it, without the joining punctuation, is the evidence candidate under rule 2's bar, and rules 1 and 3 do not apply. Nothing inside a declared label is evidence, even a separator, a code span, a link, or an Ex: …: - [x] PR title format: `Add project-name` ticked as the template wrote it is a bare tick, while …release (`vX.Y.Z`). Latest is `v0.40.2`. carries evidence after its declared label.
  2. Otherwise, split text at the first occurrence of a separator: : (colon followed by a space — required so the split never lands on a URL's scheme colon), - (an ASCII hyphen with a space on each side, so a hyphen inside a word never splits), – (en dash), — (em dash), ->, =>, or → (spaces optional around an arrow). With none of these, a trailing parenthesised remainder ends the label: Tests run (…). This is the ONE separator set: labels.eq=@template (what may follow a declared label), the evidence split, and the not-done reason rule (rule 2 of disposition) all end the label at the same place, after rule 0's declared template label when one matches. labels.eq=@template does not accept a parenthetical that says nothing (no letter, not-applicable wording, or a bare not done), so a hollow (N/A) keeps its label mismatch.
  3. If a separator is found, the text after it is the evidence candidate; it counts as evidence when it contains a concrete signal (a number, an inline code span, or a URL), at any length, or when it is at least 4 words and contains a verification verb (implementation-defined open list; an implementation MAY use its own). An implementation MUST implement the concrete-signal condition exactly as specified, since it is language-neutral.
  4. If no separator is found, the whole text is the item's label. On a checkbox item (checked != null) the label is never its own evidence: a number, code span, or URL inside it names the task (Ran `npm test`, See [CI](https://…)) and does not show it was done, so only rule 4 can supply evidence. On a plain bullet (checked == null) the whole text is the candidate, and it counts only through the concrete-signal condition — never through the verb condition alone, or an ordinary imperative task label ("Run the test suite") would qualify as its own evidence.
  5. Otherwise, nested evidence: a plain (non-checkbox) sub-item or an indented paragraph nested under a checkbox item is that item's note, not an item of its own. The first note that counts as evidence under rule 2 (the nesting itself stands in for the separator) is the item's evidence. text is always the label line alone, so labels stay byte-exact.
  6. Otherwise evidence = "".
Item textcheckeddispositionWhy
Ran `npm test`; 0 failures.trueclaimedNo separator: the whole line is the label, and a code span or number in a label is not evidence.
Ran `npm test`; 0 failures.nulldoneA plain bullet has no label to attest; its concrete signal (digits + code span) makes it its own evidence.
Tests pass — Ran `npm test`; 412 pass, 0 fail.truedoneSplit remainder is falsifiable (digits).
Tests added: added `tests/unit/x.test.ts`truedoneSplit remainder is two words, but a code span is a concrete signal at any length.
Docs updated: done alreadytrueclaimedSplit remainder has no concrete signal and is under 4 words.
Tests passtrueclaimedNo separator, no concrete signal — a bare tick.
Docs updated (N/A: docs-only change is out of scope)true or falsenaWell-formed N/A marker, either way.
Tests run (not done: no test runner in this sandbox)falsenot-doneWork that applies, not done, with its reason.
Tests run: not run, no test runner was availablefalsenot-doneUnticked, with author text after its label: that text is the reason.
Tests run: not donefalseopenA bare "not done" is not a reason.
Run the test suitefalseopenNothing after the label: no evidence, no marker, no reason.
Run the test suitenullopenNo evidence, no N/A, and nothing was ticked.

5. Props — Core

5.1 Syntax

A prop is <target>.<op>=<value>, or <target>.<op> for a flag operator, where a body target carries its content. scope (§4). Grounding's extract-then-check targets are plural nouns with no parentheses (content.paths.in, content.dates.in; §4.1). A prop exists only where an implementation's rule set defines it (the rule reference, mdv explain): §5.3 gives what each operator means wherever it applies. Any other <target>.<op> (heading.chars.max=, items.color.eq=) is an unknown rule.

5.2 Per-element application

When the target is a list, value operators (mask, pattern, in, none) apply to every element (D16). Count operators (min, max, range, eq with an integer) apply to the list's size. age.max reads the most recent date: a section that logs every verification is as fresh as its latest one.

5.3 Operators and their definitions

T is the target. e ranges over its elements when T is a list. v is the value. The definitions use the notation of §6.

PropApplies toDefined as
T.min=nintT >= n
T.min=nlistsize(T) >= n
T.max=nint / listT <= n / size(T) <= n
T.range=a..bint / listT.min=a and T.max=b together. a.. is T.min=a alone, ..b is T.max=b alone, and n is T.min=n T.max=n.
T.eq=nint / listT == n / size(T) == n
T.eq=<ref>stringnormalize(T) == normalize(source(v))
T.eq=<ref>list(string)T == source(v)
T.mask="<mask>"string / listT.matches(mask(v)) / T.all(e, e.matches(mask(v)))
T.pattern="<re2>"string / listT.matches(v) / T.all(e, e.matches(v))
T.in=<ref>string / listT in source(v) / T.all(e, e in source(v)) (Grounding module)
T.in=[a, b]string / listT in v / T.all(e, e in v): an array literal lists the allowed values inline.
T.none=<ref or re2>string / list!T.matches(v) / T.all(e, !e.matches(v)). On a section, content.none reads everything under the heading except child sections, its leaves included: none means nowhere in the section.
T.all=<predicate>listT.all(e, predicate(e))
T.uniquelistsize(T) == size(distinct(T))
T.age.max=<duration>list(timestamp)age(max(T)) <= v: the most recent date is at most v old. With no date there is nothing to judge, and it passes. A duration is whole days (90d, or a bare 90) or hours (24h). Only the maximum is defined: T.age.min and T.age.range are not props.

Ranges. A range is a..b, a.., ..b, or a single n, over non-negative whole numbers, with both bounds inclusive and a no greater than b. T.range= takes a range wherever both T.min= and T.max= count (items.range=1..5, content.words.range=20..180, checked.range=1..1), and occurs= (§3.3) reads the same grammar. A T.range= on a T that has only one bound, or none (title.words.range=, content.dates.age.range=), a malformed range, or a T.range= beside a T.min= or T.max= for the same T on one directive, is a compile error (invalid-option) on the range as written, whose help names the way to write the bound. T.min and T.max stay: a range is one more way to write the pair.

One value per atom. An empty pattern or mask is a compile error (invalid-regex, invalid-mask): it would match everything. One directive that sets the same atom twice with different values is a compile error (duplicate-atom), never last-wins. Atoms that resolve to different checks, such as content.none=@placeholders content.none=/x/, are not the same atom.

items.all=done-or-na (Core; §4.2) passes when every item is "done", or "na" and unticked. It is the RECOMMENDED checklist completion gate; items.all=checked remains valid but cannot distinguish done from claimed. N/A passes the gate because the item does not apply; work that applies but was not done fails it while it stays open, and that FAIL is the correct result. A not-done item is reported as not-done, an error whose message names the reason, distinct from an open-item that says nothing; it is never repaired automatically, since the item is honest and the work is what is missing. A ticked "na" item is contradictory-disposition, an error ("ticked but marked as not applying"). An open item (unticked, nothing after its label) is open-item, also an error: a final FAIL is correct while work is open. Under items.none=bare-tick an unticked item, with or without a reason, still passes. items.none=bare-tick passes when no item's disposition is "claimed", so a tick needs evidence while an honest unticked box (a not-done one included) and a marked item both pass. Under either prop, a ticked not-done or "na" item is contradictory-disposition, an error: the box records the work as done and the text says it was not, or that the item does not apply.

A contract that asks for evidence. items.all=done-or-na and items.none=bare-tick ask for tick evidence, and each reports a claimed item with exactly one of two codes (§8.4). A bare tick offers no evidence at all: the item's own text has no separator (§4.2 rule 1) followed by non-empty text, and no note is nested under it. A code span, link, or number inside the label does not change this (§4.2 rule 3): - [x] Ran `npm test` is a bare tick. Whether an item offers evidence is a fact about its text, so a bare tick is tick-without-evidence, an error. A tick that offers evidence which fails the evidence test (a split remainder or a note with no concrete signal and no verb route, such as Docs updated: done already) is weak evidence: only the verb heuristic judged it, so it is weak-tick-evidence, an advisory warning (§8.3) that never fails a check on its own. Unticked items are never either. This is an evidence requirement, not completion verification: mediva checks that a tick states evidence, not that the work happened. A contract MAY lower the error with warn=tick-without-evidence (§3.3).

State findings. These codes and not-in-change-set (§7.1) are state findings: an implementation MUST NOT propose an edit that clears one without the work. For tick-without-evidence, weak-tick-evidence and open-item the help MAY say to do the work now and record its observed result, or to leave the item unticked with (not done: reason); the others name what a person must confirm. The vocabulary predicate items.all=@evidence/specific judges every item's wording, ticked or not, and stays advisory.

A <predicate> is a field name of the element (checked, disposition), a named predicate (items.all=done-or-na, items.none=bare-tick; defined on content.refs, a reference with a definition, §6.3), or a vocabulary predicate (@evidence/specific). A prop whose operator doesn't apply to the target's type is a compile error.

Examples from the hero set: content.words.min=20, items.all=checked, items.all=done-or-na, checked.eq=1, items.mask="<mention> <text> by <date>", content.entities.in=./openapi.json#/paths, content.eq=./risk-wording.txt, content.dates.age.max=90d, content.links.max=1.

5.4 Named rule sets

A named rule set is a reusable, fixed bundle of atoms that a contract author defines. It is the author-level form of a vocabulary preset (§7.2). It adds no evaluation semantics: a rule set means exactly the atoms it expands to (Principle 1, §2). (Design record: SYNTAX-SIMPLE.md, item B2; issue #1433.)

<!-- mdv: define summary required content.words.min=20 content.none=@placeholders -->

<!-- mdv: section use=summary -->
## Summary

The section above means exactly <!-- mdv: section required content.words.min=20 content.none=@placeholders -->.

Definition. <!-- mdv: define <setname> <atoms…> --> (§3.1):

  • The name follows [a-z][a-z0-9-]*, the fragment-name grammar: prose or long-prose. It has no @ prefix. Directive names, closers and known atom keys or flags are reserved; declaring one reports rule-set-name-reserved.
  • The body is atoms that are valid on a block directive: flags, props, checks (§7.2.1), and uses of other rule sets. It MUST NOT contain a block kind, a closer, define, include, or extends=, and it MUST NOT be empty. Any of those is a compile error, invalid-rule-set. A rule set takes no parameters: the bundle is fixed, and a use site changes it only by writing its own atoms next to it (see Precedence).
  • A define is a declaration (§3.1.1). It opens no scope, and it may sit anywhere in the contract outside fenced code. Definitions are hoisted: resolution does not depend on directive order, the same as empty states (§3.5). Definitions SHOULD come at the top of the contract, after the document directive.

Where definitions come from. A contract can use rule sets from these places:

  1. the contract itself;
  2. the files it includes (§9.2), transitively. Every definition in an included file is imported, even when a # anchor selects only one of its sections;
  3. its extends parents (§9). A child inherits its parent's definitions. A child definition with the same name replaces the parent's.

Two definitions with the same name from sources 1 and 2 are a compile error, duplicate-rule-set. The exception is one definition reached twice, such as a file included along two paths.

Names. Local names resolve only through sources 1–3. A name that resolves to nothing is a compile error, unknown-rule-set. Vocabulary checks retain their @namespace/name spelling (§7.2.1), and =@value continues to select a vocabulary value (§7.1).

Use. Writing use=name on a block directive, on document, or in another definition expands the set there. The directive behaves as if the set's atoms were written on it. A set is not tied to one block kind. Each expanded atom is compiled against the kind of block that uses it. An atom that does not apply to that kind fails the way it would if it were written there directly, and the error is reported at the use. Several sets use one comma-separated assignment, use=prose,titled; repeated use= assignments and malformed names report invalid-rule-set. id= identifies a block for inheritance (§9); it does not apply a rule set.

Precedence. An atom written on the directive itself always wins over the same key from a set.

  1. Single-valued keys. Every key except those in rule 2 holds one value. If the directive writes a key and a set also writes it, the directive's atom replaces the set's, whatever the order: section use=prose content.words.min=40 keeps the set's other atoms and uses content.words.min=40.
  2. Presence. The presence flags required, recommended, and optional count as one key, and allowEmpty and na, which modify required, go with it. So a use site switches a set's required off by writing optional (or relaxes it with recommended or required allowEmpty), and the set's allowEmpty or na goes too.
  3. Accumulating keys. T.none=, severity overrides (error=, warn=, off=), and check uses (@ns/name) add up: the set's atoms and the directive's atoms all apply. Identical atoms collapse into one (§8.4).
  4. Nesting. Inside a set, the same rules apply recursively: the set's own atoms win over the atoms of the sets it uses.
  5. Conflicts. Sometimes two sets used on the same directive, or by the same set, write one single-valued key with different values, and the directive (or the using set) does not write that key itself. That is a compile error, rule-set-conflict, and it names both sets. Equal values do not conflict.

There is no enforced tier: every atom a set contributes can be replaced at a use site.

Cycles. A set that uses itself, directly or through other sets, is a compile error, rule-set-cycle. It is reported once per cycle, at the definition that comes first in the contract, and the message names the whole path.

Unused. A set defined in the contract itself that nothing uses is a warning, rule-set-unused. Definitions that are included, inherited, or come from a vocabulary are exempt.

Provenance. A diagnostic raised by an expanded atom SHOULD report its constraint (§8.1) as the set's name, then →, then the atom as written in the definition: summary → content.words.min=20.

Example: before and after. Draft 2 repeats the same atoms on every section:

<!-- mdv: section required content.words.min=20 content.none=@placeholders -->
## Summary
<!-- mdv: section required content.words.min=20 content.none=@placeholders -->
## Motivation
<!-- mdv: section required content.words.min=40 content.none=@placeholders -->
## Design

With a rule set, and a use-site prop where one section needs more:

<!-- mdv: define prose required content.words.min=20 content.none=@placeholders -->

<!-- mdv: section use=prose -->
## Summary
<!-- mdv: section use=prose -->
## Motivation
<!-- mdv: section use=prose content.words.min=40 -->
## Design

Example: precedence. This directive:

<!-- mdv: section use=prose recommended content.words.min=5 content.none=@agent/cut-off -->

expands to recommended content.words.min=5 content.none=@placeholders content.none=@agent/cut-off. The directive's presence flag and its content.words.min replace the set's. The two content.none atoms both apply. <!-- mdv: section use=prose optional --> keeps content.words.min=20 and content.none=@placeholders but makes the section optional.

6. Definitions — Core

6.1 Notation

The definitions in §5.3 and §6 use a small expression notation: size(L) is a list's length, L.all(e, P) holds when P holds for every element, L.filter(e, P) keeps the elements for which P holds, x in L is membership, s.matches(re) is a regex search, and T == v compares values. It is notation for the definitions only: a contract writes props (§5), never expressions.

When a per-element prop (L.all(e, P)) fails, the implementation MUST report every element of L for which P is false, not only the first, each at the element's own line (§8.1). For any other failing prop, it reports the block.

6.2 Context

now, title, issues, and values come from explicit context supplied by the host. An implementation MUST NOT read the wall clock during a check: it MUST accept now as an explicit override from its caller, and MUST NOT let a default for it depend on anything but the moment the run started.

Reference implementation note. The reference implementation's command-line surface supplies the current time as now by default and accepts an explicit override from its caller.

6.3 Functions the definitions use

FunctionMeaning
mask(s)Compiles a mediva mask (the token/filter grammar) to a pattern matches accepts.
normalize(s)Collapses whitespace, trims, and normalizes quotes and dashes.
plain(s)Folds characters that read the same but defeat a literal match (zero-width, non-breaking space, curly quotes).
fold(s)The loose comparison key: plain, NFC, dashes, collapsed whitespace, edge punctuation trimmed, lowercased. Column and row names in the tables checks (§7.2) compare by fold.
isPlaceholder(s)s contains placeholder text (TODO, TBD, an unfilled stub); the @placeholders predicate.
num(s)Parses a figure: currency symbol, thousands separators, %/bps, a scale suffix (K, M, bn, million) and an accounting negative (1,234) are understood. An ambiguous token (a range 10–12, a fraction 37/120, <0.1, N/A) is not a number and yields NaN, which no comparison accepts.
decimals(s)Decimal places written in the figure s (42.10% → 2); 0 when it doesn't parse. Half a unit in the last displayed place of s is 0.5 × 10^-decimals(s).
approx(a, b, tol)|a − b| ≤ tol, plus a 1e-9 guard against binary floating-point dust. The comparison every recomputed figure uses.
format(x, n)x rounded to n decimals, . as the decimal point and , grouping every three integer digits (10,000.50). Deterministic: no host locale.
hasWord(s, w)w occurs in s as a whole word, ignoring case. A boundary is any character that is not a Unicode letter, digit or _, required only on an edge of w that is itself a word character.
resolved(citation)The citation has a definition in the document, or the configured resolver confirms it. An in-page #anchor link resolves when one of the document's headings has that slug, by GitHub's rules: lowercased, punctuation dropped, spaces turned into hyphens, and -1, -2, … for repeated headings. content.refs.all=defined applies it.

The numbers checks (§7.2) read one more target, lines: every line of the block's prose-bearing content (paragraphs, headings, block quotes, list items, and each table cell) as {text, line}, with fenced code and HTML skipped and inline code blanked, so a figure written as code is never read.

7. Sources and vocabularies — Grounding module

7.1 Sources

RefResolves to
./file.jsonParsed JSON.
./file.json#/a/bThe value at that JSON pointer.
./file.txt, ./file.mdThe file's text.
./dir/The texts of every file directly inside dir.
@nameA source declared in configuration.
@vocab/nameA pattern, mask, source, or predicate exported by a vocabulary.
@templateThe contract's own template content for this block (for example its labels).
@changesA reserved declared source: the CHANGE SET the host supplies for this run (below). Not itself bound to a consumable value in the reference implementation — only its two projections are.
@change/pathsEvery path the change set touched, including a rename's pre-rename path, as a projection of @changes.
@change/refsEvery issue/ticket ref the change set carries, normalized as written, as a projection of @changes.

Relative refs resolve against the contract's directory and MUST NOT escape it. A source the contract requires (no trailing ?) that the run does not supply is source-unavailable: the check could not run (§8.2), on every surface, never a pass. An optional one (ref?) the run lacks is skipped by the rule that reads it.

Membership (x in source(ref)) depends on the source's shape:

Source shapex in S is true when
list of stringsx equals an element.
mapx equals any leaf scalar, compared as a string; or equals a key when the pointer selects a map of paths.
textnormalize(x) is a substring of normalize(S).
list of texts (directory)true for some text.

Change sets

A CHANGE SET is a tool-neutral JSON record, supplied by the host (never computed by core):

{
  "base": "main",
  "head": "HEAD",
  "paths": [
    { "path": "src/lib/retry.ts", "status": "modified" },
    { "path": "src/new-file.ts", "status": "added" },
    { "path": "src/renamed-to.ts", "status": "renamed", "from": "src/renamed-from.ts" }
  ],
  "refs": ["#123", "ABC-456"]
}
  • base/head are free-form provenance; nothing in grounding reads them.
  • paths[].status is one of added / modified / deleted / renamed; renamed requires from (the pre-rename path).
  • refs are issue/ticket references normalized as written in the tracker's own convention (#123, ABC-123) — mediva never renumbers, reformats, or resolves them against a real tracker. Absent or null means the host does not know the refs; [] means it checked and found none.

An implementation MUST validate a change set on load and report a field-naming error for a malformed record, rather than a downstream crash or a silent empty grounding.

Two new entity kinds ground against a change set, recognized by standalone recognizers (they are not ClaimKinds — see below):

  • path — a token with a file-shaped tail: a known filename (Makefile, .gitignore, .env.local), a bare name with a recognized extension (README.md, notes.rst, knip.jsonc, widget.lua, widget.dart), or a slash-qualified filename with an extension. Bare and qualified forms share one extension/basename vocabulary. Compound names such as store.events.test.ts stay whole. Word pairs (and/or, CI/CD, N/A), ratios (17/17), bare versions (v2.3.1), unqualified framework names (Next.js, Node.js), and entire URL spans including their query and fragment are excluded. A sentence-final trailing . is stripped. Extensionless directory names and unusual paths containing whitespace or non-ASCII characters are not inferred.
  • ref — #123 (a bare issue number) or ABC-123 (1–10 uppercase letters/digits starting with a letter, a dash, then ≥1 digit).

content.paths.in=<ref> / content.issues.in=<ref> mirror the general entity-grounding prop (§5.3's T.in=<ref>) exactly, narrowed to one recognizer kind, and report a dedicated diagnostic code, not-in-change-set — deliberately not the generic unknown-entity code, because the two situations mean different things: unknown-entity says a name doesn't match anything in the source of truth (a likely spelling/drift problem); not-in-change-set says a file or ref was never touched by the change at all (a fabrication problem). A caller bucketing diagnostics benefits from the codes staying distinct. It is a state finding (§5.3): an implementation cannot tell which path was meant, so it proposes no edit, not even a nearest match.

Path membership ignores leading ./. An exact member wins; otherwise a shortened suffix or basename MUST identify exactly one source path, at a / segment boundary. An absolute mention may include a workspace prefix before the source path. Multiple matches report not-in-change-set as ambiguous and require a fuller path; an invented relative directory is not discarded to force a basename match. These comparisons retain the source's comparison mode.

Both props are valid on a section or leaf, where they check that block's text, and on the document directive, where they check every line of the document except directive comments. The document form is RECOMMENDED for release notes, PR descriptions, and changelogs: a section-scoped check only guards its own section, so a claim written under another heading would escape it.

A missing change set fails loud, not silent-skip. Like every required source, a required reference to @change/paths/@change/refs that receives no change set at all means the check could not run (source-unavailable), whether the host supplies a sources capability or none: a claim about what changed is checked against a real change, or not at all. An optional reference (content.paths.in=@change/paths?) degrades to a no-op like any other optional source, exactly as it would for any other source name.

The same rule applies when the host supplies a change set with absent/null refs: required @change/refs reports source unavailability once, before checking individual #N references. Only an explicitly optional @change/refs? may skip it. An empty array is available data and rejects every mentioned ref. Hosts that formerly used [] to mean unknown should omit refs or write null; authored contracts require no syntax migration.

Reference implementation note. The reference implementation resolves @change/paths and @change/refs to plain internal logical names before they reach the grounding engine, rather than implementing a general @ns/name vocabulary-projection ref grammar; §10 tracks formalizing the general grammar. It supplies a change set from a command-line flag naming a revision range (computed via its own git-log/git-diff adapter) or from a pre-built JSON file; a library caller supplies a ChangeSet object directly. @changes itself (the un-projected record) is not yet bound to a consumable value; §10 tracks binding it as a records projection exposing path/status/from per row.

7.2 Vocabularies

A vocabulary is a named package of patterns, masks, sources, predicates, presets (named sets of props), and checks (§7.2.1). Official vocabularies are published with this spec; each has its own conformance level.

Reference implementation note. As of this draft, every @ name below is a fixed spelling that maps to a built-in rule (content.none=@placeholders is the built-in "no placeholder" check). Loading vocabularies as third-party-publishable data is future work.

A vocabulary preset is a named rule set that the vocabulary exports (§5.4). It expands and resolves precedence the same way a contract's own define does. A vocabulary check is different: it takes parameters, as §7.2.1 describes.

VocabularyExports
evidence@evidence/specific, @evidence/cited (predicates for items.all=)
safety@safety/secrets, @safety/pii (not yet implemented; content.none=/regex/ covers secrets today)
git@git/conventional (not yet implemented)
email@email/template-tags (not yet implemented)
changelog@changelog/version (not yet implemented)
tableschecks that read a table: @tables/total[=<column>] (+ .totalLabel, .tolerance), @tables/equation, @tables/returns, @tables/percent-of (§7.2.1; codes total-mismatch, column-equation-mismatch, return-chain-mismatch, cell-percent-mismatch)
listschecks that read a list: @lists/timeline (§7.2.1; code timeline-mismatch)
numberschecks that read figures anywhere in the prose: @numbers/percent, @numbers/sums, @numbers/interval (§7.2.1; codes percent-mismatch, sum-mismatch, interval-mismatch)
placeholders@placeholders (the current built-in placeholder list)
agentwhat an agent left in its output: @agent/cut-off, @agent/chatter, @agent/repair (Repair module, §9.1). A whole-document fence is the lint gate's (§8.6), whatever the contract says.

Unknown names fail. A value written as @namespace/name or a one-segment @name (bare, or an item of a […] list) is a vocabulary reference, never text. It MUST resolve to a vocabulary the implementation provides, or compilation fails: a name in an official namespace that nothing implements (every @git/…, @email/…, @changelog/… and @safety/… name today, a misspelt @evidence/…, or a one-segment name such as @placeholder) is unknown-vocabulary, and a name outside every official namespace is unknown-rule-set (§5.4). The same holds for an undeclared mask <token> (invalid-mask). To match the text @git/conventional itself, write a /regex/ or quote it: content.none="@git/conventional".

7.2.1 Checks: presets with parameters

A vocabulary MAY export a check: a named, parameterised check over the block's targets, with its own codes. Its parameters:

  • @ns/name=<v> binds the string args.value. The value MAY carry ;key=value segments, each binding a parameter the check declares (@tables/returns=Return;from=Jan|Feb;to=Q1 binds args.value, args.from, args.to); the first segment that names no declared parameter is args.value. A parameter MAY also be spelled as its own atom on the same directive, @ns/name.<param>=<v> (a quoted value is unquoted; \", \' and \\ escape). An absent parameter is null. A segment naming no declared parameter after args.value is set reports invalid-option at the directive, and the check does not run. A check that applies per element reports every failing element at the element's own line (§6.1). A problem with the arguments themselves reports invalid-option at the directive. A figure that isn't one (§6.3 num) skips that element: a check never guesses. Diagnostic severity is the code's default (§8).

8. Diagnostics — Core (§8.1/§8.5: Protocol module)

8.1 Shape of a diagnostic — Protocol module

Implementations MUST be able to emit diagnostics as JSON objects with these fields:

FieldRequiredContent
codeyesStable kebab-case code (§8.3).
severityyeserror or warn.
gateyeslint, shape, or state: the gate the code belongs to (§8.2); contract for a finding about the contract itself (principle 5); input for an input the run lacks (§8.2).
lineyes1-based line in the document; the contract line for compile errors.
columnno1-based column.
blockyesPath of the block: headings from the root, then the leaf kind and ordinal.
constraintyesThe prop as the author wrote it.
expectedwhen knownWhat the constraint compared against (a mask, a source ref, a limit).
messageyesOne sentence naming the value, the expectation, and the gap.

A check's result names the gate that failed (lint, shape, state), none (null), or could-not-run (§8.2), and carries that gate's diagnostics after any contract or input finding, each group ordered by line, then column, then code. The order is the implementation's, made once: a surface that renders the result MUST NOT sort or regroup it.

Location: the most specific element at fault. A diagnostic's line (and column, when given) SHOULD point at the most specific element to blame, not at the block's anchor line. When one table row is at fault — a cell that breaks a column constraint, the Total row a @tables/total column does not add up to — line is that row's own document line. When one list item is at fault — a failing item of a per-element prop (§6.1), the first unchecked box of items.all=checked, an item labels.eq does not list — line is that item's line. When a match inside prose is at fault (content.none), line and column point at the match. A failure no single element owns — a count bound such as rows.min/items.max, a missing column or item — stays at the block's anchor line, as does any failure whose element has no known position.

Reference implementation note. The reference implementation's JSON output additionally carries title, suggestion, ruleId, and fix fields on a diagnostic. These are additive and don't change what §8.1 requires.

8.2 Gates — Core

A check runs three gates in order and reports only the first that fails:

GateAsksFor example
lintCan the document be read as written?unclosed-fence, fenced-output (§8.6), a Markdown linter's findings
shapeDoes its form match the contract: sections, blocks, labels, items, and the form of their values?missing-section, unexpected-checklist-item, label-line-mismatch, value-wrong-form
stateDoes what it claims hold: is the work done, the evidence given, a value given, one the contract allows?tick-without-evidence, not-done, value-not-given, value-not-allowed

Every code belongs to exactly one gate, and a code's gate does not depend on the contract or the document. A rule that checks a value's form and then what it claims reports the two through separate codes. The test that places a code: if the honest fix changes a claim (a tick, a status, evidence, a value picked from a list, an item kept or dropped) or needs work or facts from the world, the code is state; if it only changes form, shape. An empty required section, a leftover placeholder, too many items, and a value outside a oneOf list are state; a missing section, an item the template does not list, and a value written in another form are shape.

The capture rule. Every slot a mask reads (a labeled line, a lead or first line, a heading, a repeated section's heading, a list item, a table cell, a section's content.mask, a front-matter value) is judged the same way, capture by capture. A capture holding a value of its type written another way, with ONE canonical spelling, is the shape finding value-wrong-form, whose fix names that spelling: an allowed value in another case (Done for done), or one of the token's declared loose forms (Table 8.2). A capture holding no value of its type is the state finding value-not-given: nothing, a placeholder, a word where a date belongs, a date that does not exist (2026-13-45), or text that names more than one value (07/02/2026 is July 2 or February 7). One outside its allowed set is the state finding value-not-allowed. When the mask's literal parts are not there (a missing label, a wrong separator) while every capture's value is in the text, the slot's own form code reports it (shape: label-line-mismatch, lead-mismatch, first-line-mismatch, heading-format, headings-mask-mismatch, item-mismatch, table-cell-invalid, pattern-mismatch); when a value is nowhere in the text, value-not-given. An empty slot (a labeled line with nothing after its label, an empty front-matter key, an empty table cell in a typed or required column) gives no value: value-not-given. A pattern (a regex) has no tokens to tell its literals from its values, so the whole pattern is the value's type: text that fails it holds no value of that type (value-not-given, state), on every surface (a section's content.pattern, a heading's, a front-matter value's, a column's, the title's). A slot whose value is a claim, with a form of its own, wants a mask.

The selection rule. A heading at a selector's level is one of its instances when it matches the mask, or when it holds a value for every capture, each of a closed type (a <date>, a <semver>, a |oneOf: value), with nothing but the mask's own literal words around them in any case or punctuation (1.2.0 - 2026-06-23 for [<semver>] - <date>, sprint 3 for Sprint <num>); the instance's own heading.mask then reads it by the capture rule (shape or state, as above). A heading with other words, or without the values (Release notes, Migration from 1.2.0 on 2026-07-02), is no instance: it is free text in an open scope. missing-repeat-instance means no heading reads as an instance at all. One selection rule serves every place a mask with alternatives picks the one to read a slot by: the alternative the slot matches, else the one whose literal parts and closed values the slot holds most of.

Table 8.2. Loose forms. A built-in token's other written forms, each with its canonical spelling (the token's own grammar). Any other text is no value of the type.

TokenWritten another wayCanonical
<date>year first with other separators or no padding (2026/6/23); day and month around a year when only one reading exists (23/06/2026, 05/05/2026); a month name (23 June 2026, June 23, 2026)2026-06-23
<semver>a leading v (v1.2.0)1.2.0
<issue>issue 12, Issue #12#12
<num>thousands grouped with commas, two groups or a decimal point (1,000,000, 1,000.5)1000000, 1000.5
<percent>a space before %, or the word (42 %, 42 percent)42%
<time>a 12-hour time (2:30 pm, 9:30am, 2 p.m.)14:30, 09:30, 14:00

1,000 names a thousand or one with three decimals, 1.2 no patch, jay no @handle: none has one reading, so each is no value of its type.

Placeholders. A template's slot left in place as a whole value is no value (value-not-given, and no reason for a disposition): angle brackets around a bare name of letters, digits, -, _ and spaces (<why>, <what changed>; never a URL, an address or a file name such as <https://x.dev>, <a@b.dev> or <notes.md>, which are values); on a value slot, square brackets around ONE word ([reason], [ALERT_ID]; a bracketed phrase such as [blocked upstream] is the author's own words); or one of the blank words standing alone, in any case: reason, why, xxx, tbd, todo. A heading is no value slot for the square-bracket form: Keep a Changelog's ## [Unreleased] is a real heading.

Each gate runs, then decides. A gate fails when it reports a finding at the run's threshold: an error by default, or any finding when the host asks warnings to fail the run. The result names the gate that failed and the findings it failed on; a host decides its exit status from that alone, never again from severities. An implementation MUST NOT run a gate's rules once an earlier gate failed: shape reads the document lint could read, a state rule reads the values the shape gate bound, and a document whose shape failed has none it can trust. The result carries only the failed gate's diagnostics. When every gate passes it carries every gate's warnings, gate by gate; an earlier gate's warnings are not reported beside a later gate's errors, and come back once that gate passes.

Lint. The lint gate runs first, on every document, whatever the contract declares — and on a document no contract governs, where it is the only gate that runs. It does not run when the check could not run (below): a broken contract or a missing input comes first. It runs a Markdown linter's rules and the checks of §8.6. Which linter rules run is the host's configuration alone. The reference implementation runs markdownlint, whose default set holds constructs that do not parse as what they were written to be, never style:

RuleWhy mediva cannot read it
MD005Sibling list items indented apart nest or split the list, so its items are not the ones written.
MD056A table row with the wrong number of cells loses or shifts cells, so a column's values are not the ones written.
MD046 { style: "fenced" }An indented code block reads as prose to mediva and as code to a renderer; only a fenced block reads as code to both.

MD018 (##Summary, no space after the hashes) is not in it: markdownlint finds it with a bare line pattern that also matches prose opening with an issue reference (#1343 ruled …) or #include, and its fix would turn that prose into a heading. A section written that way is still found: a required section reads as missing (missing-section, shape), while an optional one is not reported unless the host switches MD018 on.

The host's linter configuration (the reference implementation: config lint, markdownlint's own configuration object) replaces the default set whole. Each rule in it is on (true, or its options) or off (false); a severity ("warning", "error", { severity }) is a configuration error, because no lint finding is a warning, and so is a key naming one of §8.6's checks, which always run. A document's own linter comments (<!-- markdownlint-disable -->, <!-- markdownlint-configure-file … -->) switch nothing off or on. A lint finding is always an error.

Could not run. A contract error found while checking (principle 5), or an input the contract needs and the run lacks (a title it checks, a context value it reads, issue data, a source of truth it grounds a check in; code gate input), belongs to no gate: the check could not run (§8.5), and the result names could-not-run. Whether a check can run never depends on the document: an implementation asks every rule application what input it needs (from its options and the run's own input alone), and checks every source the contract reads (its rules' declarations and the refs in its masks, front … in= and placement clauses) against the sources the run supplied, before the lint gate runs, on every surface (a command line, a library call, an editor). A missing title is could-not-run whether the document's form is right or not. An input the contract marks optional (key?, ref?) the run lacks is a note, or nothing, and the rule that reads it skips. A contract or input warning is a note (an unused fragment, an optional context value not passed, a document no contract governs): it is reported, and decides nothing.

State checks run only in the state gate. Every check that judges what the document claims (a rule's state half, a state rule, a structure count, a requiredWhen absence) runs only once the shape gate passed; a run whose shape failed never runs one. A state finding reported during the shape pass is the contract error undeclared-code: the property holds by construction, never by holding back findings that were already computed.

Levels. Only a shape or state finding has a level anyone sets: a host's severity configuration, a contract's warn=/error=/off= (§3.3) and document maxSeverity reach those and no other. A lint finding is always an error; a contract or input finding says whether the check could run, which no setting changes. A host rejects a setting that names a lint, contract or input code, and a contract's override naming one is the contract error unlevelable-override: the check could not run. An override whose target names nothing is the contract error unknown-override-target, like an unknown rule: the check could not run. off= naming a rule turns the rule off: nothing it would report remains, whatever a host sets.

Precedence. A finding's level is resolved once, when it is reported, in this order: the catalog's level (or the rule's own), then the contract's warn=/error=/off= naming its code or its rule, then the host's configuration (a setting naming the code, else the rule), then the cap (a heuristic code, and document maxSeverity=warn, never report an error). The host's setting wins over the contract's: a contract's off=<code> is a level the host can raise again. Only off= naming a rule is final: whether a rule runs is the contract's to say.

Each check reports its own gate. A rule declares the codes it reports, all of one gate (a state check's apart); a code it does not declare is a contract error (undeclared-code). No rule reports into the lint gate. A contract's own text never crosses gates: a field's help= replaces the help of that field's state findings only, and a code=/message=/suggestion= replaces the host rule's primary finding only (the first code it declares: a presence directive's absence finding missing-section or missing-frontmatter-key, a mask's form finding), which keeps its gate and its level; the rule's other findings keep their own codes and texts.

Readout. When the shape gate passes, the result also carries a readout of what the document claims, read from the same binding the shape gate made, so a caller compares state without parsing Markdown or messages. It gives the title the run was given (when the contract reads one) and each declared front-matter key's line and value (null when unwritten). For each bound block — each instance of a repeated section on its own — it gives the block path (§8.1), its kind, whether it is present and empty, and, for a block written in its empty-state marker's form (§3.5), disposition: "na" and the reason it gives (null when it gives none); a list block adds its items, each with its label (the template label it bound to), line, ticked, evidence (every nested note, whole), disposition (done, claimed — ticked without the evidence done needs, not-done, na, or open; §4) and the reason a not-done or na item gives — a plain item (no box) gives its text as label and null for the rest; a line block adds each labeled line's label, line, value, parts (the value each mask token captured), disposition (given or not-done) and reason; a table block adds its rows, each with its line and its cells by column header. A repeated instance's block path is its own matched headings, so its list never shares its section's path. A template's own slot (<why>, [reason]) or a stub (TODO) is never a reason or a value: it is a state finding. Prose is not read: what a paragraph claims has no structure to bind. A result whose shape gate failed has no readout.

Binding never judges a claim. A checklist item keeps the label it binds to whatever disposition it writes after it, well-formed or not: (N/A), (N/A: cli) and (not done) are shed like (N/A: no UI in this change). That a disposition gives no reason, too short a one, or a placeholder for one is the state finding open-item, never a reason to rename the item. One reason predicate serves every disposition ((N/A: …), a declared marker's {reason}, (not done: …), a line's not done: …): a reason has a letter in it, meets its floor (two words for N/A and a declared marker unless it sets another, one for not done), and is no template slot or stub.

8.3 Codes — Core

Structural codes are fixed: missing-section, missing-block, unmatched-construct, out-of-order, unexpected-section, too-few-repeats, too-many-occurrences. A strict boundary reports what crossed it: unexpected-section for a child section, unexpected-checklist-item for a list item, table-extra-columns for a column, and unexpected-frontmatter-key for a front-matter key. A prop that the spelling table maps to a named rule keeps that rule's code (too-few-words, unknown-entity, not-in-change-set, tick-without-evidence, weak-tick-evidence, open-item, not-done, contradictory-disposition, …; a closed catalog, because codes are the interface agents branch on). Of the checklist disposition codes (§5.3), tick-without-evidence, open-item, not-done, and contradictory-disposition are errors by default and weak-tick-evidence is an advisory warning.

A code has one name. A retired code is an unknown name wherever an author writes one (a severity override, host configuration), never an alias for its replacement.

Value codes follow the capture rule (§8.2). State: value-not-given, a slot holding no value of its type (an empty slot included: a required front-matter key, a labeled line or a table cell with nothing in it); value-not-allowed, a value outside its allowed set (a mask token's oneOf list, a column's in=, a front-matter key's in=[…]). Shape: value-wrong-form, a value of its type written another way with one canonical spelling; missing-frontmatter-key, a required key not written at all; checkbox-missing, a plain item in a list whose items record dispositions. Of the structural codes, missing-block, too-few-repeats, missing-repeat-instance, exactly-one-group-unmet, too-many-occurrences, too-many-items, too-many-rows, forbidden-section-present and missing-conditional-section are state findings: what fills a block, how many items or instances there are, which of a group applies, and a section a front-matter value forbids or requires are what the document claims. code-lang-not-allowed is state too: the honest fix is an example in an allowed language, never a relabelled fence.

Draft 3 adds these contract codes. §3.1.1 adds section-without-heading and ambiguous-closer (a warning). §5.4 adds invalid-rule-set, unknown-rule-set, duplicate-rule-set, rule-set-conflict, rule-set-cycle, and rule-set-unused (a warning). §9.2 adds include-not-found, include-cycle, include-anchor-missing, include-anchor-ambiguous, include-level-mismatch, include-duplicate-section, and include-conflict. For a compile error inside an included or extended file, line (§8.1) is the line in that file, and the diagnostic SHOULD name the file.

8.4 One cause, one diagnostic — Core

An implementation MUST NOT report the same failure twice for one block instance, and SHOULD NOT report failures that follow from another reported failure (a missing section's children, a renamed column's cells). A document wrapped whole in one code fence (fenced-output, §8.6) is one such cause: the fence hides every heading, list, and table, and the lint gate it fails stops the run before any structural finding.

A section written at the wrong level is another (§3.1.1 Binding). Against this contract:

<!-- mdv: section required -->
## Summary

the document ### Summary on line 3 gets one diagnostic, the section's presence one, at that heading and carrying the fix that sets its level:

3:1  error  Required section "Summary" needs ##; found ### here  missing-section

It does not also get heading-wrong-level for that heading, nor a missing-section at line 1. Turning heading-wrong-level off therefore never waives a required section; turning the presence code off does.

8.6 Checks no atom asks for — Core

An implementation runs these without an atom asking, because without them it cannot read the document against its contract. They are not a linter's rules (principle 6): each names something that would make the contract's blocks read as missing or wrong. unclosed-fence, unclosed-comment, fenced-output and frontmatter-unreadable are the lint gate's errors (§8.2): nothing after an unclosed fence or comment, nothing inside a fence that holds the document, and no key of front matter that does not read, can be read. None is configurable, and none can be lowered. The others are shape warnings, whose level a host's configuration or a contract sets like any code's (error=, warn=, off=; §3.3), on the document directive or, for a section's codes, on the section.

CodeRunsReports
unclosed-fenceon every documentA code fence opened and never closed, at its opening line. Everything after it reads as code, so every block below it would read as missing. A markdown example whose content carries its own fences is one: the inner bare fence closes the example and the example's own closer opens a fence; it is reported there, with the fix that gives the example's fences one more character than the fences inside it.
unclosed-commenton every documentAn HTML comment opened (<!--) and never closed (-->), at its opening line. It runs to the end of the document, so every block after it would read as missing. It has no fix: where the comment ends is the author's to say.
fenced-outputon every documentA document wrapped whole in one code fence (only blank lines outside it, a markdown wrapper whose content carries its own fences, or a fence opened on its first line and never closed), at the opening fence, with the unwrap of R.1 (§9.1) as its fix when the pair is closed and the info string is empty, md, or markdown. Also a document whose body is one markdown or md fence, read lexically as for a wrapper and holding a heading, with at most one paragraph before it ("Sure! Here is the description:") and at most one after it ("Let me know if you want changes."); its fix deletes the fence's opening and closing lines and keeps both paragraphs. An unlabelled fence there is code, never a wrapper.
heading-wrong-levelfor a sectionA heading with a section's text, in that section's scope, at another level than the contract's anchor heading. It does not bind the section (§3.1.1 Binding). A required or recommended section reports it through its presence diagnostic instead (§8.4).
duplicate-headingfor a sectionTwo headings in one parent's span that both match a contract section (§3.1.1 Binding), once.
frontmatter-unreadableon every documentFront matter mediva cannot read: a --- block never closed, one below the first line (under a comment or blank line), a key written twice, or a top-level line that is not key: value YAML. A lint error, at its line: no front-matter key is checked until it reads.

What is already reported is not reported again (§8.4): the fence of a whole-document wrapper is fenced-output, not unclosed-fence; a heading at the wrong level that a required or recommended section's presence diagnostic already reports; a placeholder in a slot whose block has its own placeholder rule (content.none=@placeholders, heading.none) is that rule's finding, not also value-not-given.

8.5 The repair-loop contract — Protocol module

A conforming Protocol implementation exposes at least one operation that:

  • checks, taking a contract and a document and producing the diagnostics of §8.1, with no side effect on either input (an idempotent check: running it twice on the same inputs MUST produce the same diagnostics, in the same order);
  • reports a stable exit status distinguishing three outcomes: the check ran and found nothing to report, the check ran and found diagnostics, and the check could not run at all (a usage or configuration error, including a required source that was never supplied — §7.1's change-set case — and a contract with errors, §2.5). An implementation SHOULD use three distinct exit codes for these outcomes (the reference implementation uses 0, 1, and 2 respectively).

This is what makes a repair loop possible for a caller that never inspects an implementation's internals: diagnostic in, edit the document, re-check, repeat until the check outcome is "found nothing to report" or a caller-chosen iteration limit is reached. The loop's correctness depends only on §2's determinism, §8.1's stable codes and ordering, and this section's idempotence — never on any detail of how a particular implementation is invoked.

9. extends — Core

extends=<ref> on document composes a contract from another: the child's blocks are applied to the parent's resolved block tree, and the parent's other blocks are kept, in order. It is core: every surface of an implementation (library, command line, editor) MUST apply it identically. A contract has at most one parent, a chain of parents is at most 8 deep, and a cycle is a compile error, invalid-extends.

Block keys. id=<id> on a block directive (section, list, table, code, line, media) gives the block a key. An id matches [a-z][a-z0-9-]*; any other form is a compile error, invalid-id. Ids are unique within the resolved contract, after every include (§9.2) and extends is applied; two blocks with one id is a compile error, duplicate-id. A block without id= is keyed by its heading text, compared case-insensitively with whitespace collapsed, together with its heading level. id= plays no part in how a block binds to a document.

Overrides. Only the child's top-level blocks are matched against the parent; a block nested in one of them belongs to its extent.

  • A child block marked override replaces a parent block. When the override has an id, its target is the parent block with the same id; if no parent block has it, the override is a compile error, override-target-missing, even when a parent block has the same heading, and SHOULD suggest the closest parent id. An override without an id targets the one parent block with the same heading text and level, at any depth, and the implementation reports a warning, implicit-override-key: when that parent block has an id, the warning names it as the key to target; when it has none, it names the parent's file as the place to add one. The replacing block then carries the parent block's id, so a contract further down the chain can target it by that id. An override without an id that matches no parent block is override-target-missing, and SHOULD suggest the closest parent id or heading. An override whose heading matches more than one parent block, or two overrides of one block, is a compile error, override-target-ambiguous. An override on a nested block is override-target-missing.
  • The replacement is wholesale: the parent block's whole extent, nested blocks included, is removed, and the child's block, with its own extent, takes its position.
  • A child block without override whose id, or heading text and level, a parent block already has is a compile error, override-required.
  • A child block with a new key is added after the parent's blocks, in the child's order. Under ordered it therefore comes after every inherited block.
  • override in a contract without extends= is a compile error, override-without-extends.

The implicit-key warning concerns extends only: a contract that extends nothing never reports it.

A parent in another folder passes on every block and atom except a relative source ref (§7.1): a ref resolves against the directory of the contract being checked, so the parent's ./team.json would name a different file. A retained parent block that carries one is a compile error, invalid-extends; the child can replace that block, or keep both contracts in one folder.

A block's extent, which is what a replacement removes, is its scope under §3.1.1. A section's identity is its heading text. When a section binds to a document, its identity is its heading text, its heading level and its place in the contract's block tree (§3.1.1 Binding). include (§9.2) is the complement of extends: extends inherits a whole contract and replaces sections, while include inserts shared sections and never replaces one.

9.1 Repair — Repair module (normative shape-defect repairs)

A conforming implementation MAY offer three normative repairs: any two conforming implementations, given the same document, MUST produce byte-identical output. R.1 repairs the lint gate's fenced-output (§8.6), which every document gets; a contract opts into R.2 and R.3, the chat preamble and postamble, with content.none=@agent/repair.

A repair is defined ONLY for a document's SHAPE. It never inspects, infers, or rewrites content; a defect whose correction would require judgment about what the document actually says (including a whole-document fence naming a real language other than md/markdown) is flagged, not repaired.

R.1 Whole-document fence wrapper. After trimming leading and trailing blank lines, if the entire remaining document is one fenced code block, closed, whose info string — the fence's language token, compared case-insensitively — is empty, md, or markdown: replace the whole document with the fence's own content, blank-trimmed the same way. A fence naming any other language, or anything other than blank lines outside the fence, is out of scope for this repair, except at most one paragraph before and at most one paragraph after (at least one of the two) a fence whose info string is md or markdown and which holds an ATX heading, its closing line read lexically as for a wrapper (§8.6 fenced-output): delete the fence's opening and closing lines and keep both paragraphs, which only R.2 (preamble) and R.3 (postamble) may remove.

R.2 Chat preamble. Let S be the document's first structural line: an ATX heading, a list item, a table row, or a front-matter delimiter. If S exists and is not the document's first line, and EVERY non-blank line strictly before S matches one of the implementation's published preamble patterns, delete every line before S. If any such line fails to match, no repair is made.

R.3 Chat postamble. Let C be the document's last line that is part of its recognized structure. If every non-blank line strictly after C matches one of the implementation's published postamble patterns, and at least one such line exists, delete every line after C. If the last non-blank line does not match, or removing the trailing run would leave no line before it, no repair is made.

R.4 Composition. One repair is applied per pass, repeated to convergence (a defect a repair exposes is corrected in a later pass). In each pass R.1 comes first whenever it applies, since its defect is the lint gate's fenced-output and a lint finding stops the check before any shape repair; otherwise R.3 (postamble), then R.2 (preamble). Applying an implementation's repairs to an already-repaired document MUST be a no-op. A repair keeps the document's final line break: a document that ended with one still does.

R.5 Pattern publication. An implementation that offers R.2/R.3 MUST publish its exact pattern lists, semantically versioned, and MUST NOT alter a published list's matched set without a version change.

Reference implementation note. The reference implementation's preamble/postamble pattern lists (versioned independently of the package's own version number) and detection details — which line counts as "structural", the exact regular expressions — live alongside its source; see the design record this section was drafted from for the full closed lists and the idempotence argument. A Core-only or Grounding-only implementation MAY compile content.none=@agent/repair as an ordinary reference to a vocabulary name it doesn't ship (§0): the directive is well-formed, it simply never triggers a repair.

9.2 include — Core

(Design record: SYNTAX-SIMPLE.md, item B3; issue #1434.)

<!-- mdv: include ./shared/checklist.mdv.md --> inserts the blocks of another contract at the directive's position. <!-- mdv: include ./shared/checklist.mdv.md#definition-of-done --> inserts only the section whose heading slug is definition-of-done, with everything nested in it. The included file is an ordinary contract, so it can be checked on its own as well.

Resolution.

  • The path resolves against the directory of the file that holds the directive. A nested include therefore resolves against its own file's directory, not the top-level contract's. ../ segments are allowed, so any file in the project can be included, but the resolved path MUST stay inside the project root. The project root is the directory the host names as the project (for example, the directory holding its configuration file); failing that, the root of the version-control checkout that holds the top-level contract; failing that, the top-level contract's directory. A path that escapes the root, or a file that is missing or unreadable, is a compile error, include-not-found.
  • Relative refs inside included atoms, such as sources (§7.1) or a nested include, resolve against the included file's own directory. §7.1's no-escape rule applies from that directory.
  • The included file is resolved on its own first. Its own includes and its extends are applied, and its scopes (§3.1.1) are computed within that file alone. A closerless block never reaches across a file boundary, in either direction.

Selection. Without an anchor, every top-level block of the included file is inserted, in order. With #<anchor>, the one section whose heading slug equals the anchor is inserted, at any depth, together with its subtree. The slug is the heading text lowercased, with every character except letters, digits, spaces, and - removed, and spaces replaced by -. That is the usual Markdown heading-anchor convention. If no section matches, the compile error is include-anchor-missing. If more than one matches, it is include-anchor-ambiguous. An anchor selects by heading text only; the name= meta atom plays no part.

What comes along.

  • The inserted blocks, with all their atoms.
  • Every define in the included file and in the files it includes (§5.4).
  • From the included file's document directive, only its empty-state declarations (empty.<name>=, §3.5). They resolve after the including contract's own document directive. If two included files declare the same empty state differently, the compile error is include-conflict. The including contract's own declaration always wins without a conflict.

The included file's document-wide constraints, its front matter, and its Markdown outside the inserted blocks are not included. There is no way to import them: a contract that wants another contract's document-wide constraints uses extends.

Placement. The inserted sections behave as if they were written at the directive's position, each with an explicit closer at the end of its inserted content. For scoping (§3.1.1), each inserted top-level section's directive stands at the include, in order. Before each one, close-down runs with that section's level, so an inserted ## section ends an open closerless ## section, exactly as a pasted one would. If an inserted section would land inside a section with an explicit closer whose level is not higher than its own, the compile error is include-level-mismatch. An inserted section MUST be deeper than the section that contains it. An include never re-levels headings: an inserted section keeps its own heading level, and a level that does not fit is always include-level-mismatch, never an adjustment.

Identity. include inserts; it never replaces. An inserted section whose heading text matches a sibling section's is a compile error, include-duplicate-section. To replace a section, use extends.

Cycles. A file that includes itself, directly or through other files, is a compile error, include-cycle, and the message names the chain. Including the same file more than once, at different positions or with different anchors, is allowed, but ids stay unique in the resolved contract (§9): including a block that carries id= twice is a compile error, duplicate-id. An implementation MAY cap include depth at 8 levels or more, and reports the cap as include-cycle.

Interaction with extends. In every file of an extends chain, the file's includes are expanded before its extends is applied. The resolved contract is therefore the parent, resolved, merged with the child after the child's includes are expanded. This has two consequences. A child can override a section its parent included; the replacement takes the included section's position, but it is the child's own block, not included content. And a section a child includes is one of the child's blocks: a key that collides with a parent block is override-required unless the included block carries override.

extends (§9)include (§9.2)
Written asdocument extends=<ref> (one per contract)include <ref>[#anchor] (any number)
Brings ina whole parent contractselected sections and all definitions
Same heading as an existing sectionthe child's override block replaces the parent's; without override, compile error (override-required)compile error (include-duplicate-section)
Document-wide constraintsinheritednot included

Example: before and after. Draft 2 copies the checklist into every contract:

<!-- mdv: section required content.words.min=20 -->
## Summary
<!-- mdv: endsection -->

<!-- mdv: section required -->
## Definition of Done

<!-- mdv: list required items.min=1 items.all=done-or-na -->
- [ ] Tests pass
- [ ] Docs updated
<!-- mdv: endlist -->
<!-- mdv: endsection -->

<!-- mdv: section required -->
## Decision
<!-- mdv: endsection -->

With include, the checklist lives once in docs/shared/release.mdv.md:

<!-- mdv: define checklist required items.min=1 items.all=done-or-na -->

<!-- mdv: section required -->
## Definition of Done

<!-- mdv: list use=checklist -->
- [ ] Tests pass
- [ ] Docs updated

<!-- mdv: section recommended -->
## Rollback plan

and each contract that needs it, such as docs/rfcs/RFC.mdv.md, writes:

<!-- mdv: section required content.words.min=20 -->
## Summary

<!-- mdv: include ../shared/release.mdv.md#definition-of-done -->

<!-- mdv: section required -->
## Decision

The result is the same block tree as the draft 2 contract. The inserted ## Definition of Done ends Summary's scope, @rel/checklist is imported with it, and Rollback plan is not inserted because the anchor selects only one section.

10. Open questions

  1. Honest N/A — decided in Phase 3: core, as the na flag (§3.3); its wording is the contract author's, through empty states (§3.5).
  2. Default codes — decided in Phase 3: named rules keep their codes (§8.3).
  3. Vocabulary versioning and how a contract pins one.
  4. Whether content.entities.in (named things) belongs in core given its imprecision, or only in a vocabulary.
  5. Preset invocation: presets are written @vocab/name or @vocab/name=<arg> as an atom (@tables/total=Amount). Confirm this doesn't collide with @ref values.
  6. Capitalized-name and route recognition (content.entities.in) as a claim kind, so a names target has one normative recognizer (§4.1).
  7. A per-element pass hook (two rules that walk passing elements twice: once for the predicate, once for a second check such as mask-capture membership).
  8. Which of the remaining imperatively-decided rules get a generic primitive, which become vocabularies, which are dropped.
  9. Formalize the @ns/name vocabulary-projection ref grammar (§7.1) as the literal spelling every vocabulary projection uses, widening the logical-name matching it requires — or formally bless "a vocabulary projection resolves to an implementation-defined logical name" as the permanent convention instead.
  10. Give ref a real ClaimKind slot and decide precedence against id's existing ABC-123- shape claim (§4.1/§7.1), once real usage shows which one a dashed-alphanumeric token usually means. Consider a contract-declared tracker-prefix vocabulary (@tracker/prefixes=[...]) to narrow ref's ambiguity against a versioned proper noun (COVID-19).
  11. Decide whether @changes (the un-projected record) should bind as a records projection exposing status/from per row, for a rule that wants to distinguish added from modified from deleted (§7.1).
  12. Should the default grounded-kind set (amount, date, duration, percent, id) be spec-normative, or left implementation-defined the way number's exclusion already is (§4.1)?
  13. Should the duration unit/number-word closed lists (§4.1) be part of the spec text itself (byte- identical across implementations), or merely "MUST recognize ISO 8601 duration strings and SHOULD recognize common English phrasing" with the exact word list left to a conformance fixture? This draft assumes the former.
  14. Should §7.1's source-matching table gain a general "closed suffix key inference" clause for duration/percent, or should each kind's suffix list stay spelled out kind-by-kind?
  15. Seconds (PT{n}S) and sub-day ISO components below minutes are canonicalizable but have no natural-language recognizer (§4.1) — worth closing, or out of scope for support-reply prose?
  16. Repair (§9.1): table-row and front-matter structural-line detection is line-based, not block-based; the preamble anchor set (heading/list/table/front-matter) and the postamble anchor set (any non-blank content) are asymmetric by design, but read as inconsistent on first glance; nested/differently-languaged fences are not specially handled; the published pattern lists are intentionally small and conservative for v1 and are expected to grow under R.5's versioning rule.
  17. Should the module boundary in §0 be exactly {Core, Grounding, Repair, Protocol}, or does a conformance suite pass over real implementations suggest a different split (for example, hiving off Item.disposition into its own module after all, or merging Repair into Grounding)?
  18. Optional closers, named rule sets, and include — decided in draft 3 (§3.1.1, §5.4, §9.2). The decisions are recorded in SYNTAX-SIMPLE.md, under "Open questions". Rule sets with parameters, and rule packs loaded from packages, remain future work there.

Appendix A. Spelling table

Non-normative. The spellings earlier drafts used (Old), beside the one this draft defines (Current). Only the current spelling compiles (§3.4); the old one is an unknown name. This table is for rewriting an old contract by hand. = means the spelling did not change. — in the Old column marks an atom with no earlier spelling.

A. Presence and structure

OldCurrentNotes
required=Present and non-empty.
presentrequired allowEmptyPresent; may be empty.
recommended=Its own code (missing-recommended-section) and no empty check.
optional=The default presence; next to use= it switches a set's presence off.
requiredOrNArequired na
allowEmpty=With required: may be empty.
ordered (section)=Child order only.
numbered (section, document)orderednumbered is list numbering; a section's child order is ordered.
ordered (list)numbered
only, noExtraSections, noExtraCols, front noExtraKeysstrictOn the block whose boundary closes.
exactLabelslabels.eq=@templatestrict on the list says the same.

A. Prose

OldCurrent
minWords / maxWordscontent.words.min / content.words.max
maxLinkscontent.links.max
noPlaceholdercontent.none=@placeholders
pattern=/re/content.pattern=/re/
pattern.mask / .anyOfcontent.mask="…" / content.mask=[…]
lead.maskcontent.lead.mask
forbid=… (regex, phrase, or list)content.none=…
forbid.maskcontent.none.mask="…"
forbid.lexiconcontent.none.phrases=<ref>
forbid.scanFences / .scanCommentscontent.none.scan.fences / content.none.scan.comments
forbid.maxHitscontent.none.max=N
forbid.excludePlaceholdercontent.none.skipPlaceholders
contains=<kind>content.blocks.has=<kind>
distinctFrom="Heading"content.unlike="Heading"
sentence.all, .target, .gate, .gate.direction, .scope, .nodescontent.sentences.all, .target, .gate, .gate.direction, .scope, .nodes

A. Grounding

OldCurrent
entitiesIncontent.entities.in
groundedIncontent.claims.in (every claim kind)
groundedIn.kinds=[amount, date]one target per kind: content.amounts.in / content.numbers.in / content.dates.in / content.durations.in / content.percents.in / content.ids.in / content.urls.in
verbatim matches=<ref>content.eq=<ref>
quotesMatchcontent.quotes.in
citations.resolvecontent.refs.all=defined
mentions.shape / mentions.incontent.mentions.mask / content.mentions.in
path.in / ref.incontent.paths.in / content.issues.in
lexicon, lexicon.undefined@glossary/terms=<ref>, @glossary/undefined=<ref>
maxAgecontent.dates.age.max

A. Headings and title

OldCurrent
heading.pattern / heading.mask=
—heading.raw.mask="…": heading.mask on the heading as written
heading.noPlaceholderheading.none=@placeholders
title= (document flag: a title must be supplied)
title.pattern=
title.noPlaceholdertitle.none=@placeholders
title.minWordstitle.words.min
firstLine.mask / .anyOfcontent.firstLine.mask="…" / content.firstLine.mask=[…] on the document
headings.level, .select.mask, .order, .order.compare, .order.extract.mask=
headings.mask / .anyOfheadings.mask="…" / headings.mask=[…], on the heading as a section binds it
—headings.raw.mask="…" / headings.raw.mask=[…]: headings.mask on each heading as written

A. Lists

OldCurrent
taskList, choice, evidenceList (+ closers)list / endlist
minItems / maxItemsitems.min / items.max
minChecked / maxCheckedchecked.min / checked.max
oneCheckedchecked.eq=1
allCheckeditems.all=checked
allDisposeditems.all=done-or-na (§4.2)
noneClaimeditems.none=bare-tick: no tick without evidence; open items pass (codes tick-without-evidence / weak-tick-evidence)
requiredOrNA media/N/A text (hard-coded)empty=@empty/<name> + document empty.<name>="…" (§3.5)
each.minWordsitems.words.min
each.mask / .anyOfitems.mask="…" / items.mask=[…]
each.initems.in
each.falsifiableitems.all=@evidence/specific
each.sourceditems.all=@evidence/cited
each.definition / .delimiteritems.all=@glossary/definition / items.all.delimiter=…
covers / covers.initems.covers / items.covers.in
timelineSums@lists/timeline (a lists check, §7.2.1)

A. Tables and code

OldCurrent
minRows / maxRowsrows.min / rows.max
cols=[…]columns.has=[…]
col.<Name>.<op>rows.<Name>.<op>, column flags too: rows.Owner.required, rows.ID.unique
col.<Name>.matches=/re/rows.<Name>.pattern=/re/, the column's regex, read like content.pattern=
rows.in=
crossFoot (+ .totalLabel, .tolerance)@tables/total or @tables/total=<column> (+ .totalLabel, .tolerance), a tables check (§7.2.1)
columnEquation, returnChain, cellPercent@tables/equation, @tables/returns, @tables/percent-of, tables checks (§7.2.1)
percent, sumToTotal, ciContains@numbers/percent, @numbers/sums, @numbers/interval, numbers checks (§7.2.1)
lang=ts / lang.oneOf=[…]lang.eq=ts / lang.in=[…]

A. Document hygiene, integrations, meta

OldCurrent
noTruncationcontent.none=@agent/cut-off
noLLMResiduecontent.none=@agent/chatter
noFenceWrapper— (a whole-document fence is fenced-output on every document, §8.6)
—content.none=@agent/repair (Repair module, §9.1)
pairedDisposition.*, document.placement.*@markers/disposition.*, @markers/placement.*
scan.maskcontent.scan="…"
issueKeyword / issueStateissue.keyword=[…] / issue.state=…
containsContextcontext.has=<key>
requiredSet / .when@sets/required=<ref> / .when
noExemplarResidue= (contract self-check)
name, desc, help, match.heading, maxSeverity, code=, error=/warn=/off=, occurs==

A. New atoms

AtomMeaning
items.all=done-or-naCore checklist completion gate (§4.2, §5.3). Code tick-without-evidence (error) / weak-tick-evidence (advisory) / open-item.
content.paths.in=<ref> / content.issues.in=<ref>Grounding module: ground path/ref claims against a change set or any other source (§7.1). Code not-in-change-set.
content.amounts.in=<ref>, content.numbers.in, content.dates.in, content.durations.in, content.percents.in, content.ids.in, content.urls.inGrounding module: ground one claim kind (§4.1). Code ungrounded.
content.none=@agent/repairRepair module: opt into R.1–R.4 (§9.1).
<!-- mdv: define name … -->, use=nameDefine and use a named rule set (§5.4).
<!-- mdv: include ./file.mdv.md[#anchor] -->Insert shared sections (§9.2).
endsection, endlist, …Optional (§3.1.1). A redundant closer can be deleted.

Appendix B. Draft 2 changelog

Non-normative.

Draft 1 covered the core chapters (data model, directive grammar, targets, props, expressions, sources, vocabularies, diagnostics, and the spelling table). Draft 2 turns mediva into a modular standard: a small mandatory Core plus optional modules, so an implementation can state exactly what it conforms to. It also promotes four features from "implemented but not yet ratified" to normative text, verified against the reference implementation and its conformance suite rather than against earlier design-note prose where the two disagreed:

  • Dispositions (§4, §5): Item.disposition/Item.evidence and items.all=disposed, folded into Core (the feature is small and applies to every list, not only a "grounding" concern).
  • Claim kinds (§4.1): the duration and percent kinds, and the full normative recognition/canonicalization/equivalence table for every kind, in the Grounding module.
  • Change sets (§7.1): @changes, the @change/paths/@change/refs projections, the path and ref entity kinds, and the not-in-change-set diagnostic, in the Grounding module.
  • Repair (§9.1): the three normative shape repairs (R.1–R.4) and pattern publication (R.5), in the Repair module.

Draft 2 also adds a machine-readable Protocol module (§8.1, §8.5: JSON diagnostics, exit codes, the idempotent-check and repair-loop contracts), and moves every implementation-specific detail (a particular engine, a command-line flag, a package name) out of normative text and into clearly marked, non-normative "Reference implementation note" boxes. Normative text names concepts, never tools: a host is whatever runs an implementation and supplies data an implementation cannot produce itself (the current time, a document's title, a change set); an implementation MUST treat host-supplied data as an opaque input, never assume how it was produced.

Section numbers are unchanged from draft 1 wherever content did not move, so an existing reference to §2.5, §6.5, or §8.1 still resolves to the same material. New material was placed in a new §0 (so nothing after it had to shift), in the numbered sub-items of existing sections (§4's Item shape, §5's new prop, §7.1's new source rows, §8's new §8.5), or in a section that was always a stub in draft 1 (§9.1 Repair). No section was renumbered.

Appendix C. Draft 3 changelog

Non-normative.

Draft 3 makes contracts shorter to write without changing what any draft 2 contract means. Every draft 2 contract compiles to the same blocks under draft 3.

  • Optional closers (§3.1.1): a section's scope follows heading levels, and a leaf owns the next construct of its family. Explicit closers stay valid and make a region opaque. New codes: section-without-heading (error) and ambiguous-closer (warning). A setext heading ends scopes like an ATX heading. The mechanical rewrite (§3.4) removes redundant closers.

  • Entity checks once per document (§3.2): content.entities.in and an implementation's own entity checks (the reference implementation's match/* rules) may sit on the document directive, where they read all the document's prose and report each finding at its own line. content.entities.in now reports an unknown name at the line where it first appears in a section too; it had reported every name at the section's first line. (Issue #1449.)

  • Title lead (§3.1.1 "Lead"): with exactly one H1 in the contract, a closerless section on it covers only the H1's lead, up to the next heading of any level, instead of every ## section after it. In a document with one H1 that section's span is the lead too. An explicit closer still wins, and a contract or document with two or more H1s keeps the level rule. A closer right after the H1's lead is now redundant, and the rewrite removes it. A strict or ordered on such a section checks nothing and warns flag-on-title-lead. (Issue #1450.)

  • Named rule sets (§5.4): define name <atoms…> declares a fixed bundle of atoms, and use=name on a directive expands it. Atoms written at the use site win, and optional switches a set's required off. Official vocabulary namespaces are reserved. New codes: invalid-rule-set, unknown-rule-set, duplicate-rule-set, reserved-rule-set-name, rule-set-conflict, rule-set-cycle, and rule-set-unused (warning).

  • include (§9.2): inserts another contract's blocks, or one section selected by #slug, from any file inside the project root. Includes expand before extends merges. New codes: include-not-found, include-cycle, include-anchor-missing, include-anchor-ambiguous, include-level-mismatch, include-duplicate-section, and include-conflict.

  • Line numbers (§8.3): a compile error inside an included or extended file reports the line in that file.

  • Split evidence (§4.2): a remainder after a label separator counts as evidence through a concrete signal (number, code span, URL) at any length, the same test an unsplit line gets. Only the verb route keeps the 4-word floor. Tests added: added `x.test.ts` was claimed and is now done.

  • Alignment with the reference implementation. A cheat sheet that runs every pattern through the reference CLI found places where the implementation and this text disagreed. Where the implementation was clearly simpler for authors, this text changed; otherwise the implementation did. Each change, and why:

    • front strict (§3.3) closes the front matter, as Appendix A always said. The implementation had read it as a key named strict; it now reads the flag, and a flag-like bare word warns.
    • rows.<Column>.required and rows.<Column>.unique (Appendix A) now compile. Only the assignment forms (rows.Status.oneOf=…) had a working spelling.
    • strict and ordered (§3.3) each mean one thing. The implementation's strict also checked order, and it rejected ordered on a section as a retired spelling, so order alone was spelled numbered, the list flag. It now follows this text: strict closes, ordered orders, strict ordered does both, and numbered on a section is a retired spelling of ordered. The text gained one sentence saying strict implies no order.
    • content.dates.maxAge (§5.2, §5.3) reads the most recent date. "Every date" made the common case wrong: a runbook that logs each verification would always be stale once its first entry aged out. The implementation had read the last date in the text; it now reads the latest.
    • A mention (§4.1, and the <mention> mask token) includes inner hyphens, so @payments-oncall is one handle. The implementation had stopped at the first hyphen.
    • content.refs.all=defined (§6.3) resolves an in-page #anchor against the document's own heading slugs. The implementation had flagged every #anchor link as pointing to nothing.
    • A severity override takes several codes (§3.3): error=a,b or error=[a, b]. No code contains a comma, so the bare list is unambiguous; the implementation had rejected it.
    • A contract with errors checks nothing (§2.5), including an assert= that compiles but cannot evaluate (an unknown function, a type error). The implementation had reported such an error beside findings from the document it could not judge. It now reports the contract's errors alone, and the run's outcome is "could not run" (§8.5).
    • A whole-document code fence (§8.4) is reported once. The implementation had also reported every section the fence hid as missing.
    • content.none on a section (§5.3) covers the section's leaves. A section's text leaves its declared leaves out, so content.words.min counts only the section's own prose; applied to none, that let a placeholder in a declared list pass a section's content.none=@placeholders. A match is reported once, at its own line (§8.1), even when the leaf checks it too.
    • A repair (§9.1 R.4) keeps the document's final line break. The implementation's fence unwrap and postamble removal had dropped it, so every repaired file lost its last newline.
    • Cross-folder extends (§9) inherits everything but a relative source ref. The implementation had rejected any parent block with an assignment (content.words.min=5 included) and any plugin rule, a limit wider than its reason; the error now names the one ref at fault.
    • strict on a list or table (§3.3) closes it, as this text said. The implementation had silently ignored it there: a list now takes exactly its template's items, and a table no column outside columns.has=[…] (with a warning when the table declares none).
    • Structural codes (§8.3) take the implementation's names, which say what the reader sees: unexpected-section (was closed-boundary), too-few-repeats (was occurs-min), and too-many-occurrences (was occurs-max). A strict boundary on a list, table, or front matter keeps its own code. A generic prop's code is prop-failed, as §8.3 already said; Appendix A had named it <target>-<op>.
  • Well-formedness is built in (principle 6, §8.6). A contract says what a document must contain and mean; whether its Markdown is well formed is a linter's job, and nobody wants to author "my fences are closed". So fences.all=closed, links.all=anchored, @markers/pairs, lang.eq=required and level.eq=N are retired (§3.4, Appendix A): a contract that still writes one compiles, with a retired-atom warning, and the rewrite deletes it. Instead every document gets nine checks that only warn and never fail a check on their own: unclosed-fence, fence-without-language, broken-anchor, duplicate-heading, heading-wrong-level, misplaced-frontmatter, duplicate-frontmatter-key, orphaned-marker and crossed-marker. Six of them already existed and moved here: misplaced-frontmatter was an error and heading-wrong-level an error when level.eq was written. Each switches off with off=<code>, disable-next-line, or host configuration. The atoms that record what an agent's output contains (@agent/chatter, @agent/cut-off, @agent/fenced, @agent/repair) stay authored, and so does marker placement (@markers/placement.*, @markers/disposition.*), which names the author's own pivots and markers. So do lang.eq=<lang> and lang.in=[…], which say which language a block must be, heading.words.max, headings.level=, which selects a heading collection, and numbered on a list, which says the steps must be numbered.

  • Plain names. Draft 3 renames the syntax so that it says what it means. Each older spelling compiled with a renamed-atom warning through draft 4; it is now an unknown name (Appendix D):

    • Ranges (§5.3). T.range=a..b bounds any counted target in one atom (items.range=1..5, title.words.range=3..12), beside T.min and T.max, which stay. Both bounds are inclusive; a.. sets only the minimum and ..b only the maximum. A range beside a T.min or T.max for the same target is a compile error. occurs= reads the same grammar, so occurs=2.. is the open range (was occurs=2..*).
    • Three scopes (§4). A prop reads title. (the title), heading. (the section's heading), or content. (the body). Every target that measures the body moved under content.: words.*, paragraphs.*, links.*, sentences.*, blocks.has and dates.maxAge are now content.words.*, content.paragraphs.*, and so on; entities.in, entities(kind).in, entities(mention).mask, claims.in (and .kinds) and quotes.in are now content.entities.in, content.entities(kind).in, content.entities(mention).mask, content.claims.in and content.quotes.in. The text predicates drop the word text: text.none, text.mask, text.eq, text.pattern, text.scan and text.unlike (with their sub-options, such as text.none.phrases) are now content.none, content.mask, and so on. A section's lead.mask is content.lead.mask; the document's firstLine.mask is content.firstLine.mask. A block's own structure (items, checked, labels, rows, columns, lang, headings) and front matter keys take no scope; a front matter key's predicates are written on the key (front title pattern=…, front owner none=@placeholders; was text.pattern, text.none=@placeholders).
    • Vocabularies named for what they read (§7.2). A check that reads a table is a tables check, one that reads a list a lists check, and what an agent left in its output is the agent vocabulary: @numbers/foots is now @tables/total, @numbers/equation @tables/equation, @numbers/percentOf @tables/percent-of, @numbers/returns @tables/returns, @numbers/timeline @lists/timeline, and @numbers/ci @numbers/interval; @hygiene/llmResidue is now @agent/chatter, @hygiene/truncation @agent/cut-off, @hygiene/fenceWrapper @agent/fenced, and @hygiene/repair @agent/repair; @evidence/falsifiable is now @evidence/specific and @evidence/sourced @evidence/cited. Sub-options follow their check (@tables/total.tolerance). @numbers/percent and @numbers/sums read figures anywhere in the prose, so they keep their names. Codes that named the older jargon are renamed too: total-mismatch (was cross-foot-mismatch), interval-mismatch (was ci-outside), agent-chatter (was llm-residue) and fenced-output (was fence-wrapper). An older code is an unknown name wherever an author writes one (a severity override, host configuration): no alias is kept.
    • One list operator (§5.3). .in=[…] lists allowed values inline, as lang.in and content.entities.in already did: rows.Status.in=[todo, done] (was rows.Status.oneOf=) and front status in=[draft, done] (was oneOf=). in= with a source is unchanged.
    • One spelling per flag (§3.3). present is required allowEmpty, requiredOrNA is required na, a table's noExtraCols is strict, and path.in is content.entities(path).in.
    • Plain checklist and link words (§4.2, §5.3). items.all=disposed is items.all=done-or-na, and items.none=claimed is items.none=bare-tick, whose code is tick-without-evidence (was claimed-without-evidence). The link checks say what they check: citations.all=resolved is content.refs.all=defined (reference links and footnotes have definitions) and citations.all=resolvedLocal is content.links.all=exist (relative file links point at files that exist). Item.disposition keeps its values (done, claimed, na, open), which assert= expressions read.
  • CJK word counting (§4): words used to split only on whitespace and punctuation, so a Chinese or Japanese sentence with no spaces counted as one or two words no matter how long it was. Within a token, a Han ideograph or a Hiragana/Katakana character now counts as its own word, matching how word processors count CJK prose; every other script's count is unchanged. Thai, Lao, Khmer, and Myanmar stay unsegmented (a documented limitation) rather than reach for Intl.Segmenter, whose dictionary-based results vary by ICU version and would make counts non-deterministic across runtimes.

Section numbers are unchanged from draft 2, apart from the new §8.6.

Appendix D. Draft 4 changelog

Non-normative.

Draft 4 makes a contract's block tree the tree a document is matched against, gives blocks keys so extends can say exactly which block it replaces, and makes an unknown vocabulary name an error instead of text. Contracts that use none of these features compile and check as before; the breaking changes are listed last.

Appendix C already records the changes that landed after draft 3 was first stamped (2026-09-27) and before this draft: optional closers (#1448), the alignment with the reference implementation (#1454), built-in well-formedness (#1456), plain names (#1459), the title lead and document-level entity checks (#1465), and CJK word counting (#1467). They are not repeated here.

  • Unknown names fail (§7.2). A value written as @namespace/name, bare or inside a […] list, is a vocabulary reference and never text. It MUST resolve to a vocabulary the implementation provides. A name in an official namespace that nothing implements is unknown-vocabulary; a name outside every official namespace is unknown-rule-set (§5.4); an undeclared mask <token> is invalid-mask. The @git/…, @email/…, @changelog/… and @safety/… entries of the vocabulary table are marked "not yet implemented", so a contract that uses one now fails to compile. To match the text itself, write a /regex/ or quote it: content.none="@git/conventional". (#1477)

  • The contract's block tree is the binding tree (§3.1.1 Binding, §8.4, §8.6). A nested section binds only a heading inside the span of the heading its parent bound; before, it bound a matching heading anywhere in the document. A section's identity is its heading text and its heading level, so a heading at another level never binds it. When the parent bound nothing, the nested section is not checked, and the parent's presence rule reports the absence. A diagnostic names a nested section by its path (Options > Pros). Two matching headings in one parent's span report duplicate-heading once, and the last one binds. (#1478)

  • One diagnostic for a section at the wrong level (§3.1.1 Binding, §8.4, §8.6). A heading with a section's text at another level reports once, at that heading, with the fix that sets its level: missing-section for a required section (required, requiredOrNA, or requiredWhen with its condition met), missing-recommended-section for a recommended one, and otherwise heading-wrong-level. It is not also reported as heading-wrong-level, nor as a missing-section elsewhere in the file. Turning heading-wrong-level off therefore never waives a required section. (#1478)

  • Block keys and override (§9). id=<id> on a section, list, table, code, line or media directive gives the block a key ([a-z][a-z0-9-]*; ids are unique in the resolved contract, after every include and extends). A block without an id is keyed by its heading text and level. extends now applies the child's blocks to the parent's resolved block tree: a child block replaces a parent block only when it is marked override, the replacement is wholesale and keeps the replaced block's position, and a new block is added after the parent's blocks. An override without an id targets the one parent block with the same heading and level and warns implicit-override-key. The chain of parents is at most 8 deep. New codes: invalid-id, duplicate-id, override-required, override-target-missing, override-target-ambiguous, override-without-extends and invalid-extends, plus the warning implicit-override-key. (#1479)

  • include and extends interact by key (§9.2). Ids stay unique across includes (including a block that carries id= twice is duplicate-id). A child can override a section its parent included, and a section a child includes collides with a parent block as override-required unless it carries override. (#1479)

  • A bare tick fails (§5.3): when a contract asks for tick evidence (items.all=done-or-na, items.none=bare-tick), a ticked item that offers no evidence at all (no text after a separator, no nested note) reports tick-without-evidence as an error; it was an advisory warning, so the headline failure, an agent ticking work it did not do, passed. A tick whose offered evidence is too vague to check reports the new advisory warning weak-tick-evidence. Tests pass ticked is an error; Docs updated: done already ticked is a warning. (#1483)

  • An implementation does not lint (principle 6, §8.6). The checks that ran on every document because a linter would (fence-without-language, broken-anchor, duplicate-heading between sibling headings, orphaned-marker, crossed-marker) are gone, with nothing in their place: markdownlint's MD040, MD051 and MD024 cover the first three. unclosed-fence stays on every document, because an unclosed fence hides every block after it, and so does frontmatter-unreadable (§8.6), which replaces misplaced-frontmatter and duplicate-frontmatter-key: front matter mediva cannot read is a lint error. None of the remaining checks is advisory any more: a contract or host that raises one to an error fails the check. The atoms links.all=anchored and @markers/pairs, which asked for the removed checks, are no longer retired atoms but unknown rules. lang.eq=required is no longer read as "some language": like any lang.eq=<lang>, it asks for a fence whose language is literally required.

  • Gates (§8.1, §8.2). A check runs lint, shape, then state, and reports only the first gate that fails; a later gate's rules do not run. A diagnostic's concern is replaced by gate, one per code, and a result names its failed gate and, once shape passes, carries a readout of what the document claims. unclosed-fence is a lint error and label-line-mismatch a shape error, both warnings before. A value outside a mask token's oneOf list or a column's in= list is the new state code value-not-allowed, no longer a mask mismatch or table-cell-invalid, and a required front-matter key with no value is value-not-given, no longer missing-frontmatter-key.

  • The lint gate, and the rest of the gate model (principle 6, §8.1, §8.2, §8.6). The lint gate runs a Markdown linter's rules, chosen by the host's configuration alone (the reference implementation: markdownlint, with the default set of §8.2), and mediva's own checks of §8.6; it runs on every document, contracted or not, unless the check could not run, and stops the run when it fails. Its findings are always errors. A whole-document fence is now fenced-output on every document: @agent/fenced is an unknown name, and @agent/repair covers the chat preamble and postamble. A gate fails at the run's threshold, so warnings can stop the gates after them. A missing input (missing-title, missing-context, missing-context-value, regex-input-limit; gate input) means the check could not run, as a contract error does, and the result names could-not-run; a contract or input warning is a note that decides nothing. Only shape and state findings have a level anyone sets. A rule reporting a code it does not declare is undeclared-code. The capture rule (§8.2) judges every mask slot alike: a value in another form is value-wrong-form (shape), no value of its type value-not-given (state), one outside its set value-not-allowed (state, now also for a front-matter key, replacing frontmatter-not-allowed-value); a section's content.pattern not found is value-not-given. Reclassified: forbidden-content, missing-block, too-few-repeats, missing-repeat-instance, exactly-one-group-unmet, too-many-items, too-many-rows, too-many-occurrences, forbidden-section-present and code-lang-not-allowed are state; pattern-mismatch (a content.mask's form) and the placement codes are shape. New: item-mismatch (shape, an items.mask item's form), thin-definition-item and missing-conditional-section (state), markdownlint (lint), no-contract (input). The readout adds the title, front matter, table rows, plain items, every note of evidence, each repeated instance on its own block path, and a line's disposition and reason.

  • The lint step, finished (principle 6, §8.2, §8.6, §9.1). The reference implementation's default markdownlint set adds MD046 fenced (indented code reads as prose) to MD005 and MD056; MD018 stays out (its line pattern also matches prose that opens with #1343), and a host that wants it switches it on. The host's lint configuration replaces the default set and is validated: a severity in it, or a key naming one of mediva's own checks, is a configuration error. New lint check unclosed-comment: an HTML comment never closed. fenced-output also covers a body fenced whole in a markdown fence with a paragraph before and/or after it, with the fence pair's removal as its fix; the repairs no longer delete prose around a wrapper that matches no published pattern. A contract's warn=/error=/off= naming a lint, contract or input code is the new contract error unlevelable-override, no longer an unknown-override-target warning that did nothing.

  • Style atoms are gone (§4, §5.1, Appendix A). heading.words.min, heading.words.max, title.words.max, items.words.max, content.paragraphs.min, content.paragraphs.max, items.sentences.eq and numberedDeep limited how the Markdown looks rather than what it holds, and headings.clean changed nothing. They are unknown rules now, and so are the paragraphs and numberedDeep targets. A word floor stays where it stops a token gesture (content.words.min, items.words.min, title.words.min), and so does a contract's size budget for its body (content.words.max, content.links.max). A prop's field path must name a field its target has (§5.1), so a removed limit cannot come back as a generic prop (heading.words.max=8 was wordCount(heading) <= 8).

  • Links are a link checker's (§5.3, Appendix A). content.links.all=exist, which checked that every relative link named a file on disk, is an unknown rule: whether a link resolves is the job of a link checker (lychee, markdown-link-check) run beside the contract. content.refs.all=defined stays: it asks that a citation names a source, which is what the document claims.

  • Casing is a linter's (Appendix A). The @glossary/terms check no longer reports a glossary term written in another case (term-casing, markdownlint's MD044 proper-names), and a glossary entry has no casing field. A banned spelling, whatever its case, is still a deprecated entry, and @glossary/undefined is unchanged.

Not spec text: #1481 (syntax reference consistency), #1484 (@agent/cut-off accepts a line ending in a URL, #123 or @handle as complete) and #1482 (list rule identifiers numbered and numberedDeep, with ordered and orderedTree kept for one release as aliases that warn renamed-rule) change the reference implementation and its generated syntax reference, not this document. #1469 (docs) changed no spec text.

Breaking changes for contract authors, and how to migrate.

  • A bare tick is an error (#1483). Under items.all=done-or-na or items.none=bare-tick, a ticked item with no evidence now fails the check. Write what was run or seen after a separator (- [x] Tests added: tests/foo.test.ts, 12 pass), untick work that was not done, or lower it with warn=tick-without-evidence on the list or config rules.
  • extends needs override (#1479). A child section, list, table, code, line or media block whose id, or heading text and level, a parent block already has is now the compile error override-required; it used to replace the parent block silently. Add override to the directive of every block that is meant to replace a parent block. Give the parent block an id= and the child block override id=<same id> so the link survives a heading edit; an override without an id still works and warns implicit-override-key, naming the key to use. An override in a contract with no extends= is override-without-extends.
  • Heading level must match (#1478). A section now binds only a heading at its own level, and nested sections only inside their parent's span. A document, or a contract, that relied on a section matching at a different level, or under a different parent, now reports the section missing, at that heading when one exists at another level. Fix the heading to the level the contract names (the diagnostic carries the fix), or change the contract's heading to the level the documents use.
  • Unknown @… names are errors (#1477). Replace a name the implementation does not provide with one it does, remove it, or quote it as text.
  • disable-next-line, disable and enable are removed. A document could hide its own findings with these comments, unmatched-contract included, so it could relax the contract it is checked against. In a document such a comment is now an HTML comment that changes nothing; in a contract it is must-wrap. The codes unused-suppression, unknown-suppression-code and suppress-missing-reason are gone with them. Move each exception to a severity override on the block's own directive, naming the diagnostic code (off=too-few-words), or to host configuration.
  • No retired atoms. fences.all=closed and level.eq=N (and their internal names codeFenceClosed and heading.level=N) are unknown rules, not retired atoms that compile with a warning: an unclosed fence is reported on every document, and a section's anchor heading already declares its level (§8.6). The code retired-atom is gone. A fence no longer exposes closed and a section no longer exposes a level target, so neither comes back as a generic prop. The rule ID ordered, which named numbered in a severity override or config key for one release, is an unknown target too, and with it the code renamed-rule.
  • One spelling per prop, with no migration path. Every older spelling that compiled with a renamed-atom warning (Appendix A: minWords=, noPlaceholder, forbid=, taskList, noExtraSections, oneOf=[…], occurs=N..*, define @ns/name, …) is now an unknown name, and the codes renamed-atom, syntax-version-removed and reserved-rule-set-name are gone with the mdv migrate command. The *.anyOf twins are the array form of their mask (items.mask=["<a>", "<b>"]), and content.claims.in.kinds=[…] is one plural target per kind. Rewrite each spelling by hand from Appendix A.
  • No generic props and no assert= (§6). The built-in definitions are the only definitions: assert=<expr>, and a generic prop with no definition, are unknown names. The codes assert-failed, prop-failed and invalid-expression are gone.
  • No experimental opt-in (§0.1). An experimental atom always runs; the config key experimental and the codes experimental-block, unknown-experimental-feature, experimental-not-enabled and experimental-forbidden are gone.
  • Lint and style atoms are unknown rules. Delete links.all=anchored, @markers/pairs and lang.eq=required (now a fence whose language is literally required) from a contract, and run a Markdown linter for what they asked. Delete heading.words.*, title.words.max, items.words.max, content.paragraphs.*, items.sentences.eq, numberedDeep and headings.clean; a title.words.range=a..b or items.words.range=a..b becomes title.words.min=a or items.words.min=a. Delete content.links.all=exist, and run a link checker for what it asked.

Section numbers are unchanged from draft 3.

On this page