| id | 241 | ||||
|---|---|---|---|---|---|
| title | Schema-per-file config under `.mdsmith/schemas/` | ||||
| status | ✅ | ||||
| model | opus | ||||
| depends-on |
|
||||
| summary | Add a top-level `schemas:` registry to `.mdsmith.yml`, mirrored at `.mdsmith/schemas/<name>.yaml` — the same pattern plans 208 and 209 set for `kinds:` and `conventions:`. A kind references a schema by name (`schema: rfc-v1`). Also adds the missing `docs/guides/conventions.md` guide. |
Plans 208 and 209 let a crowded kinds: or
conventions: block move into one file per entry
under .mdsmith/kinds/ and .mdsmith/conventions/.
Inline schemas have the same problem and no such
escape. This plan adds a top-level schemas:
registry mirrored at .mdsmith/schemas/<name>.yaml,
so each schema moves to its own file. A kind
references one by name (schema: rfc-v1); one
schema can drive several kinds.
Today a kind embeds its schema inline under
kinds.<kind>.schema: or points at a proto.md.
Inline doesn't share; proto.md shares but uses
a different matcher grammar. The registry
shares AND uses the plan 156 matcher engine via
schema.ParseInline.
docs/guides/conventions.md does not exist; the
plan adds it as a side deliverable.
- Migrating
proto.mdto YAML. - Removing inline
schema:maps on kinds. - Schema-file-level
extends:. - Adopting
.mdsmith/schemas/in this repo (deferred until the pinned mdsmith ships it).
.mdsmith.yml # unchanged
.mdsmith/
kinds/ # plan 208
conventions/ # plan 209
schemas/
rfc-v1.yaml
runbook.yaml
Both *.yaml and *.yml are scanned.
Subdirectories and symlinks are rejected. The
1 MB .mdsmith.yml cap applies unchanged.
A file .mdsmith/schemas/<name>.yaml is the
per-file split of an inline schemas.<name>:
block. The body parses through
schema.ParseInline. Top-level keys allowed:
frontmatterfilenameclosedsectionscross-referencesacronymsindex
The basename is the schema's name, matching
[a-z][a-z0-9-]* — same rule kind and convention
files carry.
A kind's schema: becomes polymorphic: a string
is a registry name, a map stays inline.
# .mdsmith.yml
schemas: # inline equivalent of
rfc-v1: # .mdsmith/schemas/rfc-v1.yaml
filename: "RFC-[0-9][0-9][0-9][0-9].md"
sections:
- heading: "Overview"
- heading: "Decision"
kinds:
rfc:
schema: rfc-v1 # registry reference
rfc-internal:
schema: rfc-v1 # one schema drives both kinds
draft:
schema: # inline body still allowed
filename: "DRAFT-*.md"The merge layer resolves the registry reference
at load time so the rule sees one inline body.
An undeclared name errors. A name declared both
inline under schemas: AND as a file errors,
naming both — same rule as kinds and conventions.
Interactions:
extends:— registry refs resolve to bodies beforeResolveKindInlineSchemawalks the chain.path-pattern:— runs independently from the resolved schema'sfilename:. Both fire on a mismatch.
A named schema: resolves to a map before
validateKindSchemaSources
runs, so a two-source kind is rejected as today.
A referenced schema's body is grammar-checked
when its kind validates — the same point an
inline schema is checked today. An undeclared
name errors at load.
Composition per
plan 156 is
unchanged: each kind adds one schema-sources
entry, merged by schema.Compose.
KindSchemaRef carries the schema's own
SourcePath, set at resolution. It is the .yaml
path for a file entry, .mdsmith.yml for an
inline-registry entry, empty for an inline-on-kind
body (the kind's file then applies).
applyInlineSchemaSource threads it, so "go to
schema" lands on the schema, not the kind.
Per
Go architecture patterns:
no new package; the type change ripples from
internal/config into internal/kindsout.
internal/config(+internal/kindsout): add aKindSchemaReftype withUnmarshalYAMLdispatching onyaml.Node.Kind(scalar → named ref, mapping → inline body) plus aSourcePathfield for the schema's own origin. ReplaceKindBody.Schema map[string]anyand route every map reader through aMap()accessor:resolvedInlineSchema,extendsChainSchemas,validateKindSchemaSources,effectiveExplicit,resolveLayerInlineSchema, andkindsout's frontmatter index.copyKindsmust deep-copy the resolved body so load-time resolution survives the merge. Unit test covers scalar, mapping, sequence, null, malformed scalar.internal/config: adddiscoverSchemas(workspaceDir)mirroringdiscoverKinds's checks (basename[a-z][a-z0-9-]*, no subdirs, no symlinks, no.yaml/.ymlduplicates, no unknown top-level keys). Unit test per rejection.internal/config: store the registry asmap[string]discoveredSchema({body, sourcePath}) so each entry keeps its origin. Inlineschemas:entries are tagged.mdsmith.yml;mergeSchemaFilestags file entries with the.yamlpath and errors on an inline-vs-file collision.ParseBytesskips disk discovery.internal/config: inLoad, after the kind/convention merges and beforeValidateKinds, callmergeSchemaFilesthenresolveNamedSchemas(cfg). The resolver replaces each kind's namedKindSchemaRefwith the discovered body and sets the ref'sSourcePathfrom the entry's origin. An undeclared name errors. Unit tests cover happy path, undeclared name, inline passing through.internal/config: adaptvalidateKindSchemaSourcesto readbody.Schema.Map()(filled by resolution). A namedschema:plusrules.required-structure.schema:— or plusinline-schema:— then trips the existing pairwise checks with "pick one source".internal/config: thread the ref'sSourcePaththroughapplyInlineSchemaSource, falling back to the kind's file when empty (inline-on-kind). Provenance tests assert thesourcekey is.mdsmith/schemas/<name>.yamlfor a file entry,.mdsmith.ymlfor an inline-registry entry.internal/integration: contract test covers directory layout, basename rule, subdirectory/symlink rejection, the two dual-source rejections, inline-vs-file and cross-extension collisions, undeclared names.internal/integration: parallel fixtures — one kind defined inline, one via named YAML reference, same Markdown input — assert byte-equal diagnostic streams.- CLI: extend
mdsmith kinds resolveto print the schema's defining-source path. JSON gainsschema-source-path:, populated for named-YAML andproto.mdsources, omitted for inline (already onsource-path). - Docs — new reference: add
docs/reference/schema-files.mdwith the H2s kind-files.md and convention-files.md share (file shape, basename rule, composition, audit); the directory tree goes in the preamble. - Docs — schemas guide: in schemas.md grow the source list to three, add a "File-based YAML schemas" H2 between inline and proto.md, and add a column to the "Choosing a source" table.
- Docs — kind-files reference: in kind-files.md's "Schema sources" section, add the named reference as a fourth source.
- Docs — conventions guide: add
docs/guides/conventions.mdwith H2s for built-in vs user conventions, declaring one inline, theconvention:selector, the flavor-must-agree rule, layering rules over a preset, and the "split into a file" recipe.mdsmith fixregenerates the CLAUDE.md catalog to include it. - Docs — architecture boundaries: add a
row in cross-system.md for
.mdsmith/schemas/. - Repo migration: deferred to a follow-up.
- A
schema: foo(file) kind and a kind with the same body inline produce the same messages and anchors on a reference doc; only the schema-source location differs. - The inline-vs-named fixture pair emits
matching message+anchor streams; two kinds
sharing
schema: fooboth validate. - An undeclared
schema:name errors, naming the kind and the missing schema. - Each of these errors with a clear
message: basename outside
[a-z][a-z0-9-]*, file in a subdirectory, symlink, duplicate basename across.yaml/.yml, unknown top-level key, name declared inline underschemas:AND as a file. - A kind that sets a named
schema:andrules.required-structure.schema:errors; same for namedschema:andinline-schema:. Both quote "pick one source". -
mdsmith kinds resolve <file>prints the schema's source path;schema-source-path:is set for file and inline-registry sources, omitted for inline-on-kind. -
docs/reference/schema-files.mdexists. schemas.md documents all three sources. - kind-files.md and cross-system.md each
reference
.mdsmith/schemas/. -
docs/guides/conventions.mdcovers the H2s named in task 13. - Unit tests cover
KindSchemaRef.UnmarshalYAML,discoverSchemas,mergeSchemaFiles, andresolveNamedSchemas. - All tests pass:
go test ./... -
go tool golangci-lint runclean. -
mdsmith check .passes.