fix(triage): the startup sweep resolved its columns from a SENTINEL task id — a characterization test already pinned it (#3201)
Fourth and last convertible site in `triage.ts`. This one needed a
different fix from the other three, and the codebase already said so.
## The defect
```ts
const sweepLanes = resolvePlannerLanes(this.store, "");
const sweepColumns = [...new Set(["triage", "todo", sweepLanes.intake, sweepLanes.hold])];
```
There is no task `""`. No selection can be read for it, no board
resolved — the lanes come back as the **default** board's and the union
collapses to the legacy pair `{triage, todo}`. On a renamed board the
sweep queries columns the card is not in, so its stale `planning` status
survives and it **holds a planning admission slot indefinitely**.
## A characterization test already pinned this, and called the fix
correctly
`workflow-sweep-sentinel-task-id-live-e2e.pg.test.ts` documents it as a
third inert-conversion mechanism — *"inert by construction rather than
by environment"* — and its header says:
> making `resolvePlannerLanes` async would **NOT** repair this site,
because the defect is the argument, not the resolver
That is right, and it is why this fix differs from #3191 / #3193 /
#3195, which all used the async twin. Here the sweep has **no task to
resolve against** and wants every column playing these roles **anywhere
in the project** — so the correct resolver is
`resolveProjectColumnsForRoles(store, ["intake", "hold"])`, the same
helper `self-healing.ts` already uses for the same purpose.
The legacy pair stays in the union deliberately: the note at the site
explains that `triage` and `todo` must both be swept for pre-U11 and
Coding (Ideas) rows, and extra columns are free because the sweep only
**reads** and filters on `status === "planning"` first.
## The test is inverted, not deleted
It asserted `"planning"` survives — the bug. It now asserts the status
is cleared. Keeping the case with its original reasoning intact
preserves the file's record of what the defect *was*.
## Measured
| | result |
|---|---|
| broad suite (triage / planning / self-healing) | **76 files, 1302
tests passed** |
| differential | restoring the sentinel call → **1 failed \| 2 passed**
|
| `census --strict`, `check-fnxc-future-dates` | exit 0 |
**Inert count unchanged at 4 for `triage.ts`** — these lanes fed an
*array literal*, not a comparison, so the ratchet never counted them.
Third fix this session in that blind-spot class, stated so the number is
not read as the whole picture.
## What remains in this file
Two sites: the `task:moved` wake handler and the evacuation handler —
both **synchronous arrow callbacks** whose answers are consumed in-tick.
Genuinely blocked on the emitter-side work in #3082, with corroborating
evidence attached there.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -116,15 +116,25 @@ pgDescribe("startup sweep lane vocabulary, resolved from a sentinel task id", ()
|
||||
expect(await sweepThenRead(store, taskId)).toBeNull();
|
||||
});
|
||||
|
||||
it("CHARACTERIZATION — the same card on a RENAMED board is NEVER swept", async () => {
|
||||
/*
|
||||
The card is in none of the four queried columns (`triage`, `todo`, and the two sentinel-resolved
|
||||
lanes, which are both `todo`), so the sweep does not see it. Its stale `planning` status survives
|
||||
and it holds a planning admission slot indefinitely.
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-23:59:
|
||||
WAS A CHARACTERIZATION, IS NOW A REGRESSION TEST — the defect it pinned is fixed.
|
||||
|
||||
It asserted `"planning"` survives: the card sat in none of the four queried columns, because the
|
||||
sentinel `""` task id could resolve no board and the union collapsed to the legacy pair. The header
|
||||
above notes that making `resolvePlannerLanes` async would NOT repair this, "because the defect is
|
||||
the argument, not the resolver" — which is right, and is why the fix is a PROJECT-level resolver.
|
||||
|
||||
`resolveProjectColumnsForRoles(store, ["intake", "hold"])` asks the question this sweep actually has:
|
||||
there is no task to resolve against, and it wants every column playing those roles anywhere in the
|
||||
project. The renamed planning column is now queried, so the stale status is cleared.
|
||||
|
||||
The assertion is inverted rather than deleted, so the file keeps its record of what the bug WAS.
|
||||
*/
|
||||
it("clears a stale planning status on a RENAMED board", async () => {
|
||||
const store = h.store();
|
||||
const taskId = await stalePlanningCard(store, RENAMED_VOCAB, "wf-renamed-sweep");
|
||||
|
||||
expect(await sweepThenRead(store, taskId)).toBe("planning");
|
||||
expect(await sweepThenRead(store, taskId)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
resolveWorkflowIrForTask,
|
||||
resolveLifecycleColumns,
|
||||
resolveWorkflowIrForTaskWithProvenance,
|
||||
resolveProjectColumnsForRoles,
|
||||
workflowHasColumn,
|
||||
getStepParser,
|
||||
computePlanApprovalFingerprint,
|
||||
@@ -970,8 +971,25 @@ export class TriageProcessor {
|
||||
Querying extra columns is free here: the sweep only READS and every row is
|
||||
filtered on `status === "planning"` before anything is written.
|
||||
*/
|
||||
const sweepLanes = resolvePlannerLanes(this.store, "");
|
||||
const sweepColumns = [...new Set(["triage", "todo", sweepLanes.intake, sweepLanes.hold])];
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-23:59 (SYNC -> PROJECT-LEVEL, the last convertible site
|
||||
in this file):
|
||||
`resolvePlannerLanes(this.store, "")` was called with an EMPTY task id — there is no task here, so
|
||||
it could never resolve one and returned the DEFAULT board's lanes. The sweep therefore queried
|
||||
`{triage, todo}` on every board, and a stale `planning` card resting in a RENAMED planning column
|
||||
was never swept: it holds a planning admission slot permanently, which is the exact failure the
|
||||
note above describes.
|
||||
|
||||
The right shape is PROJECT-level, not per-task: this sweep has no task to resolve against and
|
||||
wants every column that plays these roles anywhere in the project. `resolveProjectColumnsForRoles`
|
||||
is that resolver, already used for the same purpose in `self-healing.ts`.
|
||||
|
||||
The legacy pair stays in the union deliberately — the note above explains why (`triage` and `todo`
|
||||
must both be swept for pre-U11 and Coding (Ideas) rows), and querying extra columns is free here
|
||||
because the sweep only READS and every row is filtered on `status === "planning"` first.
|
||||
*/
|
||||
const projectPlannerColumns = await resolveProjectColumnsForRoles(this.store, ["intake", "hold"]);
|
||||
const sweepColumns = [...new Set(["triage", "todo", ...projectPlannerColumns])];
|
||||
const swept = await Promise.all(
|
||||
sweepColumns.map((column) => this.store.listTasks({ column, slim: true })),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user