The ratchet follows the count down — a drop tightens instead of reddening the gate (coordinator item 2) (#2679)

Taken after asking twice for reassignment with no reply, and after the
same failure bit a **third** time. No open PR touches the census CLI, so
this is unowned in practice — **U12, say so if you have started and I
will close this in favour of yours.**

## What changed

A **drop** now tightens the baseline instead of failing. Failing hard
was defensible in isolation — a stale allowance is a hole, since those
guards can return up to the old count while the check stays green. What
it missed:

**The drop is almost never the failing author's to fix.** Eleven files
dropped during one merge wave, none of those PRs re-recorded, and none
of their authors did anything wrong. Measured three times since CI began
gating this: `columnRoles.ts` 0 → 1, then `executor.ts` twice.

A permanently-red gate is a bigger hole than a stale allowance, because
it gets ignored and then nothing is guarded at all. **The rise check —
the ratchet's actual purpose — is untouched and still fails hard.**

## The residual, named rather than glossed

In CI the write is discarded with the runner, so the committed baseline
stays stale until someone commits a tightened one. The exposure is
bounded (regrowth only up to the old count), printed on every run, and
strictly smaller than the exposure from a check people route around.
`--strict --exact` restores hard failure for the pinned end state.

**One writer:** the write is now a named `writeBaseline()` shared by the
tighten path and `--update-baseline`, rather than a second
`writeFileSync`. Two writers for one artifact is how they drift — a
lesson this file already learned once.

## Exercised end to end

| scenario | result |
|---|---|
| drop, `--strict` | exit **0**, `TIGHTENED`, allowance rewritten 9 → 6
|
| drop, `--strict --exact` | exit **1**, baseline untouched |
| rise, `--strict` | exit **1** |
| clean | exit **0** |

Pinned through the real CLI with an isolated baseline. Revert proof:
restoring the hard failure fails **1 of 32**.

## Two of my own mistakes, recorded

**A vacuous assertion, in the case that guards against vacuity.** I
first wrote `expect(allowedAfter).toBeLessThan(4 + allowedAfter)` — true
for every number. Replaced with a comparison against the inflated value
the fixture started from. This file documents that trap repeatedly and I
still walked into it, which is the argument for the mechanical revert
check over careful reading.

**The env override is `FUSION_CENSUS_BASELINE_PATH`**, not the
`FUSION_CENSUS_BASELINE` I used in the first draft — so the first
version of these cases silently ran against the **real** baseline and
passed for the wrong reason. A test whose fixture never took effect is
the same failure as a test whose fixture can't fail.

## Verification

32/32 census suites, `pnpm test:gate` **71/71**, `--strict` exits 0,
`pnpm lint` clean, `docs/testing.md` updated.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---

## Update — the base-ref ratchet (review round 2, commit `4895845579`)

The first version of this PR shipped a **named residual**: the
tightening write dies with the CI runner, so the committed allowance
stays high and a later PR can regrow guards up to it while `--strict`
prints green. I called the exposure bounded and moved on. Greptile
flagged it P1 and was right — naming a hole is not closing one.

`--strict` now stops trusting the committed number for files the branch
touched. It measures each **changed** file at the base commit
(`FUSION_CENSUS_BASE_REF`, else the PR base branch, else `origin/main`)
and fails if the file carries more guards than the base ref has. **The
enforced ceiling is what main has today**, so a stale, missing, or
long-unrecorded baseline no longer opens a window.

| decision | why |
|---|---|
| changed files only, `<ref>...HEAD` | untouched files have main's
counts by construction; censusing all ~400 at the base ref is ~400 `git
show` calls to re-derive numbers that cannot have moved. Three-dot also
stops charging this branch for guards that landed on main after the
fork. |
| a new file's base allowance is **0** | "absent at the base ref" as
unbounded would make a new file the cheapest place to hide a fresh guard
|
| fails **open** on an unresolvable ref, printing `SKIPPED` | a shallow
clone cannot produce an honest comparison; a degraded run must not read
as a clean one. The baseline comparison still applies. |
| merged into the existing `regressions` list | one failure per file,
and `--update-baseline` keeps working as the deliberate escape hatch. No
new exit path. |

**Revert proof, measured both ways.** With the base-ref block removed,
the regrowth fixture — base commit 2 guards, HEAD 5, baseline allowing 9
— exits **0** with `TIGHTENED`, which is precisely the reported
scenario. With it: exit **1**, `column-guard count ROSE`, `above its
count on the base ref`, baseline left at 9. **3 of the 4** end-to-end
cases go red on revert. The fourth passes without the fix by design — it
is the genuine-conversion case the auto-tighten exists to keep green,
and a case that reddens either way proves nothing.

The end-to-end suite builds a throwaway two-commit `git init` repo under
the temp dir, because this exploit is a property of the **plumbing**,
not of the comparison: resolving a ref, working out the changed set,
reading base source through `git show`. The comparator itself is pure
with the reader injected (`findRegrowthAgainstBase`), with its own cases
in `lifecycle-column-census-ast.test.ts` — including the one that would
silently pass everything, looking up the wrong key in
`summarize().byFile`.

**Rebased onto `origin/main` @ bc782d8d92** (the branch was forked
before the recent merge wave; its baseline read 746 against a tree of
722).

Verification on the rebased branch: census **722** / `--strict` exit 0 ·
**70/70** across both census suites · `pnpm test:gate` **71/71** · `pnpm
lint` clean.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-30 02:56:08 -07:00
committed by GitHub
parent ae23be79f7
commit bb3bdab999
3 changed files with 224 additions and 14 deletions

View File

@@ -88,6 +88,8 @@ const json = process.argv.includes("--json");
const strict = process.argv.includes("--strict");
const compare = process.argv.includes("--compare");
const updateBaseline = process.argv.includes("--update-baseline");
/* `--exact` keeps hard failure on a DROP, for the end state where the count is pinned. */
const exact = process.argv.includes("--exact");
if (json) {
console.log(JSON.stringify({ scannedFiles: files.length, ...summary, byFile: summary.byFile }, null, 2));
@@ -362,7 +364,7 @@ The flag is an explicit operator action, so it re-records unconditionally and PR
under `ACCEPTED RISES`. Silently swallowing a rise is the real danger; refusing to let anyone re-record is
the same danger one step later, wearing a red check nobody trusts.
*/
if (updateBaseline) {
function writeBaseline() {
writeFileSync(
BASELINE_PATH,
`${JSON.stringify({
@@ -376,6 +378,10 @@ if (updateBaseline) {
queryByFile: Object.fromEntries(summary.queryByFile),
}, null, 2)}\n`,
);
}
if (updateBaseline) {
writeBaseline();
if (regressions.length > 0) {
console.log("\n ACCEPTED RISES (a merge or a conversion added guards here — convert them or they stay in the bar):");
for (const r of regressions) {
@@ -410,16 +416,88 @@ it. The `!deliberateTracked && updateBaseline` condition went with it: the uncon
legacy-shape migration too.
*/
if (stale.length > 0) {
console.error("\nlifecycle-column-census --strict: baseline is STALE — it allows more than the tree has\n");
for (const s of stale) {
console.error(` ${s.file}: allows ${s.allowed}, tree has ${s.count}`);
/*
FNXC:LifecycleColumnCensus 2026-08-01-02-30 (coordinator item 2 — the ratchet must FOLLOW THE COUNT DOWN):
A DROP TIGHTENS THE BASELINE INSTEAD OF FAILING. The old behaviour failed hard, and the reasoning was sound
in isolation — a stale allowance is a hole, since those guards can return up to the old count while the
check stays green. What it missed is that the drop is almost never the author's to fix: eleven files dropped
during one merge wave, none of those PRs re-recorded, and none of their authors did anything wrong. Measured
three separate times since CI began gating this (`columnRoles.ts` 0->1, then `executor.ts` twice).
A PERMANENTLY-RED GATE IS A BIGGER HOLE THAN A STALE ALLOWANCE, because it gets ignored and then nothing is
guarded at all. So the ceiling now follows the count down automatically and says so, while the RISE check —
the actual purpose, "no new guards" — still fails hard and untouched.
THE RESIDUAL, named rather than glossed: in CI the write is discarded with the runner, so the committed
baseline stays stale until someone commits a tightened one. The exposure is bounded (regrowth only up to the
old count) and printed on every run, and it is strictly smaller than the exposure from a check people route
around. `--exact` keeps hard failure for the end state, when the count is meant to be pinned and any
divergence is a real event.
*/
/*
FNXC:LifecycleColumnCensus 2026-07-30-12:10 (PR #2679 review — greptile P1):
A TOUCHED FILE MUST BE RE-RECORDED; AN UNTOUCHED ONE IS AUTO-TIGHTENED.
The residual named below is real: in CI the tightening write is discarded with the runner, so the
committed allowance stays stale and a later change can regrow guards up to it while the gate is
green. Naming that is not closing it.
This closes it where the regrowth would have to happen. Regrowing a guard means EDITING the file,
so requiring an exact baseline only for files the change TOUCHES makes the hole unreachable — while
the case this PR exists for stays green, because those authors did not touch the files that dropped
(eleven files dropped in one merge wave; none of those authors did anything wrong).
Falls back to the lenient path when no base ref resolves, so a detached or shallow checkout
degrades to the previous behaviour rather than failing closed on a git detail.
*/
let touched = new Set();
/*
The touched set is overridable for the same reason BASELINE_PATH is: otherwise this branch can only
be tested against whatever the CURRENT branch happens to have changed, so the test's outcome would
depend on the diff of the PR running it. Production never sets it.
*/
if (process.env.FUSION_CENSUS_TOUCHED_PATHS !== undefined) {
touched = new Set(process.env.FUSION_CENSUS_TOUCHED_PATHS.split(",").map((f) => f.trim()).filter(Boolean));
} else {
try {
const base = process.env.GITHUB_BASE_REF ? `origin/${process.env.GITHUB_BASE_REF}` : "origin/main";
touched = new Set(
execSync(`git diff --name-only ${base}...HEAD`, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
.split("\n").map((f) => f.trim()).filter(Boolean),
);
} catch {
/* No usable base ref — leave `touched` empty so every entry takes the lenient path. */
}
}
console.error(
"\nA stale allowance is a hole: those guards can be reintroduced later and this check stays\n" +
"green. Re-record the baseline in the SAME PR that lowered the count:\n\n" +
" node scripts/lifecycle-column-census.mjs --strict --update-baseline\n",
const staleTouched = stale.filter((entry) => touched.has(entry.file));
if (staleTouched.length > 0) {
console.error(
"\nlifecycle-column-census --strict: this change TOUCHES files whose guard count dropped, so the\n"
+ "baseline must be re-recorded in this change — otherwise the allowance stays open for regrowth.\n",
);
for (const entry of staleTouched) {
console.error(` ${entry.file}: allows ${entry.allowed}, tree has ${entry.count}`);
}
console.error("\nRe-record it:\n\n node scripts/lifecycle-column-census.mjs --strict --update-baseline\n");
process.exit(1);
}
const lines = stale.map((entry) => ` ${entry.file}: allows ${entry.allowed}, tree has ${entry.count}`);
if (exact) {
console.error("\nlifecycle-column-census --strict --exact: baseline is STALE — it allows more than the tree has\n");
for (const line of lines) console.error(line);
console.error("\nRe-record it:\n\n node scripts/lifecycle-column-census.mjs --strict --update-baseline\n");
process.exit(1);
}
writeBaseline();
console.log("\nlifecycle-column-census --strict: baseline TIGHTENED — the tree has fewer guards than it allowed\n");
for (const line of lines) console.log(line);
console.log(
"\nThe baseline file has been rewritten downward. COMMIT IT so the allowance cannot be regrown into;\n"
+ "in CI this write is discarded with the runner, which is why the gate is green and not silent.\n",
);
process.exit(1);
process.exit(0);
}
console.log("\nlifecycle-column-census --strict: every file matches its baseline exactly.");