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.), named checks (words. min=20 @tables/), and named rule sets (§5.4) are what a contract says.total - 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.
| Level | Covers | Required 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., 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. (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:
| Tier | Promise | How a contract gets it |
|---|---|---|
| Stable | Changes 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. |
| Experimental | May change or be removed in any release. | Always on. |
| Official plugin | Versioned 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
*.) whose HTML comments carry mdv directives. Its visible Markdown is the template a document is expected to resemble.mdv. md - 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
wordsoritems. - 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
- One meaning, one definition. Every prop has one definition (§5.3). Diagnostics SHOULD be reported in the spelling the author used.
- 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.
- Surrounding content is allowed. A contract declares the blocks it cares about. Content it
doesn't declare passes unless
strictcloses a boundary. - Honest incompleteness is well-formed. A short honest answer (
No blockers.,Not established.) satisfiesrequired(§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). - 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.
- 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 slugdefine (§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.
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.
- A
sectiondirective is pushed onto the stack. - A leaf directive (
list,table,code,line,media) is never pushed. For a leaf closerend<K>, find the nearest preceding block directive or closer, skipping declarations. If that is an unpaired leaf directive of kindK, the closer pairs with it. If it is a block directive of a different kind, the closer ismismatched-tag. Otherwise the closer isunexpected-closer. endsectionpairs with the section on top of the stack, then pops it. On an empty stack it isunexpected-closer.- 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 endA 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
endsectionkeeps 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, ormedialeaf 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
lineleaf is always bare. It binds lines bylabel=, 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.text when it has one), and its heading level. A document heading at another level never binds it.heading= - 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.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.heading= - 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
requiredorrequired na, orrequiredWhenwith its condition met, it ismissing-sectionat that heading, naming the near miss (arequiredWhensection with no such heading at all is the state findingmissing-conditional-section: what the document says requires it); for arecommendedsection it ismissing-recommended-sectionthere. Any other section getsheading-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.
| Code | Severity | When |
|---|---|---|
unexpected-closer | error | A closer pairs with nothing (Step 1). |
mismatched-tag | error | The nearest block directive before a leaf closer is a block of another kind (Step 1). |
section-without-heading | error | A 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-anchor | error | A block directive sits between a section directive and that section's heading (unchanged). |
unclosed-tag | error | Only for an implementation-defined block kind that does not opt into optional closers. The core kinds never report it. |
flag-on-title-lead | warn | strict 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-closer | warn | An 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 -->
## ImpactSummary 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
## AppendixRelease 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
| Kind | Binds to | Nests | Notes |
|---|---|---|---|
document | the whole file | — | Holds document-wide constraints. No closer. |
section | an ATX heading and its body | sections and leaves | The only heading-owning block. Closer optional: scope follows heading levels (§3.1.1). |
list | one top-level Markdown list | no | Bulleted, numbered or checkbox. |
table | one pipe table | no | |
code | one fenced code block | no | |
line | lines selected by label= | no | |
media | an image or video span | no |
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. 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., content., content., heading., headings., a column's mask, a
section's content.). A repeated section's selector picks its instances by the selection rule
(§8.2), and each instance's heading. 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>. binds every heading its selector picks as an
instance; zero parent instances means no child presence failures. A section with heading. and
no occurs= is a set of one (occurs=1 when required, else occurs=0.): 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. (§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
| Flag | Meaning |
|---|---|
required | The 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. |
allowEmpty | With required: the block MUST be present and MAY be empty. |
recommended | A missing block is a warning (missing-recommended-section), never an error. |
optional | The default presence: the block MAY be absent. Written explicitly it says so; next to use= it switches a set's presence off (§5.4). |
na | With required: the block's na empty state (§3.5) MAY stand in for the content. Equivalent to empty=@empty/. |
empty=<marker> | The block's empty state (§3.5): @empty/ or an inline quoted pattern. Content that is exactly the marker satisfies presence; the block's other constraints are not evaluated. |
strict | The 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.); on a table, a column outside its columns. is an error, and a table that declares no columns has nothing to close, which SHOULD be a warning. |
ordered | Declared children MUST appear in declared order. It means nothing else; list numbering is numbered (§4). |
freeText | The 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. |
exhaustive | On 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., N. (at least N), . (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.), the error MUST name that spelling.
An array value means "any of" wherever an operator takes one value (items.);
there is no separate . 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.),
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>: a lowercase letter,
then lowercase letters, digits, or -; words is reserved) or inline with empty="<pattern>"
(reason floor: empty.). The @empty/ family is reserved for this purpose; it is not a
source (§7.1). na is sugar for empty=@empty/.
Resolution. A reference resolves by the first of these that defines the name:
- the directive itself (an inline pattern);
- the contract's
documentdirective:empty., optionally<name>="<pattern>" empty.;<name>. words. min=N - project configuration (implementation-defined key; the reference implementation uses
emptyStatesinmediva.);config. * - an implementation's programmatic default;
- 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/, minReasonWords 2, matched with the historical
tolerances: N/ 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 noempty=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/): 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/, (N/, (N/) 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.,
items., rows. 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/, 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).
| Target | Type | Blocks | Value |
|---|---|---|---|
text | string | all | The block's own prose, without HTML comments. For a section, child section and leaf spans are excluded, except under none (§5.3). |
words | int | all | Word count of text (word defined below the table). |
heading | string | section | Heading text without the # marker. |
items | list(Item) | list | Top-level items in order (§4.2 defines Item). |
checked | list(Item) | list | The items whose box is ticked. |
labels | list(string) | list | Each item's label: its text up to the first —, :, or end. |
numbered | bool | list | True for an ordered (1.) list. |
rows | list(map(string, string)) | table | Body rows keyed by header cell text. |
columns | list(string) | table | Header cells in order. |
prose | string | all | text with fenced code removed. |
blocks | list(string) | all | The kinds of block present (paragraph, list, table, code, …). |
lead | Line or null | all | The first line of the first non-empty paragraph. |
fences | list(Fence) | all | Fenced code blocks. Fence = {lang, line}. |
links | list(Link) | all | Inline links, resolvable reference links, and autolinks. Link = {text, line}. |
claims | list(Claim) | all | Grounding module. Claim tokens in text (§4.1). Claim = {kind, text, value, index}. |
refs | list(Ref) | all | Grounding module. Reference links and footnote references. content. requires each to have a definition (§6.3). |
quotes | list(Quote) | all | Grounding module. Blockquote paragraphs and inline "…" quotations. Not yet a target in the reference implementation. |
dates | list(timestamp) | all | Grounding module. ISO-8601 dates in text. Not yet a target in the reference implementation. |
lang | string | code | Fence info string's first word. |
title | string | document | The document title, supplied by the host. A title. prop with no title supplied fails with missing-title; it never passes unchecked. |
front | map(string, dyn) | document | Parsed front matter. |
headings | list(Heading) | document | The 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.), read as the section binds it: a linknone=@placeholders [text](url)and a bare[text]read astext, and a closing#run and any leading characters that are not letters or digits are dropped, so## [1.reads2. 0] - 2026-07-02 1.. A heading mask (2. 0 - 2026-07-02 heading., which picks its section's heading (§3.2), and the document'smask headings.) reads the heading the same way but keeps its leading characters other than an emoji:mask ## @lira — Lirareads@lira — Liraforheading..mask="<mention> — <text>" heading.andraw. mask= headings.match the heading as written, brackets included.raw. mask= 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., the plural grounding targets (entities content.,paths content.,issues content.,mentions content., …; §4.1) andamounts content.. The body text itself islead content, so the text predicates arecontent.,none content.,mask content.,eq content.,pattern content.andscan content.. On theunlike documentdirective the body is the whole document:content.is then the document's first line, its first heading or paragraph.lead - 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.
| Kind | Recognized as | Canonical form (value) | Equivalence |
|---|---|---|---|
amount | A currency symbol or ISO 4217 code adjacent to a number (£412, $1,200., 412., 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). |
number | A plain number with no unit, currency, or sign context | Comma-stripped, trailing-zero-dropped decimal | Exact string equality. |
date | One of three unambiguous surface forms: YYYY-MM-DD, D Month YYYY, Month D, YYYY | ISO 8601 calendar date YYYY-MM-DD | Exact string equality. |
duration | A 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. |
percent | A number directly followed by %, percent, or per cent (90%, 12.) | 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. |
id | A short uppercase-alnum prefix, a dash, and ≥3 trailing digits (INV-2041, INC-2044); a source-side / also grounds as id | Trimmed verbatim | Exact 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 it | Lowercased, @ stripped | Exact string equality. |
url | An explicit-scheme URL, or a bare domain with a path | Scheme and trailing slash stripped, lowercased | Exact string equality. |
quote | A quoted span of ≥3 words (straight or smart quotes) | Lowercased, whitespace-collapsed, smart-quote/dash-straightened, edge-punctuation-trimmed | Exact string equality (mechanical folding only, never semantic). |
Plugins MAY contribute namespaced kinds (clinical/); 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-fiveintwenty-five hoursnever matches on its own. Fractional durations (1.) do not extract.5 days 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,_yearsfor duration;_percent,_pctfor 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.) or a clock time (2 14:30) MUST NOT become adurationorpercentclaim. Both recognizers require an explicit unit word or%/percentdirectly 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 (/, §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/, src/) 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., with no parentheses:
| Target | Checks | Code |
|---|---|---|
content., content., content., content., content., content., content. | Every claim of that one kind (table above) is in the source, canonicalized. Two kinds may name two sources. | ungrounded |
content. | Umbrella over typed values: every claim of the default kinds amount, date, duration, percent, id. | ungrounded |
content. | Umbrella over named things: capitalized names, @mentions and slash routes (§3.2). Not yet a claim kind; see §10. | unknown-entity |
content., content. | Every file path / issue ref (§7.1). issues, not refs: content. already names citation refs (§4). | not-in-change-set |
content. + content. | Every span shaped by the mask is a member of the source (§7.1). | unknown-entity |
content. | Every quotation reproduces the source (§7.1). | quote-drift |
The mention and quote claim kinds are read by content. and content.;
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..
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., or labels sees no behavior
difference from adding disposition/evidence.
disposition is one of five strings, in this classification order (first match wins):
-
The item's
textends in its block's item marker (§3.5; with no declared marker, the built-in(N/, 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) →A: <reason>) "na", regardless ofchecked. N/A means the item does not apply here; it never means "could not do it". A ticked N/A (- [x] Screenshots (N/) is stillA: CLI-only change) "na", not"claimed", but it contradicts itself: the tick records the work as done and the marker says the item does not apply.items.andall=done-or-na items.report it asnone=bare-tick contradictory-disposition(§5.3). -
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, ornot donewith its reason right after a rule 1 separator of the evidence split (Tests run — not done: <reason>,Tests run: not done, <reason>);not donein 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 availableis"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 doneor 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 declaredTests run. - The explicit form, regardless of
-
checked == true→"done"ifevidence != "", else"claimed". -
checked == false→"open". -
checked == null(a plain bullet) →"done"ifevidence != "", 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:
- Template label. If
textstarts with a label the block's contract list itself declares (its- [ ] …items, compared aslabels.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,eq=@template ;,,,-,(,[, 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 anEx: …:- [x] PR title format: `Add project-name`ticked as the template wrote it is a bare tick, while…release (`vX.carries evidence after its declared label.Y. Z`). Latest is `v0. 40. 2`. - Otherwise, split
textat 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.(what may follow a declared label), the evidence split, and the not-done reason rule (rule 2 ofeq=@template disposition) all end the label at the same place, after rule 0's declared template label when one matches.labels.does not accept a parenthetical that says nothing (no letter, not-applicable wording, or a bareeq=@template not done), so a hollow(N/keeps its label mismatch.A) - 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.
- 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. - 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.textis always the label line alone, solabelsstay byte-exact. - Otherwise
evidence = "".
| Item text | checked | disposition | Why |
|---|---|---|---|
Ran `npm test`; 0 failures. | true | claimed | No separator: the whole line is the label, and a code span or number in a label is not evidence. |
Ran `npm test`; 0 failures. | null | done | A 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. | true | done | Split remainder is falsifiable (digits). |
Tests added: added `tests/ | true | done | Split remainder is two words, but a code span is a concrete signal at any length. |
Docs updated: done already | true | claimed | Split remainder has no concrete signal and is under 4 words. |
Tests pass | true | claimed | No separator, no concrete signal — a bare tick. |
Docs updated (N/ | true or false | na | Well-formed N/A marker, either way. |
Tests run (not done: no test runner in this sandbox) | false | not-done | Work that applies, not done, with its reason. |
Tests run: not run, no test runner was available | false | not-done | Unticked, with author text after its label: that text is the reason. |
Tests run: not done | false | open | A bare "not done" is not a reason. |
Run the test suite | false | open | Nothing after the label: no evidence, no marker, no reason. |
Run the test suite | null | open | No evidence, no N/A, and nothing was ticked. |
5. Props — Core
5.1 Syntax
A prop is <target>., or <target>. 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., content.; §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>.
(heading., items.) 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. 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.
| Prop | Applies to | Defined as |
|---|---|---|
T. | int | T >= n |
T. | list | size(T) >= n |
T. | int / list | T <= n / size(T) <= n |
T. | int / list | T. and T. together. a. is T. alone, . is T. alone, and n is T.. |
T. | int / list | T == n / size(T) == n |
T. | string | normalize(T) == normalize(source(v)) |
T. | list(string) | T == source(v) |
T. | string / list | T. / T. |
T. | string / list | T. / T. |
T. | string / list | T in source(v) / T. (Grounding module) |
T. | string / list | T in v / T.: an array literal lists the allowed values inline. |
T. | string / list | !T. / T.. On a section, content. reads everything under the heading except child sections, its leaves included: none means nowhere in the section. |
T. | list | T. |
T. | list | size(T) == size(distinct(T)) |
T. | 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. and T. are not props. |
Ranges. A range is a., a., ., or a single n, over non-negative whole numbers, with
both bounds inclusive and a no greater than b. T. takes a range wherever both T.
and T. count (items., content., checked.), and
occurs= (§3.3) reads the same grammar. A T. on a T that has only one bound, or none
(title., content.), a malformed range, or a T. beside a
T. or T. 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. and T. 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., are not the same atom.
items. (Core; §4.2) passes when every item is "done", or "na" and unticked. It
is the RECOMMENDED checklist completion gate; items. 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. an unticked item,
with or without a reason, still passes. items. 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. and items. 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. 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., items.; defined on content., a reference with a definition, §6.3), or a vocabulary
predicate (@evidence/). A prop whose operator doesn't
apply to the target's type is a compile error.
Examples from the hero set: content., items., items.,
checked., items.,
content., content., content.,
content..
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 -->
## SummaryThe section above means exactly <!-- mdv: section required content..
Definition. <!-- mdv: define <setname> <atoms…> --> (§3.1):
- The name follows
[a-z][a-z0-9-]*, the fragment-name grammar:proseorlong-prose. It has no@prefix. Directive names, closers and known atom keys or flags are reserved; declaring one reportsrule-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, orextends=, 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
defineis 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 thedocumentdirective.
Where definitions come from. A contract can use rule sets from these places:
- the contract itself;
- 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; - its
extendsparents (§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/ 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.
- 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.keeps the set's other atoms and useswords. min=40 content..words. min=40 - Presence. The presence flags
required,recommended, andoptionalcount as one key, andallowEmptyandna, which modifyrequired, go with it. So a use site switches a set'srequiredoff by writingoptional(or relaxes it withrecommendedorrequired allowEmpty), and the set'sallowEmptyornagoes too. - Accumulating keys.
T., severity overrides (none= error=,warn=,off=), and check uses (@ns/) add up: the set's atoms and the directive's atoms all apply. Identical atoms collapse into one (§8.4).name - Nesting. Inside a set, the same rules apply recursively: the set's own atoms win over the atoms of the sets it uses.
- 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..
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 -->
## DesignWith 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 -->
## DesignExample: precedence. This directive:
<!-- mdv: section use=prose recommended content.words.min=5 content.none=@agent/cut-off -->expands to recommended content.. The
directive's presence flag and its content. replace the set's. The two content. atoms both
apply. <!-- mdv: section use=prose optional --> keeps content. and
content. 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. holds when P holds for every element, L. keeps the elements for which
P holds, x in L is membership, s. 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.) 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
nowby default and accepts an explicit override from its caller.
6.3 Functions the definitions use
| Function | Meaning |
|---|---|
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/, <0., N/) is not a number and yields NaN, which no comparison accepts. |
decimals(s) | Decimal places written in the figure s (42. → 2); 0 when it doesn't parse. Half a unit in the last displayed place of s is 0.. |
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.). 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. 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
| Ref | Resolves to |
|---|---|
. | Parsed JSON. |
. | The value at that JSON pointer. |
., . | The file's text. |
. | The texts of every file directly inside dir. |
@name | A source declared in configuration. |
@vocab/ | A pattern, mask, source, or predicate exported by a vocabulary. |
@template | The contract's own template content for this block (for example its labels). |
@changes | A 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/ | Every path the change set touched, including a rename's pre-rename path, as a projection of @changes. |
@change/ | Every 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 shape | x in S is true when |
|---|---|
| list of strings | x equals an element. |
| map | x equals any leaf scalar, compared as a string; or equals a key when the pointer selects a map of paths. |
| text | normalize(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/headare free-form provenance; nothing in grounding reads them.paths[].is one ofstatus added/modified/deleted/renamed;renamedrequiresfrom(the pre-rename path).refsare 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 ornullmeans 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 .), a bare name with a recognized extension (env. local README.,md notes.,rst knip.,jsonc widget.,lua widget.), or a slash-qualified filename with an extension. Bare and qualified forms share one extension/basename vocabulary. Compound names such asdart store.stay whole. Word pairs (events. test. ts and/,or CI/,CD N/), ratios (A 17/), bare versions (17 v2.), unqualified framework names (3. 1 Next.,js Node.), and entire URL spans including their query and fragment are excluded. A sentence-final trailingjs .is stripped. Extensionless directory names and unusual paths containing whitespace or non-ASCII characters are not inferred.ref—#123(a bare issue number) orABC-123(1–10 uppercase letters/digits starting with a letter, a dash, then ≥1 digit).
content. / content. mirror the general entity-grounding prop
(§5.3's T.) 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//@change/ 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.) 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/ reports source unavailability once, before checking individual #N references.
Only an explicitly optional @change/ 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/andpaths @change/to plain internal logical names before they reach the grounding engine, rather than implementing a generalrefs @ns/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 aname ChangeSetobject directly.@changesitself (the un-projected record) is not yet bound to a consumable value; §10 tracks binding it as arecordsprojection exposingpath/status/fromper 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.is the built-in "no placeholder" check). Loading vocabularies as third-party-publishable data is future work.none=@placeholders
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.
| Vocabulary | Exports |
|---|---|
evidence | @evidence/, @evidence/ (predicates for items.) |
safety | @safety/, @safety/ (not yet implemented; content. covers secrets today) |
git | @git/ (not yet implemented) |
email | @email/ (not yet implemented) |
changelog | @changelog/ (not yet implemented) |
tables | checks that read a table: @tables/ (+ ., .), @tables/, @tables/, @tables/ (§7.2.1; codes total-mismatch, column-equation-mismatch, return-chain-mismatch, cell-percent-mismatch) |
lists | checks that read a list: @lists/ (§7.2.1; code timeline-mismatch) |
numbers | checks that read figures anywhere in the prose: @numbers/, @numbers/, @numbers/ (§7.2.1; codes percent-mismatch, sum-mismatch, interval-mismatch) |
placeholders | @placeholders (the current built-in placeholder list) |
agent | what an agent left in its output: @agent/, @agent/, @agent/ (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/ 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/
itself, write a / or quote it: content..
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/binds the stringname=<v> args.. The value MAY carryvalue ;key=valuesegments, each binding a parameter the check declares (@tables/bindsreturns=Return;from=Jan|Feb;to=Q1 args.,value args.,from args.); the first segment that names no declared parameter isto args.. A parameter MAY also be spelled as its own atom on the same directive,value @ns/(a quoted value is unquoted;name. <param>=<v> \",\'and\\escape). An absent parameter isnull. A segment naming no declared parameter afterargs.is set reportsvalue invalid-optionat 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 reportsinvalid-optionat the directive. A figure that isn't one (§6.3num) 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:
| Field | Required | Content |
|---|---|---|
code | yes | Stable kebab-case code (§8.3). |
severity | yes | error or warn. |
gate | yes | lint, 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). |
line | yes | 1-based line in the document; the contract line for compile errors. |
column | no | 1-based column. |
block | yes | Path of the block: headings from the root, then the leaf kind and ordinal. |
constraint | yes | The prop as the author wrote it. |
expected | when known | What the constraint compared against (a mask, a source ref, a limit). |
message | yes | One 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/ 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., an item
labels. does not list —
line is that item's line. When a match inside prose is at fault (content.), line and column
point at the match. A failure no single element owns — a count bound such as rows./items.,
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, andfixfields 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:
| Gate | Asks | For example |
|---|---|---|
lint | Can the document be read as written? | unclosed-fence, fenced-output (§8.6), a Markdown linter's findings |
shape | Does 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 |
state | Does 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., 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/ 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., 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. for [<semver>] - <date>, sprint 3 for Sprint <num>); the
instance's own heading. 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.), 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.
| Token | Written another way | Canonical |
|---|---|---|
<date> | year first with other separators or no padding (2026/); day and month around a year when only one reading exists (23/, 05/); a month name (23 June 2026, June 23, 2026) | 2026-06-23 |
<semver> | a leading v (v1.) | 1. |
<issue> | issue 12, Issue #12 | #12 |
<num> | thousands grouped with commas, two groups or a decimal point (1,000,000, 1,000.) | 1000000, 1000. |
<percent> | a space before %, or the word (42 %, 42 percent) | 42% |
<time> | a 12-hour time (2:30 pm, 9:30am, 2 p.) | 14:30, 09:30, 14:00 |
1,000 names a thousand or one with three decimals, 1. 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:/, <a@b. or <notes., 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:
| Rule | Why mediva cannot read it |
|---|---|
| MD005 | Sibling list items indented apart nest or split the list, so its items are not the ones written. |
| MD056 | A 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/, (N/ and (not done) are
shed like (N/. 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 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/ 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 -->
## Summarythe 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-sectionIt 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.
| Code | Runs | Reports |
|---|---|---|
unclosed-fence | on every document | A 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-comment | on every document | An 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-output | on every document | A 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-level | for a section | A 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-heading | for a section | Two headings in one parent's span that both match a contract section (§3.1.1 Binding), once. |
frontmatter-unreadable | on every document | Front 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., heading.) 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, and2respectively).
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
overridereplaces 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 isoverride-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. Anoverrideon a nested block isoverride-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
overridewhose 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
orderedit therefore comes after every inherited block. overridein a contract withoutextends=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 .
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..
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.as an ordinary reference to a vocabulary name it doesn't ship (§0): the directive is well-formed, it simply never triggers a repair.none=@agent/ repair
9.2 include — Core
(Design record: SYNTAX-SIMPLE.md, item B3; issue #1434.)
<!-- mdv: include . inserts the blocks of another contract at the
directive's position. <!-- mdv: include . 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
includetherefore 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 itsextendsare 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
definein the included file and in the files it includes (§5.4). - From the included file's
documentdirective, only its empty-state declarations (empty., §3.5). They resolve after the including contract's own<name>= documentdirective. If two included files declare the same empty state differently, the compile error isinclude-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 as | document extends=<ref> (one per contract) | include <ref>[#anchor] (any number) |
| Brings in | a whole parent contract | selected sections and all definitions |
| Same heading as an existing section | the child's override block replaces the parent's; without override, compile error (override-required) | compile error (include-duplicate-section) |
| Document-wide constraints | inherited | not 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/:
<!-- 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 planand each contract that needs it, such as docs/, writes:
<!-- mdv: section required content.words.min=20 -->
## Summary
<!-- mdv: include ../shared/release.mdv.md#definition-of-done -->
<!-- mdv: section required -->
## DecisionThe result is the same block tree as the draft 2 contract. The inserted ## Definition of Done
ends Summary's scope, @rel/ is imported with it, and Rollback plan is not inserted
because the anchor selects only one section.
10. Open questions
Honest N/A— decided in Phase 3: core, as thenaflag (§3.3); its wording is the contract author's, through empty states (§3.5).Default codes— decided in Phase 3: named rules keep their codes (§8.3).- Vocabulary versioning and how a contract pins one.
- Whether
content.(named things) belongs in core given its imprecision, or only in a vocabulary.entities. in - Preset invocation: presets are written
@vocab/orname @vocab/as an atom (name=<arg> @tables/). Confirm this doesn't collide withtotal=Amount @refvalues. - Capitalized-name and route recognition (
content.) as a claim kind, so aentities. in namestarget has one normative recognizer (§4.1). - 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).
- Which of the remaining imperatively-decided rules get a generic primitive, which become vocabularies, which are dropped.
- Formalize the
@ns/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.name - Give
refa realClaimKindslot and decide precedence againstid's existingABC-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/) to narrowprefixes=[. . . ] ref's ambiguity against a versioned proper noun (COVID-19). - Decide whether
@changes(the un-projected record) should bind as arecordsprojection exposingstatus/fromper row, for a rule that wants to distinguishaddedfrommodifiedfromdeleted(§7.1). - Should the default grounded-kind set (
amount, date, duration, percent, id) be spec-normative, or left implementation-defined the waynumber's exclusion already is (§4.1)? - 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.
- 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?
- 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? - 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.
- 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.into its own module after all, or merging Repair into Grounding)?disposition Optional closers, named rule sets, and— 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.include
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
| Old | Current | Notes |
|---|---|---|
required | = | Present and non-empty. |
present | required allowEmpty | Present; 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. |
requiredOrNA | required na | |
allowEmpty | = | With required: may be empty. |
ordered (section) | = | Child order only. |
numbered (section, document) | ordered | numbered is list numbering; a section's child order is ordered. |
ordered (list) | numbered | |
only, noExtraSections, noExtraCols, front noExtraKeys | strict | On the block whose boundary closes. |
exactLabels | labels. | strict on the list says the same. |
A. Prose
| Old | Current |
|---|---|
minWords / maxWords | content. / content. |
maxLinks | content. |
noPlaceholder | content. |
pattern=/ | content. |
pattern. / . | content. / content. |
lead. | content. |
forbid=… (regex, phrase, or list) | content. |
forbid. | content. |
forbid. | content. |
forbid. / . | content. / content. |
forbid. | content. |
forbid. | content. |
contains=<kind> | content. |
distinctFrom="Heading" | content. |
sentence., ., ., ., ., . | content., ., ., ., ., . |
A. Grounding
| Old | Current |
|---|---|
entitiesIn | content. |
groundedIn | content. (every claim kind) |
groundedIn. | one target per kind: content. / content. / content. / content. / content. / content. / content. |
verbatim matches=<ref> | content. |
quotesMatch | content. |
citations. | content. |
mentions. / mentions. | content. / content. |
path. / ref. | content. / content. |
lexicon, lexicon. | @glossary/, @glossary/ |
maxAge | content. |
A. Headings and title
| Old | Current |
|---|---|
heading. / heading. | = |
| — | heading.: heading. on the heading as written |
heading. | heading. |
title | = (document flag: a title must be supplied) |
title. | = |
title. | title. |
title. | title. |
firstLine. / . | content. / content. on the document |
headings., ., ., ., . | = |
headings. / . | headings. / headings., on the heading as a section binds it |
| — | headings. / headings.: headings. on each heading as written |
A. Lists
| Old | Current |
|---|---|
taskList, choice, evidenceList (+ closers) | list / endlist |
minItems / maxItems | items. / items. |
minChecked / maxChecked | checked. / checked. |
oneChecked | checked. |
allChecked | items. |
allDisposed | items. (§4.2) |
noneClaimed | items.: no tick without evidence; open items pass (codes tick-without-evidence / weak-tick-evidence) |
requiredOrNA media/N/A text (hard-coded) | empty=@empty/ + document empty. (§3.5) |
each. | items. |
each. / . | items. / items. |
each. | items. |
each. | items. |
each. | items. |
each. / . | items. / items. |
covers / covers. | items. / items. |
timelineSums | @lists/ (a lists check, §7.2.1) |
A. Tables and code
| Old | Current |
|---|---|
minRows / maxRows | rows. / rows. |
cols=[…] | columns. |
col. | rows., column flags too: rows., rows. |
col. | rows., the column's regex, read like content. |
rows. | = |
crossFoot (+ ., .) | @tables/ or @tables/ (+ ., .), a tables check (§7.2.1) |
columnEquation, returnChain, cellPercent | @tables/, @tables/, @tables/, tables checks (§7.2.1) |
percent, sumToTotal, ciContains | @numbers/, @numbers/, @numbers/, numbers checks (§7.2.1) |
lang=ts / lang. | lang. / lang. |
A. Document hygiene, integrations, meta
| Old | Current |
|---|---|
noTruncation | content. |
noLLMResidue | content. |
noFenceWrapper | — (a whole-document fence is fenced-output on every document, §8.6) |
| — | content. (Repair module, §9.1) |
pairedDisposition., document. | @markers/, @markers/ |
scan. | content. |
issueKeyword / issueState | issue. / issue. |
containsContext | context. |
requiredSet / . | @sets/ / . |
noExemplarResidue | = (contract self-check) |
name, desc, help, match., maxSeverity, code=, error=/warn=/off=, occurs= | = |
A. New atoms
| Atom | Meaning |
|---|---|
items. | Core checklist completion gate (§4.2, §5.3). Code tick-without-evidence (error) / weak-tick-evidence (advisory) / open-item. |
content. / content. | Grounding module: ground path/ref claims against a change set or any other source (§7.1). Code not-in-change-set. |
content., content., content., content., content., content., content. | Grounding module: ground one claim kind (§4.1). Code ungrounded. |
content. | Repair module: opt into R.1–R.4 (§9.1). |
<!-- mdv: define name … -->, use=name | Define and use a named rule set (§5.4). |
<!-- mdv: include . | 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.andevidence items., folded into Core (the feature is small and applies to every list, not only a "grounding" concern).all=disposed - Claim kinds (§4.1): the
durationandpercentkinds, and the full normative recognition/canonicalization/equivalence table for every kind, in the Grounding module. - Change sets (§7.1):
@changes, the@change//paths @change/projections, therefs pathandrefentity kinds, and thenot-in-change-setdiagnostic, 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) andambiguous-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.and an implementation's own entity checks (the reference implementation'sentities. in match/rules) may sit on the* documentdirective, where they read all the document's prose and report each finding at its own line.content.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.)entities. in -
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. Astrictororderedon such a section checks nothing and warnsflag-on-title-lead. (Issue #1450.) -
Named rule sets (§5.4):
define name <atoms…>declares a fixed bundle of atoms, anduse=nameon a directive expands it. Atoms written at the use site win, andoptionalswitches a set'srequiredoff. 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, andrule-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 beforeextendsmerges. New codes:include-not-found,include-cycle,include-anchor-missing,include-anchor-ambiguous,include-level-mismatch,include-duplicate-section, andinclude-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.wastest. ts` claimedand is nowdone. -
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 namedstrict; it now reads the flag, and a flag-like bare word warns.rows.and<Column>. required rows.(Appendix A) now compile. Only the assignment forms (<Column>. unique rows.) had a working spelling.Status. oneOf=… strictandordered(§3.3) each mean one thing. The implementation'sstrictalso checked order, and it rejectedorderedon a section as a retired spelling, so order alone was spellednumbered, the list flag. It now follows this text:strictcloses,orderedorders,strict ordereddoes both, andnumberedon a section is a retired spelling ofordered. The text gained one sentence sayingstrictimplies no order.content.(§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.dates. maxAge - A
mention(§4.1, and the<mention>mask token) includes inner hyphens, so@payments-oncallis one handle. The implementation had stopped at the first hyphen. content.(§6.3) resolves an in-pagerefs. all=defined #anchoragainst the document's own heading slugs. The implementation had flagged every#anchorlink as pointing to nothing.- A severity override takes several codes (§3.3):
error=a,borerror=[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.on a section (§5.3) covers the section's leaves. A section'snone textleaves its declared leaves out, socontent.counts only the section's own prose; applied towords. min none, that let a placeholder in a declared list pass a section'scontent.. A match is reported once, at its own line (§8.1), even when the leaf checks it too.none=@placeholders - 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.included) and any plugin rule, a limit wider than its reason; the error now names the one ref at fault.words. min=5 stricton 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 outsidecolumns.(with a warning when the table declares none).has=[…] - Structural codes (§8.3) take the implementation's names, which say what the reader sees:
unexpected-section(wasclosed-boundary),too-few-repeats(wasoccurs-min), andtoo-many-occurrences(wasoccurs-max). Astrictboundary on a list, table, or front matter keeps its own code. A generic prop's code isprop-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.andeq=required level.are retired (§3.4, Appendix A): a contract that still writes one compiles, with aeq=N retired-atomwarning, 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-markerandcrossed-marker. Six of them already existed and moved here:misplaced-frontmatterwas an error andheading-wrong-levelan error whenlevel.was written. Each switches off witheq 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/) stay authored, and so does marker placement (repair @markers/,placement. * @markers/), which names the author's own pivots and markers. So dodisposition. * lang.andeq=<lang> lang., which say which language a block must be,in=[…] heading.,words. max headings., which selects a heading collection, andlevel= numberedon 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-atomwarning through draft 4; it is now an unknown name (Appendix D):- Ranges (§5.3).
T.bounds any counted target in one atom (range=a. . b items.,range=1. . 5 title.), besidewords. range=3. . 12 T.andmin T., which stay. Both bounds are inclusive;max a.sets only the minimum and. .only the maximum. A range beside a. b T.ormin T.for the same target is a compile error.max occurs=reads the same grammar, sooccurs=2.is the open range (was. occurs=2.).. * - Three scopes (§4). A prop reads
title.(the title),heading.(the section's heading), orcontent.(the body). Every target that measures the body moved undercontent.:words.,* paragraphs.,* links.,* sentences.,* blocks.andhas dates.are nowmaxAge content.,words. * content., and so on;paragraphs. * entities.,in entities(kind).,in entities(mention).,mask claims.(andin .) andkinds quotes.are nowin content.,entities. in content.,entities(kind). in content.,entities(mention). mask content.andclaims. in content.. The text predicates drop the wordquotes. in text:text.,none text.,mask text.,eq text.,pattern text.andscan text.(with their sub-options, such asunlike text.) are nownone. phrases content.,none content., and so on. A section'smask lead.ismask content.; the document'slead. mask firstLine.ismask content.. A block's own structure (firstLine. mask 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; wastext.,pattern text.).none=@placeholders - Vocabularies named for what they read (§7.2). A check that reads a table is a
tablescheck, one that reads a list alistscheck, and what an agent left in its output is theagentvocabulary:@numbers/is nowfoots @tables/,total @numbers/equation @tables/,equation @numbers/percentOf @tables/,percent-of @numbers/returns @tables/,returns @numbers/timeline @lists/, andtimeline @numbers/ci @numbers/;interval @hygiene/is nowllmResidue @agent/,chatter @hygiene/truncation @agent/,cut-off @hygiene/fenceWrapper @agent/, andfenced @hygiene/repair @agent/;repair @evidence/is nowfalsifiable @evidence/andspecific @evidence/sourced @evidence/. Sub-options follow their check (cited @tables/).total. tolerance @numbers/andpercent @numbers/read figures anywhere in the prose, so they keep their names. Codes that named the older jargon are renamed too:sums total-mismatch(wascross-foot-mismatch),interval-mismatch(wasci-outside),agent-chatter(wasllm-residue) andfenced-output(wasfence-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).
.lists allowed values inline, asin=[…] lang.andin content.already did:entities. in rows.(wasStatus. in=[todo, done] rows.) andStatus. oneOf= front status in=[draft, done](wasoneOf=).in=with a source is unchanged. - One spelling per flag (§3.3).
presentisrequired allowEmpty,requiredOrNAisrequired na, a table'snoExtraColsisstrict, andpath.isin content..entities(path). in - Plain checklist and link words (§4.2, §5.3).
items.isall=disposed items., andall=done-or-na items.isnone=claimed items., whose code isnone=bare-tick tick-without-evidence(wasclaimed-without-evidence). The link checks say what they check:citations.isall=resolved content.(reference links and footnotes have definitions) andrefs. all=defined citations.isall=resolvedLocal content.(relative file links point at files that exist).links. all=exist Item.keeps its values (disposition done,claimed,na,open), whichassert=expressions read.
- Ranges (§5.3).
-
CJK word counting (§4):
wordsused 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 forIntl., whose dictionary-based results vary by ICU version and would make counts non-deterministic across runtimes.Segmenter
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/, bare or inside aname […]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 isunknown-vocabulary; a name outside every official namespace isunknown-rule-set(§5.4); an undeclared mask<token>isinvalid-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… /or quote it:regex/ content.. (#1477)none="@git/ conventional" -
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 reportduplicate-headingonce, 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-sectionfor a required section (required,requiredOrNA, orrequiredWhenwith its condition met),missing-recommended-sectionfor arecommendedone, and otherwiseheading-wrong-level. It is not also reported asheading-wrong-level, nor as amissing-sectionelsewhere in the file. Turningheading-wrong-leveloff therefore never waives a required section. (#1478) -
Block keys and
override(§9).id=<id>on asection,list,table,code,lineormediadirective gives the block a key ([a-z][a-z0-9-]*; ids are unique in the resolved contract, after everyincludeandextends). A block without an id is keyed by its heading text and level.extendsnow applies the child's blocks to the parent's resolved block tree: a child block replaces a parent block only when it is markedoverride, 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 warnsimplicit-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-extendsandinvalid-extends, plus the warningimplicit-override-key. (#1479) -
includeandextendsinteract by key (§9.2). Ids stay unique across includes (including a block that carriesid=twice isduplicate-id). A child canoverridea section its parent included, and a section a child includes collides with a parent block asoverride-requiredunless it carriesoverride. (#1479) -
A bare tick fails (§5.3): when a contract asks for tick evidence (
items.,all=done-or-na items.), a ticked item that offers no evidence at all (no text after a separator, no nested note) reportsnone=bare-tick tick-without-evidenceas 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 warningweak-tick-evidence.Tests passticked is an error;Docs updated: done alreadyticked 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-headingbetween sibling headings,orphaned-marker,crossed-marker) are gone, with nothing in their place: markdownlint's MD040, MD051 and MD024 cover the first three.unclosed-fencestays on every document, because an unclosed fence hides every block after it, and so doesfrontmatter-unreadable(§8.6), which replacesmisplaced-frontmatterandduplicate-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 atomslinks.andall=anchored @markers/, which asked for the removed checks, are no longer retired atoms but unknown rules.pairs lang.is no longer read as "some language": like anyeq=required lang., it asks for a fence whose language is literallyeq=<lang> required. -
Gates (§8.1, §8.2). A check runs
lint,shape, thenstate, and reports only the first gate that fails; a later gate's rules do not run. A diagnostic'sconcernis replaced bygate, one per code, and a result names its failed gate and, once shape passes, carries a readout of what the document claims.unclosed-fenceis alinterror andlabel-line-mismatcha shape error, both warnings before. A value outside a mask token'soneOflist or a column'sin=list is the new state codevalue-not-allowed, no longer a mask mismatch ortable-cell-invalid, and a required front-matter key with no value isvalue-not-given, no longermissing-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-outputon every document:@agent/is an unknown name, andfenced @agent/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 (repair missing-title,missing-context,missing-context-value,regex-input-limit; gateinput) means the check could not run, as a contract error does, and the result namescould-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 isundeclared-code. The capture rule (§8.2) judges every mask slot alike: a value in another form isvalue-wrong-form(shape), no value of its typevalue-not-given(state), one outside its setvalue-not-allowed(state, now also for a front-matter key, replacingfrontmatter-not-allowed-value); a section'scontent.not found ispattern 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-presentandcode-lang-not-allowedare state;pattern-mismatch(acontent.'s form) and the placement codes are shape. New:mask item-mismatch(shape, anitems.item's form),mask thin-definition-itemandmissing-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'slintconfiguration 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 checkunclosed-comment: an HTML comment never closed.fenced-outputalso covers a body fenced whole in amarkdownfence 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'swarn=/error=/off=naming a lint, contract or input code is the new contract errorunlevelable-override, no longer anunknown-override-targetwarning 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.andsentences. eq numberedDeeplimited how the Markdown looks rather than what it holds, andheadings.changed nothing. They are unknown rules now, and so are theclean paragraphsandnumberedDeeptargets. A word floor stays where it stops a token gesture (content.,words. min items.,words. min title.), and so does a contract's size budget for its body (words. min content.,words. max content.). 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 (links. max heading.waswords. max=8 wordCount(heading) <= 8). -
Links are a link checker's (§5.3, Appendix A).
content., 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.links. all=exist content.stays: it asks that a citation names a source, which is what the document claims.refs. all=defined -
Casing is a linter's (Appendix A). The
@glossary/check no longer reports a glossary term written in another case (terms term-casing, markdownlint's MD044proper-names), and a glossary entry has nocasingfield. A banned spelling, whatever its case, is still adeprecatedentry, and@glossary/is unchanged.undefined
Not spec text: #1481 (syntax reference consistency), #1484 (@agent/ 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.orall=done-or-na items., a ticked item with no evidence now fails the check. Write what was run or seen after a separator (none=bare-tick - [x] Tests added: tests/), untick work that was not done, or lower it withfoo. test. ts, 12 pass warn=tick-without-evidenceon the list or configrules. extendsneedsoverride(#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 erroroverride-required; it used to replace the parent block silently. Addoverrideto the directive of every block that is meant to replace a parent block. Give the parent block anid=and the child blockoverride id=<same id>so the link survives a heading edit; anoverridewithout an id still works and warnsimplicit-override-key, naming the key to use. Anoverridein a contract with noextends=isoverride-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,disableandenableare removed. A document could hide its own findings with these comments,unmatched-contractincluded, 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 ismust-wrap. The codesunused-suppression,unknown-suppression-codeandsuppress-missing-reasonare 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.andall=closed level.(and their internal nameseq=N codeFenceClosedandheading.) 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 codelevel=N retired-atomis gone. A fence no longer exposesclosedand a section no longer exposes aleveltarget, so neither comes back as a generic prop. The rule IDordered, which namednumberedin a severity override or config key for one release, is an unknown target too, and with it the coderenamed-rule. - One spelling per prop, with no migration path. Every older spelling that compiled with a
renamed-atomwarning (Appendix A:minWords=,noPlaceholder,forbid=,taskList,noExtraSections,oneOf=[…],occurs=N.,. * define @ns/, …) is now an unknown name, and the codesname renamed-atom,syntax-version-removedandreserved-rule-set-nameare gone with themdv migratecommand. The*.twins are the array form of their mask (anyOf items.), andmask=["<a>", "<b>"] content.is one plural target per kind. Rewrite each spelling by hand from Appendix A.claims. in. kinds=[…] - 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 codesassert-failed,prop-failedandinvalid-expressionare gone. - No experimental opt-in (§0.1). An experimental atom always runs; the config key
experimentaland the codesexperimental-block,unknown-experimental-feature,experimental-not-enabledandexperimental-forbiddenare gone. - Lint and style atoms are unknown rules. Delete
links.,all=anchored @markers/andpairs lang.(now a fence whose language is literallyeq=required required) from a contract, and run a Markdown linter for what they asked. Deleteheading.,words. * title.,words. max items.,words. max content.,paragraphs. * items.,sentences. eq numberedDeepandheadings.; aclean title.orwords. range=a. . b items.becomeswords. range=a. . b title.orwords. min=a items.. Deletewords. min=a content., and run a link checker for what it asked.links. all=exist
Section numbers are unchanged from draft 3.
Specification
mediva is a standard for Markdown contracts. The specification is the source of truth; implementations must pass its conformance suite.
Conformance scenarios
Language-neutral cases any mediva implementation can run: a contract, the documents checked against it, and the verdict each must get.