U7 item 3: the replan no-match fallback named a column no lineage declares (#2659)
**Item 3 from the closing bar.** Behaviour change, own commit. ## The defect `resolveReplanTargetColumn` fell back to the literal `"triage"` when a workflow declared neither legacy planner id. That names a column the workflow doesn't declare — and since #2515 the **default lineage doesn't declare it either**, so the fallback pointed at a column that exists nowhere. The replan move then either failed outright or put the card somewhere no sweep owns. Resolved through `resolveReboundTarget` (KTD-10: hold → intake → first declared) — the same helper every other rebound path uses, so replan lanes and rebound lanes stay consistent instead of drifting. ## The catch path keeps its literal, deliberately It's reached only when resolution **throws** — not when it silently falls back to the default IR, which returns a real workflow and takes the `todo` branch above. With no IR there's nothing to resolve, and swapping one arbitrary literal for another changes behaviour without evidence about the workflow. Documented at the site so the asymmetry reads as a decision, not an oversight. ## Test Written first and observed **red**. It asserts the target is a column the workflow actually declares: ```ts expect(workflowHasColumn(ir, target)).toBe(true); ``` rather than pinning a specific id — so it can't pass by naming a *different* wrong column, which is how the previous version of this test stayed green while the fallback was broken. **Mutation-verified:** restoring the literal fails it. ## Verification 40 replan-target tests green, engine tsc clean, lint clean, merge gate green (487 + 132 + 10). No changeset: `@fusion/engine` is private. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { Task, TaskStep, TaskStore } from "@fusion/core";
|
||||
import { resolveWorkflowIrForTask, workflowHasColumn } from "@fusion/core";
|
||||
import { hasAdvancedPastPlanning, isTaskStillInPlanningStage, moveTaskToReplanColumn, resolveReplanTargetColumn } from "../replan-target.js";
|
||||
|
||||
/*
|
||||
@@ -276,12 +277,24 @@ Updated to the post-merge truth, not loosened: each still pins one exact column.
|
||||
await expect(resolveReplanTargetColumn(store, "FN-1")).resolves.toBe("todo");
|
||||
});
|
||||
|
||||
it("falls back to triage for workflows declaring neither triage nor todo (never a custom column)", async () => {
|
||||
// builtin:marketing declares ideation/backlog/drafting/... — no triage, no todo.
|
||||
// A custom entry column would strand the needs-replan card (triage only scans
|
||||
// "triage" and "todo") and the legacy move path throws on custom targets.
|
||||
it("targets its OWN rebound lane for a workflow declaring neither triage nor todo", async () => {
|
||||
/*
|
||||
FNXC:WorkflowReplan 2026-07-31-13:30 (U11 — the flagged fallback, now converted):
|
||||
builtin:marketing declares ideation/backlog/drafting/... — no `triage`, no `todo`.
|
||||
The old fallback handed it the literal `triage`, a column that lineage does not
|
||||
declare AND that the default lineage no longer declares either since #2515. So the
|
||||
replan move targeted a nonexistent column: the card either failed to move or landed
|
||||
somewhere no sweep owns.
|
||||
|
||||
Resolved through `resolveReboundTarget` (KTD-10: hold -> intake -> first declared),
|
||||
which is the same helper every other rebound path uses. The card now lands in a
|
||||
column its own workflow actually declares.
|
||||
*/
|
||||
const store = storeWithSelection("builtin:marketing");
|
||||
await expect(resolveReplanTargetColumn(store, "FN-1")).resolves.toBe("triage");
|
||||
const target = await resolveReplanTargetColumn(store, "FN-1");
|
||||
expect(target).not.toBe("triage");
|
||||
const ir = await resolveWorkflowIrForTask(store as never, "FN-1");
|
||||
expect(workflowHasColumn(ir, target)).toBe(true);
|
||||
});
|
||||
|
||||
/* Resolution failure no longer reaches the `triage` catch: the resolver swallows
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Task, TaskStore } from "@fusion/core";
|
||||
import { resolveLifecycleColumns, resolveWorkflowIrForTask, workflowHasColumn } from "@fusion/core";
|
||||
import { resolveLifecycleColumns, resolveReboundTarget, resolveWorkflowIrForTask, workflowHasColumn } from "@fusion/core";
|
||||
import type { WorkflowIr } from "@fusion/core";
|
||||
|
||||
/*
|
||||
@@ -349,8 +349,26 @@ export async function resolveReplanTargetColumn(store: TaskStore, taskId: string
|
||||
*/
|
||||
if (workflowHasColumn(ir, "triage")) return "triage";
|
||||
if (workflowHasColumn(ir, "todo")) return "todo";
|
||||
return "triage";
|
||||
/*
|
||||
FNXC:WorkflowReplan 2026-07-31-13:35 (U11 — the flagged fallback, now converted):
|
||||
Was `return "triage"`, naming a column this workflow does not declare AND that the
|
||||
DEFAULT lineage stopped declaring at #2515. A workflow with neither legacy planner
|
||||
id was therefore handed a nonexistent target, so the replan move either failed or
|
||||
put the card somewhere no sweep owns.
|
||||
|
||||
`resolveReboundTarget` is the KTD-10 helper every other rebound path already uses
|
||||
(hold -> intake -> first declared), so the card lands in a column its own workflow
|
||||
declares and the replan lanes stay consistent with the rebound lanes.
|
||||
*/
|
||||
return resolveReboundTarget(ir) ?? "triage";
|
||||
} catch {
|
||||
/*
|
||||
NO IR MEANS NOTHING TO RESOLVE, so this keeps the legacy literal deliberately
|
||||
rather than guessing. It is reached only when resolution THROWS — not when it
|
||||
falls back to the default IR, which returns a real workflow and takes the `todo`
|
||||
branch above. Changing it to another literal would trade one arbitrary column for
|
||||
another without evidence about the workflow.
|
||||
*/
|
||||
return "triage";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user