Declarative YAML reference
The markdown-contract/declarative entry point compiles YAML documents into the
same runtime objects the combinators build. A YAML-authored contract is
indistinguishable downstream: the same typed model and the
same findings as the equivalent
TypeScript contract.
This page is the complete authoring vocabulary for mcVersion: 2. The
vocabulary is deliberately closed — every key documented here is compiled;
anything outside it is a DeclarativeError at load time, not a silently ignored
field.
import { loadContract, // (yamlText) => Contract loadContractFile, // (path) => Contract loadConfig, // (yamlText, baseDir) => CorpusConfig loadConfigFile, // (path) => CorpusConfig} from "markdown-contract/declarative";See the API reference for the runtime surface and getting started for building from source.
Document envelope
Section titled “Document envelope”Every declarative document is a YAML mapping with two required envelope keys. They are validated once, before any kind-specific compilation.
| Key | Type | Meaning |
|---|---|---|
mcVersion |
number | Format version. 2 is the only supported value in this build; any other value (or a non-number) throws. |
kind |
contract | config |
Which compiler the rest of the document is handed to. |
mcVersion: 2kind: contract# … contract body …Editor completion — $schema
Section titled “Editor completion — $schema”The v2 vocabulary is published as a JSON Schema 2020-12 meta-schema at
/schema/mcv2.json. Point yaml-language-server (VS Code’s
YAML extension, and most other editors) at it with a modeline for completion and
inline validation while authoring:
# yaml-language-server: $schema=https://markdown-contract-docs.pages.dev/schema/mcv2.jsonmcVersion: 2kind: contractThe meta-schema is a faithful mirror of the compiler’s closed vocabulary; the compiler remains the source of truth (its errors carry migration hints and did-you-mean suggestions the editor cannot).
Contract document
Section titled “Contract document”A kind: contract document has two optional planes — frontmatter (the YAML
frontmatter schema) and body (the section/structure grammar) — plus an optional
root description. The contract root is closed: any key beyond the envelope
and these three is rejected.
mcVersion: 2kind: contractdescription: a work-item note # the outermost hint fallbackfrontmatter: type: object required: [title] properties: title: { type: string, minLength: 1 } status: { enum: [draft, active, done] }body: order: recognized-relative sections: - section: Summary - section: Detailsdescription is accepted at (almost) every level of a contract — the contract
root, the body root, section/oneOf nodes, content leaves, and schema nodes. It
becomes the hint a finding carries: the nearest enclosing description in scope
at the mint site wins (see findings).
Frontmatter
Section titled “Frontmatter”frontmatter is itself a schema node with an explicit type: object — the
same node vocabulary as any nested object (below). v2 frontmatter is JSON Schema
spelling: properties / required / additionalProperties, not v1’s strict /
fields.
frontmatter: type: object additionalProperties: false # reject undeclared keys (frontmatter/unknown-key) required: [id, status] # everything NOT listed here is optional properties: id: { type: string, pattern: '^D-[0-9A-Z]{4}$' } status: { enum: [open, closed] } reviewer: { type: string, format: email }Schema violations surface as frontmatter/* findings — see
findings.
Schema nodes — the JSON Schema 2020-12 subset
Section titled “Schema nodes — the JSON Schema 2020-12 subset”Each schema node (a frontmatter field, a nested object property, an array
items, a table cell, a list item schema) is a mapping with exactly one
base selector — type, enum, or const — plus that shape’s constraint
keys. Every node also admits default and description.
Base — pick exactly one:
| Key | Value | Compiles to |
|---|---|---|
type |
string | number | integer | boolean | array | object, or the null union [T, "null"] |
The typed schema (per-type constraints below). |
enum |
non-empty list of strings | z.enum([...]) over the listed strings. |
const |
string | number | boolean (validated) | z.literal(value). |
Nullability is the two-element type union — type: [string, "null"] (either
order) — not v1’s nullable: true. Any other union form is outside the subset.
Per-type constraints:
type |
Extra keys | Compiles to |
|---|---|---|
string |
minLength / maxLength / pattern / format |
z.string() (or the named format constructor) with .min() / .max() / .regex(new RegExp(pattern)) chained on. |
number |
minimum / maximum |
z.number() with .min() / .max(). |
integer |
minimum / maximum |
z.int() with .min() / .max() (v1’s int: true). |
boolean |
— | z.boolean(). |
array |
items (element schema, required), minItems / maxItems |
z.array(<items>) with .min() / .max() length bounds. |
object |
properties (map, required), required (list of declared names), additionalProperties (boolean) |
A nested object schema — additionalProperties: false compiles to z.strictObject, otherwise z.object. |
additionalProperties accepts only a boolean in the subset — the JSON
Schema “schema form” (additionalProperties: { type: … }) is rejected.
Named string formats (type: string, format: <name>) — the closed set:
| Group | Formats |
|---|---|
| Web / identity | email, url, uuid, hostname |
| ISO-8601 temporals | datetime, date, time, duration |
| Network | ipv4, ipv6, cidrv4, cidrv6 |
| Id forms | nanoid, cuid, cuid2, ulid |
| Misc | base64, emoji, e164 |
An unrecognized format is a DeclarativeError listing the allowed set.
default — a documented divergence. A default actively fills the
value when the key is absent: the compiled Zod schema substitutes it, so the
typed model reads the default back. This is the
ajv useDefaults: true stance, not vanilla JSON Schema’s annotation-only
default. A defaulted property never needs to be in required.
description on a schema node is stored via .describe() and becomes the
hint on findings that fail that node (e.g. a missing required field carries
its own description; see findings).
properties: title: { type: string, minLength: 1 } owner: { type: string, format: email } version: { type: string, pattern: "^\\d+\\.\\d+\\.\\d+$" } weight: { type: integer, minimum: 0, maximum: 100 } due: { type: [string, "null"], format: date } tags: { type: array, items: { type: string }, minItems: 1 } status: { enum: [draft, active, done], default: draft } meta: type: object additionalProperties: false properties: created: { type: string, format: date, description: creation date, ISO }What a schema node rejects
Section titled “What a schema node rejects”The vocabulary is closed per node shape (v1 silently ignored unknown schema keys; v2 does not). After the base is picked, every present key must be in that shape’s allowed set. Rejections speak three distinct dialects:
-
A v1 spelling gets a migration hint naming the v2 form:
frontmatter.tags: 'of' is the v1 spelling — v2 uses 'items' (see the v1→v2 codemod) -
Recognized JSON Schema outside the subset is named as such:
frontmatter.id: 'oneOf' is JSON Schema outside the supported v2 subset -
Anything else is an unknown key, with a did-you-mean suggestion when a supported key is within edit distance 1–2:
frontmatter.title: unknown key 'minLenght' (did you mean 'minLength'?)
What’s outside the subset — recognized JSON Schema 2020-12 keywords that are
deliberately rejected by name (class 2 above): composition (oneOf, anyOf,
allOf, not, if / then / else), references ($ref, $defs, $id,
$schema, $comment), tuple/array extras (prefixItems, contains,
minContains, maxContains, uniqueItems — note minContains / maxContains
ARE the body grammar’s occurrence keys,
just not schema-node keys), object extras (patternProperties, propertyNames,
minProperties, maxProperties, dependentRequired, dependentSchemas,
unevaluatedProperties), numeric extras (multipleOf, exclusiveMinimum,
exclusiveMaximum), content/annotation (unevaluatedItems, contentEncoding,
contentMediaType, title, examples, deprecated, readOnly, writeOnly).
body is a mapping describing the document’s section structure. It compiles to
the sections(opts, specs) grammar.
Level options (on body and on any hoisted nested level):
| Key | Value | Meaning |
|---|---|---|
order |
none | recognized-relative | strict |
How strictly section order is enforced (structure/section-order). |
additionalSections |
boolean | Whether headings not named by the grammar are tolerated (v1’s allowUnknown). |
sections |
list of nodes (required) | The ordered section grammar (below). |
description |
string | The level’s hint — carried by findings minted at this level with no nearer description. |
The body root additionally admits requires / forbids.
Section-node grammar — each entry in sections is a mapping with exactly
one of section, oneOf, or gap:
| Node key | Value | Compiles to |
|---|---|---|
section |
heading name (string) | section(name, opts). |
oneOf |
non-empty list of section names | oneOf(names, opts) — one of several allowed headings. |
gap |
empty, or { min?, max? } |
gap(...) — an unconstrained run of intervening content. A gap node admits only the gap key. |
Options on a section / oneOf node:
| Key | Value | Meaning |
|---|---|---|
aliases |
list of strings (section only) |
Alternate spellings accepted for the heading. (A oneOf IS an alias set, so it takes none.) |
anchor |
string | Require a ^anchor on the section heading (structure/anchor-missing). |
minContains / maxContains |
non-negative integers | The occurrence window (below). |
content |
leaf or named-leaf map | Content requirements for the section body (below). |
sections (+ order, additionalSections) |
a hoisted nested level | Sub-sections, directly on the node (below). |
requires / forbids |
match-spec lists | Node-local text constraints. |
description |
string | The section’s hint. |
Occurrence — minContains / maxContains
Section titled “Occurrence — minContains / maxContains”v2 spells occurrence with JSON Schema’s counting names, replacing v1’s
optional / repeatable / min / max:
| Declared | Window | Meaning |
|---|---|---|
| (both absent) | exactly once | A plain slot — the section must appear exactly one time. |
minContains: 0, maxContains: 1 |
0–1 | Optional, at most once. |
minContains: 0 |
0–∞ | Optional and repeatable. |
maxContains: 5 |
1–5 | Required, up to five occurrences. |
minContains: 2 |
2–∞ | At least twice, unbounded. |
minContains: 3, maxContains: 3 |
exactly 3 | A fixed count. |
The defaults once either key appears: minContains defaults to 1,
maxContains defaults to unbounded. Bounds must be non-negative integers
with maxContains ≥ 1 (to forbid a section, leave it undeclared) and
maxContains ≥ minContains. Repeat-count violations surface as
structure/repeat-count.
Nested sections — hoisted
Section titled “Nested sections — hoisted”A nested level sits directly on the section node: sections (plus optional
order / additionalSections) — v1’s children: wrapper is gone. Declaring
order or additionalSections on a node without a sections list is a
compile error (level knobs with no level to govern).
- section: Example order: none sections: - section: Input - section: OutputContent leaves
Section titled “Content leaves”A section’s content is either a single leaf (a one-key mapping) or a
named-leaf map — a record of ^anchor-named leaves. Each leaf is one of:
| Leaf | Value | Compiles to |
|---|---|---|
maxWords |
number | maxWords(n) — word budget for the section (content/max-words). |
code |
empty, or { lang?, description? } |
code({ lang }) — require a fenced code block, optionally of a given language. |
table |
see below | table(...) — a table with named columns. |
list |
see below | list(...) — a list with item constraints. |
table config:
| Key | Value | Meaning |
|---|---|---|
columns |
list of strings (required) | Expected column headers. |
cells |
map column → schema node |
Per-column cell schema (the v2 schema subset). |
minRows |
number | Minimum data-row count. |
anchor |
string | Require a ^anchor on the table. |
extraColumns |
ignore | error |
How to treat columns beyond columns. |
description |
string | The leaf’s hint. |
list config:
| Key | Value | Meaning |
|---|---|---|
items |
checkbox | a schema node |
Constrain every item — a task-list checkbox, or a v2 schema each item’s text must satisfy (v1’s everyItem). |
minItems |
number | Minimum item count. |
ordered |
boolean | Require an ordered (true) or unordered (false) list. |
description |
string | The leaf’s hint. |
body: order: strict additionalSections: false sections: - section: Overview description: what and why, briefly content: { maxWords: 200 } - section: Tasks minContains: 0 maxContains: 1 content: list: { items: checkbox, minItems: 1 } - section: Fields content: table: columns: [name, type, required] minRows: 1 cells: required: { enum: ["yes", "no"] } - section: Example order: none sections: - section: Input - section: OutputConfig document
Section titled “Config document”A kind: config document is the data form of defineConfig({ rules }). It
compiles to the runner’s CorpusConfig — a list of routing
rules mapping globs to contracts.
| Key | Value | Meaning |
|---|---|---|
rules |
list (required) | Ordered routing rules (below). |
contracts |
map name → path |
Optional named-contract lookup, so a rule can reference a contract by name. |
Each rule:
| Key | Value | Meaning |
|---|---|---|
include |
non-empty list of globs (required) | Files this rule matches. |
exclude |
list of globs | Files to remove from the match. |
contract |
name | .yaml path | inline contract |
The contract to validate matched files against. |
Contract references resolve three ways:
- An inline mapping (
{ description?, frontmatter?, body? }) — compiled directly viacompileContractObjectwith the v2 compilers, no envelope needed. - A name — looked up in the
contractsmap; the mapped value is a path. Any string contract ref (a name or a path) also becomes the rule’s label in the CLI run summary. - A path — a string that is itself a
.yaml/.ymlpath.
Relative paths resolve relative to the config file’s directory (via
loadConfigFile); absolute paths are used as-is. A ref that is not a .yaml /
.yml file — e.g. a code-authored .js / .ts contract module — is rejected;
that is the deferred code escape hatch, out of scope in the declarative format.
mcVersion: 2kind: configcontracts: capability: ./contracts/capability.contract.yamlrules: - include: ["capabilities/**/*.md"] contract: capability # by name - include: ["tasks/**/*.md"] exclude: ["tasks/_archive/**"] contract: ./contracts/task.contract.yaml # by path - include: ["notes/**/*.md"] contract: # inline frontmatter: type: object properties: title: { type: string, minLength: 1 }Section text constraints — requires / forbids
Section titled “Section text constraints — requires / forbids”A section / oneOf node, and the body root, may carry requires: /
forbids: lists that constrain the section’s (or the whole document’s) text.
Each is a list of match specs compiled onto the requires(...) /
forbids(...) / textRule builders — the data-authoring twin of the TS text
predicates (D-0011 / C-0009). The vocabulary is unchanged from v1.
- On a section node, they compile to node-local rules over that section’s subtree.
- On the body root (sibling of
sections:), they compile to one document-scopedtextRule(aDocRulewhose id istext/doc).
Either surface emits the same findings as the equivalent TS builder: a missing
required phrase is text/requires, a present forbidden phrase is text/forbids,
and a violated min / max occurrence bound is text/count.
Match-spec vocabulary (closed — any other key is a DeclarativeError):
| Key | Value | Meaning |
|---|---|---|
pattern |
string | A literal needle. |
regex |
string | A regex source. Exactly one of pattern / regex is required. |
normalize |
boolean | Whitespace-fold the literal before matching. |
ignoreCase |
boolean | Case-insensitive match. |
min / max |
number | Count bounds on occurrences (text/count). |
id |
string | Override the synthesized finding id. |
note |
string | Attach a note to the finding. |
level |
error | warn |
The finding level. |
body: requires: - pattern: "Decision" sections: - section: Summary requires: - pattern: outcome forbids: - pattern: TBD level: warnThe compiler rejects authoring mistakes up front rather than letting them surface
as confusing findings: a duplicate matcher within one list (same needle /
normalize / ignoreCase), a requires / forbids contradiction over the
same literal pattern at one scope, or a count max below its effective floor.
Contradiction and duplicate detection over regex needles is byte-identity only
— no overlap analysis.
YAML ⇄ TypeScript parity
Section titled “YAML ⇄ TypeScript parity”The declarative front end is a pure front end. A Contract from loadContract
is the same object contract(...) builds from the combinators — same typed
model, same emitted findings (identical ids, levels, positions, messages). A
contract that authors no description produces findings byte-identical to its
combinator twin (no hint key at all). Pick whichever authoring surface fits:
data (YAML) for contracts that ship as config, or code (TS) when you need the
deferred escape hatches.
See the API reference for the combinator surface, and the authoring examples for worked contracts and corpus configs.