Skip to content

Commit 91c15ca

Browse files
jedudenclaude
andauthored
Plan 208: kind files under .mdsmith/kinds/ (#391)
* Plan 208: implement kind files under `.mdsmith/kinds/` Each YAML file at `.mdsmith/kinds/<name>.{yaml,yml}` now declares one kind. The basename is the kind's name; the body is the full `KindBody` (schema, rules, path-pattern, extends). Inline `kinds.<name>:` in `.mdsmith.yml` stays a first-class source — projects can mix file-defined and inline kinds freely. The same name in both is a config error naming both sources so a merged kind cannot defeat the "read one file to know one kind" property this surface ships. `discoverKinds` walks the directory at load time and rejects bad basenames (`[a-z][a-z0-9-]*`), subdirectories, `.yaml`/`.yml` extension collisions, and unknown top-level keys (strict decoding). The merged kind layer threads through `ValidateKinds` unchanged so the pairwise schema-source mutex and the `extends:` cycle detector apply uniformly across sources. Every kind body now carries a `SourcePath` populated by `Load` (the `.mdsmith.yml` path for inline kinds, the kind-file path for file kinds). Provenance threads that path through `LayerEntry.SourcePath` and `ResolvedKind.SourcePath`, and `mdsmith kinds resolve` / `kinds show` print a `defined-in` line next to each kind they report. The JSON shape carries `source-path` on both the body and per-resolved-kind entries. The repository's own `.mdsmith.yml` migration is deferred: the pinned `mdsmith` binary this repo lints itself with does not yet support `.mdsmith/kinds/`. Migration is scheduled for after the next release bumps the pinned version. Plan 209 is filed as the follow-up for the parallel `.mdsmith/conventions/<name>.yaml` slot. https://claude.ai/code/session_01Ctz5CK1Zhz99xn4RYtmDGn * Plan 208: cover kind-file error paths and drop dead branch codecov/patch reported `∅` for the Go component on PR #391 because the new helpers had uncovered defensive branches that pulled patch coverage below the project baseline. Close them by either driving the branch with a test or removing it: - `mergeKindFiles`: drop the `cfgPath == ""` early return. Load is the only caller and always passes a non-empty path. CLAUDE.md's "defensive only when you can drive it red/green" rule applies here — no test could realistically reach it. - `discoverKinds`: add tests for the non-IsNotExist ReadDir branch (plant a regular file at `.mdsmith/kinds`), the non-YAML-extension skip branch (drop a `.md` alongside), the YAML anchor/alias rejection branch, and the malformed-YAML decode branch. Each test names the offending file in the assertion so future refactors keep the file-naming contract. - `parseKindFile`: add a chmod-0000 read-error test. Skips on test users (root) where mode-0000 files remain readable; CI runs as `runner`, where the branch is exercised. - `Load`: add a propagation test so a discovery error from a bad basename surfaces as the `loading kind files` wrap on the Load surface. - `WriteBodyText` (kindsout): add a failingWriter test for the new `defined-in:` line so its error path matches the rest of the writer surface. After these, the three new helpers (`discoverKinds`, `mergeKindFiles`, `parseKindFile`) hit 100% / 100% / ~91% (the last branch covered in CI). `WriteBodyText` is back at 100%. https://claude.ai/code/session_01Ctz5CK1Zhz99xn4RYtmDGn --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f579e53 commit 91c15ca

20 files changed

Lines changed: 1534 additions & 44 deletions

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ row: "- [{summary}](../{filename})"
109109
- [Built-in Markdown conventions, the rule presets each one applies, and how user config layers on top via deep-merge.](../docs/reference/conventions.md)
110110
- [Glob pattern syntax across mdsmith config, directives, and CLI argument expansion, with the supported exclusion semantics for each surface.](../docs/reference/globs.md)
111111
- [Look up exact CLI commands, config glob and schema syntax, the built-in conventions, and the section-schema grammar.](../docs/reference/index.md)
112+
- [Each file under `.mdsmith/kinds/` declares one kind. The basename is the kind name; the file body carries the full `KindBody` — schema, rules, `path-pattern:`, `extends:`. Sits alongside inline `kinds.<name>:` in `.mdsmith.yml`.](../docs/reference/kind-files.md)
112113
- [Named field-type shortcuts for inline schema frontmatter values — the registered names, the canonical CUE each one resolves to, and example usage.](../docs/reference/schema-types.md)
113114
- [Section-schema reference for inline `kinds.<name>.schema:` blocks. Covers the `heading:` discriminator, the `regex:` matcher (a Go RE2 body with `\#(digits)` and `\#(fmvar(...))` helpers), the `repeat: {min, max}` cardinality field, and the matching algorithm. `proto.md` files are parsed into the same shape by the schema package, but MDS020's file-schema check still uses its legacy parser; see the proto.md section below for what is and is not migrated.](../docs/reference/section-schema.md)
114115
- [mdsmith collects no telemetry, no usage analytics, no error reports, and no identifiers. The CLI and the LSP server make no outbound network calls at runtime.](../docs/reference/telemetry.md)

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ row: "- [{summary}]({filename})"
115115
- [Built-in Markdown conventions, the rule presets each one applies, and how user config layers on top via deep-merge.](docs/reference/conventions.md)
116116
- [Glob pattern syntax across mdsmith config, directives, and CLI argument expansion, with the supported exclusion semantics for each surface.](docs/reference/globs.md)
117117
- [Look up exact CLI commands, config glob and schema syntax, the built-in conventions, and the section-schema grammar.](docs/reference/index.md)
118+
- [Each file under `.mdsmith/kinds/` declares one kind. The basename is the kind name; the file body carries the full `KindBody` — schema, rules, `path-pattern:`, `extends:`. Sits alongside inline `kinds.<name>:` in `.mdsmith.yml`.](docs/reference/kind-files.md)
118119
- [Named field-type shortcuts for inline schema frontmatter values — the registered names, the canonical CUE each one resolves to, and example usage.](docs/reference/schema-types.md)
119120
- [Section-schema reference for inline `kinds.<name>.schema:` blocks. Covers the `heading:` discriminator, the `regex:` matcher (a Go RE2 body with `\#(digits)` and `\#(fmvar(...))` helpers), the `repeat: {min, max}` cardinality field, and the matching algorithm. `proto.md` files are parsed into the same shape by the schema package, but MDS020's file-schema check still uses its legacy parser; see the proto.md section below for what is and is not migrated.](docs/reference/section-schema.md)
120121
- [mdsmith collects no telemetry, no usage analytics, no error reports, and no identifiers. The CLI and the LSP server make no outbound network calls at runtime.](docs/reference/telemetry.md)

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ row: "- [{summary}]({filename})"
101101
- [Built-in Markdown conventions, the rule presets each one applies, and how user config layers on top via deep-merge.](docs/reference/conventions.md)
102102
- [Glob pattern syntax across mdsmith config, directives, and CLI argument expansion, with the supported exclusion semantics for each surface.](docs/reference/globs.md)
103103
- [Look up exact CLI commands, config glob and schema syntax, the built-in conventions, and the section-schema grammar.](docs/reference/index.md)
104+
- [Each file under `.mdsmith/kinds/` declares one kind. The basename is the kind name; the file body carries the full `KindBody` — schema, rules, `path-pattern:`, `extends:`. Sits alongside inline `kinds.<name>:` in `.mdsmith.yml`.](docs/reference/kind-files.md)
104105
- [Named field-type shortcuts for inline schema frontmatter values — the registered names, the canonical CUE each one resolves to, and example usage.](docs/reference/schema-types.md)
105106
- [Section-schema reference for inline `kinds.<name>.schema:` blocks. Covers the `heading:` discriminator, the `regex:` matcher (a Go RE2 body with `\#(digits)` and `\#(fmvar(...))` helpers), the `repeat: {min, max}` cardinality field, and the matching algorithm. `proto.md` files are parsed into the same shape by the schema package, but MDS020's file-schema check still uses its legacy parser; see the proto.md section below for what is and is not migrated.](docs/reference/section-schema.md)
106107
- [mdsmith collects no telemetry, no usage analytics, no error reports, and no identifiers. The CLI and the LSP server make no outbound network calls at runtime.](docs/reference/telemetry.md)

PLAN.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,5 +133,6 @@ footer: |
133133
| 205 | 🔲 | | [Move extension.ts concerns to wiring.ts](plan/205_arch-fix-extension-ts-srp.md) |
134134
| 206 | 🔲 | | [Document cue/ in architecture layering map](plan/206_arch-fix-cue-types-docs.md) |
135135
| 207 | 🔲 | sonnet | [LSP fix preview via ChangeAnnotation](plan/207_lsp-fix-preview.md) |
136-
| 208 | 🔲 | opus | [Kind-per-file config under `.mdsmith/kinds/`](plan/208_kind-files.md) |
136+
| 208 | 🔳 | opus | [Kind-per-file config under `.mdsmith/kinds/`](plan/208_kind-files.md) |
137+
| 209 | 🔲 | opus | [Convention-per-file config under `.mdsmith/conventions/`](plan/209_convention-files.md) |
137138
<?/catalog?>

docs/development/architecture/cross-system.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ doc" column says.
3434
| LSP wire protocol | `internal/lsp` | [CLI reference: `lsp`](../../reference/cli/lsp.md) | VS Code extension, other editors |
3535
| CLI flags + exit codes | `cmd/mdsmith` | [CLI reference](../../reference/cli.md) | shell scripts, CI, git hooks |
3636
| `.mdsmith.yml` schema | `internal/config` | [Conventions](../../reference/conventions.md) | every project using mdsmith |
37+
| `.mdsmith/kinds/` directory | `internal/config` | [Kind files](../../reference/kind-files.md) | every project using mdsmith |
3738
| Generated section markers | `internal/archetype/gensection` | [Generated sections](../../background/concepts/generated-section.md) | every project's Markdown files |
3839
| Claude plugin manifest (published) | `editors/claude-code/.claude-plugin/plugin.json` | [Install: Claude plugin](../../guides/install.md) | end users via Claude Code marketplace |
3940
| Claude plugin manifest (contributors) | `editors/claude-code-dev/.claude-plugin/plugin.json` | [editors/claude-code-dev/README.md](../../../editors/claude-code-dev/README.md) | mdsmith contributors |

docs/guides/file-kinds.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,43 @@ effective kinds:
230230
- plan (from kind-assignment[2]: glob plan/*.md AND fields-present id)
231231
```
232232

233+
## Split a kind into its own file
234+
235+
When a project's `kinds:` block has grown large
236+
enough that one kind's edits dirty the same
237+
`.mdsmith.yml` as every other config change, lift
238+
the kind into a standalone file under
239+
`.mdsmith/kinds/<name>.yaml`. The basename is the
240+
kind's name; the body is identical to the inline
241+
`kinds.<name>:` body.
242+
243+
```yaml
244+
# .mdsmith/kinds/audit-log.yaml
245+
schema:
246+
frontmatter:
247+
title: 'string & != ""'
248+
"summary?": 'string'
249+
closed: false
250+
rules:
251+
max-file-length:
252+
max: 600
253+
```
254+
255+
The same name in both a file and inline is a
256+
config error naming both sources, so split a kind
257+
in one move rather than leaving both copies
258+
behind. Names stay stable, so `kind-assignment:`
259+
entries that referenced the inline kind keep
260+
working with no edit.
261+
262+
`mdsmith kinds resolve <file>` and `mdsmith kinds
263+
show <name>` print the defining-source path next
264+
to each kind so a mixed configuration stays
265+
auditable. See the
266+
[kind files reference](../reference/kind-files.md)
267+
for the full layout, basename rule, and JSON
268+
shape.
269+
233270
## Schema inheritance with `extends`
234271

235272
A kind can build on another kind's schema via the `extends:` key.

docs/reference/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ row: "- [{summary}]({filename})"
3636
- [Print the mdsmith build version and exit.](cli/version.md)
3737
- [Built-in Markdown conventions, the rule presets each one applies, and how user config layers on top via deep-merge.](conventions.md)
3838
- [Glob pattern syntax across mdsmith config, directives, and CLI argument expansion, with the supported exclusion semantics for each surface.](globs.md)
39+
- [Each file under `.mdsmith/kinds/` declares one kind. The basename is the kind name; the file body carries the full `KindBody` — schema, rules, `path-pattern:`, `extends:`. Sits alongside inline `kinds.<name>:` in `.mdsmith.yml`.](kind-files.md)
3940
- [Named field-type shortcuts for inline schema frontmatter values — the registered names, the canonical CUE each one resolves to, and example usage.](schema-types.md)
4041
- [Section-schema reference for inline `kinds.<name>.schema:` blocks. Covers the `heading:` discriminator, the `regex:` matcher (a Go RE2 body with `\#(digits)` and `\#(fmvar(...))` helpers), the `repeat: {min, max}` cardinality field, and the matching algorithm. `proto.md` files are parsed into the same shape by the schema package, but MDS020's file-schema check still uses its legacy parser; see the proto.md section below for what is and is not migrated.](section-schema.md)
4142
- [mdsmith collects no telemetry, no usage analytics, no error reports, and no identifiers. The CLI and the LSP server make no outbound network calls at runtime.](telemetry.md)

docs/reference/kind-files.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
---
2+
title: Kind files under `.mdsmith/kinds/`
3+
weight: 25
4+
summary: >-
5+
Each file under `.mdsmith/kinds/` declares one
6+
kind. The basename is the kind name; the file body
7+
carries the full `KindBody` — schema, rules,
8+
`path-pattern:`, `extends:`. Sits alongside inline
9+
`kinds.<name>:` in `.mdsmith.yml`.
10+
---
11+
# Kind files under `.mdsmith/kinds/`
12+
13+
A **kind file** is a YAML file under
14+
`.mdsmith/kinds/` whose basename is the kind's name
15+
and whose body is the full kind definition. One file
16+
per kind, no nesting. The directory sits next to
17+
`.mdsmith.yml` at the workspace root.
18+
19+
```text
20+
.mdsmith.yml # unchanged
21+
.mdsmith/
22+
kinds/
23+
audit-log.yaml
24+
secret-rotation.yaml
25+
architecture-doc.yaml
26+
```
27+
28+
Use kind files when the `kinds:` block has grown
29+
large. Each rule edit dirties the same
30+
`.mdsmith.yml` as every other config change.
31+
Splitting kinds into one file each isolates the
32+
history. The read path shortens too: open
33+
`audit-log.yaml` to see the whole `audit-log`
34+
kind.
35+
36+
## File shape
37+
38+
The file body matches the inline
39+
`kinds.<name>:` body. It accepts every
40+
[`KindBody`](../guides/file-kinds.md) key:
41+
`extends:`, `path-pattern:`, `categories:`,
42+
`schema:`, `rules:`. A key outside that set is
43+
a config error.
44+
45+
```yaml
46+
# .mdsmith/kinds/audit-log.yaml
47+
schema:
48+
frontmatter:
49+
title: 'string & != ""'
50+
"summary?": 'string'
51+
audit-from: '=~"^[0-9a-f]{7,40}$"'
52+
filename: "architecture-audit.md"
53+
closed: false
54+
sections:
55+
- heading: null
56+
- heading:
57+
regex: '.+'
58+
repeat: { min: 0 }
59+
rules:
60+
max-file-length:
61+
max: 600
62+
```
63+
64+
## Basename rule
65+
66+
The kind's name is the basename minus extension.
67+
The basename must match `[a-z][a-z0-9-]*` — lower
68+
case, starting with a letter, with optional
69+
hyphen-separated segments. The rule applies only to
70+
filenames (OS case folding, path safety); inline
71+
`kinds.<name>:` keys stay unvalidated.
72+
73+
Both `*.yaml` and `*.yml` are scanned. Two kind
74+
files with the same basename across the two
75+
extensions is a config error naming both files.
76+
77+
Subdirectories under `.mdsmith/kinds/` are
78+
rejected. One kind per file, flat layout.
79+
80+
## Composition with `.mdsmith.yml`
81+
82+
`kinds.<name>:` blocks inside `.mdsmith.yml`
83+
remain a first-class source. A project can mix
84+
inline and file-defined kinds freely.
85+
86+
The same kind name declared in **both** a file and
87+
inline is a config error naming both sources. The
88+
two sources do **not** merge — a merged kind would
89+
defeat the "read one file to know one kind"
90+
property kind files ship.
91+
92+
`kind-assignment:`, `overrides:`, and `ignore:` are
93+
glob-keyed and stay in `.mdsmith.yml`. A
94+
kind-assignment entry references a kind by name —
95+
inline or file kind — with no extra wiring.
96+
97+
## Schema sources
98+
99+
A kind file accepts the same three schema sources
100+
as an inline kind, and they remain mutually
101+
exclusive (acceptance criterion #6 of plan 208):
102+
103+
- inline `schema:` block
104+
- `rules.required-structure.schema:` path to a
105+
`proto.md`
106+
- legacy `rules.required-structure.inline-schema:`
107+
map
108+
109+
Setting two on the same kind errors at config load
110+
with both source names.
111+
112+
A schema shared across kinds is shared via
113+
`extends:` — the inheritance chain works
114+
seamlessly across sources. A file kind may extend
115+
an inline kind and the reverse. The cycle detector
116+
runs on the merged kinds map.
117+
118+
## Audit
119+
120+
`mdsmith kinds resolve <file>` prints the
121+
defining-source path next to each kind it
122+
reports. A mixed resolution shows the path each
123+
kind came from. You can jump straight to the
124+
right file:
125+
126+
```text
127+
file: docs/audit.md
128+
effective kinds:
129+
- audit-log (from kind-assignment[3]: glob docs/**/*.md) defined-in .mdsmith/kinds/audit-log.yaml
130+
```
131+
132+
`mdsmith kinds show <name>` adds a `defined-in:`
133+
line to the body output, so the same info is
134+
available without going through a target file.
135+
136+
The JSON shape (`--json`) carries a
137+
`source-path:` key on each kind body and on every
138+
resolved-kind entry so editor integrations can
139+
key off a stable field.

internal/config/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ type KindBody struct {
155155
// sections. A cycle in the extends chain (single- or multi-hop)
156156
// is reported by ValidateKinds. See plan 135.
157157
Extends string `yaml:"extends,omitempty"`
158+
159+
// SourcePath is the workspace-absolute path of the file that
160+
// defined this kind — either `.mdsmith.yml` for inline kinds
161+
// or `.mdsmith/kinds/<name>.{yaml,yml}` for file-defined kinds
162+
// (plan 208). Not serialized to YAML; populated by Load.
163+
// Provenance surfaces it as `kinds.<name> (<path>)`; CLI
164+
// resolve/show prints it next to each kind.
165+
SourcePath string `yaml:"-"`
158166
}
159167

160168
// KindAssignmentEntry assigns one or more kinds to files matching the

0 commit comments

Comments
 (0)