A Rust CLI conformance engine for typed markdown: JSON Schema over frontmatter, nine data-driven check families, and a small Schematron-derived DSL for cross-file rules. Descended from XML's Schematron (ISO/IEC 19757-3).
mdatron checks markdown documents on two axes:
- Structure. JSON Schema (draft 2020-12) over the frontmatter — the schema
family. Required fields, enums, types,
additionalProperties: false. Universal vocabulary; zero learning curve for anyone who has authored an OpenAPI schema, a Kubernetes CRD, or a tsconfig. - Semantics. Cross-field, cross-file, and cross-document constraints — the eight further check families, driven by adopter data, plus a small Schematron-derived rule DSL over frontmatter. The 80% of validation value that JSON Schema cannot express: "the number of rows in this table matches the count declared in frontmatter," "every owner listed here appears in the team registry," "every link target resolves to a heading in the project."
Where mdatron fits relative to neighbouring tooling: markdownlint enforces
style; Vale catches prose-quality concerns; dprint and mdformat reformat;
mdatron is the only conformance engine built around the typed-frontmatter + cross-
document rules pattern. Errors are rustc-shaped — codes, source spans,
= help: hints, = explain: references to per-code prose, structured JSON
output for machine consumers.
Why it matters now: when agents write and read your documents, a document stops being prose and becomes cache — derived state that everything downstream trusts. Observability Engineering (2nd ed.) frames the test for any such document: can you validate it where it lives? Can your coworker? Can an agent? mdatron is the "yes" to all three — the validation runs in CI, from one committed configuration, with output built for both human and machine readers.
mdatron ships as an early 0.x crate — usable now, with the CLI and the
--json diagnostic contract still evolving under SemVer (pre-1.0, a breaking
change moves the 0.MINOR position, so a caret on the previous minor will not
silently pull a breaking one). Install from crates.io:
cargo install mdatron --locked
Pin the minor to avoid an unintended breaking upgrade in CI (the pin below is rendered from the crate version, and a test holds it there):
cargo install mdatron --locked --version "0.7"
--locked pins transitive dependencies to the shipped Cargo.lock; recommended
for reproducible builds. To build from a checkout instead (the clone creates a
mdatron/ directory, so --path mdatron resolves to it):
git clone https://github.com/magnificentlycursed/mdatron
cargo install --path mdatron --locked
mdatron --version
Scaffold with mdatron init, which deploys the .mdatron/ skeleton — the
schemas/ and patterns/ directories, a seeded config.yaml (adopter-owned
from then on), a seeded .gitattributes that keeps the files mdatron hashes
LF on every checkout, the init manifest (the record of the engine-managed partition),
and one inert *.example template per family file (routes, pins,
vocabulary, code-catalogs, links) whose header states its activation rule, keys,
scope, and codes — copy one to its real name to activate that family, or read
mdatron docs inputs (a tree initialized before a template existed gains it
on its next mdatron init, which also refreshes any template you have not
edited to the running version's content — forward only from 0.8.0 on: an
older mdatron leaves a template a newer one recorded as it is):
mkdir my-typed-docs && cd my-typed-docs
mdatron init
config.yaml's file_globs are your declared jurisdiction: mdatron walks
only what they claim, so third-party markdown deployed into your tree by other
tools is never mdatron's to refuse. A tree with no config refuses loudly
(no jurisdiction declared) rather than guessing — pass explicit --files
globs for an ad-hoc run without one. Re-running init is a no-op on an intact
tree; a hand-modified managed file is refused with MDATRON-E0060, and
mdatron verify reports the same drift, so a CI that runs only verify sees
it.
Drop a JSON Schema at .mdatron/schemas/blog.json (a schema example follows
below), drop a markdown file with matching frontmatter inside your globs, and
run:
mdatron verify
A clean run looks like:
$ mdatron verify
mdatron verify: clean
A run with diagnostics emits rustc-shape blocks on stderr and exits 1. Your document's own text never rides inline in an engine line — it renders as a prefix-marked quoted block:
$ mdatron verify
error[MDATRON-E0050]: frontmatter-schema-violation
--> bad.md:3:1
= note: unexpected property not permitted by the schema
= unexpected:
> extra
= explain: mdatron explain MDATRON-E0050
mdatron verify: 1 error(s), 0 warning(s) across 1 finding(s)
The = explain: line is copyable: paste mdatron explain MDATRON-E0050 into
your shell to read the per-code prose — it names the fix (here: remove the
undeclared property, or declare it in the schema). Apply it and re-run:
mdatron verify → clean. That loop — violation, coded diagnostic, explain
page, fix, clean — is the working rhythm, for you and equally for an agent
reading the machine forms below. Pipeline failures (missing config,
malformed pattern file, IO failure) print on stderr and exit 2. The exit
contract is exactly three values: 0 clean, 1 findings at error severity,
2 pipeline failure — anything else (a raw panic code, a signal abort) is an
engine defect, never a documented mode; please report it. Two machine
forms share the same findings: --json emits a single output object on
stdout, and --compact emits one size-capped block per finding (512 bytes,
a contract limit) for agent-context consumers; add --quiet to silence the
stderr rendering (and, under --json, to keep stdout the only stream).
The --json envelope is a published, versioned contract (mdatron_output_version,
currently 3.1.0). The load-bearing fields for a machine consumer:
pipeline_status—"ok"or"failed"; on failure,pipeline_error{code, kind, message}carries the reason in-band (it survives--quiet),kinddisambiguating the failure class (config,io,bound_exceeded, …).summary.files_checked— the true count of files validated (a clean run over N reports N, not 0).families— each of the nine check families (schema, route, pin, vocabulary, citation, link, marker, code_catalog, section) and the rule-DSL lane (rule_dsl) as{state, reason}(active/inert/inactive), so "checked N, all clean" is distinguishable from "checked nothing"; the object is forward-extensible, so a consumer must tolerate unknown family keys. On a failed pipeline every family readsinactiveandinputsis{}(partial lineage is deliberately not attested), so read an assertion such asfamilies.link.state == "active"together withpipeline_status.findings[].code/.severity/.location; andfindings[].quoted[], which carries adopter-derived text markedorigin: "adopter",trusted: falseso a consumer never mistakes document content for engine output.findings[].fingerprint— a line-churn-stablev1:identity for cross-run trending: the same defect keeps the same fingerprint across reflows and across operating systems, so a baseline or a trend line keys on it, not on line numbers.envelope_schema— the published schema's$id, verbatim: the exact contract this envelope was produced under.inputs— governance-input lineage: each configuration input the run actually read, mapped to asha256:digest of the bytes it consumed — attest which configuration produced a verdict, and detect config drift between runs.timings— optional, only underverify --json --timings: per-phase wall-clock (load/capture/check/total, milliseconds). Flag-gated so the default envelope stays byte-identical across runs on an unchanged tree (determinism is a contract property).
Pin and validate against the schema at
schema/mdatron-output.schema.json; a
binary-only install can print it with mdatron envelope-schema. mdatron explain --list
enumerates every diagnostic code; mdatron explain <code> (the short form
E0050 works too) shows a code's page.
For fast local runs, mdatron verify --changed <file> verifies only the changed
file plus its transitive dependents (a .mdatron/ change falls back to
whole-tree); whole-tree verify remains the gate and CI mode.
Wire mdatron verify into your pre-commit hook so typed-document errors block
the commit that introduces them. Make the wrapper fail closed: if the
mdatron binary is missing — not yet installed, off PATH, or absent from the
hook's shell environment — block the commit rather than skip the check
silently. A checker that silently skips is invisible in exactly the moment it
is needed.
#!/bin/sh
# .git/hooks/pre-commit (or your pre-commit-framework entry)
if ! command -v mdatron >/dev/null 2>&1; then
echo "pre-commit: mdatron not found on PATH — refusing to commit unverified." >&2
echo " install it (see Install above) or bypass explicitly with 'git commit --no-verify'." >&2
exit 1
fi
mdatron verifymdatron verify exits 0 when clean, 1 on findings, and 2 on a pipeline
failure (see First run). Add --deny-warnings (alias --strict) to also fail a
warnings-only run (exit 0 → 1) — the switch a hard CI gate wants when warnings
must block. In particular, MDATRON-W0048 (a reference check skipped over a
present-but-unverifiable target — non-UTF8, unreadable, oversized) is a warning
by design; a gate that must not pass an unverified reference needs this switch
(this repo's own self-validation CI job uses it). The wrapper above blocks on all three of a missing
binary, findings, and pipeline failure. Reserve git commit --no-verify for a
deliberate, visible bypass rather than letting a missing checker pass unseen.
A minimal blog-post schema that requires schema_class, title, and
published_on; rejects extra frontmatter fields:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["schema_class", "title", "published_on"],
"properties": {
"schema_class": { "const": "blog" },
"title": { "type": "string", "minLength": 1 },
"published_on": { "type": "string", "format": "date" }
},
"additionalProperties": false
}A markdown post that satisfies it:
---
schema_class: blog
title: hello mdatron
published_on: 2026-06-07
---
# Hello, mdatron
This file's frontmatter binds to the `blog` schema because the
`schema_class:` field selects it.Drop both files into a project, run mdatron verify, and the file passes
the schema family. Add a frontmatter field the schema does not allow (e.g.,
extra: "nope") and it emits MDATRON-E0050: frontmatter-schema-violation. Run mdatron explain MDATRON-E0050 for the
per-code prose.
JSON Schema is great for shapes but cannot express "this published_on must
not be in the future." That is rule-DSL territory — DSL patterns at
.mdatron/patterns/<name>.yaml:
mdatron_dsl_version: 1 # optional (absent = v1); unsupported versions refuse at load
pattern:
id: blog-validation
rules:
- id: title-must-be-set
context: blog
assert: $self.title != ""
code: BLOG-W0001
message: "title is empty for the blog post"context: blog selects every file whose frontmatter schema_class is blog.
assert: fires the diagnostic when the expression evaluates to false. See
docs/dsl-reference.md for the complete,
canonical operator + function inventory — including every, some, in,
not_in, filter, let: bindings, defined, count, len, union,
intersect, difference, concat, join, and the path-confined key()
cross-file index mechanism (this list is illustrative, not exhaustive). The
DSL's scope is
cross-file and registry validation; body-content extraction functions are
out of scope.
The schema family is the first of nine check families. Beyond it, eight
generic families activate on adopter data under .mdatron/ — each
inactive until its data exists, each strict-parsed, every path confined to the
governed tree:
Routes (routes.yaml) — the closed-world allowlist, and the gateway to
four more families: the moment routes.yaml exists, every walked file must be
claimed by exactly one route (E0030 unclaimed, E0032 claimed twice; no
catch-all, no precedence), each route names the governed_by document it
answers to (E0031 if it cannot be opened), and citations, links,
marker_rules, and section_rules exist only on a route. "Supplied" means the
file exists — routes: [] activates the closed world and is announced as
W0053; delete the file to deactivate the family. A route whose glob claims no
walked file is announced as W0054. Required and optional keys:
routes:
- files: "docs/adr/**/*.md" # required: root-relative glob (`*` one segment, `**` any depth)
governed_by: DESIGN.md # required: must open inside the governed tree
naming: "^[0-9]{4}-[a-z0-9-]+\\.md$" # optional: filename grammar (W0041)
citations: true # optional: citation family, see below
links: true # optional: link family, see below
link_root: true # optional: resolve /root-relative links (needs links)
# link_policy: {schemes: [https], hosts: [...], forbid_query: ["^utm_"]} # optional: absolute-URL policy (needs links)
# marker_rules: [...] # optional: marker family, see below
# section_rules: [...] # optional: section family, see belowA first routes.yaml for a tree with one governing document — everything
claimed, nothing opted in yet:
routes:
- files: "**/*.md"
governed_by: README.mdWith routes supplied: an unclaimed walked file blocks (E0030), a route
citing an absent governing document blocks (E0031), two routes claiming one
file is an error (E0032), a filename underivable from the naming grammar
warns (W0041), an empty table warns (W0053), and a route claiming no walked
file warns (W0054).
Pins (pins.yaml) — governing documents pin sha256 over governed files:
pins:
- governed_by: DESIGN.md # required: the governing document
file: src/codes.rs # required: the governed file, root-relative
sha256: "…" # required: `mdatron pin --update` writes it
- governed_by: contract.md # a second shape: pin ONE section, not the whole file
file: plan/build-plan.md
section: "## Decomposition" # optional: the heading whose span is hashed
sha256: "…"
# unpinned: # optional tombstones: file, governed_by, reason, owner (all required)A governed-file change with a stale pin fails (E0061) until you re-read the
governing document and re-pin: mdatron pin --update (preview with
--dry-run; bare mdatron pin checks). Add section: to scope a pin to one
heading-delimited span — the hash covers that heading through just before
the next heading of the same or higher level, so an edit elsewhere in the file
doesn't trip it; a section: whose heading can't be found is loud (E0063).
Un-pinning persists as a justified unpinned: tombstone — justified means it
carries both a reason and an owner — that stays loud as an informational
lint (L0001); one missing either field warns (W0042):
unpinned:
- governing: contract.md
file: plan/build-plan.md
reason: "plan superseded by ROADMAP.md 2026-09-01; kept for history"
owner: "docs-maintainers"Vocabulary (vocabulary.yaml) — registry-driven prose scan. A coined term
with a definition but no codified governance drifts until it is lost — the
authors of Observability Engineering coined "observability" precisely and
watched vendors redefine it out from under them; this family is that
codification for your project's own terms. The registry:
mdatron_format_version: 1 # optional (absent = 1)
terms: # optional; per term: term, status, sense all required
- term: "jurisdiction"
status: registered # a coined term, formally registered
sense: "the file set the declared file_globs walk"
- term: "spend shape"
status: draft # draft terms are exempt from strict findings
sense: "how a review round's agent budget is declared"
- term: "contract"
status: reserved # reserved: use outside the sense flags E0092
sense: "a versioned behavioral commitment, never a soft promise"
coinage_globs: # optional: where **bold** introduces a term (E0090);
- "docs/spec/**/*.md" # absent = wherever the register scans
label_schemes: # optional; the cluster scan is active only when allow is non-empty
allow:
- "^C\\d+$" # local scheme: C1, C2, ...
anti_patterns: # optional; per entry: pattern, guidance both required
- pattern: "very unique"
guidance: "say 'unique' — uniqueness does not grade"
numeric_claims: # optional; per entry: field required
- field: itemsWhat it flags: unregistered
bold-introduced coinages (E0090, draft-status exempt — inside coinage_globs
when the registry sets them, since bold-means-coinage rarely holds across a
whole corpus of **Label:** lead-ins and emphasis), letter-plus-number
label clusters outside your allowlist (E0091 — structured reference-IDs
REQ-<n>/AC-<n>/ADR-<n>/RFC-<n>/Q<n> are exempt by default and unioned
with your allowlist, so specs validate out of the box; add local schemes like
^C\d+$ to label_schemes.allow), reserved-word use (E0092),
listed register anti-patterns (E0093), and numeric claims — a prose numeral
restating a configured frontmatter field's count and drifting from it
(E0094). By default the scan covers every walked file; set vocabulary_globs
in config.yaml (a scope list beside require_frontmatter) to restrict it —
e.g. to apply the register to your live specs while leaving a historical archive
walked and routed but unscanned. A vocabulary_globs or coinage_globs that
matches nothing is loud (W0043), so a mistyped glob can't silently disable a
check. A term
declared both registered and draft resolves to draft with a warning
(W0044), so a conflicting declaration is surfaced, not silently resolved.
Citations — route-attached: the opt-in exists only on a route in routes.yaml,
so writing that route puts every walked file under the closed world (see
Routes). Data-less; opt a route in with citations: true and its
files' path:line / path:start-end references are verified against the
working-tree snapshot (uncommitted content counts; no git subprocess): a dead citation
blocks (E0100), one past the target's end blocks (E0101). Historical
corpora simply don't opt in.
Links — route-attached: the opt-in exists only on a route in routes.yaml, so
writing that route puts every walked file under the closed world (see
Routes). Data-less by default (the register below is optional); opt a route in with links: true and its files' inline
markdown links are resolved against the working-tree snapshot via a CommonMark parse
(pulldown-cmark), so inline [text](target), reference-style
[text][ref], image  and autolink <https://…> / <x@y.z>
destinations are all checked, while a link
inside an inline `code` span or a fenced block is a syntax example and
skipped. A link to a relative path that isn't there blocks (E0110); an existing
markdown target — or the same document — whose #fragment matches no heading
blocks (E0111, fragments resolved with GitHub's heading-slug rules, covering
ATX + setext headings, duplicate-heading -N suffixes, and explicit HTML
anchors). Targets resolve document-relative (as GitHub renders them):
[api](api.md) from docs/guide.md is docs/api.md, and a one-level
../README.md that stays in-tree is fine — only a target that escapes the
governed tree is refused (E0010/E0011/E0012). Destinations are
percent-decoded first (my%20doc.md → my doc.md), and link_root: true
opts a route into resolving a leading-slash /docs/x.md from the project root
(still confined) for static-site corpora that author links that way. An
absolute URL (any scheme, or //host) is never fetched — mdatron does not
reach the network, by design — but it is checked offline: a destination that
is not a well-formed URL blocks (E0117, before any liveness check could
run); with a .mdatron/links.yaml register of the URLs the corpus may
point at, an undeclared URL blocks (E0115) and an undeclared #fragment on a
declared page blocks (E0116, the entry's fragments being the anchors you
confirmed on the live page); and a route's link_policy (schemes, hosts,
forbid_query) blocks a link outside it (E0118). On a whole-tree run from config.yaml's jurisdiction, a
register no link-checked file consults warns (W0056), as does an entry no
link uses (W0057). mdatron links --external exports every markdown link
to an absolute URL — fragment included; --json adds file and line — for
the liveness tool that does fetch (lychee or its class), and the register is
the list such a tool checks on a schedule. A bare URL in prose or a raw HTML
<a href> is not a markdown link: it is neither checked nor exported.
mdatron_format_version: 1 # required on this file (born in 0.8.0)
links:
- url: https://docs.example.com/ # every URL under this prefix…
prefix: true
- url: https://example.com/reference # …this page only…
fragments: [installation, usage] # …and only these anchors on itMarkers — route-attached: marker_rules exist only on a route in
routes.yaml, so writing that route puts every walked file under the closed
world (see Routes). Data-less; give a route one or more marker_rules and each body
line matching a rule's pattern names a reference whose captured <name> must
resolve to an existing element in a rule-named target doc — the name-anchor
sibling of citations. A rule is { pattern, element, target_doc, target_section? }:
routes:
- files: "plan/**/*.md"
governed_by: contract.md
marker_rules:
- pattern: "^Provenance: (.+)$" # required: first capture = the referenced name
element: list-item-bold-name # required: an element class (see Section rules)
target_doc: contract.md # required: root-relative, confined
target_section: "## Decomposition" # optional: scope to this heading's spanelement: list-item-bold-name resolves the name against the leading **bold**
of a - list item (- **Slice 1 — …the guardrail.** ← Provenance: Slice 1 — …the guardrail); heading resolves against heading text. Resolution is
name-equality, a trailing . on the target tolerated (not slug-based). A
reference that resolves to nothing blocks (E0112) — as does a matched line
whose capture group captured nothing (an optional group that didn't
participate); the target_doc is project-root-relative and confined
(E0010/E0011/E0012). A target_section whose heading is absent from the
target doc blocks once per governed file (E0114, marker-target-section-not-
found) and that rule's references in the file are not evaluated until the
target heading is restored (the file already blocks, so nothing is lost) — a
renamed target heading never
mass-flags healthy references. Two misconfigs are refused at load: a pattern
with no capture group (nothing to resolve), and a target_section that is not
a full ATX heading line with non-empty text.
Code catalogs (code-catalogs.yaml) — the adopter-side twin of mdatron's
own every-code-resolves-in-explain: declare your code namespace and every code
token cited in the corpus must resolve to it. Required and optional keys:
mdatron_format_version: 1 # required on this file (born in 0.6.0)
catalogs:
- namespace: "ADOPTER-" # required: the ownership prefix
comprehensive: true # optional (default false): sole authority for the prefix — only then can a token orphan (E0113)
codes: ["E0010", "W0180"] # required: the declared legal set (class letter + digits)The scan covers every walked file by default, and inline code spans are
scanned (a backticked code is a real citation; only fenced blocks are
examples). To keep a comprehensive catalog beside a walked archive that cites
retired codes, scope the scan with code_catalog_globs in config.yaml (a
scope list beside vocabulary_globs); a scope that matches no walked file is
loud (W0055) and the family reports inert.
Every adopter input file carries mdatron_format_version — the input
contract's own version axis (independent of the DSL's mdatron_dsl_version
and the JSON mdatron_output_version), so a future format change breaks
legibly instead of mis-parsing silently. It is required on files born versioned
(this one, 0.6.0; links.yaml, 0.8.0) and optional on routes.yaml/vocabulary.yaml/
pins.yaml (absent = the v1 legacy baseline; a 0.5.0-authored file still
parses); pin --update stamps it going forward.
Under a comprehensive catalog, a cited token that isn't declared blocks
(E0113, orphaned-adopter-code) — the fix for codes left dangling after a
defining doc is sunset. The detector is intentionally broader than the legal
grammar (prefix + a digit-bearing tail), so a mistyped or unknown class is
caught rather than silently skipped; the codes: list, not the detector, is
the closed legal set. Set comprehensive: false if codes for the prefix may
legitimately live outside the catalog.
Section rules (a section_rules: block on a route in routes.yaml, the
sibling of marker_rules — route-attached, so writing that route puts every
walked file under the closed world; see Routes) — declarative count, every, order and disjointness
assertions over markdown body sections (the body-content counterpart of the
DSL's frontmatter arity rules), scoped by the route's files glob so a
file-specific structural invariant lives on the route that claims those files
and can't misfire corpus-wide:
routes:
- files: "plan/**/*.md"
governed_by: ROADMAP.md
section_rules:
# a count rule: element, match, count
# (at least one open-phase H3 in ## Requirements)
- section: "## Requirements"
element: h3
match: '^### Phase \d+: .*\((parallel|sequential)\)$'
count: ">= 1" # one of >= <= == != > < and an integer
# no section: the whole document (here: the file is not empty)
- element: line
match: "."
count: ">= 1"
# an every rule: each list item in the section is a link
- section: "## References"
element: list-item
every: '^- \[[^\]]+\]\([^)]+\)'
# an order rule: the summary blockquote comes before any phase heading
- section: "## Requirements"
order:
- element: blockquote
match: "."
- element: h3
match: "."
# a slice is open XOR complete — ids extracted per element, never a full-span scan
- disjoint:
- section: "## Requirements"
element: h3 # ids from the H3 heading text
id_pattern: 'Slice (\d+)'
- section: "## Completed phases"
element: list-item-bold-name # ids from the `- **bold**` lead
id_pattern: 'Slice (\d+)'A count rule counts the elements of the element class in the section's span
(until the next heading of the same or higher level) whose line matches match
(or whose name does, with match_on: name — the heading text, the list item's bold name or its
text, the quoted text; the same text a disjoint operand's id_pattern sees),
and asserts the count predicate — one of >=, <=, ==, !=, >, < and
an integer (>= 1, == 1, < 3); a violation is E0120.
element is the one vocabulary marker rules use too: heading (a heading of
any level), h1…h6 (one level), list-item-bold-name (the **bold** lead
of a - list item), list-item (any bullet or numbered item), blockquote
(a > line), or line (any non-blank line). Elements are lines, recognised
by the line's own prefix, and a line inside a fenced code block is not one
(docs/inputs.md has the exact rules and their limits). An every rule requires each
element of the class to match its every pattern (E0123 for each that does
not). An order rule lists items of element and match; an element
matching an earlier item must not follow one matching a later item (E0124);
a budget rule bounds a section's bytes (max_bytes, E0125) or each
paragraph's (max_bytes with per: paragraph, E0126) — for readers that
inject a section or a page only up to a size;
an element belongs to the first item it matches. A section's own heading
line is its container, never one of its elements.
On a count, every or order rule section is optional: without it the rule
covers the whole document body (after the frontmatter and a leading
byte-order mark, fenced code excluded). A disjoint rule extracts an id (the id_pattern's first
capture) from each section's declared element and asserts the two sets share
none; an overlap is E0121. Ids come only from the declared element (an h3
heading's text, or a list-item-bold-name bullet's bold name), never
surrounding prose and never the section's own heading line — so a body mention
of an id doesn't cause a false overlap. A section spec that matches no heading in the
document blocks (E0122, section-not-found; matching is exact on level and
text) instead of silently passing, and the assertion is not evaluated; when a
heading occurs more than once, the rule evaluates over all matching spans
(counts sum, ids union, an every rule checks each span's elements, an order
rule orders each span on its own), so content under a duplicate heading can't
evade the gate. A spec that is not a full ATX heading line with non-empty text is
refused at load.
Every family code has an explain page: mdatron explain MDATRON-E0061.
The adoption sequence — each step optional after the first, except that step 3 is the prerequisite of the four route-attached families:
mdatron init— scaffold; scopefile_globsinconfig.yamlto your typed corpus (your jurisdiction).- Add a schema per
schema_classunder.mdatron/schemas/; opt strictness in withrequire_frontmatterglobs inconfig.yaml(W0040flags a governed file that silently lacks frontmatter). - Route your corpus (
routes.yaml) to its governing documents; add anaminggrammar if filenames are a contract. This is the gateway: citations, links, marker rules, and section rules exist only on a route, and the momentroutes.yamlexists every walked file must be claimed by exactly one route (E0030otherwise) — so the first route you write for any of them is a route table over the whole jurisdiction. - Pin what governs you (
pins.yaml+mdatron pin --update) so governed drift blocks instead of rotting. - Wire the fail-closed pre-commit hook (next section) and a CI job that
builds mdatron and runs
mdatron verify --project-root .— a repository that verifies itself is the intended end state (this repo's ownself-validateCI job is the worked example).
DESIGN.md— the standing design: behavioral contracts, the nine check families, output marking discipline, path confinement, governance-data governancedocs/dsl-reference.md(mdatron docs dsl) — the complete rule-DSL construct inventory with evaluation semantics; held to the implementation continuously by CI tripwires (the construct-inventory check and the operator-semantics pins), so an engine construct absent from the reference — or reference semantics the engine does not implement — fails the builddocs/inputs.md(mdatron docs inputs) — every.mdatron/input file in one place: shape, required and optional keys, activation, scope, codesdocs/cookbook/— recipes for real agent-configuration standards, each with a runnable example project underexamples/standards/, the sources and revisions it is evaluated against, what it cannot check, and the actual output of each silent failure it catches (all compared with the examples in CI); recipes so far:SKILL.md(Agent Skills, Claude Code, Codex), Claude Code subagents, GitHub Copilot.instructions.md, andllms.txtdocs/faq.md(mdatron docs faq) — prior-art comparisons, influences, and frequently asked questionsdocs/limits.md(mdatron docs limits) — every declared input and enumeration bound (file sizes, nesting depths, walk budgets, the concurrent-invocation count), shipped as data and held to the implementation by a testmdatron explain <code>— per-code prose for every emitted diagnostic;mdatron explain --listenumerates the catalog (--compactadds the fix hint per line)
See LICENSE.