## Unowned: the R7 sweep's "do not guess a column" guard could not fire Picked up from my own #2543 finding. Independent of my other PRs. ### The guard existed in comment form only `reconcileUndeclaredTaskColumns` wraps IR resolution in a try/catch whose comment reads: > An unresolvable workflow is its own fault path; do not guess a column. But `resolveWorkflowIrById` catches **every** failure and returns `defaultCodingWorkflowIr()`, and `resolveWorkflowIrForTask` does the same for a failed selection read. The resolver never rejects, so that catch is **dead code**. What actually happened to a card whose workflow could not be loaded: it was judged against the **default** workflow, and if its column was not one the default declares, the sweep re-homed it to the **default's** rebound target. It guessed, using a workflow that is not the card's own — the precise outcome the guard was written to prevent, in a **startup recovery path that runs against every task**. ### How it was found, which is the part worth keeping By being **unable to make a test of the guard fail**. Three separate mutations all passed — deleting the `continue`, deleting the try/catch, and simulating a whole-sweep abort at that very catch. I had written that off once as "this case pins the outcome, not the mechanism". The inability was the signal, not a limitation of the assertion: the branch is unreachable. This is the seventh instance of the program's core shape, and the first I found in a guard I had just finished writing coverage for. ### The fix The sweep now **proves the resolved IR belongs to the task** before moving its card: it reads the task's workflow selection and confirms that id resolves to a real definition (built-in or stored). - A task with **no** selection legitimately resolves to the default workflow — not treated as unresolvable. - An unreadable selection **read** is itself grounds not to guess. Placed at the **move site**, not at resolution, deliberately: it costs one definition read only for a card already about to be moved — a healthy board reaches that line for nobody — and it keeps the fix inside the sweep instead of changing a resolver whose soft-failure many other callers depend on. Changing `resolveWorkflowIrById` to reject would have been the tidier-looking fix and a much wider blast radius. ### Revert-proof, both directions - Remove the proof → the case fails `expected 2 to be 1`: the unloadable card is re-homed on a guess. - The same case asserts the neighbour **is** still repaired, so the fix cannot be mistaken for letting one bad card disable the sweep for everyone else. That is the per-task isolation property, and a single-task fixture cannot distinguish it from a whole-sweep abort — verified by injecting a throw at the loop head (`expected 0 to be 2`). ### Verification `pnpm test:gate` (482 + 10 + 71), `pnpm lint`, engine typecheck green. Sweep suite + `legacy-tombstones`: 13 passed. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Prevented startup recovery from moving cards into incorrect columns when their workflow cannot be loaded or resolved. * Cards with unreadable workflow information now remain in place, while other recoverable cards continue to be repaired correctly. * Added safeguards to avoid guessing a fallback workflow during column reconciliation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Changeset Format Guide
Each changeset file in this directory describes one user-facing change for release notes.
Required body format
---
"@runfusion/fusion": minor
---
summary: Add a Command Center productivity control for LOC backfills.
category: feature
dev: Uses the new `fn_backfill_loc` tool; settings key `commandCenter.locBackfill`.
Fields
| Field | Required | Description |
|---|---|---|
summary |
Yes | One line, user-facing, max 120 chars. Describe what changed for the operator. |
category |
Yes | One of: feature, fix, breaking, security, performance, internal. |
dev |
No | Developer or migration detail. Preserved in per-package CHANGELOGs but excluded from distilled release notes. |
Audience
The summary is the only content that appears in end-user release notes by default. Write for Fusion operators — describe behavior, fixes, and what changed. Avoid internal class names, file paths, and implementation detail.
Bump types
patch— bug fixes, internal changesminor— new features, CLI additions, toolsmajor— breaking changes
Validation
Run pnpm check:changesets to validate. The linter runs in the PR-check gate and test:gate. Legacy freeform changesets pass with a warning during the transition period.