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:
gsxdsm
2026-07-31 11:50:14 -07:00
committed by GitHub
parent 78d87f0a10
commit cfcbba6f81
3 changed files with 111 additions and 12 deletions

View File

@@ -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.`];
}

View 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();