| id | 113 |
|---|---|
| title | User-defined Markdown conventions |
| status | ✅ |
| summary | Extend the convention system from plan 112 with a top-level `conventions:` block in `.mdsmith.yml`. Teams can define their own convention inline, with the same shape as the built-in `portable`, `github`, and `plain`. No inheritance — each convention stands alone. |
| model | sonnet |
Let a team define a convention inline in their
.mdsmith.yml without forking mdsmith. The three
built-in conventions cover common cases; this plan
covers everything else. A team that wants "GFM plus
no inline HTML plus dash bullets but allow
footnotes" gets there in seven lines of YAML.
Plan 112 ships
conventions as a closed table baked into the binary.
The Lookup(name string) (Convention, error) helper
returns one of portable, github, or plain, or
a config error. Adding a fourth convention requires
a code change.
Plan 112 considered three ways to open the system:
inline in .mdsmith.yml, separate convention files,
and inheritance via extends:. This plan ships
only the inline form. Reasons:
- The repo's
.mdsmith.ymlis already the source of truth for everything else mdsmith reads. Teams know where to look. - Separate files add a path-resolution layer (relative? absolute? from which directory?) that pays off mostly when the convention is shared across repos. Within one repo, inline is shorter.
extends:is genuinely useful but doubles the validation surface. Defer until a team asks.
A team that wants cross-repo reuse can copy the seven lines, which is the same friction as managing a separate file with no resolver bugs.
A new top-level conventions: key in
.mdsmith.yml, sibling to kinds::
conventions:
our-team:
flavor: gfm
rules:
no-inline-html:
allow: [details, summary, kbd]
list-marker-style:
style: dash
no-reference-style:
allow-footnotes: true
convention: our-teamThe conventions: map mirrors the built-in
convention table from plan 112. Each entry is a
{ flavor, rules } pair. The rules block uses
the same schema as the top-level rules block.
User-defined names must not collide with the three
built-in convention names. Defining a
conventions.portable in .mdsmith.yml produces a
config error. The built-in names are reserved so
docs and tutorials keep meaning what they say.
When the top-level convention: selector resolves:
- Look up the name in user-defined
conventions:first. - Fall back to the built-in table.
- If neither matches, emit a config error listing both sets of names.
User conventions cannot shadow built-ins. Collisions are rejected at parse time. The lookup order is documented anyway so future maintainers see the precedence explicitly.
Each user-defined convention is validated at config load:
flavormust be one ofcommonmark | gfm | goldmark.- Each key under
rules:must name a registered rule. - Each rule's settings must validate against that
rule's existing schema (the same code path that
validates a top-level
rules:block).
Validation errors name the convention and the rule:
convention "our-team" rule "no-inline-html": unknown setting "allowed".
User-defined conventions apply as a base layer
beneath any top-level rules: overrides. This
matches how built-in conventions work in plan 112.
A team can set convention: our-team and then
override one rule in the top-level rules: block.
The override wins.
Lookup(name) from plan 112 grows a second arg —
the user-defined convention map from config. The
signature becomes Lookup(name string, userConventions map[string]Convention) (Convention, error). The
config loader reads conventions: once at startup
and passes the map through.
No new diagnostics. The check logic in MDS034 is unchanged.
- Add
Conventionsfield to the top-level config struct ininternal/config/. - Add YAML parsing for the
conventions:block with the same schema as the built-in convention table. - Add reserved-name validation rejecting
portable,github, andplainas user names. - Per-rule settings validation: reuse the
ApplySettingsvalidation path each rule already implements, called against an empty instance. - Extend
markdownflavor.Lookupto consult the user map first, then the built-in table. - Wire the user map through the config loader to the lookup site.
- Update
mdsmith kinds resolveso user convention names appear with a(user)suffix to distinguish them from built-ins. - Add tests covering: a valid user convention, a name collision with a built-in, an unknown rule name, an invalid rule setting, and a top-level rules override on a user convention.
- Document
conventions:in the same place plan 112 documents the built-ins.
-
conventions:block in.mdsmith.ymldefines a named convention withflavor:andrules:. - Top-level
convention: our-teamselects a user-defined convention and applies its rule presets. - Defining
conventions.portable(orgithub/plain) produces a config error naming the reserved name. - An unknown convention name lists both built-in and user-defined options in the error message.
- Top-level
rules:overrides win over user convention presets via deep-merge. - Invalid rule names or settings inside a user convention produce a config error naming the convention and the rule.
-
mdsmith kinds resolvedistinguishes user conventions from built-ins in its output. - All tests pass:
go test ./... -
go tool golangci-lint runreports no issues