Files
fusion/scripts/__tests__
gsxdsm bb6c08d9d6 fix(scripts): the blocked-by recovery reported "Repairs: 0" on a board it never examined (#2992)
## A recovery tool that reports "Repairs: 0" without having examined
anything

Every lane test in `recover-stale-blocked-by.mjs` is a legacy id:

```js
function isTerminalColumn(column) { return column === "done" || column === "archived"; }
const isActive = row.column === "in-progress" || (row.column === "in-review" && row.worktree && !row.paused);
if (row.column !== "todo" || !row.blockedBy) continue;          // ← the candidate gate
```

On a board whose lanes are named anything else, that gate matches
**nothing**. The planner returns no findings and the script prints
`Repairs: 0`.

An operator running a recovery reads that as *"the board is fine"* when
the tool never examined a single card. **A silently empty answer from a
recovery tool is the worst shape available** — indistinguishable from
success, and consulted precisely during an incident.

This is not dead code: `docs/soft-delete-verification-matrix.md` cites
it as the GREEN backstop for FN-5528, and it has its own test file.

## Detection only — and why I did not "fix" the classification

Correct classification needs the board's resolved trait vocabulary. This
script holds a **raw backend** (`openBackend` → `asyncLayer` + `sql`),
not a `TaskStore`, so resolving lanes here would mean reimplementing IR
trait resolution inside a `.mjs` script — a worse bug than the one it
fixes, and precisely the kind of second, drifting copy this migration
keeps deleting.

So the assumptions are not repaired; they are made **loud**. That is the
same principle the lane-wiring gate applies to itself:

> a gate whose errors land on "nothing to report" is the one failure
mode a ratchet must not have

The unknown-lane list rides on the returned array as a
**non-enumerable** property rather than widening the return type —
`recoverBlockedBy` is consumed as `findings[]` by the entry point and by
tests, and an operator may be scripting around that shape.

## The first test pins the gap rather than papering over it

```js
assert.deepEqual(unrecognisedLanes(rows), ["backlog", "checking"]);
// The gap this warns about, pinned rather than claimed fixed: the planner still sees nothing.
assert.deepEqual(planRecoverBlockedBy({ rows, tasksDir }), []);
```

I would rather the next reader find that assertion than discover it
themselves during an incident.

## Revert proof

With `unrecognisedLanes` returning `[]` (the pre-fix behaviour):

```
✖ names lanes the planner does not understand, so an empty result cannot read as healthy
✔ stays quiet on a legacy board, so the warning means something when it appears
✖ reports each unknown lane once, ignoring rows with no column at all
ℹ pass 5   ℹ fail 2
```

The legacy-board case passes **both ways by design** — it guards against
the warning firing spuriously, so I am not counting it as coverage of
the defect.

## Verification (measured)

- `node --test` — **7 passed** (4 pre-existing + 3 new), 0 failed
- `node --check`, `eslint` — clean
- `check-sql-column-literals`, `lifecycle-column-census --strict`,
`check-lane-wiring`, `check-fnxc-future-dates` — green

No changeset: root `scripts/` is repo tooling, not part of the published
package.

## How this was found, since the method matters more than the fix

My batch is "cli + plugins + anything left", and I had been reading
*"anything left"* as nothing. Eight packages and all of `scripts/` sit
outside the four named batches. This is the first thing I found there;
sibling one-shot scripts (`reconcile-task-state-consistency.mjs`,
`reconcile-leaked-soft-deletes.mjs` — which contains a raw `UPDATE … SET
"column" = 'archived'`) carry the same hardcoded assumptions and are
**not** addressed here.
2026-07-30 23:41:03 -07:00
..