Skip to content

feat(cli): cofferdam invariants subcommand (CD-308) + fix inert cofferdam.toml [layers] (CD-312) - #200

Merged
TAJD merged 2 commits into
mainfrom
feat/invariants-subcommand
Aug 9, 2026
Merged

feat(cli): cofferdam invariants subcommand (CD-308) + fix inert cofferdam.toml [layers] (CD-312)#200
TAJD merged 2 commits into
mainfrom
feat/invariants-subcommand

Conversation

@TAJD

@TAJD TAJD commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Closes CD-308. Closes CD-312.

CD-308 — cofferdam invariants

The parsed architectural spec was reachable only through the MCP server's cofferdam.invariants tool. docs/mcp.md said so plainly: "No CLI equivalent exists — this is MCP-only." That inverts the server's own contract — every other tool there is a thin wrapper over a CLI command — and it matters because the spec is the load-bearing config surface. It merges two files, resolves globs, and validates a predicate DSL whose failure aborts the run. A user with a rule that was not firing had no way to ask the CLI what it had loaded.

Three verbs:

  • show — the resolved, merged spec. Which files were read, which of them the layers in force came from, public API, boundaries, invariants, scripted rules, load warnings. Text by default, --robot / --format json for machines, --pretty to indent. Exits 0 whenever the spec loads, including when nothing is declared — but says "Nothing declared" rather than printing an empty skeleton, because "you have no spec" and "your spec is empty" send a user to different fixes.
  • validate — parse and report without running the engine. Exits 1 on a malformed predicate or an unsupported schema version, so CI can gate on the config alone. --strict extends that to warnings (missing/deprecated schema_version, [layers] in both files).
  • normalize — canonical TOML to stdout. Round-trip tested: normalising an already-normalised spec is a no-op.

No migrate. It earns its keep at the first MAJOR schema bump and not before, and the docs have already been wrong about it twice.

CD-312 — [layers] in cofferdam.toml did nothing

Found while writing show, which reports which file the layers in force came from and could not answer honestly while one of the two sources did not work.

loader::load built the config with an unconditional layers: None, having already deserialised the block into TomlDoc::layers. parse_layers — the function that turns it into a LayersConfighad no callers anywhere in the workspace. So Design.LayerViolation fired only for layers declared in cofferdam.invariants.toml; the cofferdam.toml spelling parsed, validated, and was thrown away.

Reproduced before the fix on a two-layer fixture with a deliberate violation: layers in cofferdam.toml → no finding; byte-identical layers in cofferdam.invariants.tomlDesign.LayerViolation. Same failure shape as the exclude key from the docs sweep, and silent for the same reason — the config is accepted, so nothing tells you.

It also made the [layers] declared in both files deprecation hint unreachable: merge_invariants_from tests cfg.layers.is_some(), which was always false. Both work now, verified end to end.

This is a behaviour change in the honest direction. A project whose cofferdam.toml [layers] block has been quietly inert will start reporting the violations it always had. Filed under Fixed rather than Changed for that reason.

Verification

  • cargo test --workspace — 1334 passed (12 new: 9 CLI integration tests for the subcommand, 3 config unit tests for the layers fix)
  • cargo clippy --workspace --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean
  • gen-docs --check — clean (the llms.txt drift gate caught the missing subcommand entry, which is the gate doing its job)
  • vitepress build — clean, no dead links

Note

docs/schema-versioning.md still describes cofferdam invariants normalize as future work and migrate as existing. That file is owned by #197, which is rewriting the same section; it gets corrected in whichever of the two merges second rather than conflicting here.

🤖 Generated with Claude Code

https://claude.ai/code/session_019afugU2bTYCtXn6Vy9dn1v

The parsed architectural spec was reachable only through the MCP server's
`cofferdam.invariants` tool. That inverted the server's own contract —
every other tool there is a thin wrapper over a CLI command — and left a
user whose rule was not firing with no way to ask the CLI what it had
actually loaded. The docs twice promised verbs of this subcommand that
did not exist, and an error message in cofferdam-core sent users to one.

`show` prints the resolved spec: which files were read, which of them the
layers in force came from, the public API, boundaries, invariants and
scripted rules, plus load warnings. `validate` parses without running the
engine and exits 1 on failure, so CI can gate on the config separately
from the findings it produces; `--strict` extends that to warnings.
`normalize` emits the canonical TOML. No `migrate` — it earns its keep at
the first MAJOR schema bump and not before.

fix(config): load [layers] from cofferdam.toml (CD-312)

Found while writing `show`, which reports which file the layers in force
came from and could not answer honestly. `loader::load` built the config
with an unconditional `layers: None`, having already deserialised the
block; `parse_layers`, the function that turns it into a `LayersConfig`,
had no callers anywhere in the workspace. So `Design.LayerViolation`
fired only for layers declared in cofferdam.invariants.toml, while the
cofferdam.toml spelling parsed, validated and was discarded — accepted,
therefore silent. It also made the "declared in both files" deprecation
hint unreachable, since the condition it tests could never be true.

Both now work. The change reports violations that were always there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019afugU2bTYCtXn6Vy9dn1v
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

Criterion benchmark comparison (PR head vs base branch)

�[1m�[92m   Compiling�[0m cofferdam-engine v0.4.1 (/home/runner/work/cofferdam/cofferdam/crates/cofferdam-engine)
�[1m�[92m    Finished�[0m `bench` profile [optimized] target(s) in 39.08s
�[1m�[92m     Running�[0m benches/engine_bench.rs (target/release/deps/engine_bench-ca23a59a1638a55e)
Gnuplot not found, using plotters backend
Benchmarking full_run_no_cache
Benchmarking full_run_no_cache: Warming up for 3.0000 s
Benchmarking full_run_no_cache: Collecting 20 samples in estimated 6.9946 s (420 iterations)
Benchmarking full_run_no_cache: Analyzing
full_run_no_cache       time:   [16.250 ms 16.404 ms 16.667 ms]
                        change: [-6.4329% -2.3810% +1.5089%] (p = 0.28 > 0.05)
                        No change in performance detected.
Found 2 outliers among 20 measurements (10.00%)
  2 (10.00%) high severe

Benchmarking single_file_edit_incremental
Benchmarking single_file_edit_incremental: Warming up for 3.0000 s

Warning: Unable to complete 20 samples in 5.0s. You may wish to increase target time to 9.7s, enable flat sampling, or reduce sample count to 10.
Benchmarking single_file_edit_incremental: Collecting 20 samples in estimated 9.6947 s (210 iterations)
Benchmarking single_file_edit_incremental: Analyzing
single_file_edit_incremental
                        time:   [7.7842 ms 7.8378 ms 7.8939 ms]
                        change: [-3.4766% -1.5542% +0.2974%] (p = 0.13 > 0.05)
                        No change in performance detected.
Found 2 outliers among 20 measurements (10.00%)
  1 (5.00%) high mild
  1 (5.00%) high severe


@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

Criterion benchmark comparison (PR head vs base branch)

�[1m�[92m   Compiling�[0m cofferdam-engine v0.4.1 (/home/runner/work/cofferdam/cofferdam/crates/cofferdam-engine)
�[1m�[92m    Finished�[0m `bench` profile [optimized] target(s) in 37.74s
�[1m�[92m     Running�[0m benches/engine_bench.rs (target/release/deps/engine_bench-ca23a59a1638a55e)
Gnuplot not found, using plotters backend
Benchmarking full_run_no_cache
Benchmarking full_run_no_cache: Warming up for 3.0000 s
Benchmarking full_run_no_cache: Collecting 20 samples in estimated 7.1705 s (420 iterations)
Benchmarking full_run_no_cache: Analyzing
full_run_no_cache       time:   [16.385 ms 16.594 ms 16.806 ms]
                        change: [-2.3513% -0.0957% +1.6676%] (p = 0.94 > 0.05)
                        No change in performance detected.
Found 1 outliers among 20 measurements (5.00%)
  1 (5.00%) high mild

Benchmarking single_file_edit_incremental
Benchmarking single_file_edit_incremental: Warming up for 3.0000 s

Warning: Unable to complete 20 samples in 5.0s. You may wish to increase target time to 9.5s, enable flat sampling, or reduce sample count to 10.
Benchmarking single_file_edit_incremental: Collecting 20 samples in estimated 9.4585 s (210 iterations)
Benchmarking single_file_edit_incremental: Analyzing
single_file_edit_incremental
                        time:   [7.4070 ms 7.4593 ms 7.5164 ms]
                        change: [-2.7541% -1.7139% -0.5857%] (p = 0.01 < 0.05)
                        Change within noise threshold.


@TAJD
TAJD merged commit 35f312d into main Aug 9, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant