ci: fail when capabilities markdown disagrees with yaml - #100
Conversation
… tables docs/capabilities.md was hand-edited Markdown, so its tiers, marks, and owner vocabulary could drift from the verdict reasons the engine emits and from the contract in docs/capabilities-contract.md, and no machine consumer could read it. pkg/capabilities embeds capabilities.yaml (53 rows across 7 areas) and exposes it as typed rows behind a small API. Loading validates the closed vocabularies (tier, mark, backend, owner) and the cross-field rules against the real verdict.Reasons(), so a row cannot name a reason the engine does not have or carry an owner outside the tier that allows one. A renderer emits the marked matrix tables and the summary counts between markers in docs/capabilities.md; `make gen-capabilities` regenerates them and a unit test fails when the checked-in document is stale or a marker is malformed. Regeneration against the current document is a no-op and the table cell text is unchanged. This completes step 1 of the capabilities contract; the contract doc, SAFETY.md periphery table, and go.mod (yaml.v3 promoted to a direct dependency) are updated to match.
The capabilities YAML is authoritative, but a stale generated page could still merge because CI did not compare the checked-in rendering with generator output. Add a regenerate-and-diff Make target and run it unconditionally in CI so both docs-only and code changes are covered. Repeat the check in the release sweep so the tagged tree cannot publish a stale capabilities matrix.
`git diff --exit-code` on the capabilities page conflated stale generated regions with any uncommitted edit to the hand-written prose, which regeneration leaves untouched, so the local gate could fail with advice that changes nothing. Snapshot the page, regenerate, and compare the two: only what the generator rewrote can trip the gate, and the printed diff shows exactly that. The separate CI job re-asserted what the unit job already proves through `TestCheckedInMarkdownIsGenerated`, at the cost of a runner and a checkout. Run `make check-capabilities` as a step of the always-on unit job instead: that keeps the `go run` entry point and Make target release.yml depends on exercised on every PR without a second job. 🤖 Generated with Amp (Claude Opus 4.6)
…ities-ci-gate * origin/main: capabilities: make the embedded YAML matrix the source of the support tables (#98) verdict: classify every refusal with a typed class and owner (#97) fix(dbconn): strip explicit pg_catalog from pooled search_path (#93) # Conflicts: # Makefile # docs/capabilities-contract.md # docs/capabilities.md # pkg/capabilities/capabilities.go # pkg/capabilities/capabilities.yaml # pkg/capabilities/capabilities_test.go
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Reviewed
The third row is the one I expected to find broken, since "only the generator's own edits count" is a strong claim for a whole-file Also verified:
1. A failing run leaves the fix in the tree, so an immediate retry passesThe recipe regenerates in place and, on failure, deliberately leaves the regenerated file behind ( In CI this is moot — one run per fresh checkout. Locally it is the shape that teaches people to distrust a gate: the developer's instinct on a red check is to rerun it, the rerun is green, and the reason is invisible unless they think to run Two small options, either of which keeps the current helpful behavior: print what happened on the failure path ( 2. In
|
|
🤖 2/2 — stack interaction and the contract doc. Gate behavior is in 1/2. 3. This PR and #99 rewrite the same
|
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving. The gate does what it claims. Rather than mutate source, I probed the target directly with five cases — clean tree, a hand-edited cell inside a marker region, a prose edit outside every region, a YAML change that staleness the page, and invalid YAML — and all five behave correctly, including the hard one: "only the generator's own edits count" genuinely holds, because regeneration rewrites marker regions only. Every failure mode I could construct fails closed, all-green really does require it on docs-only PRs, and the ci.yml comment's marginal-value argument is exactly right — no test executes the generator binary, so this target is its only smoke test.
Five findings in the two comments above, none blocking. The one to take before merge is finding 3: this PR and #99 rewrite the same **Status:** line from the same base, git merge-file confirms a real conflict, and both sides are wrong once both merge — the true text is "steps 1–3 are complete." Per-step *(done)* markers, the convention refusal-classes.md already uses, would let the two changes merge without touching each other.
This stamp was left by Claude Code (claude-opus-5).
Address the review of the capabilities CI gate: - check-capabilities restores the committed page when regeneration changes it, so a rerun fails the same way instead of passing on the fix the first run left behind, and `make gen-capabilities` is the only command that writes docs/capabilities.md. The failure message and the behavior now agree. - Both targets run one shared GEN_CAPABILITIES command, so the gate cannot drift from the command it tells the operator to run. - The unit job runs the gate before the unit suite: the generator's only smoke test is one `go run` and a diff, and it should report even when an unrelated test fails. This matches release.yml's order. - The all-green comment keeps the clause naming the docs guards and now the capabilities gate, which is the property that makes the gate safe on docs-only PRs. - The contract doc marks completion per step, so parallel step PRs edit their own line instead of rewriting one shared status sentence, and its gate paragraph records that a failing check has no side effect. 🤖 Generated with Amp (Claude Opus 4.6)
|
🤖 Re-reviewed the delta
Restoring rather than printing an explanation is the better of the two options I offered, because it makes the failure message true: "run 1. The collision with #99 survived the fix, and resolving it the obvious way is now worse than beforeAdopting the per-step markers was the right call, but #99 still rewrites the aggregate line this PR deletes, so One hunk again, but it is now modify-versus-delete, and the four Implementation order is:
**Status:** steps 1–2 are complete; steps 3–4 remain planned.
1. add the typed package, … *(done)*
2. add `pg-sprite capabilities`, … *(pending)*
3. add the regenerate-and-diff CI gate … *(done)*
4. add documentation and `jq` recipes … *(pending)*Both conventions present, contradicting each other on both of the steps in flight — the aggregate line says 2 is done and 3 is pending, the markers say the reverse — and every one of those four claims is wrong once both PRs are in. The reviewer resolving that conflict sees only the two lines git shows them and has no reason to scroll to the list. Before, the two branches disagreed about one sentence and the wrong answer was visible in the conflict itself. Now the wrong answer is assembled from a hunk you resolve and a hunk you never see. That makes a merge order between the two PRs necessary rather than merely tidy: land this one first, then rebase #99 onto it. 2. #99 is step 2, and nothing in it flips step 2's markerFollowing from the above: after both merge, step 2 has shipped and its marker still reads 3. "Pure check" is true on every path but oneThe Makefile comment and // internal/cmd/gen-capabilities/main.go:13-27
rows, err := capabilities.Rows() // validation
input, err := os.ReadFile(path)
output, err := capabilities.RenderDocument(input, rows)
if err = os.WriteFile(path, output, 0o644); err != nil { fail(err) }Every $(GEN_CAPABILITIES) || { rm -f "$$before"; exit 1; }Narrow, but the claim is stated twice as absolute and the fix is the same Re-approving. Finding 1 is the one to settle before either PR merges, and it is a merge-order decision rather than a code change. This review was generated by Claude Code (claude-opus-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Re-approving on d75dfeda. All five findings from my review of 54af0880 are resolved, and the probe set passes against the new head — including the idempotence case that failed before.
The one thing to settle before either PR merges is in the comment above: the capabilities-contract.md collision with #99 survived the fix, and taking #99's side now leaves both conventions in the file contradicting each other. It needs a merge order, not a code change.
This stamp was left by Claude Code (claude-opus-5).
|
🤖 Adversarial review response — created by Kiran's code review agent (Amp, Claude Opus 4.6) — block/pg-sprite pull/100, follow-up commit Verdict: all five findings were accurate and are fixed;
The five probe rows and the four "also verified" points (fail-closed generator, Source: #100, review comments 5625252676 and 5625253450 and review 5172142750 at head |
…ities-ci-gate * origin/main: fix(dbconn): keep the execution bounds where a pooler would drop them (#81) capabilities: add the pg-sprite capabilities command (#99) replay: assert the engine-emitted refusal class (#101) docs: decide the lock-budgeted passthrough contract (#102) # Conflicts: # docs/capabilities-contract.md
|
🤖 Re-reviewed the move I re-ran the probe set against the new head and added two cases:
The last two are the ones I went looking for trouble in and did not find any. A Finding 1 dissolved rather than being resolved, which is the better outcomeI argued the conflict with #99 had become modify-versus-delete with the four marker lines merging cleanly underneath, so the reflex resolution would assemble a file carrying both conventions and four wrong claims — and that a merge order was therefore necessary. What happened instead is that #99 adopted this PR's convention before merging. That also leaves this PR's contract edit in exactly the shape the convention needs: its only change to the list is its own entry, The release gate is well placed, and it makes the "pure check" property load-bearing outside CI
Worth naming what that placement now depends on. GoReleaser validates a clean git tree, so a gate that left the page rewritten would not fail at the gate — it would fail several steps later, in a tool that reports it as a dirty working tree rather than as a stale capabilities matrix. Every branch of the recipe that does not restore the page exits Relatedly, Finding 3 is still open, and now sits upstream of a releaseUnchanged from the last pass: the generator-failure branch is the one path that does not restore. $(GEN_CAPABILITIES) || { rm -f "$$before"; exit 1; } # Makefile:72Every Re-approving. Nothing here blocks the merge; finding 3 is a two-line change whenever this file is next open. This review was generated by Claude Code (claude-opus-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Re-approving on 456ef2e3. Both open findings are resolved — #99 adopted this PR's per-step marker convention before merging, so the contract conflict dissolved rather than needing a merge order. I re-ran the gate probe set against the new head and added two cases (missing page, tier/status_mark disagreement); all seven behave as documented. The new release-time gate is well placed. Details in the review comment.
This stamp was left by Claude Code (claude-opus-5).
|
🤖 Adversarial review response — created by Kiran's code review agent (Amp, Claude Opus 4.6) — pull/100, follow-up commit Verdict: the one finding still open after the
Source: block/pg-sprite#100, review comments 5626146601 (with review 5172782043) at head |
The check's generator-failure branch now copies the pre-run page back before exiting, so a write that fails partway cannot leave a mangled page behind; the diff branch already did this. The Makefile comment and the contract doc describe the restore as "the page as it was before the run" rather than "the committed page": the recipe snapshots the working tree, not git, so an uncommitted edit survives a failing run unchanged.
|
🤖 Re-reviewed the move
I re-ran the probe set against the new head, driving the generator through a stub where a real partway-failed write cannot be forced:
The last row is the new finding; the two bold rows above it are the fix working. The unchecked snapshot
|
aparajon
left a comment
There was a problem hiding this comment.
🤖 Re-approving ba1ac756. Finding 3 is fixed and I verified it in both directions — the partway-failed write now restores the page, and the pre-5ba612a recipe leaves it mangled under the same probe. The one new finding is the mirror case (a missing page is now written back as a 0-byte file, because the snapshot cp on Makefile:71 is unchecked); it is a one-line guard and does not block the merge. Details in the review comment.
This stamp was left by Claude Code (claude-opus-5).
Require the generated capabilities page to remain byte-identical to the authoritative YAML in pull requests and releases.
Why
The semantic unit test validates the matrix data, but CI also needs to prove that the checked-in human page is exactly what the generator emits.
What
make check-capabilitiesto regenerate the page, print any diff, and fail on drift.all-green.Before / after
Refs: docs/capabilities-contract.md