batch: self-healing sweeps were blind on renamed boards (26 sweeps, folds 23 PRs) (#2944)
**Consolidation of 23 open PRs into one.** Every one shared a single root cause and mostly touched a single file; 23 CI runs for that was indefensible. Folds and supersedes: #2867 #2869 #2876 #2879 #2883 #2891 #2899 #2901 #2902 #2905 #2906 #2914 #2916 #2918 #2919 #2920 #2922 #2927 #2929 #2932 #2934 #2937 #2939. (#2865, #2882, #2897, #2909, #2912 already merged and are not re-folded.) ## The root cause A self-healing sweep selects its work with `listTasks({ column: "in-review" })`. On a board whose lanes are renamed that returns **nothing**, so the sweep never runs — no error, no log line, no failed task. Several sweeps had already had their *predicates* converted to resolved lanes, which dropped a census count and changed nothing, because the query above the loop had already returned an empty list. **26 sweeps converted.** Each one: read the project's columns for the role, then decide each card against **its own** workflow, with the legacy ids unioned so a board mid-rename is never skipped. ## What each sweep stops silently failing to do | | | | --- | --- | | stale merger status | one finished card held the **merge queue** for everything behind it | | stale `blockedBy` / completed-task release | dependents stayed blocked on work that had already finished — the board stops moving | | workspace partial lands | a task left with **some repos merged and some not** | | mid-merge retry stamp | the card stalled *and* the operator's manual Retry was gated by the same stamp | | in-progress limbo / no-progress failures | dead cards held a work slot forever | | partial-progress retry | real work parked failed with its **retry budget unspent** | | orphaned-execution signal | visibility only — the one signal pointing at an orphan went silent | | zero-commit audit | went **half-blind**: the error arm kept working, the lane arm did not | Plus: ghost review cards, transient merge failures, misclassified failures, branch misbinding, missing-worktree failures, merged-but-unfinished finalization, done-metadata repair, self-owned branch conflicts, orphan-only scope violations, post-done wedges, idle assigned agents, PR-conflict worktree ownership, and orphaned workspace worktrees. ## Two defects the conversion itself introduced, both caught and fixed 1. **Missed pairs.** Widening a read without converting the guards beneath it is *worse than not converting*: the sweep starts admitting renamed-board cards and then mis-decides every one. Review caught a second guard on a re-read row; the audit that triggered found **five more**, one of which gates the `reviewProof` triple-proof — a renamed review card would have been moved backward with the safety check silently skipped. Column guards 86 → 81. 2. **Duplicate processing.** The literal reads were disjoint by construction; resolved reads are not, so a column carrying two role flags put one card in two buckets — duplicate moves, duplicate audit rows, inflated counts. Both now have ratchets. `self-healing-converted-sweeps-have-no-literal-lane-guards.test.ts` **derives** its sweep list (a sweep counts as converted when its body calls `resolveProjectColumnsForRoles`), so it cannot go stale, and it carries two positive controls because a broken regex finds no offenders and a broken derivation iterates nothing — an empty loop registers no tests and reads green. ## Deliberately unchanged - 22 `moveTask` destinations carrying `recoveryRehome: true` — `moves.ts` exempts these so a card stranded in an undeclared column stays rescuable. - One literal in `clearStaleBlockedBy`'s log-dedup closure (allowed by name in the ratchet, with the reason). - `surfaceInReviewStalls` — hot list-read path, needs a batched prefetch; that is a performance design decision, not a conversion. - `scheduler.ts` and `replan-target.ts` — built on `resolveTaskWorkflowIrSync`, which returns the default IR for every task in production. Converting there produces inert code. ## The fold itself is worth one note All 23 branches appended to the **same test file at the same anchor**, so every automatic strategy — git 3-way, `merge-file --union`, and three hand-written resolvers — interleaved them mid-block. Two attempts committed conflict markers before I caught it. The file is therefore **reconstructed**: head authored once, body assembled as the union of each branch's own intact top-level segments keyed by test title, with the nested `already-merged hard blocker` describe appended whole (flattening it orphaned its helper). Verified by *parsing after every step* rather than trusting the merge — which is how each interleaving was caught. ## Verification `pnpm test:gate` 161 + 487 + 13 + 71. Scoped suites 592 passed (self-healing, the blindness suite at 68 cases, the ratchet, and the notification suite). `tsc` engine clean; `pnpm lint`, `check:changesets`, `lifecycle-column-census --strict` and `check-sql-column-literals` all clean, each run explicitly. Each folded conversion was individually revert-proven on its original branch — the read reverted alone, and the per-card verdict reverted alone — and those measurements are recorded in the commit messages carried into this branch. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.changeset/self-healing-branch-misbound-query.md
Normal file
7
.changeset/self-healing-branch-misbound-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Branch-misbinding is detected again on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `recoverBranchMisboundInReviewTasks` read the literal `in-review`, so on a renamed board a review card whose branch tip belongs to another task was never detected. Read resolves via `resolveProjectColumnsForRoles`, per-card check resolves per card.
|
||||
7
.changeset/self-healing-done-merge-metadata-query.md
Normal file
7
.changeset/self-healing-done-merge-metadata-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Completed tasks get their merge metadata repaired again on renamed boards.
|
||||
category: fix
|
||||
dev: `recoverDoneTaskMergeMetadata` read the literal `done`, so on a renamed board a completed card's merge metadata was never repaired and could keep pointing at a commit that is not the one that landed. Read resolves via `resolveProjectColumnsForRoles(["complete"])` — deliberately not the terminal union — and the per-card check resolves per card.
|
||||
7
.changeset/self-healing-ghost-review-query.md
Normal file
7
.changeset/self-healing-ghost-review-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Ghost review cards are detected again on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `recoverGhostReviewTasks` read the literal `in-review`, so a card parked past the stuck timeout with no merge-lane owner was never found on a renamed board. Read resolves via `resolveProjectColumnsForRoles` and the per-card check resolves per card; the kick-back keeps its literal target because it passes `recoveryRehome: true`.
|
||||
7
.changeset/self-healing-inprogress-limbo-query.md
Normal file
7
.changeset/self-healing-inprogress-limbo-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Dead cards no longer hold a work slot forever on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `recoverInProgressLimbo` read the literal `in-progress`, so a card holding a wip slot with no worktree, no branch and no started step was never reclaimed on a renamed board. The read resolves via `resolveProjectColumnsForRoles` and the per-card column check resolves per card, falling back to the project set when a card's own workflow is unreadable.
|
||||
7
.changeset/self-healing-misclassified-failures-query.md
Normal file
7
.changeset/self-healing-misclassified-failures-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Falsely-failed tasks with all steps done are cleared again on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `recoverMisclassifiedFailures` read the literal `in-review`, so a task parked failed for "without calling fn_task_done" whose steps were all actually done stayed visibly failed on a renamed board. Read resolves via `resolveProjectColumnsForRoles`, per-card check resolves per card.
|
||||
7
.changeset/self-healing-missing-worktree-review-query.md
Normal file
7
.changeset/self-healing-missing-worktree-review-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Review failures from a missing worktree recover on renamed boards, and on boards with two review columns.
|
||||
category: fix
|
||||
dev: `recoverMissingWorktreeReviewFailures` had per-candidate lane wiring but still read the literal `in-review`, so only boards whose review lane kept that name benefited. The read now resolves via `resolveProjectColumnsForRoles`. Its per-candidate set also came from `resolveTaskLifecycleColumns().review` (first column per role) while the classifiers take a membership set; it now unions `columnsWithFlag` across the three review roles.
|
||||
7
.changeset/self-healing-no-commits-audit-query.md
Normal file
7
.changeset/self-healing-no-commits-audit-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: The zero-commit audit sees quietly-parked review cards again on renamed boards.
|
||||
category: fix
|
||||
dev: `auditNoCommitsExpectedCandidates` read the literal `in-review`, so on a renamed board only the `no_commits` error path fed the audit and a card sitting in a renamed review lane with zero commits and no error was never flagged. Read resolves via `resolveProjectColumnsForRoles`, the lane verdict resolves per card.
|
||||
7
.changeset/self-healing-no-progress-notaskdone-query.md
Normal file
7
.changeset/self-healing-no-progress-notaskdone-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Failed tasks that produced nothing release their work slot again on renamed boards.
|
||||
category: fix
|
||||
dev: `recoverNoProgressNoTaskDoneFailures` read the literal `in-progress`, so a wip card failed for "no fn_task_done" with no step progress and no git work was never requeued on a renamed board and kept holding its slot. Read resolves via `resolveProjectColumnsForRoles`, per-card check resolves per card.
|
||||
7
.changeset/self-healing-orphan-scope-query.md
Normal file
7
.changeset/self-healing-orphan-scope-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Orphan-only scope-violation recovery now runs on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `recoverOrphanOnlyScopeViolations` queried the literal `in-review`, so it never ran on a renamed board and such a task stayed failed. Read now resolves via `resolveProjectColumnsForRoles`; the per-card verdict and its `getTaskHardMergeBlocker` resolve from the task's own workflow.
|
||||
7
.changeset/self-healing-orphaned-executions-query.md
Normal file
7
.changeset/self-healing-orphaned-executions-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: The orphaned-execution signal is now emitted on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `recoverOrphanedExecutions` read the literal `in-progress`, so on a renamed board it never emitted `task:orphan-detected-no-action` and an operator had no signal that a wip card had no live session. The sweep takes no lifecycle action; this restores visibility only. Read resolves via `resolveProjectColumnsForRoles`, per-card check resolves per card.
|
||||
7
.changeset/self-healing-partial-progress-query.md
Normal file
7
.changeset/self-healing-partial-progress-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Partially-completed work is retried again on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `recoverPartialProgressNoTaskDoneFailures` read the literal `in-review`, so on a renamed board a card failed for "no fn_task_done" that had made real step progress was never retried and its retry budget was never spent. Read resolves via `resolveProjectColumnsForRoles`, per-card check resolves per card.
|
||||
7
.changeset/self-healing-post-done-wedge-query.md
Normal file
7
.changeset/self-healing-post-done-wedge-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Post-done wedge recovery now unsticks completed tasks on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `recoverPostDoneNonContinuableWedge` queried the literal `in-review`, so a task that finished every step and was wedged `failed` by a post-done continuation error stayed failed on a renamed board. Read now resolves via `resolveProjectColumnsForRoles`, and its `getTaskHardMergeBlocker` judges each card against its own workflow.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: PR-conflict reclaim no longer reads a busy checkout as unowned on renamed boards.
|
||||
category: fix
|
||||
dev: `reclaimPrConflictForTask` built its worktree-owner map from a literal `in-progress` read, so on a renamed board the map was empty and a checkout another task was live in read as unowned. Read resolves via `resolveProjectColumnsForRoles(["countsTowardWip"])`; the map is keyed by worktree path so there is no per-card lane verdict to convert.
|
||||
7
.changeset/self-healing-reattach-assigned-query.md
Normal file
7
.changeset/self-healing-reattach-assigned-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Idle assigned agents are reattached to their work on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `reattachOrphanedAssignedExecutions` read the literal `in-progress`, so on a renamed board an agent that stopped executing a task it was still assigned to was never resumed, leaving the card assigned-but-idle. Read resolves via `resolveProjectColumnsForRoles`, per-card check resolves per card.
|
||||
7
.changeset/self-healing-reclaim-self-owned-query.md
Normal file
7
.changeset/self-healing-reclaim-self-owned-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Self-owned branch conflicts are now reclaimed on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `reclaimSelfOwnedBranchConflicts` read the literal `todo`/`in-progress`/`in-review` and kept three lane guards on column ids, so a task whose own worktree held its own branch stayed wedged on a renamed board. Reads resolve via `resolveProjectColumnsForRoles` and the three guards resolve per card; the `recoveryRehome` re-home keeps its legacy target by design.
|
||||
7
.changeset/self-healing-reconcile-completed-query.md
Normal file
7
.changeset/self-healing-reconcile-completed-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Completing a task now releases its dependents on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `reconcileCompletedTask` read the literal `todo`/`in-progress`/`in-review`, so on a renamed board it released nothing and every dependent stayed blocked on finished work. The three reads resolve via `resolveProjectColumnsForRoles`, and dependency satisfaction resolves per dependency (complete/review/archived roles, legacy ids unioned).
|
||||
7
.changeset/self-healing-stale-blockedby-query.md
Normal file
7
.changeset/self-healing-stale-blockedby-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Stale dependency blocks now clear on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `clearStaleBlockedBy` read the literal `todo`/`in-progress`/`in-review`, so its already-lane-resolved body never ran on a renamed board and cards stayed blocked behind finished blockers. The three reads now resolve via `resolveProjectColumnsForRoles` and each card is bucketed against its own workflow.
|
||||
7
.changeset/self-healing-stale-incomplete-review-query.md
Normal file
7
.changeset/self-healing-stale-incomplete-review-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Review cards with unfinished steps are requeued again on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `recoverStaleIncompleteReviewTasks` read the literal `in-review`, so a card that reached review on a graph failure with steps still unfinished was never requeued on a renamed board. Read resolves via `resolveProjectColumnsForRoles`, per-card check resolves per card; the requeue keeps its literal target because it passes `recoveryRehome: true`.
|
||||
7
.changeset/self-healing-stale-merger-status-query.md
Normal file
7
.changeset/self-healing-stale-merger-status-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: A finished task no longer blocks the merger queue on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `reconcileStaleMergerStatus` read the literal `done`/`archived`, so a terminal card still carrying `merging`/`merging-pr` was never cleared on a renamed board and held the merger queue for every task behind it. One resolved union read over `TERMINAL_ROLES`, deduped.
|
||||
7
.changeset/self-healing-stuck-completed-query.md
Normal file
7
.changeset/self-healing-stuck-completed-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Step-complete tasks stranded by a dead session now reach review on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `recoverCompletedTasks` read the literal `in-progress`, so a task whose steps were all done but whose session died before the hand-off to review was never found on a renamed board. The read resolves via `resolveProjectColumnsForRoles` and the per-card column check resolves per card, falling back to the project set when a card's own workflow is unreadable.
|
||||
7
.changeset/self-healing-transient-merge-query.md
Normal file
7
.changeset/self-healing-transient-merge-query.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Merges that failed on a transient fault recover again on boards with renamed columns.
|
||||
category: fix
|
||||
dev: `recoverTransientMergeFailures` read the literal `in-review` and kept two lane guards on column ids, so a card that burned its retry budget on a network blip or provider fault stayed failed permanently on a renamed board. Read resolves via `resolveProjectColumnsForRoles`; both the slim-snapshot filter and the full-row re-check resolve per card.
|
||||
7
.changeset/self-healing-workspace-lane-queries.md
Normal file
7
.changeset/self-healing-workspace-lane-queries.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Workspace tasks finish their partial lands and release their worktrees on renamed boards.
|
||||
category: fix
|
||||
dev: `reconcileWorkspacePartialLands` and `reconcileOrphanedWorkspaceWorktrees` read the literal `in-review`/`done`, so on a renamed board a workspace task stranded mid-land was never re-enqueued and a finished one never released its per-repo worktrees. Reads resolve via `resolveProjectColumnsForRoles` (review roles, and `complete` only for the cleanup); the partial-land per-card check resolves per card.
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-11:55 (the missed-pair ratchet):
|
||||
A sweep whose READ was converted to resolved lanes but whose LOOP-BODY guards still compare column ids
|
||||
is worse than one converted nowhere. The widened read admits renamed-board cards, and every literal
|
||||
guard below it then mis-classifies exactly those cards.
|
||||
|
||||
This is not hypothetical and it is not rare. #2916 found a second lane guard on a re-read row that the
|
||||
behavioural test caught; a scan of the other converted sweeps then found FIVE more in
|
||||
`reclaimSelfOwnedBranchConflicts`, and one of them decides whether a backward move needs its
|
||||
triple-proof — so left literal it would have moved a renamed review card back with the safety gate
|
||||
silently skipped, a regression INTRODUCED by the conversion.
|
||||
|
||||
WHY A SOURCE SCAN AND NOT A BEHAVIOURAL TEST. Those five sit behind `inspectBranchConflict` and a real
|
||||
`execAsync`, so reaching them means a git fixture rather than a lane test. This asserts the property the
|
||||
scan found — no literal lane comparison survives inside a converted sweep — which is exactly the defect
|
||||
class, and nothing more. It makes no claim about behaviour, and the behavioural cases for each sweep
|
||||
live in `self-healing-query-filter-blindness.test.ts`.
|
||||
|
||||
REVERT CHECK, measured: restoring any one of the five (e.g. `if (task.column === "in-review") {` inside
|
||||
`reclaimSelfOwnedBranchConflicts`) fails this test naming that sweep and that literal.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const SOURCE = fileURLToPath(new URL("../self-healing.ts", import.meta.url));
|
||||
|
||||
/*
|
||||
The list is DERIVED, not written down: a sweep counts as converted when its own body calls
|
||||
`resolveProjectColumnsForRoles`. Written down, this test would be wrong on every branch that converts a
|
||||
different sweep, and stale the moment one lands — the maintenance burden is what turns a ratchet off.
|
||||
*/
|
||||
const LIFECYCLE_IDS = ["todo", "in-progress", "in-review", "done", "archived", "triage"];
|
||||
|
||||
/**
|
||||
* Strips comments before scanning.
|
||||
*
|
||||
* Load-bearing: these files carry long FNXC notes that QUOTE the old form to explain why it was
|
||||
* removed, and an earlier version of this scan reported those as live code. A ratchet that fires on
|
||||
* its own documentation gets disabled, not fixed.
|
||||
*/
|
||||
function stripComments(source: string): string[] {
|
||||
const out: string[] = [];
|
||||
let inBlock = false;
|
||||
for (const line of source.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (inBlock) {
|
||||
out.push("");
|
||||
if (line.includes("*/")) inBlock = false;
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith("/*")) {
|
||||
out.push("");
|
||||
if (!line.includes("*/")) inBlock = true;
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith("//")) { out.push(""); continue; }
|
||||
out.push(line);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The method each line belongs to, by nearest preceding 2-space-indented declaration. */
|
||||
function owningMethod(lines: string[], index: number): string | null {
|
||||
for (let i = index; i >= 0; i--) {
|
||||
const match = /^ {2}(?:private |public )?(?:async )?([a-zA-Z][A-Za-z0-9_]*)\s*[(<]/.exec(lines[i]!);
|
||||
if (match) return match[1]!;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
A literal on the FALLBACK arm of a resolved ternary is the correct shape, not a missed pair:
|
||||
|
||||
own.length > 0 ? own.includes(task.column) : task.column === "in-review"
|
||||
|
||||
The resolved answer wins whenever it exists; the literal answers only when resolution yielded nothing,
|
||||
and deleting it would make an unresolvable workflow match no lane at all. Three such lines
|
||||
(`reconcileDoneTaskIntegrity`, `recoverInterruptedMergingTasks`, `recoverStuckMergeDeadlocks`) are why
|
||||
this exclusion exists — the first version of this ratchet reported all three as defects.
|
||||
|
||||
Deliberately narrow: it only excuses a literal that shares its line with a RESOLVED membership test. A
|
||||
bare `task.column === "in-review"` on its own line is still an offender, which is the case that matters.
|
||||
*/
|
||||
function isResolvedFallbackArm(line: string): boolean {
|
||||
return /(?:\.includes|\.has)\(\s*(?:task|t|entry|dep)\.column\s*\)/.test(line);
|
||||
}
|
||||
|
||||
/** Every method whose body resolves project lanes — i.e. whose READ has been converted. */
|
||||
function convertedSweeps(lines: string[]): string[] {
|
||||
const found = new Set<string>();
|
||||
lines.forEach((line, index) => {
|
||||
if (!line.includes("resolveProjectColumnsForRoles(")) return;
|
||||
const owner = owningMethod(lines, index);
|
||||
if (owner) found.add(owner);
|
||||
});
|
||||
return [...found].sort();
|
||||
}
|
||||
|
||||
/*
|
||||
Documented exceptions, each with the reason recorded at the site too. An entry here is a claim that the
|
||||
degraded answer is harmless — not that the literal is invisible.
|
||||
*/
|
||||
const ALLOWED: ReadonlyArray<{ sweep: string; because: string }> = [
|
||||
{
|
||||
sweep: "clearStaleBlockedBy",
|
||||
because: "one literal in a log-dedup closure defined before the lane prefetch; the degraded answer costs a duplicate log line, not a lifecycle decision",
|
||||
},
|
||||
];
|
||||
|
||||
describe("a converted sweep keeps no literal lane comparison in its body", () => {
|
||||
const lines = stripComments(readFileSync(SOURCE, "utf8"));
|
||||
const pattern = new RegExp(`\\.column\\s*(?:!==|===)\\s*"(${LIFECYCLE_IDS.join("|")})"`);
|
||||
|
||||
for (const sweep of convertedSweeps(lines)) {
|
||||
it(`${sweep} compares no column id`, () => {
|
||||
const allowance = ALLOWED.find((entry) => entry.sweep === sweep);
|
||||
const offenders: string[] = [];
|
||||
lines.forEach((line, index) => {
|
||||
if (!pattern.test(line)) return;
|
||||
if (isResolvedFallbackArm(line)) return;
|
||||
if (owningMethod(lines, index) !== sweep) return;
|
||||
offenders.push(`${SOURCE.split("/").pop()}:${index + 1} — ${line.trim()}`);
|
||||
});
|
||||
/* An allowed sweep keeps AT MOST its documented one; a second is still a failure. */
|
||||
if (allowance) {
|
||||
expect(offenders.length, `${sweep} is allowed one documented literal (${allowance.because}) but has ${offenders.length}:\n${offenders.join("\n")}`).toBeLessThanOrEqual(1);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(offenders, `${sweep} still compares a lifecycle column id:\n${offenders.join("\n")}`).toEqual([]);
|
||||
});
|
||||
}
|
||||
|
||||
it("the scan can actually see a literal, and finds converted sweeps at all", () => {
|
||||
/*
|
||||
Two positive controls, because both halves can fail silently. A broken regex makes every case above
|
||||
pass by finding no offenders; a broken `convertedSweeps` makes them pass by iterating nothing at all
|
||||
— and an empty `for` loop registers no tests, which reads as green.
|
||||
*/
|
||||
const anyLiteralAnywhere = lines.some((line) => pattern.test(line));
|
||||
expect(anyLiteralAnywhere, "the literal scan matched nothing — the regex is broken").toBe(true);
|
||||
|
||||
expect(convertedSweeps(lines).length, "no converted sweep found — the derivation is broken").toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10863,6 +10863,24 @@ describe("SelfHealingManager reclaimStaleActiveBranches (FN-4546)", () => {
|
||||
mockedIsUsableTaskWorktree.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-31-01:40 (#2879 review — greptile, "multi-role tasks run
|
||||
recovery twice"): THE FIX IS IN `self-healing.ts`; NO TEST HERE, AND THE ABSENCE IS DELIBERATE.
|
||||
|
||||
`readBucket` dedupes by id inside ONE role's read, so a custom column carrying two queried traits —
|
||||
`hold` plus `countsTowardWip`, or a review role beside either — is returned by two reads and the
|
||||
concatenation handed the recovery loop the same STALE SNAPSHOT twice.
|
||||
|
||||
I wrote a case here and removed it: it reported 0 recoveries, because the card it built reaches the
|
||||
BRANCH-LEVEL scan (subsumed branch, no worktree) rather than the candidates loop the dedupe lives
|
||||
in. Reaching that loop needs `branch` AND `worktree` set plus matching git state, i.e. the git
|
||||
fixture this suite deliberately avoids. A test that passes without exercising the loop would have
|
||||
been worse than none — that is the vacuous shape this program keeps finding.
|
||||
|
||||
So the dedupe ships uncovered and stated. What would cover it: a candidates-loop fixture with a live
|
||||
branch/worktree pair, asserting the recovery COUNT (a double-processed card reports 2 for one card's
|
||||
work) rather than a call count.
|
||||
*/
|
||||
it("reclaims subsumed fusion task branch with no worktree", async () => {
|
||||
(store.listTasks as any).mockResolvedValueOnce([
|
||||
{ id: "FN-1001", column: "todo", checkedOutBy: null, userPaused: false, worktree: null, branch: null, lineageId: "lin-1" },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"generatedFrom": "node scripts/lifecycle-column-census.mjs --strict --update-baseline",
|
||||
"byFile": {
|
||||
"packages/engine/src/self-healing.ts": 84,
|
||||
"packages/engine/src/self-healing.ts": 56,
|
||||
"packages/engine/src/scheduler.ts": 12,
|
||||
"packages/engine/src/executor.ts": 8,
|
||||
"packages/core/src/task-store/async-comments-attachments.ts": 6,
|
||||
@@ -40,6 +40,7 @@
|
||||
},
|
||||
"deliberateByFile": {
|
||||
"packages/dashboard/app/components/TaskContextMenu.tsx\u0000in-review": 3,
|
||||
"packages/engine/src/self-healing.ts\u0000in-review": 3,
|
||||
"packages/core/src/live-agent-count.ts\u0000in-progress": 2,
|
||||
"packages/core/src/live-agent-count.ts\u0000in-review": 2,
|
||||
"packages/core/src/store.ts\u0000in-review": 2,
|
||||
@@ -58,7 +59,7 @@
|
||||
"packages/engine/src/scheduler.ts\u0000done": 2,
|
||||
"packages/engine/src/scheduler.ts\u0000in-progress": 2,
|
||||
"packages/engine/src/scheduler.ts\u0000in-review": 2,
|
||||
"packages/engine/src/self-healing.ts\u0000in-review": 2,
|
||||
"packages/engine/src/self-healing.ts\u0000done": 2,
|
||||
"packages/engine/src/usage-limit-detector.ts\u0000archived": 2,
|
||||
"packages/engine/src/usage-limit-detector.ts\u0000done": 2,
|
||||
"plugins/fusion-plugin-reports/src/store/report-store.ts\u0000archived": 2,
|
||||
@@ -120,7 +121,7 @@
|
||||
"packages/engine/src/hold-release.ts\u0000in-review": 1,
|
||||
"packages/engine/src/project-engine.ts\u0000in-review": 1,
|
||||
"packages/engine/src/scheduler.ts\u0000todo": 1,
|
||||
"packages/engine/src/self-healing.ts\u0000done": 1,
|
||||
"packages/engine/src/self-healing.ts\u0000archived": 1,
|
||||
"packages/engine/src/triage.ts\u0000triage": 1,
|
||||
"plugins/fusion-plugin-even-cards/src/cards/board-cards.ts\u0000archived": 1,
|
||||
"plugins/fusion-plugin-even-cards/src/cards/board-cards.ts\u0000done": 1,
|
||||
@@ -128,9 +129,9 @@
|
||||
"plugins/fusion-plugin-reports/src/store/report-types.ts\u0000archived": 1
|
||||
},
|
||||
"queryByFile": {
|
||||
"packages/engine/src/self-healing.ts": 32,
|
||||
"packages/core/src/task-store/async-persistence.ts": 2,
|
||||
"packages/core/src/task-store/merge-queue-ops.ts": 2,
|
||||
"packages/engine/src/self-healing.ts": 2,
|
||||
"packages/core/src/async-mission-store.ts": 1,
|
||||
"packages/core/src/task-store/archive-lifecycle-2.ts": 1,
|
||||
"packages/core/src/task-store/async-archive-lineage.ts": 1,
|
||||
|
||||
Reference in New Issue
Block a user