FEAT-096: gate the release-ordering inversion that was found by hand once - #194
Merged
Conversation
…once v3.3.0's FEAT-064 is satisfiable only via FEAT-077 and FEAT-087, both scheduled v3.4.0 and both already shipped; FEAT-069 depends on FEAT-095, scheduled v3.5.0. v3.3.0 therefore cannot be cut before v3.4.0, and no work on its remaining items changes that. The previous commit made those dependencies VISIBLE by typing them as `depends-on` links, replacing prose buried in an acceptance criterion. This makes them CHECKED. Visible is not checked: the inversion was found by an ad-hoc script run by hand exactly once, and nothing re-ran it, which is the same defect as a gate that never fails — it reads as rigour and enforces nothing. The allowlist is the design. Three inversions are real, understood, and recorded on scry#187, so failing on them would put main red for a known condition; each is listed with its reason and anything new fails. A STALE entry also fails, which makes the three known pairs three facts the detection logic must keep re-deriving from live artifacts. Mutation 3 demonstrated the consequence and I had not designed for it: flipping the comparison operator turns all three stale and the gate red. The allowlist is a permanent live test of the checker, which is the answer to "who checks the checker" for a gate whose normal state is green. Mutation-checked against REAL artifacts, not fixtures — a structural YAML edit gave FEAT-057 a dependency on a v3.5.0 item and the gate named it. Plus a stale entry, the inverted comparison, and a clean control. Runs as a step on rivet-validate rather than a job of its own: same artifacts, same PyYAML, and that context is already required, so the gate binds on merge with no ruleset edit to forget. A forgotten post-merge ruleset edit is scry#130. Required set unchanged at 12, verified. This does not RESOLVE the inversions — that is the planning decision still open on scry#187. If the answer is "move FEAT-064 and REQ-020 to v3.4.0", the gate will fail until the allowlist entries are deleted, which is the correct pressure. Refs: FEAT-096 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkNzkNYzPh7366DkNijeNc
📐 rivet artifact deltaPR: #194 Base SHA: Validationhead — `rivet validate` resultbase — `rivet validate` result (for comparison)Artifact stats
full stats — headDiff (base → head)AADL model — headPosted by the |
…lled safe Two holes, both found by review of text that was about to ship. 1. FAIL-OPEN ON THE SOURCE SIDE. An unparseable release mapped to a (99,99,99) sentinel, and the residual claimed that "sorts last, so it biases toward false alarms, the safe direction". True when the garbage is the TARGET. False when it is the SOURCE: nothing can exceed (99,99,99), so an artifact with a garbage release became a source under which no inversion could ever be reported. The residual asserted a safety property the code did not have — the overclaim shape this repo keeps catching. release_key now returns None and every caller fails. Mutation-checked on a real artifact: FEAT-064's release set to `v3.3` exits 1. 2. LAST-GLOB-WINS ACROSS 23 FILES. release_of is flat, so a second declaration of the same id silently won. G-005 living outside safety-case.yaml is the precedent check-undeveloped-goals.py exists for. Now fails naming both releases; mutation-checked with a second FEAT-064 declaring v9.9.9. Also measured the gate's REACH, which was not stated: 59 of 266 artifacts carry a release, because the axis arrived with rivet 0.22 and FEAT-001..031 predate it. 37 depends-on edges have an unscheduled source, all shipped history. That leaves one asymmetric case undetected — a SCHEDULED artifact depending on an UNSCHEDULED one is equally uncuttable and the gate skips it, a skip the self-test codifies as intended, which is how it would have stayed invisible. Probed and measured at zero today, and now named in the residual rather than implied absent. Self-test 6 -> 13 cases, now covering collect() as well as verdict(). All five mutants red, control green. Refs: FEAT-096 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KkNzkNYzPh7366DkNijeNc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
v3.3.0's FEAT-064 is satisfiable only via FEAT-077 and FEAT-087 — both scheduled v3.4.0, both already shipped. FEAT-069 depends on FEAT-095, scheduled v3.5.0. So v3.3.0 cannot be cut before v3.4.0, and no work on its remaining items changes that.
The previous PR made those dependencies visible by typing them as
depends-onlinks instead of prose buried in an acceptance criterion. This PR makes them checked.Visible is not checked. The inversion was found by an ad-hoc Python snippet I ran by hand once, and nothing re-ran it — the same defect as a gate that never fails: it reads as rigour and enforces nothing. Same distinction FEAT-091 drew for commit trailers and FEAT-093 for the required-check set.
The allowlist is the design, not an escape hatch
Three inversions are real, understood, and recorded on #187. Failing on them would put main red for a known condition, so each is listed with its reason and anything new fails.
Critically, a stale entry — one naming an inversion that is no longer real — also fails. The three known pairs are therefore three facts the detection logic must keep re-deriving from live artifacts on every run.
Mutation 3 showed a consequence I had not designed for: flipping the comparison operator turns all three entries stale and the gate red. The allowlist is a permanent live test of the checker — the answer to who checks the checker for a gate whose normal state is green. A suppression list you can only ever add to is how gates rot; this one costs something to keep.
Mutation-checked against real artifacts, not fixtures
depends-ona v3.5.0 item (structural YAML edit, applied-count asserted)FEAT-999 -> FEAT-998>flipped to<Plus a
--self-testof 6 cases over the pureverdict()function, run before the real check in CI, covering earlier-release and same-release dependencies, an unknown target, and two-new-inversions-both-reported.A step, not a job
It runs on
rivet-validate: same artifacts, same PyYAML as the goal guard beside it, and that context is already required — so the gate binds the moment this merges, with no ruleset edit to forget. A new required context needs a post-merge admin step, and a forgotten post-merge admin step is #130. Required set unchanged at 12, verified with--against-file.What this does not do
It does not resolve the inversions — that is the planning decision still open on #187 (move FEAT-064/REQ-020 to v3.4.0, accept that v3.3.0 ships after it, or re-cut). If the answer is "move them", the gate will fail until the allowlist entries are deleted, which is the correct pressure.
It checks only typed links. A dependency stated in prose stays invisible, which is exactly how these three hid.
Refs: FEAT-096 · scry#187
🤖 Generated with Claude Code
https://claude.ai/code/session_01KkNzkNYzPh7366DkNijeNc