ci: run the unit tests and their docs guards on docs-only PRs - #96
Conversation
The changes filter classifies *.md and docs/** as non-code and every downstream job is gated on it, so the docs_test.go guards that pin prose to code (the proof-type registries, the report and CLI output examples) never ran on the one kind of PR they exist to catch. A docs-only PR could drop a registry row and merge green; the guards passing locally was a fact about the author's machine, not a property CI held. The unit suite needs no Docker and finishes in well under a minute, so it now runs unconditionally on every PR and push and joins all-green's required set. Running it redundantly beside the version matrix on code PRs is cheaper than maintaining a list of which packages are guards, and that list would drift the same way the prose does.
Add a machine-readable refusal class contract so consumers can route every refusal without reinterpreting overloaded reason strings. ## Why Existing refusal reasons explain the immediate cause but conflate capability gaps, work owned by another tool class, permanent safety decisions, run-site conditions, and pg-sprite's own defects. `unsupported-statement` and `unsupported-partitioned-parent` each span several of those categories, and the split runs one level below the reason: along the typed cause (`PartitionRefusalCause`, `CreateShapeCause`, admission sentinels) that the code already carries. ## What - Define five additive classes: `capability-boundary`, `no-online-safety-problem`, `by-design`, `environmental`, and `invariant-violation` (the refusal-verdict face of the fail-closed `ErrInvariantViolation` rule). - Define structured owner values for `no-online-safety-problem` routing: `data-change-runner`, `provisioning`, `direct-operator` (the ⚪ half of T3), `declarative-front-door`. - Key the map on the typed cause where one exists and on the refusal site where none does: one table per cause set, plus the site-keyed table for the rest. Name the parse-boundary work (`pkg/statement` distinguishing the kinds behind the `KindOther` catch-all) that classification requires. - Pin the map to the code: `docs_test.go` guards in `pkg/verdict`, `pkg/executor`, and `pkg/preflight` fail when a `Reason`, `CreateShapeCause`, or `PartitionRefusalCause` exists without a row (adds `preflight.PartitionRefusalCauses()`). - Cite RF-5 (what `by-design` encodes) and RF-6 (which already separates the not-implemented from the version-gated partitioned-parent case); name the invariant rollout step 2 adds once the field ships. - Bring `capabilities-contract.md`'s shared-vocabulary section into agreement (fifth class, `direct-operator`), and add the design to the documentation index. Ordering: #95 (capabilities contract) has merged, so the cross-link here is live. #96 (CI runs Go tests on docs-only PRs) is independent; this PR touches Go test files, so full CI runs on it regardless. ## Before / after Two refusals with the same `reason`, `unsupported-partitioned-parent`, on PostgreSQL 16: ```text Before: the consumer sees one reason and cannot tell the two apart CREATE INDEX ... ON parts_parent (col) ALTER TABLE parts_parent │ ADD FOREIGN KEY ... NOT VALID ▼ │ refused, reason: unsupported-partitioned-parent ▼ (the partition-aware build is not implemented yet) refused, reason: unsupported-partitioned-parent │ (PostgreSQL 16 cannot do this; 18 can) ▼ │ consumer: "unsupported" — wait? upgrade? give up? ▼ consumer: the same guess, same reason string After: the class is read off the typed cause, so each refusal names its own next step CREATE INDEX ... ON parts_parent (col) ALTER TABLE parts_parent │ ADD FOREIGN KEY ... NOT VALID ▼ │ cause: parent-concurrent-index-build ▼ │ cause: parent-not-valid-foreign-key ▼ │ reason: unsupported-partitioned-parent ▼ class: capability-boundary reason: unsupported-partitioned-parent │ class: environmental ▼ │ consumer: wait for the pg-sprite release ▼ that adds the partition-aware flow consumer: upgrade the server to 18; same statement and same pg-sprite then run ``` The other cause sets follow the same rule: a `CREATE INDEX CONCURRENTLY` on a table born this run is `by-design` (use the plain build), and a `planRefusal` for a disposition this build does not know is `invariant-violation` (report a pg-sprite bug, do not wait or retry). 🤖 Drafted with Amp (Claude Opus 4.6); reviewed and edited by the author.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
🤖 Reviewed This is the right fix and the mechanism works. The two things I'd have expected to be wrong are both fine, and one of them is fine for a non-obvious reason worth recording. The sentinel really does depend on the new job. The premise is real, and I verified it by mutating the prose rather than the code. Two docs-only edits, each caught by the new job's oracle ( Under the old config both of those are docs-only PRs and merge with every Go test skipped. So the hole was live, not theoretical. The guards genuinely run under the flag, rather than skipping. This is the thing that would hollow the job out silently, so I checked it directly instead of trusting the file names: all 24 tests across The missing On invariants: No blocking findings. Comment 2 has two small accuracy points, both in comments this PR writes. This review was generated by Claude Code (claude-opus-5). |
|
🤖 Comment 2 of 2 on 1. The guard list is incomplete on the day it is written, in two places, and the job does not need one
Both run and pass under The comment's own last sentence is the argument against writing it: running redundantly "costs less than maintaining a list of which packages are guards" ( 2.
|
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approving. The sentinel genuinely gates on the new job (all-green greps toJSON(needs) rather than enumerating results, so the one-line needs edit is sufficient), the guards run rather than skip under SKIP_INTEGRATION=1 (24/24 RUN, none SKIP), and I confirmed the hole was live by mutating the prose: dropping a required row from docs/cli-output-examples.md or docs/execution-model.md fails the unit job, and both edits are docs-only PRs that merged with every Go test skipped before this change. The two findings are stale enumerations in comments this PR writes — non-blocking. Details in the two comments above.
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/96, follow-up commit Verdict: both findings addressed in
Source: #96, scratch review pg-sprite-pr96-review.md at head |
Runs the Docker-free unit suite on every PR, so the tests that pin prose to code fire on docs-only PRs instead of being skipped with the rest of CI.
Why
CI's
changesjob classifies**/*.mdanddocs/**as non-code and gates lint, build, the Docker test matrix and demo oncode == 'true'. That skip is right for the matrix, but it also skipped the guards that pin prose to code:internal/safety/proof_types_test.gofor the proof-type registries inSAFETY.md,.agents/checks/review.mdanddocs/tcb-model.md, and the sevendocs_test.gofiles for the plan/suggest/execution-model report examples and CLI output examples. A docs-only PR could delete a registry row and merge green. Surfaced by the #88 review and scoped out of that PR as a CI change.Two alternatives were considered: a docs-only leg running a hand-picked list of guard packages, or treating
**/*.mdas code for thetestjob. The first keeps a list that drifts exactly the way the prose does; the second runs the 5-version Docker matrix for a prose check. The unit suite runs in about 30 s locally and already contains every guard, so running it unconditionally costs one cheap redundant job on code PRs and needs no list.What
unitjob:make test-unit(SKIP_INTEGRATION=1, no Docker), runs on every PR and push, not gated on thechangesfilter.all-greenrequires it; itsneedslist is a strict superset of the old one.docs/testing.md"matrix is a gate" bullet says docs-only PRs still pass the unit job and names which tests are the guards.Before / after
A docs-only PR deletes the
AbsentTargetrow from the proof-type registry indocs/tcb-model.md. Before, thechangesfilter saw only markdown, every downstream job was skipped, andall-greenpassed with nothing having run. After, the same PR still skips lint, build, the matrix and demo, but theunitjob runs andTestRegistriesNameEveryProofTypefails on the missing row, soall-greenfails. Code PRs are unchanged apart from one extra ~30 s job.🤖 Created by Kiran's coding agent (Amp, Claude Opus 4.6)