MDS020: required-structure
Document structure and front matter must match its schema.
# Settings
| Setting | Type | Default | Description |
|---|---|---|---|
schema | string | "" | Path to a schema file (a proto.md) |
inline-schema | map | (unset) | Inline schema injected by kinds.<name>.schema:; not usually written by hand on a rule. DefaultSettings does not list it. |
placeholders | list | [] | Placeholder tokens to treat as opaque; see placeholder grammar |
Useful tokens: cue-frontmatter.
When neither schema nor inline-schema is set the rule skips structure
and front matter validation, but still warns on misplaced <?require?>
directives. Use overrides or kinds: to apply schemas to specific file groups.
A kind may declare its schema in either form. The config loader rejects a kind that sets both — see file kinds .
Schema front matter may embed a CUE schema that
validates document front matter. The rule-readme
schema at internal/rules/proto.md
requires id, name, status, description, and
nature (one of directive, generator,
content, style, structure). See the proto
file’s leading comment for the vocabulary.
# Require directive
Use <?require?> in the schema body to declare
constraints on files validated against this schema:
| Field | Type | Description |
|---|---|---|
filename | string | Glob the document basename must match |
<?require
filename: "[0-9]*_*.md"
?># Schema composition
Schema files can use <?include?> to share
structure across schemas. Included fragment
headings are spliced into the heading list at
the include position. Fragment front matter is
ignored. <?require?> from fragments is merged.
# ?
## Goal
<?include
file: common/acceptance-criteria.md
?>Cycle detection prevents circular includes. Max include depth is 10.
# Optional and deprecated fields
Append ? to mark a key optional (absent allowed,
present still typed). To deprecate, swap the value
for {type, deprecated: true} (see file-kinds.md).
name: 'string & != ""'
"description?": stringSchema body controls section strictness:
- By default, extra sections are rejected.
- Add a heading with text
...(for example## ...) to allow extra headings in that position until the next required heading anchor.
# Inline schemas on kinds
A kind body may declare its schema directly in
.mdsmith.yml rather than referencing a proto.md
file. The two forms are equivalent — both parse to the
same in-memory scope tree — and a kind may use only one.
kinds:
rfc:
schema:
filename: "RFC-[0-9][0-9][0-9][0-9].md"
frontmatter:
id: '=~"^RFC-[0-9]{4}$"'
status: '"draft" | "ratified" | "deprecated"'
authors: '[...string] & [_, ...string]'
closed: true
sections:
- heading: null
- heading: "Overview"
- heading: "Decision"
sections:
- heading: "Outcome"
- heading:
regex: '.+'
repeat: { min: 0 }
- heading: "References"Section keys:
heading:— discriminator.nullfor the preamble (content before any heading; valid only as the first entry), a string for an exact-match literal (regex-escaped into the matcher), or a mapping{ regex, repeat?, sequential? }for the full form. The level for string headings comes from depth in the tree (root sections are H2; nested sections are H3, then H4, …).sections:— nested sections one level deeper. Rejected on preamble entries (the first heading terminates the preamble’s range).closed:— whentrue, unlisted headings inside this scope produce a diagnostic. The same flag drives thecontent:walker — unlisted body nodes outside anunlistedcontent slot also flag underclosed: true. Defaultfalse.rules:— per-scope rule-config overrides. Each entry maps a rule name to a settings map that applies on top of the rule’s defaults inside the scope’s range. Today’s apply is a plain ApplySettings call, not a config-style deep-merge — keys the override sets replace the defaults wholesale.content:— positional list of non-heading AST node constraints (code blocks, tables, lists, paragraphs) the section must contain. See the schemas guide for the entry shape and the per-kind fields. Rejected on slot scopes.
The wildcard slot — { regex: '.+', repeat: { min: 0 } } —
absorbs zero or more unlisted sections at its position.
Surrounding listed sections keep their order; out-of-order
detection still claims a heading whose text matches a later
listed scope. Slots reject sections:, rules:,
closed:, and content:. The preamble (heading: null)
accepts closed:, rules:, and content: for its line
range but rejects sections:.
See the
section-schema reference
for the full grammar — regex: body, digits /
fmvar(name) helpers, and repeat: { min, max }. H1 is
reserved for first-line-heading; inline schemas
constrain H2 and below.
# Cross-references
A cross-references: block names text patterns whose
matches must resolve to a real heading. Each entry fills
numeric ({n}, {1}, …) or named captures from the
regex into must-match:; the result is slugified and
looked up in the heading slug set.
schema:
cross-references:
- pattern: "\\bStep (\\d+)\\b"
must-match: "Step {n}"
skip-lines-matching: "^> "skip-lines-matching: (regex) exempts blockquoted stale
text and version-history notes from the check.
# Acronyms
An acronyms: block flags all-caps tokens (length 2-6,
leading letter, alphanumeric) on first use inside a
configured scope when they appear without a parenthesised
expansion. known-safe: lists exempt tokens; scope:
restricts the check to matching sections (omit it for
document-wide). First-use state is per-scope.
schema:
acronyms:
known-safe: [API, HTTP, TLS, JSON]
scope: ["Check", "Expected"]# Index side-output
An index: block asks mdsmith fix to write a JSON
side-output next to the source file. mdsmith check is
read-only (no write). Output paths are resolved relative
to the document’s directory; absolute paths and ..
traversal are rejected.
schema:
index:
output: ".runbook-index.json"
include: [step-map, cross-ref-graph, word-counts, headings]include: is a closed enum:
step-map—{section-slug: [child-slugs]}cross-ref-graph—{ref-text: target-slug}word-counts—{section-slug: int}headings— flat list of{level, text, slug, line}
# Config
Apply a schema by declaring a kind or setting
schema: on an override:
kinds:
story:
rules:
required-structure:
schema: schemas/story.md
kind-assignment:
- glob: ["stories/**/*.md"]
kinds: [story]Disable:
rules:
required-structure: false# Examples
# Good
# My Plan
## Goal
Describe the goal here.
## Tasks
List tasks here.# Bad
# My Plan
## Goal
Describe the goal here.# Diagnostics
Every schema diagnostic names the field, the value, the constraint, and (when it can) a hint:
status: got "draf", expected one of: "draft", "open", "done"
(did you mean "draft"?)
↳ plan/proto.md:4 — required by schemaThe ↳ line attaches the schema source as a related location;
editors render it as a relatedInformation entry.
| Condition | Message |
|---|---|
| section missing | ## Settings: got <missing>, expected section to be present |
| wrong level | Settings: got h3, expected h2 |
| extra section | ## Extra: got <present>, expected not declared in schema |
| out of order | ## Tasks: got <out of order>, expected in declared order |
| heading sync | heading does not match frontmatter: expected "X" (from id), … |
| body sync | body does not match frontmatter field "description": expected … |
| front matter schema | status: got "draf", expected one of: "draft", "open", "done" … |
| filename mismatch | filename: got "foo.md", expected filename matching glob [0-9]*_*.md |
| misplaced require | <?require?> is only recognized in schema files; … |
| schema include loop | cyclic include: a.md -> b.md -> a.md |
| content missing | missing required content “code-block lang=yaml” inside ## Examples |
| content unexpected | unexpected content “table” inside ## Examples (expected “paragraph”) |
| content out of order | content “table” out of order: expected after “code-block lang=yaml” |
| code block lang | code block language “json” does not match required “yaml” |
| table columns | table headers [Key Value] do not match required [Setting Default] |
CUE constraints render in user vocabulary:
| CUE shape | Rendered as |
|---|---|
"a" | "b" | "c" | one of: "a", "b", "c" |
=~"^FOO-[0-9]{4}$" | string matching ^FOO-[0-9]{4}$ |
int & >=1 & <=5 | int between 1 and 5 |
string & != "" | non-empty string |
bool | true or false |
| anything else | the raw CUE expression |
Hints fire on string disjunctions (Levenshtein ≤ 2 of a valid literal) and integer ranges (nearest bound when just outside). Other shapes get no hint.
# See also
# Meta-Information
- ID: MDS020
- Name:
required-structure - Status: ready
- Default: enabled
- Fixable: index side-output only (when
schema.index:is set) - Implementation: source
- Guide: directive guide
- Category: structural
- markdownlint: MD043 (required-headings)
- rumdl: MD043 (required-headings)