Plan 52: user-supplied archetype templates with CLI - #152
Conversation
Ship four built-in required-structure schemas for common agentic Markdown patterns (story-file, prd, agent-definition, claude-md) and expose them through a new `archetype` setting on the required-structure rule. The setting is mutually exclusive with `schema` and maps the archetype name to an embedded schema loaded via go:embed. Documented in MDS020 README and the enforcing-structure guide. Config: .mdsmith.yml adds `internal/archetypes/**` to the directory-structure allow-list and ignores `internal/archetypes/*.md` so the embedded schema templates are not linted as normal docs.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #152 +/- ##
==========================================
+ Coverage 86.76% 88.03% +1.27%
==========================================
Files 96 110 +14
Lines 10446 14106 +3660
==========================================
+ Hits 9063 12418 +3355
- Misses 900 1228 +328
+ Partials 483 460 -23 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- Simplify List() to drop unreachable embed.FS defensive branches. - Extract schemaSource() helper so the archetype-vs-schema label used in the invalid-schema diagnostic is covered by a direct unit test.
There was a problem hiding this comment.
Pull request overview
Adds first-class, built-in schema “archetypes” to the required-structure rule so users can validate common agentic Markdown document patterns without maintaining their own schema files.
Changes:
- Introduces
internal/archetypeswith embedded.mdschema assets plusLookup()/List(). - Extends
required-structure(MDS020) with anarchetypesetting (mutually exclusive withschema) and refactors schema loading. - Updates unit tests, rule documentation, structure guide docs, and plan tracking; adjusts
.mdsmith.ymlto allow/ignore the new archetype assets.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| plan/52_archetype-template-library.md | Marks the archetype library plan as complete and checks off tasks/criteria. |
| PLAN.md | Updates the plan index to show plan 52 as complete. |
| internal/archetypes/archetypes.go | Adds embedded archetype registry with lookup + listing utilities. |
| internal/archetypes/archetypes_test.go | Adds tests for listing, ordering, and lookup error behavior. |
| internal/archetypes/story-file.md | Adds the built-in “story-file” schema template. |
| internal/archetypes/prd.md | Adds the built-in “prd” schema template. |
| internal/archetypes/agent-definition.md | Adds the built-in “agent-definition” schema template. |
| internal/archetypes/claude-md.md | Adds the built-in “claude-md” schema template. |
| internal/rules/requiredstructure/rule.go | Adds Archetype support, schema loading abstraction, and mutual exclusivity enforcement. |
| internal/rules/requiredstructure/rule_test.go | Adds unit tests for archetype selection, type checks, exclusivity, and basic archetype validation. |
| internal/rules/MDS020-required-structure/README.md | Documents the new archetype setting and lists available built-ins. |
| docs/guides/directives/enforcing-structure.md | Adds user-facing guide section for configuring built-in archetypes. |
| .mdsmith.yml | Allows internal/archetypes/** under directory-structure and ignores embedded archetype .md assets from linting. |
- Add defensive mutual-exclusivity check in loadSchema so the rule behaves consistently when constructed directly (bypassing ApplySettings). - Make story-file i-want and so-that required; the "as / i-want / so-that" trio is the defining shape of the archetype. - Distinguish fs.ErrNotExist from unexpected embed read errors in Lookup via a unit-tested classifyLookupError helper.
Drop shipping built-in archetype templates. Add config-driven archetype discovery (archetypes.roots) and CLI verbs (init/list/show/path). Reverts implementation direction of the first pass; task 1 calls out removal of the go:embed registry and bundled story-file/prd/agent-definition/claude-md assets.
Removes the go:embed archetype assets (story-file, prd, agent-definition, claude-md) and reworks the feature around user-supplied schema directories. - internal/archetypes exposes a Resolver over a list of root directories backed by any fs.FS; earlier roots shadow later ones. - Top-level config gains `archetypes.roots`. Merge copies the value through; InjectArchetypeRoots pushes it into required-structure rule settings when the rule doesn't set its own roots. - required-structure accepts `archetype-roots` (list, default `[archetypes]`) and resolves `archetype: <name>` against the project RootFS. - `mdsmith archetypes` CLI with init/list/show/path verbs. init scaffolds the directory with example.md + README.md, never mutates .mdsmith.yml. list prints "<name>\t<path>" lines; show prints the schema source; path prints the filesystem path. - Docs: MDS020 README, enforcing-structure guide, and CLI reference cover the new config key, rule setting, and CLI verbs. - Revert the .mdsmith.yml changes from the first pass: no more embedded .md files under internal/archetypes to allow or ignore.
Add unit tests for: - InjectArchetypeRoots / injectRoots helpers (top-level rule, override, no-op cases) - Merge copying archetypes from loaded and defaults - Resolver.Lookup non-ErrNotExist Stat error path - Resolver.AbsPath missing-name error - asStringList typed []string and scalar inputs - loadArchetype read-after-lookup error path (via a wrapping fs.FS that implements StatFS but errors on Open) Refactored archetype read into loadArchetype helper for clarity.
Add tests for: - --help flag on archetypes and each verb - Arg-validation exits: too many, wrong count - Kept-existing README on init - Bad config propagates through list/show/path - MkdirAll failure when target is a regular file Refactor loadConfig into a thin wrapper around loadConfigRaw so the InjectArchetypeRoots call sits in a single tested location instead of four per-branch call sites.
- Size-bound archetype reads via lint.ReadFSFileLimited so --max-input-size applies uniformly. - Suppress the <?require?> has-no-effect warning when the file itself lives under an archetype root (isSchemaOrArchetypeFile consolidates the skip/schema-file logic). - List verb prints resolver.EffectiveRoots() so users see the defaulted './archetypes' root in the error when nothing was configured. - Docs: enforcing-structure guide clarifies that 'archetypes init <dir>' creates '<dir>' exactly, and the MDS020 README no longer claims <?include?> resolves via RootFS — includes still read via the OS filesystem. - loadSchema comment calls out the include-resolution limitation.
Review findings taking the user's perspective through the CLI and fixture coverage: - **Bug**: README.md inside an archetype directory was indexed as an archetype. `archetypes list` returned "README, example" after `init`; `archetypes show README` and `archetypes path README` worked on it. Fixed via `isArchetypeName` — reserves README/LICENSE/CONTRIBUTING/CODEOWNERS (case-insensitive) and rejects any `_`-prefixed or `.`-prefixed basename. Applied in both `List` and `Lookup`. - **Inconsistency**: the "unknown archetype" and "no archetypes found" errors formatted roots differently — one relative, one absolute. Both now call `EffectiveRoots()`. - **Missing fixtures**: MDS020 had fixture tests only for the `schema:` path. Added `good/archetype.md`, `bad/archetype-missing-section.md`, `bad/archetype-unknown.md` with supporting archetype files under `data/archetypes/`. - **Resolver portability**: `os.DirFS` rejects `..` path segments, which broke the fixture setup. Resolver now falls back to raw `os.*` operations (joined with `RootDir`) when `FS` is nil, so fixture-style paths like `../../internal/rules/...` resolve the same way as schema paths. `loadArchetype` mirrors the distinction and uses `lint.ReadFileLimited` vs `lint.ReadFSFileLimited`. - **Tests**: added `TestResolver_ListFiltersReservedNames`, `TestResolver_LookupRejectsReservedNames`, `TestIsArchetypeName`, and `TestEffectiveRoots_DefaultsAndRootDirJoin`. - **Docs**: MDS020 README and the `init`-scaffolded README both document the reserved-name convention.
Add unit tests that exercise loadArchetype when the archetype resolver has no RootFS (fs-less mode), covering both the successful read via lint.ReadFileLimited and the size-limit error path.
- Validate archetype-roots when RootFS is set: absolute paths and parent-traversal roots produce a clear diagnostic instead of a low-level "invalid path" error. This aligns with the constraints readSchemaFile already enforces for disk-based schemas. - Clarify the Resolver comment: the os.* fallback allowing ".." is looser than RootFS-backed resolution; callers in required-structure reject parent-traversal roots when RootFS is set, so the looseness only surfaces in tests and fs-less callers. - Fix enforcing-structure guide: <?include?> inside an archetype still resolves via OS paths relative to the process working directory, not the archetype's directory. Keep archetypes self-contained or run from project root. - Tests: validate-archetype-root table; integration-style tests for root-escapes and absolute-root diagnostics.
- Move ValidateRoot/ValidateRoots into the archetypes package so both the rule and the CLI share one implementation. - CLI archetypesResolver now validates archetypes.roots and scopes FS to os.DirFS(rootDir) when a project root is known, so archetype-roots cannot escape the project (matches the rule's RootFS-backed constraint). - Keep the rule's pre-validation in loadArchetype for RootFS-backed invocations. - Tests: moved TestValidateArchetypeRoot into the archetypes package, added ValidateRoots tests, plus CLI e2e tests for escaping-root and absolute-root rejections.
…able - Rename cmd/mdsmith/e2e_archetypes_test.go to archetypes_e2e_test.go so the feature stays the prefix and _test.go remains the conventional postfix, matching unit-test naming across the repo. - Add a Default column to the check/fix flag reference table in docs/reference/cli.md and include the previously missing --no-follow-symlinks flag.
isSchemaOrArchetypeFile matched any *.md under or below a configured archetype root. With archetype-roots: ["."] it matched the whole repo, disabling required-structure validation and suppressing the misplaced <?require?> warning for every document. Now the check accepts only files that are direct children of the root: archetype discovery only finds <root>/<name>.md, so the schema-source check should match that exact shape. Files in subdirectories get treated as normal documents again. Added tests for both cases: archetype-roots "." with a nested doc, and a nested file under "archetypes/sub/" with the default root.
- Drop the duplicate DefaultArchetypeRoot constant in the required-structure rule and reuse archetypes.DefaultRoot. Single source of truth for the default root name. - Add Resolver.ListWithErrors that returns both the successfully discovered archetypes and the non-ErrNotExist errors from each readDir. List keeps its old signature for callers that do not care about errors. - mdsmith archetypes list now surfaces read errors on stderr and exits 2 when a root errored with no discoverable archetypes. - Tests: injected fs.FS that errors on ReadDir for a specific root to verify both the error-surfacing and not-exist-is-silent branches.
isArchetypeName now rejects names with path separators (/ or \) and the literal ".." segment, so Lookup cannot be coerced into reading outside the configured archetype roots (via raw os fallback when RootFS is nil). Names rejected here surface as the regular 'unknown archetype' diagnostic. Tests: extended isArchetypeName table with separator/traversal cases and added a disk-backed Lookup test that tries "../secret", "sub/story", "a/../../etc/passwd", and ".." against a tempdir where a secret.md sits next to the archetype root.
- Validate the target of 'mdsmith archetypes init' with archetypes.ValidateRoot so absolute and parent-traversal directories are rejected with a clear diagnostic instead of scaffolding successfully and printing a config snippet that the rule will later reject. - Update existing unit tests that passed absolute paths to chdir into a tempdir and pass relative names. - Add e2e tests for init rejecting '/abs/path' and '../outside'. - cmd/mdsmith/main_unit_test.go captureStderr/captureStdout now 'defer r.Close()' after os.Pipe so the read end is released even if f() panics. - Fix stale writeArchetype comment that claimed a return value.
|
🟢 Merge Queue — picked up This PR is in the queue and will be batched with other Next: No action needed — you'll get another comment when CI starts on the batch. View merge queue run. |
|
🔵 Merge Queue — CI running Merged into batch branch Next: No action needed — you'll be notified when CI completes. |
|
✅ Merge Queue — merged This PR landed on Next: Done — nothing more to do here. |
Summary
Plan 52 was reworked mid-PR. The first pass shipped four embedded
built-in archetype schemas; the reworked plan drops embedded assets
and ships a user-supplied archetype system with CLI support instead.
This PR reflects the reworked scope.
No archetype templates are embedded in the binary. Users author
their own schema files under directories registered in
.mdsmith.yml, and mdsmith discovers, lists, shows, and locatesthem via a new
archetypesCLI verb.Key Changes
internal/archetypespackage:Resolverover a list of rootdirectories and any
fs.FS. Earlier roots shadow later ones. Thepackage ships no
.mdassets of its own.archetypes.rootskey.config.Mergecopies it through;
config.InjectArchetypeRootspushes it intorequired-structurerule settings unless the rule sets its ownarchetype-roots.required-structurerule:archetype: <name>resolves against configured roots viaf.RootFS/f.RootDir.archetype-rootsrule setting (list, default[archetypes]).archetypeandschemaare mutually exclusive. The guardapplies both in
ApplySettingsand atChecktime.--max-input-sizevialint.ReadFSFileLimited.<?require?>in an archetype file no longer produces a"has no effect" warning when the file lives under an archetype
root.
mdsmith archetypesCLI (four verbs):init [dir]scaffolds the directory (default./archetypes)with an
example.mdschema and aREADME.md. Safe to re-run;never mutates
.mdsmith.yml, prints the snippet to add.listprints discovered archetypes as<name>\t<path>lines.show <name>prints the raw schema source.path <name>prints the resolved filesystem path.substituted) so users can diagnose empty
listoutput.reference document the new rule setting, config key, and CLI.
Known limitation
<?include?>expansion inside archetype schemas still reads vialint.ReadFileLimitedagainst the process working directory.Archetype includes therefore require running
mdsmithfrom theproject root until include expansion is unified with
RootFS.Documented in the MDS020 README and in the
loadSchemacomment.Notes for release notes
mdsmith archetypes.archetypes.rootsconfig key.required-structuresettings:archetype,archetype-roots..mdsmith.ymledits from the first pass of this PR have beenreverted; no repo-wide lint-config changes remain.
https://claude.ai/code/session_01MrAcoEEB4tGYMmUz8ErbTp