| title | File Kinds |
|---|---|
| summary | How to declare file kinds, assign files to them, and read the merged rule config that results. |
A kind is a named bundle of rule settings that mdsmith applies to a set of files. Kinds let you share per-rule tuning across files that serve the same purpose — schema templates, plan documents, rule READMEs, prompts, security notes — without copying the same overrides into every glob that matches them.
mdsmith ships no built-in kinds. Each project picks the names that fit its repository.
Kinds live under the kinds: key in .mdsmith.yml. Each
kind body has the same shape as an entry under
overrides:, minus glob: — files are bound to kinds
separately.
kinds:
plan:
rules:
required-structure:
schema: plan/proto.md
paragraph-readability: false
proto:
rules:
first-line-heading: false
paragraph-readability: falseA kind that sets rules.required-structure.schema:
attaches that CUE schema to every file of the kind. A kind
that sets rule-name: false disables the rule for every
file of the kind.
Referencing an undeclared kind name from front matter or
kind-assignment: is a config error.
A kind can declare a path-pattern: glob that the
workspace-relative path of every file in the kind must
match. Use it to enforce filename conventions —
plan-ID prefixes, RFC numbering, runbook slugs —
without a custom CI script.
kinds:
plan:
path-pattern: "plan/[0-9][0-9]*_*.md"
rules:
required-structure:
schema: plan/proto.md
rfc:
path-pattern: "docs/rfc/RFC-[0-9][0-9][0-9][0-9].md"A file whose path does not match the kind's pattern
produces an MDS020 diagnostic anchored to line 1 of the
file. For plan/early-draft.md with the plan kind
above, the diagnostic reads:
filename: got "plan/early-draft.md", expected
glob plan/[0-9][0-9]*_*.md
schema: kinds[plan] / path-pattern
The pattern uses the same doublestar syntax as
overrides:, ignore:, and kind-assignment:
(see Glob patterns). Because
glob syntax has no "exactly digits" character class,
the pattern is an approximation: [0-9][0-9]* enforces a
two-digit prefix plus any trailing characters, not strict
integer-only. For tighter constraints, combine
path-pattern: with a <?require filename:?> directive
on the schema — both run, and each emits its own
diagnostic when violated.
mdsmith kinds show <name> prints path-pattern:
alongside the kind's rule settings when it's set, so the
constraint is auditable from one command.
A file's effective kind list is built from two sources, concatenated in this order:
- The file's own front-matter
kinds:field (a YAML list). - Matching entries in
kind-assignment:, in the order they appear in the config; within an entry, kinds in the order listed. An entry can select files byglob:and/orfields-present:(front-matter keys with non-null values).
Duplicate names are dropped after their first occurrence.
A file can declare its kinds inline. Use this for one-off files where a glob doesn't make sense.
---
kinds: [plan]
id: 92
status: 🔲
---
# File kindsA multi-kind file uses a multi-element list:
kinds: [draft, worksheet]. Merge order matches list
order.
kind-assignment: is a list of entries. Each entry has
glob: (the same doublestar pattern syntax as overrides:
and ignore:) and kinds: (the names to apply).
kind-assignment:
- glob: ["**/proto.md"]
kinds: [proto]
- glob: ["plan/*.md"]
kinds: [plan]
- glob: ["internal/rules/proto.md",
"internal/rules/MDS*/README.md"]
kinds: [rule-readme]Globs use the same matcher as overrides: and ignore:
(see Glob patterns). The plan
entry uses plan/*.md, which matches every plan file
including proto.md. The directory glob naturally
includes proto.md, and that is what we want here: the
proto kind disables structural rules on proto.md, while
the plan kind supplies required-structure.schema: so
the file is recognized as its own schema.
Where the directory glob targets a different filename
(for example internal/rules/MDS*/README.md, which does
not match internal/rules/proto.md), list the schema
file explicitly alongside the convention glob.
A !-prefix on a pattern re-excludes a path. Use it in
overrides: to keep content-tuning settings off
proto.md (the proto kind already handles those
files). Avoid !-exclusion in kind-assignment: for a
schema kind — excluding proto.md there would also
strip the required-structure.schema: that marks the
file as its own schema.
When a file should belong to two kinds, two entries can match it. The order of those entries fixes the merge order — kinds picked up earlier appear earlier in the effective list.
In the example above, plan/proto.md matches both
**/proto.md (proto kind) and plan/*.md (plan kind),
so its effective kind list is [proto, plan]. A regular
plan file like plan/96_kinds-adoption-and-docs.md only
matches plan/*.md, so it resolves to [plan].
An entry can require that a file's front matter carries a
configured set of keys, each with a non-null value. Pair
this with glob: when a project identifies a file's role
by its front-matter shape rather than (or in addition to)
its location.
kind-assignment:
- glob: ["docs/**"]
kinds: [doc]
- fields-present: [status, priority, assignee]
kinds: [task]
- glob: ["plan/*.md"]
fields-present: [id]
kinds: [plan]Within a single entry, glob: and fields-present:
combine with AND — every selector that is set must
match. Across entries, matches union (OR), the same as
for glob-only entries.
A field is "present" when the key appears in front matter
with a non-null value. A key set to YAML null (e.g.
status: null) does not count: the user wrote the
key but did not fill it.
For the config above, a file at
anywhere/important.md whose front matter is:
---
status: open
priority: high
assignee: alice
---resolves to [task] because the second entry's
fields-present: selector is satisfied. A file at
plan/132_inline.md with id: 132 in its front matter
resolves to [plan] — the third entry's glob: matches
the path and its fields-present: finds a non-null
id.
mdsmith kinds resolve <file> names the matching entry
index and the selectors that fired, so you can confirm
which rule did the work:
file: plan/132_inline.md
effective kinds:
- plan (from kind-assignment[2]: glob plan/*.md AND fields-present id)
Rule settings come from four layers, applied in this order from lowest to highest precedence:
- The rule's built-in defaults.
- Top-level
rules:defaults. - Each kind in the file's effective kind list, in order.
- Each
overrides:entry whoseglob:matches, in config order.
Across all four layers the config is deep-merged rule by rule:
- Maps merge key by key — a setting from an earlier layer survives if the later layer doesn't touch the same key.
- Scalar values at a leaf replace the earlier value wholesale.
- List settings replace by default. A rule can opt
a specific list key into append mode (the
placeholders:setting is the canonical example). - A bool-only entry such as
rule-name: falsetogglesenabledwithout erasing any other settings the rule inherited from earlier layers.
Because the merge is key-by-key, a kind that sets one key on a rule leaves the rule's other settings intact from whichever earlier layer configured them.
When two kinds in the effective list configure the same rule, the later kind wins — its settings deep-merge over the earlier kind's settings for that rule, with later scalar values overwriting earlier ones key by key. The same applies between kinds and overrides.
Order is list-driven, so the result is stable across runs.
For a file resolved as [proto, plan] with the kinds
above:
required-structurecomes fromplan(theprotokind doesn't set it, soplan's setting stands).paragraph-readability: falsecomes fromproto(theplankind doesn't touch it).first-line-heading: falsecomes fromproto(theplankind doesn't touch it).
If a glob override on plan/*.md then sets
max-file-length: { max: 500 }, that override applies on
top of the kinds and replaces only the
max-file-length rule.
When a file produces an unexpected diagnostic — or the diagnostic you expected doesn't fire — start with the resolved kind list and the merged rule config for that file.
mdsmith kinds resolve <file> prints both: the effective
kind list and the merged rule settings, with a per-leaf
source so you can see which layer set each value. Add
--json for a structured form. For a single rule's full
merge chain, use mdsmith kinds why <file> <rule>. To
attach the same source trailer to each diagnostic, run
mdsmith check --explain (or fix --explain).
If you'd rather walk the merge by hand, the same
information is recoverable by reading .mdsmith.yml
against the merge rules above:
- Read the file's front matter for any
kinds:field. - Walk
kind-assignment:top to bottom and collect every entry whose selectors all match the file —glob:against the path andfields-present:against the file's front matter. - Concatenate the two lists, dropping duplicates after first occurrence — that's the effective kind list.
- Apply built-in defaults, then the top-level
rules:block, then each kind body in order, then each matchingoverrides:entry. Each layer deep-merges its settings over the accumulated config.
For a quick primer on the same model from the CLI, run
mdsmith help kinds.
- Enforcing Document Structure with Schemas
— how
required-structurereads the schema attached by a kind. - Placeholder grammar — opt-in tokens that let kinds keep template files green under the same rules used for content.
- Schema field types
— named shortcuts (
date,email,url, …) for schema frontmatter values, so a kind'sschema:block does not have to re-derive the same CUE regex every project lands on.