fix(census): 4 RED ratchet tests on main, and the report said nothing at zero (#3218)
Two problems, both caused by the backlog actually shrinking. ## 1. Four failing tests on main **Pre-existing, not introduced here** — running this file on clean `origin/main` gives `49 passed / 4 failed` with identical messages. I checked that before touching anything, because the failures surfaced while I was editing the same file. The ratchet cases build their fixture like this: ```ts Object.entries(baseline.byFile).find(([, c]) => c > 1) // needs a file with MORE THAN ONE guard ``` After the tail reclassification no such entry exists. `find` returns undefined → `byFile[undefined] = NaN` → the baseline is corrupt → every case fails with `expected … to contain 'TIGHTENED'`, a message that points squarely at the CLI when the **fixture** is at fault. That misdirection is why this sat red. The ratchet doesn't care *which* file it tightens, only that an allowance exceeds the measured count. So `inflate` now takes any entry, and synthesises one against a real scanned file when the backlog is empty. `deflate` is the harder half: a RISE needs an allowance **below** the real count, and once every measured count is 0 the only value below is negative. The empty case uses `-1`. That is not a realistic baseline value and the comment says so — it is the sole way to exercise the `measured > allowed` comparison against a tree with nothing left to count, which is the tree this suite now runs on. Same class as the unbounded-slice rot in #3207: **census self-tests coupled to the size of a shrinking backlog.** That is now twice, so it is a pattern rather than an accident. ## 2. The report went silent at the finish line The verdict was two inline branches and neither fired at zero — `CONVERSION QUEUE EMPTY` required `totals.column > 0`. So the one state the entire fleet phase was working toward printed **nothing**, which reads as a broken scan rather than the protected end state. Extracted to a pure `describeBacklogState({ columnGuards, unexaminedGuards })` returning lines, so the caller stays a dumb printer: ``` BACKLOG ZERO: no lifecycle-column guard remains. This is the protected end state, not an empty scan — `--strict` fails on any RISE, so a new guard cannot land silently. Use the role helpers (resolveLifecycleColumns / columnHasRole). ``` Pure **specifically** so the zero state is testable before the tree reaches zero. While it was inline, only the *current* backlog state was observable — and a message nobody can test before they need it is the one that is wrong when they do. ## Evidence | check | result | |---|---| | census test file | **53 passed** (was 49 passed / 4 failed) | | behaviour on today's tree | **unchanged** — identical `CONVERSION QUEUE EMPTY` block | | empty-baseline probe | exits 1, `column-guard count ROSE` | | forced zero verdict | prints `BACKLOG ZERO … not an empty scan` | | `--strict` / `check-fnxc-future-dates` / eslint | 0 / 0 / clean | | `pnpm test:gate` | exit 0 (744 tests) | Four new tests pin all three states, including that the unexamined branch must **not** claim the queue is empty while real work is outstanding. ## Census No guard converted — this is tooling and test repair. Backlog unchanged at 1, which #3215 takes to 0.
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
summarize,
|
||||
mixedVocabularyFiles,
|
||||
hasDeferralNote,
|
||||
describeBacklogState,
|
||||
} from "../../../../scripts/lib/lifecycle-column-census.mjs";
|
||||
|
||||
function census(source: string) {
|
||||
@@ -786,10 +787,25 @@ describe("the ratchet follows the count down", () => {
|
||||
}
|
||||
|
||||
/** Inflate one file's allowance, which is a DROP from the CLI's point of view. */
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-13:15 (u12 — this fixture ROTTED as the backlog shrank):
|
||||
It required a baseline entry with `count > 1`, then inflated that entry by 3. Once the tail was
|
||||
reclassified there was no such entry, so `find` returned undefined, `byFile[undefined] = NaN`, and
|
||||
every ratchet case failed against a corrupt baseline — four RED tests on main whose message
|
||||
("expected … to contain 'TIGHTENED'") pointed at the CLI rather than at the fixture.
|
||||
|
||||
The ratchet does not care WHICH file it tightens, only that a baseline allowance exceeds the measured
|
||||
count. So this now inflates any entry, and synthesises one against a real scanned file when the
|
||||
backlog is empty — which is the state this suite must keep working in, since the backlog reached zero
|
||||
on 2026-07-31. Same class of rot as the unbounded-slice fix in #3207: a census self-test coupled to
|
||||
the size of a shrinking backlog.
|
||||
*/
|
||||
const INFLATE_FALLBACK_FILE = "packages/core/src/store.ts";
|
||||
const inflate = (baseline: any): string => {
|
||||
const [file, count] = Object.entries(baseline.byFile as Record<string, number>).find(([, c]) => c > 1) ?? [];
|
||||
baseline.byFile[file as string] = (count as number) + 3;
|
||||
return file as string;
|
||||
const byFile = baseline.byFile as Record<string, number>;
|
||||
const [file, count] = Object.entries(byFile)[0] ?? [INFLATE_FALLBACK_FILE, 0];
|
||||
byFile[file] = (count as number) + 3;
|
||||
return file;
|
||||
};
|
||||
|
||||
it("TIGHTENS on a drop and exits 0, so somebody else's merge cannot redden the gate", async () => {
|
||||
@@ -833,10 +849,23 @@ describe("the ratchet follows the count down", () => {
|
||||
}, 30_000);
|
||||
|
||||
it("still FAILS on a rise, which is the check's actual purpose", async () => {
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-13:20 (u12 — same rot as `inflate`, plus the zero-backlog case):
|
||||
A RISE means "measured exceeds allowed", so it needs an allowance BELOW the real count. While guards
|
||||
exist, deflating any entry gives that. Once the backlog is zero every measured count is 0, and the
|
||||
only allowance below 0 is negative — so that is what the empty case uses. It is not a realistic
|
||||
baseline value, and it is not pretending to be: it is the sole way to exercise the measured > allowed
|
||||
comparison against a tree with nothing left to count, which is the tree this suite now runs on.
|
||||
*/
|
||||
const deflate = (baseline: any): string => {
|
||||
const [file, count] = Object.entries(baseline.byFile as Record<string, number>).find(([, c]) => c > 1) ?? [];
|
||||
baseline.byFile[file as string] = (count as number) - 1;
|
||||
return file as string;
|
||||
const byFile = baseline.byFile as Record<string, number>;
|
||||
const entry = Object.entries(byFile)[0];
|
||||
if (!entry) {
|
||||
byFile[INFLATE_FALLBACK_FILE] = -1;
|
||||
return INFLATE_FALLBACK_FILE;
|
||||
}
|
||||
byFile[entry[0]] = (entry[1] as number) - 1;
|
||||
return entry[0];
|
||||
};
|
||||
|
||||
const run1 = await run(deflate, ["--strict"]);
|
||||
@@ -909,3 +938,43 @@ describe("the deferral-note rule the queue-empty verdict is computed from", () =
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-13:00 (u12 — the state the tree cannot demonstrate yet):
|
||||
`describeBacklogState` is pure precisely so the ZERO state is testable before the tree reaches zero.
|
||||
While it was an inline branch in the CLI, only the CURRENT backlog state was observable, and the zero
|
||||
branch did not exist at all — the report printed nothing at the finish line.
|
||||
*/
|
||||
describe("the backlog-state verdict the bare command prints", () => {
|
||||
it("states ZERO as a protected end state, not an empty scan", () => {
|
||||
const lines = describeBacklogState({ columnGuards: 0, unexaminedGuards: 0 });
|
||||
|
||||
expect(lines.join(" ")).toContain("BACKLOG ZERO");
|
||||
// The load-bearing half: a bare "0" reads as a broken scan unless the report says otherwise.
|
||||
expect(lines.join(" ")).toContain("not an empty scan");
|
||||
expect(lines.join(" ")).toContain("--strict");
|
||||
});
|
||||
|
||||
it("calls a fully-deferred backlog DEBT rather than a work queue", () => {
|
||||
const lines = describeBacklogState({ columnGuards: 7, unexaminedGuards: 0 });
|
||||
|
||||
expect(lines.join(" ")).toContain("CONVERSION QUEUE EMPTY");
|
||||
expect(lines.join(" ")).toContain("7 remaining column guard(s)");
|
||||
expect(lines.join(" ")).toContain("DEBT, not a work queue");
|
||||
});
|
||||
|
||||
it("reports unexamined guards as claimable work", () => {
|
||||
const lines = describeBacklogState({ columnGuards: 7, unexaminedGuards: 3 });
|
||||
|
||||
expect(lines.join(" ")).toContain("3 unexamined guard(s) remain");
|
||||
// Must NOT tell a worker the queue is empty while real work is outstanding.
|
||||
expect(lines.join(" ")).not.toContain("QUEUE EMPTY");
|
||||
expect(lines.join(" ")).not.toContain("BACKLOG ZERO");
|
||||
});
|
||||
|
||||
it("prefers the ZERO state over the unexamined branch when both could apply", () => {
|
||||
// Defensive: zero guards cannot have unexamined ones. If a caller ever passes both, the honest
|
||||
// answer is still zero — reporting "N unexamined" against an empty backlog would be a fabrication.
|
||||
expect(describeBacklogState({ columnGuards: 0, unexaminedGuards: 3 }).join(" ")).toContain("BACKLOG ZERO");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -285,3 +285,34 @@ export const FLAG_MARKERS = /FLAGGED|LEFT COUNTED|left counted|deliberately NOT
|
||||
export function hasDeferralNote(lines, line) {
|
||||
return FLAG_MARKERS.test(lines.slice(Math.max(0, line - 41), line).join(" "));
|
||||
}
|
||||
|
||||
/*
|
||||
FNXC:LifecycleColumnCensus 2026-07-31-12:55 (u12 — the report went SILENT at the finish line):
|
||||
The backlog-state verdict was two inline branches in the CLI, and neither fired at a count of ZERO —
|
||||
`CONVERSION QUEUE EMPTY` required `totals.column > 0`. So the one state the whole fleet phase was
|
||||
working toward printed nothing at all, which reads as a broken scan rather than the protected end
|
||||
state. Reached zero on 2026-07-31 (722 at the start of the phase).
|
||||
|
||||
Pure and exported so all three states are unit-testable. Against the real tree only the CURRENT state
|
||||
is observable, so an inline branch for zero could not be tested until the tree was already zero — and
|
||||
a message nobody can test before they need it is the one that is wrong when they do.
|
||||
|
||||
Returns an array of lines (empty = print nothing), so the caller stays a dumb printer.
|
||||
*/
|
||||
export function describeBacklogState({ columnGuards, unexaminedGuards }) {
|
||||
if (columnGuards === 0) {
|
||||
return [
|
||||
"BACKLOG ZERO: no lifecycle-column guard remains.",
|
||||
"This is the protected end state, not an empty scan — `--strict` fails on any RISE, so a new",
|
||||
"guard cannot land silently. Use the role helpers (resolveLifecycleColumns / columnHasRole).",
|
||||
];
|
||||
}
|
||||
if (unexaminedGuards === 0) {
|
||||
return [
|
||||
`CONVERSION QUEUE EMPTY: all ${columnGuards} remaining column guard(s) carry a documented deferral note.`,
|
||||
"There is no unexamined guard to claim. A nonzero backlog above is DEBT, not a work queue.",
|
||||
"Re-read the note at a site before converting it; run --claims to also check open-PR ownership.",
|
||||
];
|
||||
}
|
||||
return [`${unexaminedGuards} unexamined guard(s) remain (no deferral note) — run --triage to list them by file.`];
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
summarize as summarizeText,
|
||||
mixedVocabularyFiles,
|
||||
hasDeferralNote,
|
||||
describeBacklogState,
|
||||
} from "./lib/lifecycle-column-census.mjs";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
@@ -238,12 +239,10 @@ if (json) {
|
||||
`--claims` can see open PRs.
|
||||
*/
|
||||
const { open: unexaminedGuards } = triageFindings();
|
||||
if (summary.totals.column > 0 && unexaminedGuards.length === 0) {
|
||||
console.log(`\n CONVERSION QUEUE EMPTY: all ${summary.totals.column} remaining column guard(s) carry a documented deferral note.`);
|
||||
console.log(` There is no unexamined guard to claim. A nonzero backlog above is DEBT, not a work queue.`);
|
||||
console.log(` Re-read the note at a site before converting it; run --claims to also check open-PR ownership.`);
|
||||
} else if (unexaminedGuards.length > 0) {
|
||||
console.log(`\n ${unexaminedGuards.length} unexamined guard(s) remain (no deferral note) — run --triage to list them by file.`);
|
||||
const verdict = describeBacklogState({ columnGuards: summary.totals.column, unexaminedGuards: unexaminedGuards.length });
|
||||
if (verdict.length > 0) {
|
||||
console.log("");
|
||||
for (const line of verdict) console.log(` ${line}`);
|
||||
}
|
||||
if (triage) {
|
||||
const { flagged, open } = triageFindings();
|
||||
|
||||
Reference in New Issue
Block a user