| id | 112 |
|---|---|
| title | Markdown convention bundles for MDS034 |
| status | ✅ |
| summary | Introduce a top-level `convention:` config key that pairs a Markdown flavor with a set of style-rule presets. Three conventions ship: portable (CommonMark everywhere), github (GFM on github.com), and plain (Markdown that survives `cat`). Convention applies as a base layer beneath the user's top-level rules. |
| model | opus |
Give a project one knob for "what kind of Markdown do we write" without overloading the renderer's flavor. Today MDS034 only answers which extensions a renderer supports. Adding seven style rules (MDS041-MDS047) without a bundle would make a strict-Markdown setup take eight separate config keys. A convention collapses those eight choices into one.
mdsmith already names two related ideas. Flavor is a property of a renderer — what CommonMark, GFM, or goldmark recognize. Kind is a property of a file — a role tag attached via front matter or glob. Convention is the missing third concept: a property of the project — what the team writes, project-wide.
The three answer different questions:
- Flavor: "what renderer do we target?" (one)
- Convention: "what kind of Markdown does this project write?" (one)
- Kind: "what role does this file play?" (zero or many)
Conflating any two leads to broken designs. Treating flavor as opinionated style would imply renderers that don't exist. Treating convention as a kind would suggest different files in the same project target different renderers, which they cannot. See the concepts doc for the full distinction.
The markdownflavor rule defines several flavors. It emits diagnostics when the configured flavor does not interpret a feature the file uses.
rules:
markdown-flavor:
flavor: gfmThe flavor controls only MDS034's own diagnostics. Other rules (MDS012 no-bare-urls, MDS018 no-emphasis-as-heading, etc.) are configured independently.
Seven rules cover MDS041 through MDS047. Each restricts a different CommonMark ambiguity. Without a convention bundle they would each be enabled and configured separately, multiplying config friction.
A new top-level convention: config key, sibling to
rules:, kinds:, and overrides::
convention: portableThat single line pins a flavor and a curated set of
style-rule settings. The convention applies as a
base layer beneath the user's rules: block. A
user can set convention: portable and adjust one
rule on top:
convention: github
rules:
no-inline-html:
allow: [details, summary, sub, sup]Convention is project-wide. There is one selection per config file. Setting an unknown name is a config error at load.
A convention implies a flavor. Setting both is allowed only when they agree. A mismatch is a config error:
convention: portable
rules:
markdown-flavor:
flavor: gfm # ← error: portable requires commonmarkAn earlier draft put profile: inside the MDS034
rule's settings. That broke down for three reasons.
Disabling MDS034 (rules.markdown-flavor: false)
collided with rules.markdown-flavor: { profile: ... } because YAML disallows duplicate mapping keys.
Discoverability was poor — a project-level concept
buried inside a per-rule settings block. Validation
ran in two places (the rule and the loader), forcing
the cross-check to live twice.
Top-level convention: fixes all three. It sits
where users look for project decisions. Disabling
MDS034 now works the obvious way. Validation is one
config-load pass.
"Profile" reads as a near-synonym of "flavor" (both feel like "kind of Markdown"). The line between them is real but the names blur it. "Convention" reframes the concept around what it actually is: the team's writing convention, distinct from the renderer's grammar. Three concepts, three words, no name collision: flavor (renderer), convention (project), kind (file).
Three conventions ship initially. Each names a target.
portable— Markdown that renders the same in every CommonMark parser.flavor: commonmark, all of MDS041-MDS047 enabled with recommended defaults (no inline HTML, no reference links, asterisk bold, underscore italic, dash HR, dash list marker, sequential ordered numbering, max emphasis run 2).github— Markdown that renders well on github.com.flavor: gfm, MDS041 enabled with<details>and<summary>in the allowlist, MDS042 and MDS045 enabled with defaults, the rest disabled.plain— Markdown that survivescat. Same activations asportableplusallow-comments: falseon MDS041 (HTML comments leak through as literal text in plaintext readers).
The plain convention sits close to portable
today. A truly plaintext-faithful convention would
need rules that do not exist yet (no-emphasis,
no-fenced-code, prefer-bare-urls). When those rules
land in follow-up plans, plain gains them and
diverges from portable.
Conventions are declared in the convention package as a table: name → flavor + rule preset map.
Conventions do not re-implement the rule logic. They push settings into the existing rule config during config load. The flow:
- Config loader reads top-level
convention:. - If non-empty, it looks up the convention table.
- The preset is captured on
Config.ConventionPreset. effectiveRulesmerges the preset as the lowest layer (oldest), beneathdefault, kinds, and overrides.
This piggybacks on the existing deep-merge config (plan 97). User overrides still win.
A team can set convention: github and extend the
inline-HTML allowlist:
convention: github
rules:
no-inline-html:
allow: [details, summary, sub, sup]Lists default to replace, so the effective allowlist
becomes [sub, sup, details, summary] only if the
user keeps the preset entries explicitly. The
preset provides the floor.
MDS034's Check() does not gain new diagnostics.
Its job stays the same: detect features the
configured flavor does not interpret. The convention
mechanism is a config-layer concern. The reasons:
- Each rule already owns its detection logic. Duplicating into MDS034 would create two places that emit the same diagnostic.
- Tests for MDS041-MDS047 should not depend on MDS034 being enabled.
- Disabling MDS034 should not silently disable the style rules a convention turned on.
MDS034 itself does not read the convention name.
It still only reads flavor: from its own settings.
The convention preset writes flavor: into MDS034's
settings as part of its preset, and MDS034's
existing ApplySettings handles it normally.
- Unknown convention name → config error at load time with the list of valid names.
- Convention/flavor disagreement → config error naming both values.
- Convention is non-string → config error naming the field.
- Add a Convention type and built-in table in
the
convention package
with a
Lookup(name string) (Convention, error)helper. - Add a top-level
Convention stringfield onconfig.Configwith aconvention:YAML tag. - Add
applyConvention(cfg *Config) errorin internal/config/convention.go that runs afterValidateKindsinLoad, resolves the convention name, validates against any user-set flavor, and stores the preset onConfig.ConventionPreset. - Update
effectiveRulesto mergecfg.ConventionPresetas the lowest layer beneathcfg.Rules. - Add a
convention.<name>layer to the kinds resolution output (plan 95) somdsmith kinds resolveshows which rules a convention turned on. - Document the three built-in conventions in docs/reference/conventions.md.
- Cover convention vs flavor vs kind in the concepts doc.
- Setting
convention: portableenables MDS041-MDS047 with documented preset values. The preset table ships the full set; activations against the live rule registry are deferred until plans 105-111 land those rules. - Setting
convention: githubenables MDS041, MDS042, MDS045 with their documented presets and leaves the rest disabled. - Setting
convention: plainenables MDS041-MDS047 with the portable presets plusallow-comments: falseon MDS041. - User overrides win over convention presets via deep-merge (e.g. extending the inline-HTML allowlist).
- Setting both
convention: portableandrules.markdown-flavor.flavor: gfmproduces a config error naming both values. - Setting an unknown convention name produces a config error naming the field and listing the valid names.
- Setting
convention:to a non-string value produces a config error. -
mdsmith kinds resolvereports which rules a convention activated via theconvention.<name>layer source. - MDS034 itself does not emit new diagnostic types; all convention-driven diagnostics come from MDS041-MDS047.
- Disabling MDS034 (
rules.markdown-flavor: false) does not disable the rules a convention turned on; the preset has already been applied at config load. - All tests pass:
go test ./... -
go tool golangci-lint runreports no issues