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

> Engineering log · [Phase 15 — Audit remediation, v0.8.0](https://borda.github.io/lucid-YOLO/engineering/phase-15-audit-remediation/)
>
> 🔗 Depends on [WP-165b](https://borda.github.io/lucid-YOLO/work_packages/wp-165b/) · Needed by [WP-175](https://borda.github.io/lucid-YOLO/work_packages/wp-175/)
>
> ⬅️ [WP-167](https://borda.github.io/lucid-YOLO/work_packages/wp-167/) · [Roadmap](https://borda.github.io/lucid-YOLO/ROADMAP/) · [WP-169](https://borda.github.io/lucid-YOLO/work_packages/wp-169/) ➡️


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

<a id="wp-168"></a>

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.

