Skip to content

Build-level constraints/taste should compose with project-level (copy-from-root-then-append), not override it #2

Description

@robmclarty-minga

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:

  1. 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.
  2. 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

  • When a build dir has constraints.md, the resolved constraints = project-level base + build-level deltas (project first). Same for taste.md.
  • --constraints / --taste CLI flags remain full overrides (no composition).
  • If no project-level file exists, the build-level file is used alone (and vice-versa) — no crash.
  • ## Check Command parsing still works after composition. Note a related footgun: parseCheckCommand matches /## 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.
  • Update the project-template guidance text ("Add deltas only; do not duplicate what's already here") to match whatever semantics land — under composition it becomes accurate; under override it should instead say "copy the project file, then append."
  • Decide how a build can intentionally suppress a project-level rule under composition (today the docs say "escalate, don't fork" — composition makes that the only path, which is probably the desired behaviour, but worth stating).

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions