| id | 100 |
|---|---|
| title | build config block and MDS040 recipe-safety rule |
| status | ✅ |
| summary | Add a `build:` config section that declares `base-url` and user-defined `recipes`. MDS040 lints each recipe's `command` field: no shell interpreter as first token, no shell operators in static parts, no fused `{param}` placeholders. |
| model | sonnet |
Introduce the build: config schema. User-defined
recipes are declared in .mdsmith.yml. MDS040
validates each recipe command for shell-safety at
lint time, before any directive or subcommand depends
on the schema.
Plans 101 and 102 both read build.recipes to resolve
recipe names and param schemas. This plan delivers the
config layer first so those plans have a stable
foundation. MDS040 is a pure linter — it reads the
parsed config and emits diagnostics; it never runs
any external tool.
New top-level key build: in .mdsmith.yml:
build:
base-url: "" # joined to path-only `url` params
recipes: # user-defined recipes (map)
mermaid:
command: "mmdc -i {input} -o {output}"
body-template: ""
params:
required: [input]
optional: [output]
api-spec:
command: "redocly bundle {input} -o {output}"
body-template: "[API spec]({output})"
params:
required: [input]Both keys are omitempty; a config without build:
continues to parse and check without error.
String. When a url param in a <?build?> directive
is a path-only value (starts with /), base-url is
prepended at build time. Has no effect on lint.
Map from recipe name to a recipe declaration:
| Field | Required | Description |
|---|---|---|
command |
yes | Argv template; {param} tokens expand at build time |
body-template |
no | Markdown body rendered by mdsmith fix; defaults to [{output}]({output}) |
params.required |
no | Param names the directive must supply |
params.optional |
no | Param names the directive may supply |
{param} tokens in command expand to individual
os/exec arguments — no shell. An unknown {param}
token (one not in required or optional) is a
config error.
{alt} is a reserved template variable available in
body-template; it maps to the Markdown image alt-text
and must not appear in command. {output} is NOT
reserved — it may be declared as a regular recipe
parameter and used in command.
- ID:
MDS040 - Name:
recipe-safety - Category:
meta - Default: enabled
- Fixable: no
- Target:
.mdsmith.yml
MDS040 validates every command in build.recipes:
- Non-empty —
commandmust have at least one token. - No shell interpreter — the first token must not
be
sh,bash,zsh,ksh,fish,/bin/sh,/bin/bash, or similar. - No shell operators in static parts — each token
that is not entirely a single
{param}placeholder must contain none of:&&,||,;,|,>,<,>>,2>,`,$(,${. - No fused placeholders — a single token that
contains two adjacent
{param}patterns (e.g.{a}{b}) is rejected; the resulting concatenation at build time could produce unexpected values. - No
..in executable — the first token must not contain a..path component. - No unused params (warning, not error) — every
entry in
params.requiredandparams.optionalmust be referenced by at least one{param}token incommand. Catches typos before they waste a build.
command is split on whitespace into tokens for
validation (the same split used at build time by plan
102). MDS040 does not execute any binary.
Example diagnostics:
.mdsmith.yml:5:5: recipe "audit": command uses shell
interpreter "bash" — use the direct binary (MDS040)
.mdsmith.yml:9:5: recipe "audit": command contains
shell operator "&&" — use a wrapper script (MDS040)
.mdsmith.yml:13:5: recipe "render": command contains
fused placeholders "{a}{b}" — separate with a
delimiter (MDS040)
- Add
BuildConfig,RecipeCfg, andParamCfgstructs tointernal/config/. Bothbuild:keys areomitempty. Validate that{param}tokens incommandare all declared inrequiredoroptional; emit a config error for unknowns. Validate that{alt}does not appear incommand(it is reserved for body-template only). - Add MDS040 (
recipe-safety) implementation ininternal/rules/recipesafety/(Go package). Implement the six checks above (the five safety checks plus the unused-param warning). Diagnostic messages include the recipe name and the offending string. Not fixable. - Add
good/andbad/fixtures for MDS040 underinternal/rules/MDS040-recipe-safety/. - Wire MDS040 into
cmd/mdsmith/main.go. - Document MDS040 in
internal/rules/MDS040-recipe-safety/README.md.
-
build:parses correctly with both keys absent, one present, or both present - A recipe with
command: "mmdc -i {input} -o {output}",params.required: [input], andparams.optional: [output]round-trips through config parse without error - A
{param}token incommandthat is not inrequiredoroptionalproduces a config error -
{alt}appearing incommandproduces a config error;{output}is a valid declared parameter name - MDS040 flags a recipe whose
commandstarts withbash,sh,/bin/bash, or/bin/sh - MDS040 flags a recipe whose
commandcontains a shell operator token (&&,||,;,|,>, etc.) - MDS040 flags a recipe whose
commandtoken contains fused adjacent{param}placeholders (e.g.{a}{b}) - MDS040 flags a recipe whose executable token
contains
.. - MDS040 warns (not errors) when a recipe declares
a
params.requiredorparams.optionalentry that no{param}token incommandreferences - MDS040 passes a clean command like
mmdc -i {input} -o {output} - A config with no
build:key passes MDS040 without error - All tests pass:
go test ./... -
go tool golangci-lint runreports no issues