Skip to content

📦 WP-168 — ci(gates): the gates and the ratchets certify what they claim

Engineering log · Phase 15 — Audit remediation, v0.8.0

🔗 Depends on WP-165b · Needed by WP-175

⬅️ WP-167 · Roadmap · WP-169 ➡️

WP-168 — five gates that asserted less than their own prose

Each of these was written down as a check and was not one. The row's claim is narrow and demonstrable: after it, each gate asserts the thing its name says.

CI ran a different suite than the gate. The workflow step called pytest inline without --doctest-modules and without the src scripts paths, so testpaths = ["tests"] applied and CI collected 1548 tests where the Makefile collects 2668. Every test whose subject is a scripts/ module — the ten lint checkers, the golden harness, the release guard, the freeze — plus some 879 doctests ran on the developer's machine and never remotely. A pull request breaking a lint checker's own logic was green in CI. The step now runs make test.

That target then swallowed pytest's exit 5 and reported it as a pass, a scaffold from before WP-002 when there was nothing to collect. Harmless while only a human ran it; not harmless once CI depends on it, because a job collecting nothing would go green. Removed in the same row that made it reachable — which is the general rule worth stating: a change that puts weight on an existing shortcut owns that shortcut.

Nothing verified a frozen golden was unmodified. check_goldens recomputes each file's producer and compares it against that same file's own stored values and tolerances — the module docstring states this as the design. So an edit that moves the values and the tolerances together is green by construction. Demonstrated on a copy of goldens/ outside the tree: frozen/0.5/optim_toy.json with steps_to_threshold_musgd moved 47.0 → 52.0 and its tolerance widened 15.0 → 100.0 reports 43/43 goldens passed. This is the rule two principal overrides were granted against, so the project had already exercised the decision path that the mechanism did not back.

goldens/frozen/MANIFEST.sha256 pins all 34 frozen files by digest, and audit_frozen_manifest.py asserts it. It is .sha256 rather than .json because discover_goldens globs the frozen tree for every .json file and would hand a .json manifest to the golden parser as a malformed golden. freeze_goldens.py is the only writer, with two modes because a release and a sanctioned move are different acts: freeze() seals only the rows for the files it just copied and preserves the rest verbatim, so a snapshot tampered with under an earlier minor cannot be re-blessed by the next release, while --reseal recomputes every row and is what records a move that escalation trigger 4 has already approved.

What the manifest narrows rather than closes. It is committed beside the files it covers, so a commit that edits a golden and reseals in the same breath still passes. What it buys is that declaring a move becomes a separate named act that shows up in a diff as a wholesale manifest rewrite, instead of something a values-and-tolerances edit does silently. Signing would close it; there is no key here. The hook runs on every commit rather than behind a files: filter, for the same reason license-audit does — it reads tree state, not the diff — and specifically because pre-commit's staged-path filter omits deletions, so a commit that only deletes a frozen golden would match no filter and never reach the one check that notices it is gone.

The release guard did not check the version it would ship. It checked the tag, the changelog section, the dependency tiers and the gate. Tagging v0.8.0 with a matching changelog section, __version__ still 0.7.0 and no goldens/frozen/0.8 passed every one — a wheel carrying 0.7.0 under a 0.8.0 tag, and a release freezing nothing while the CHANGELOG says it did. check_version parses __version__ with ast and never imports the package, because a guard deciding whether a distribution is shippable must not require that distribution to import first.

The licence audit recognised one family. COPYLEFT_PATTERN matched the GPL family against a policy (D13, D18d) that bans source-available, research-only, non-commercial and commercially licensed dependencies by name, and a contributor checkbox that asks a human to catch exactly those. A second tier-blind pattern now covers SSPL, BUSL, Elastic, PolyForm, Prosperity, Commons Clause and the non-commercial family — second rather than folded in, because BUSL is not copyleft at all and filing it under that name would make the finding misdescribe what it found. The docstring now names which families the patterns recognise and says plainly which ones still pass.

A ratchet with 47 rows of slack is not a ratchet. _ASSUMPTION_FLOOR sat at 26 against 73 rows and tested max(ids), so 47 assumptions could be deleted from the tail before it fired, and a middle row could be deleted without it ever firing. _WP_FLOOR tests len(ids) and sits at its exact live count; the assumption floor was the one left behind, and _DECISION_FLOOR had the same predicate with one row of slack. Both are now len at their live counts, and the rule that adding a row raises the floor is written in the ASSUMPTIONS.md header, which is the text somebody adding a row actually reads.