Summary
Build-level constraints.md / taste.md currently replace the project-level files rather than extending them. Because resolution is first-match-wins, a build that ships a slim "deltas only" constraints.md silently loses the entire project-level ruleset (stack rules, forbidden deps, the numbered code-review gates, auth/determinism/storium boundaries, etc.). This contradicts the documented authoring guidance, which tells humans to write build files as deltas.
Proposal: change resolution for build-vs-project to "copy from root, then append" — compose the project-level file as the base and append the build-level deltas — so build files can be true deltas and the project ruleset is always present.
Current behaviour (observed in published ridgeline@0.12.6)
dist/stores/inputs.js — resolveFile returns the first existing path and stops:
// resolveFile(cliFlag, buildDir, filename, projectDir)
if (cliFlag && fs.existsSync(cliFlag)) return path.resolve(cliFlag)
const buildLevel = path.join(buildDir, filename)
if (fs.existsSync(buildLevel)) return buildLevel // ← build wins, returns immediately
const projectLevel = path.join(projectDir, filename)
if (fs.existsSync(projectLevel)) return projectLevel
return null
dist/config.js resolves a single constraintsPath (and a single tastePath):
const constraintsPath = resolveFile(opts.constraints, buildDir, "constraints.md", ridgelineDir)
// ...
const tastePath = resolveFile(opts.taste, buildDir, "taste.md", ridgelineDir)
The build/plan/review invokers each read only that single resolved path — the project-level file is never read when a build-level file exists:
dist/runner/buildInvoker.js → fs.readFileSync(config.constraintsPath, ...)
dist/runner/planInvoker.js → same
dist/runner/reviewInvoker.js → same
So builds/<name>/constraints.md is a full override, not a merge. (Source equivalents are presumably src/stores/inputs.ts, src/config.ts, src/runner/*Invoker.ts.)
Why this is a problem
The project-level template tells authors to write build files as deltas:
A build's constraints.md adds deltas … Add deltas only; do not duplicate what's already here.
But the override loader makes that advice unsafe: a build file written as deltas drops everything not restated in it. Today there are only two ways to be correct, and both are bad:
- Write deltas (as the docs say) → the build silently runs against a tiny constraint set; all the project gates the reviewer agent reads for are gone. The mechanical floor (
pnpm check + rules/*.yml) still fires, but every human-judgment gate and all the architectural framing the agents rely on disappears.
- Copy the whole project file verbatim + append (what real builds end up doing) → correct, but the copy drifts every time the project file changes, and it directly violates the "do not duplicate" guidance. It also forces a relative-link fixup (the nested build dir is two levels deeper, so e.g.
../docs/deep-modules.md must become ../../../docs/deep-modules.md).
The doc and the loader simply disagree, and the disagreement is invisible until something leaks.
Proposed change
For the build-vs-project step, compose instead of override:
- Load the project-level file as the base, then append the build-level file as deltas (e.g. concatenate
project + "\n\n" + build, or have the invokers emit both sections).
- Keep
--constraints <path> (the CLI flag) as a full override — an explicit path should still win outright.
- Apply the same composition to
taste.md.
This makes the documented "deltas only; do not duplicate" guidance actually correct, removes the drift/relative-link footguns, and guarantees the project ruleset is always in front of the builder/planner/reviewer.
Acceptance criteria / considerations
Context
Surfaced while reviewing a build whose specifier-generated constraints.md was written deltas-style (slim, ~76 lines) against a 700-line project file. Under the current override loader that build would have run with most project gates absent. Prior builds worked around it by copying the entire project file verbatim and appending — which is exactly the drift trap this issue is about.
Summary
Build-level
constraints.md/taste.mdcurrently replace the project-level files rather than extending them. Because resolution is first-match-wins, a build that ships a slim "deltas only"constraints.mdsilently loses the entire project-level ruleset (stack rules, forbidden deps, the numbered code-review gates, auth/determinism/storium boundaries, etc.). This contradicts the documented authoring guidance, which tells humans to write build files as deltas.Proposal: change resolution for build-vs-project to "copy from root, then append" — compose the project-level file as the base and append the build-level deltas — so build files can be true deltas and the project ruleset is always present.
Current behaviour (observed in published
ridgeline@0.12.6)dist/stores/inputs.js—resolveFilereturns the first existing path and stops:dist/config.jsresolves a singleconstraintsPath(and a singletastePath):The build/plan/review invokers each read only that single resolved path — the project-level file is never read when a build-level file exists:
dist/runner/buildInvoker.js→fs.readFileSync(config.constraintsPath, ...)dist/runner/planInvoker.js→ samedist/runner/reviewInvoker.js→ sameSo
builds/<name>/constraints.mdis a full override, not a merge. (Source equivalents are presumablysrc/stores/inputs.ts,src/config.ts,src/runner/*Invoker.ts.)Why this is a problem
The project-level template tells authors to write build files as deltas:
But the override loader makes that advice unsafe: a build file written as deltas drops everything not restated in it. Today there are only two ways to be correct, and both are bad:
pnpm check+rules/*.yml) still fires, but every human-judgment gate and all the architectural framing the agents rely on disappears.../docs/deep-modules.mdmust become../../../docs/deep-modules.md).The doc and the loader simply disagree, and the disagreement is invisible until something leaks.
Proposed change
For the build-vs-project step, compose instead of override:
project + "\n\n" + build, or have the invokers emit both sections).--constraints <path>(the CLI flag) as a full override — an explicit path should still win outright.taste.md.This makes the documented "deltas only; do not duplicate" guidance actually correct, removes the drift/relative-link footguns, and guarantees the project ruleset is always in front of the builder/planner/reviewer.
Acceptance criteria / considerations
constraints.md, the resolved constraints = project-level base + build-level deltas (project first). Same fortaste.md.--constraints/--tasteCLI flags remain full overrides (no composition).## Check Commandparsing still works after composition. Note a related footgun:parseCheckCommandmatches/## Check Command/(capital "C" in Command), but the project template header is## Check command(lowercase) — so the project file's own check command currently does not auto-resolve. Composition should pick a deterministic precedence for the check command (build delta wins if present, else project), and the casing mismatch is worth fixing while here.Context
Surfaced while reviewing a build whose
specifier-generatedconstraints.mdwas written deltas-style (slim, ~76 lines) against a 700-line project file. Under the current override loader that build would have run with most project gates absent. Prior builds worked around it by copying the entire project file verbatim and appending — which is exactly the drift trap this issue is about.