Skip to content

U12 part 1: delete the legacy board path (262 ListView + 39 Board tests were measuring it; 9-site flag inventory, moves.ts group blocked on U2b) - #2500

Merged
gsxdsm merged 4 commits into
mainfrom
feature/u12-flag-cleanup
Jul 28, 2026
Merged

U12 part 1: delete the legacy board path (262 ListView + 39 Board tests were measuring it; 9-site flag inventory, moves.ts group blocked on U2b)#2500
gsxdsm merged 4 commits into
mainfrom
feature/u12-flag-cleanup

Conversation

@gsxdsm

@gsxdsm gsxdsm commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

U12, part 1 of 2 — and one blocker you need to route

The unit's headline deletion (isWorkflowColumnsCompatibilityFlagEnabled) is blocked by U2b and is not in this PR. What is here is everything that could be deleted without making a convergence decision that belongs to another unit.

The blocker

PR #2468 landed as b941d3cba — but that was Phase A2 steps 1–2 only: the differential characterization. The convergence (pick a path, delete the other, delete the flag) has not landed; feature/workflow-move-path-convergence is still live.

Deleting the raw flag is that convergence. move-path-equivalence.pg.test.ts says so in its own header, and its second describe is literally "the flag gates MORE than side effects". The plan makes this a blocking unit with an equivalence proof obligation and an explicit "stop and escalate rather than reconcile silently" note. So I stopped.

Inventory: every read of the raw flag, with a verdict

Nine sites. All false in production because nothing writes experimentalFeatures.workflowColumns.

Blocked on U2b — one branch, not separable:

Site Silently disabled today Visible if flipped
moves.ts:312 useWorkflow typed TransitionRejectionError, workflow adjacency, the shared transition invariants (merge-blocker trait generalization), plugin column gates, the transitionPending marker, workflowId in task:move run-audit, and the trait-hook side-effect path Yes — rejections change type and message
moves.ts:931 the in-transaction capacity gate. resolveColumnCapacity never runs Yes — WIP limits begin binding
workflow-task-create-ops.ts:351 prepareWorkflowMovePolicyPreflight returns undefined unconditionally → workflow/plugin move policies have never been evaluated Yes — new rejections

On #2488: the pool-id sentinel fix is correct and still inert. Two dead layers stacked — the gate it fixed is inside if (useWorkflow && …).

Not blocked, but each moves operators' cards — deferred to PR 2 per your call:

Site Silently disabled today
workflow-ops.ts:183 OccupiedColumnsError + rehomeTo when a workflow edit removes an occupied column. Today the save succeeds and strands the cards
workflow-ops.ts:344 occupant re-home on workflow delete
workflow-definitions.ts:700 workflow-switch reconciliation, and the reconciliation field in the API response

I verified these three are not coupled to moves.ts: rehomeOccupant reaches a custom target via the isWorkflowDeclaredRecoveryRehome carve-out (moves.ts:641), which exists because the repair "silently no-oped on every store open" before it.

Not blocked, no behaviour change for current binaries (also PR 2): project-store-ops.ts:687 + lifecycle-ops.ts:1119downgradeIrToV1IfPure on persist, for binary-downgrade rollback. Needs a round-trip test, not an assumption.

What this PR deletes

Dashboard. workflowColumnsEnabled was a literal true at all three MainContent call sites; the server hardcodes flagEnabled: true. Gone: Board's legacy single-lane board (55 lines mapping the hardcoded COLUMNS enum — the last board surface deriving columns from the legacy vocabulary, an R8 violation that survived U10); tasksByColumn and its cache ref, orphaned with it; ListView's LEGACY_LIST_COLUMNS (the ListView copy of the synthesized-trait-flags defect U10 fixed in Board); both props; the shouldHydrateCache gate; TaskDetailModal's flagEnabled early return. Neither Board nor ListView imports the legacy column enum any more.

Core. evacuateCustomColumnsToLegacy (#1409) — both triggers require the previous settings to have the flag ON, which no writer produces. runWorkflowColumnsIntegrityPass — no caller anywhere, superseded by reconcileUndeclaredTaskColumns (registered in startup recovery), and it read through the sync SQLite handle, so invoking it under PostgreSQL would have thrown rather than reconciled.

Migration answer: a project with workflowColumns: false persisted needs no migration and no read-time drop. Nothing in this PR reads the key, and it stays in HIDDEN_EXPERIMENTAL_FEATURE_KEYS so Settings still suppresses it rather than resurrecting it as an unknown setting. Proven by tests, no instance booted.

flagEnabled stays on the wire as a constant. Removing it changes the response shape, and a browser tab outliving a server upgrade would read the missing field as "off" and degrade. One boolean, no client branches on it, droppable a release later.

Measured

  • Production sources: -332 / +131 (net -201). Additions are almost entirely FNXC comments recording why each branch was unreachable.
  • Dashboard production only: -168 / +93.
  • Core: -164 / +38.

The finding I'd actually flag

Board.test.tsx and ListView.test.tsx both left workflowColumnsEnabled unset and stubbed fetchBoardWorkflows with a never-resolving promise. Under the old gate that rendered the legacy board — so 262 ListView tests and 39 Board tests were asserting against a configuration production never reached, and a real regression in the workflow board or list would not have failed either file. Same shape as the other four: looked enforced, wasn't.

Both now seed the first-paint lane cache with the default workflow's real columns (ids and names copied from BUILTIN_CODING_WORKFLOW_IR) — the same seam production uses. Repointing them surfaced assertions that encoded legacy-only values: "In Progress"/"In Review" (real IR names are "In progress"/"In review"), and Planning Mode asserted to receive null as the workflow id, which is only what getTaskPlanningWorkflowId returns when workflowMode is false.

"Back to In Progress" is not one of those — it is a hardcoded i18n string in TaskContextMenu:210, not derived from the column name. Left alone, and flagged: it will not follow a renamed column. That's U11 vocabulary territory.

One test is SKIPPED, not weakened — "keeps unaffected columns stable when archived collapse toggles". Pointed at the real board the invariant is false: toggling the archived column re-renders unaffected columns (measured: todo renders 3×, not 2×). Pre-existing production behaviour this deletion exposed, never covered because the test measured the dead path. I ruled out the obvious causes (every callback prop is useCallback; the per-column task memo's deps exclude archivedCollapsed; memoizing the inline canDropTask binding did not close it — I wrote that fix, could not prove it with a failing test, and reverted it). The reason is recorded at the test: un-skip with a fix, never with a new expected number.

Verification

pnpm test:gate (299 + 10 + 71), pnpm lint, pnpm verify:fast (17 steps), and both package typechecks green. settings-defaults.test.ts > warns once per process for legacy cwd-main mode fails — pre-existing, confirmed by stashing my changes and re-running. No Fusion instance was booted.

Routing request

Per your call: the moves.ts group and the final removal of isWorkflowColumnsCompatibilityFlagEnabled go to U2b, inside the convergence PR where the equivalence proof already lives. The divergences their characterization suite does not yet cover: plugin column gates, the transitionPending marker, workflowId in task:move run-audit, and move-policy preflight.

gsxdsm and others added 3 commits July 28, 2026 12:14
…flowColumns prop threading (U12)

The `workflowColumnsEnabled` prop was a literal `true` at all three `MainContent`
call sites, and the server hardcodes `flagEnabled: true` in the board-workflows
payload. Everything gated on either was unreachable.

Deleted:
- Board's legacy single-lane board (55 lines mapping the hardcoded `COLUMNS`
  enum). It was the last board surface deriving its column set from the legacy
  vocabulary rather than each card's workflow — an R8 violation that survived U10.
- `tasksByColumn` and its stable-identity cache ref, orphaned with it; both
  hardcoded the six legacy column ids as object literals and could not have
  bucketed a workflow-defined column at all. Board no longer imports COLUMNS.
- ListView's `LEGACY_LIST_COLUMNS`, which synthesized trait flags onto the same
  six ids — the ListView copy of the defect U10 removed from Board.
- The `workflowColumnsEnabled` / `settingsLoaded` props on both surfaces, the
  `shouldHydrateCache` gate they fed (always true — the hook's own default), and
  TaskDetailModal's `flagEnabled !== true` early return.

`flagEnabled` stays on the WIRE as a constant. Removing it would change the
response shape, and a browser tab that outlives a server upgrade would read a
missing field as "off" and degrade. It is a one-boolean compatibility constant
now that no client branches on it; it can be dropped a release later.

Behaviour is preserved, including the failure mode: a board-workflows fetch that
never succeeds holds the skeleton. That was already true — the legacy board was
never the fetch-failure fallback.

Measured: production sources -168/+93 (net -75; the additions are almost entirely
FNXC comments recording why each branch was unreachable).

TESTS — this is the part worth reading. Both Board.test.tsx and ListView.test.tsx
left `workflowColumnsEnabled` unset and stubbed `fetchBoardWorkflows` with a
never-resolving promise. Under the old gate that rendered the LEGACY board, so
262 ListView tests and 39 Board tests were asserting against a configuration
production never reached, and a real regression in the workflow board or list
would not have failed either file. Both now seed the first-paint lane cache with
the default workflow's REAL columns (ids and names copied from
BUILTIN_CODING_WORKFLOW_IR), which is the same seam production uses.

Repointing them surfaced four assertions that encoded legacy-only values:
- "In Progress" / "In Review" — the real IR names are "In progress"/"In review".
- Planning Mode was asserted to receive `null` as the workflow id; that is only
  what `getTaskPlanningWorkflowId` returns when workflowMode is false. Production
  passes the task's resolved workflow.
("Back to In Progress" is NOT one of these — it is a hardcoded i18n string in
TaskContextMenu, not derived from the column name. Left alone; flagged as a
separate legacy-vocabulary defect that will not follow a renamed column.)

One test is SKIPPED, not weakened: "keeps unaffected columns stable when archived
collapse toggles". Pointed at the real board, the invariant is false — toggling
the archived column re-renders unaffected columns (measured: todo renders 3x, not
2x). That is pre-existing production behaviour this deletion exposed, never
covered because the test was measuring the dead path. The reason is recorded at
the test; it must be un-skipped with a fix, not with a new expected number.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed integrity pass (U12)

Two pieces of workflow-columns compatibility machinery whose reachability I
checked rather than assumed:

`evacuateCustomColumnsToLegacy` (#1409) re-homed cards out of custom columns when
the `workflowColumns` flag flipped OFF, so the legacy enum board would not strand
them. Both triggers (in `publishSettingsUpdated` and the global-settings write
path) require the PREVIOUS settings to have the raw flag ON. Nothing in
production writes `experimentalFeatures.workflowColumns`, so the ON→OFF
transition — and therefore the evacuation — could not occur. Custom columns are
no longer an opt-in that can be revoked; they are the runtime.

`runWorkflowColumnsIntegrityPass` had NO caller anywhere — not production, not
tests. It is the superseded predecessor of the shipped R7 sweep,
`SelfHealingManager.reconcileUndeclaredTaskColumns`, which IS registered in
startup recovery and does the same job (re-home a card whose stored column its
workflow no longer declares). It also read tasks through the synchronous SQLite
handle, which no longer resolves under the PostgreSQL runtime, so invoking it
would have thrown rather than reconciled. Deleted rather than wired up: its
successor already runs.

This removes 2 of the 9 remaining reads of the raw compatibility flag
(`isWorkflowColumnsCompatibilityFlagEnabled`), and drops the import from
settings-ops entirely. The helper itself and its other 7 readers are NOT touched
here — see the PR description for the inventory and why the moves.ts group is
U2b's to converge.

The one PG test that reached `evacuateCustomColumnsToLegacy` went with it; it was
the method's only remaining caller and got there by writing the raw flag itself,
proving a rollback path out of a runtime that is no longer opt-in.

Measured: -164/+38 across 5 files (net -126).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ListView no longer imports COLUMNS at all; neither Board nor ListView now
references the legacy column enum. tsc does not flag unused imports in this
workspace, so these only surfaced under eslint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@gsxdsm, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c9c3cdd3-adb1-486f-9d27-4d7951971d96

📥 Commits

Reviewing files that changed from the base of the PR and between 2934ccc and 8be2e48.

📒 Files selected for processing (16)
  • .changeset/u12-delete-legacy-board-path.md
  • packages/core/src/__tests__/postgres/workflow-authoritative-reads.pg.test.ts
  • packages/core/src/store.ts
  • packages/core/src/task-store/settings-ops.ts
  • packages/core/src/task-store/task-mutation-ops.ts
  • packages/core/src/task-store/workflow-integrity.ts
  • packages/dashboard/app/components/Board.tsx
  • packages/dashboard/app/components/ListView.tsx
  • packages/dashboard/app/components/TaskDetailModal.tsx
  • packages/dashboard/app/components/__tests__/Board.test.tsx
  • packages/dashboard/app/components/__tests__/ListView.test.tsx
  • packages/dashboard/app/components/__tests__/board-no-legacy-flash.test.tsx
  • packages/dashboard/app/components/__tests__/board-quickcreate-workflow-lane-visibility.test.tsx
  • packages/dashboard/app/components/__tests__/workflow-resolved-columns.test.tsx
  • packages/dashboard/app/components/dashboard/MainContent.tsx
  • packages/dashboard/app/hooks/useBoardWorkflows.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/u12-flag-cleanup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread packages/core/src/task-store/settings-ops.ts
@greptile-apps

greptile-apps Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR removes legacy workflow-column compatibility code and repoints dashboard tests at workflow-backed lanes.

  • Deletes the dashboard’s hardcoded legacy Board and ListView rendering paths and simplifies workflow metadata loading.
  • Removes the obsolete workflow integrity pass and custom-column evacuation implementation from core.
  • Updates Board, ListView, modal, and PostgreSQL tests for the workflow-backed behavior.

Confidence Score: 3/5

The PR is not yet safe to merge because settings import or rollback can still move custom-column tasks onto a path that omits workflow capacity, transition-recovery, and plugin-hook behavior.

The corrected explanation establishes that the true-to-false transition is reachable, but removing evacuation without converging the move paths leaves subsequent moves from valid custom columns governed by the reduced flag-off behavior.

Files Needing Attention: packages/core/src/task-store/settings-ops.ts, packages/core/src/task-store/task-mutation-ops.ts

Important Files Changed

Filename Overview
packages/core/src/task-store/settings-ops.ts Removes both ON→OFF evacuation hooks, but reachable settings replacement paths still switch affected tasks onto reduced flag-off move semantics.
packages/core/src/task-store/task-mutation-ops.ts Deletes the custom-column evacuation implementation used by the previously reported transition path.
packages/dashboard/app/components/Board.tsx Deletes the unreachable hardcoded legacy board and always renders workflow-resolved lanes or a loading skeleton.
packages/dashboard/app/components/ListView.tsx Removes synthesized legacy columns and derives visible columns from resolved workflow metadata.
packages/dashboard/app/hooks/useBoardWorkflows.ts Simplifies workflow-cache hydration now that all callers use the workflow-backed rendering path.

Reviews (2): Last reviewed commit: "docs(U12): correct the ON→OFF evacuation..." | Re-trigger Greptile

…greptile P1)

The claim that the transition was unreachable was wrong. settings-schema.ts
explicitly tolerates stale persisted `experimentalFeatures.workflowColumns`
values, so an upgraded project can carry `true`, and a settings import or
configuration rollback can flip it to false. The transition is reachable.

The deletion stands on different ground: the evacuation is the wrong repair
post-cutover, not a dead one. It moved cards OUT of columns their own workflow
declares and into legacy `triage`, to protect the legacy enum board — which this
same PR deletes. The stranding it guarded against does not occur, because
moves.ts resolves a non-legacy source column's targets from the task's own
workflow adjacency on the flag-OFF path (FN-7591); coding-ideas-move.test.ts
proves it in the production shape (8 tests, flag never written) across the
forward chain and the non-adjacent rejection. reconcileUndeclaredTaskColumns
correctly leaves such cards alone — their workflow declares their column.

Comments only; no code change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gsxdsm
gsxdsm merged commit fd6d005 into main Jul 28, 2026
7 checks passed
@gsxdsm
gsxdsm deleted the feature/u12-flag-cleanup branch July 28, 2026 22:45
gsxdsm added a commit that referenced this pull request Jul 29, 2026
…d the definitive answer on the raw flag (2 reads left, both U2b's) (#2535)

## U12 part 8 — deletes the lossy `normalizeColumn`, and ratchets it
shut

Independent of the #2525#2528#2530 stack; touches only
`@fusion/core` exports.

This closes **one of the two `@deprecated (workflowColumns, U12)`
markers** the unit was named for.

### The hazard

`normalizeColumn` coerced an arbitrary value to a **legacy** column,
rewriting every workflow-defined custom id to `triage`. Silent data loss
for any project whose workflow declares a column outside the six
built-ins — and it sat one line away from `normalizeColumnId`, which
sanitises structurally and passes real ids through.

The dashboard picked the wrong one for its entire task-ingest path until
that was diagnosed; `useTasks.ts` and `routes-trait-rekey.test.ts` still
carry the notes from that fix. So this is not a hypothetical footgun —
it already fired once, on the surface where it mattered most.

Deleted rather than left deprecated because it has **zero callers
anywhere in the workspace**. It was pure exported hazard: a lossy
coercion next to its safe twin, waiting to be picked again.

### The ratchet is the point

`no-lossy-column-coercion-export.test.ts` bans the **behaviour, not the
identifier**: it walks every exported single-argument function whose
name mentions "column" and fails if one maps a valid custom id onto a
different legacy id. Re-adding `normalizeColumn` under any name trips
it.

Verified by actually reintroducing the function — **two of the three
cases fail, including the name-agnostic one**. That last detail is what
stops it being a guard that checks nothing.

Coverage stated plainly: deleting an unused export has no behaviour to
revert-check. The compile is the proof it had no callers; the ratchet is
the proof it cannot return.

---

## Answering the standing question: does anything still read the raw
`workflowColumns` flag?

**Yes. Exactly two sites, and both are U2b's.** I am not able to close
this out, and here is the complete list rather than a summary:

```
packages/core/src/store.ts:38,43                                  ← the definition
packages/core/src/task-store/moves.ts:9,363                       ← `useWorkflow`
packages/core/src/task-store/workflow-task-create-ops.ts:11,351   ← move-policy preflight
```

That is the whole list in production code. Everything else that greps is
a comment, a test that writes the flag deliberately to exercise the dead
path, or the unrelated `workflowColumns.*` i18n namespace for the
Columns editor panel.

**Why I have not deleted the settings key.** It cannot go while those
two read it — the key is what they read. And the two are not separable
from each other: `workflow-task-create-ops.ts:351` computes the
`movePolicyPreflight` that `moves.ts` consumes and validates, and
un-gating the preflight alone would start evaluating workflow move
policies (with their plugin-gate side effects) while the branch that
consumes the result stays off. That is a behaviour change with no
consumer, which is worse than either state.

**Status of the blocker.** U2b has not landed. `main` at `919f68f9b`
still has both reads; the program's merged history goes `#2466#2467#2468 (characterisation only) → #2469#2479#2500#2512#2513`,
with no convergence PR. PR #2468 was Phase A2 **steps 1–2 only** — the
differential characterisation — and the convergence that deletes one of
the two move paths was never merged.

So the honest state of the unit: everything U12 owns is done except the
two reads that U2b owns, and the settings key that cannot be deleted
until they are gone. If you want me to take U2b itself, say so — I have
the inventory and the divergence list, and I would want the current U2b
worker stood down from `moves.ts` first.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gsxdsm added a commit that referenced this pull request Jul 29, 2026
…st read goes — answer: 2 reads left, key cannot be deleted (#2537)

## U12 part 9 — the flag census now answers itself

Independent of the #2530 rebase; adds one test file, no production
changes.

## The answer, first: NO, the settings key cannot be deleted yet

**Three files reference the raw flag on current main (`3ff98aae5`):**

```
packages/core/src/store.ts                                 ← declares it
packages/core/src/task-store/moves.ts:363                  ← U2b: `useWorkflow`
packages/core/src/task-store/workflow-task-create-ops.ts:351 ← U2b: move-policy preflight
```

Everything else that greps is a comment, a test writing the flag
deliberately to reach the dead path, or the unrelated
`workflowColumns.*` i18n namespace for the Columns editor panel.

**Why I can't remove them.** Both are on the move path and belong to
**U2b**, which carries an equivalence-proof obligation because the two
move implementations it arbitrates have never both run in production.
They are also **not separable from each other**:
`workflow-task-create-ops.ts:351` computes the `movePolicyPreflight`
that `moves.ts` consumes and validates, so un-gating it alone would
start evaluating workflow move policies — with their plugin-gate side
effects — while the branch consuming the result stays off. That is a
behaviour change with no consumer, which is worse than either end state.

**U2b has not landed.** Program history on main runs `#2466#2467#2468#2469#2479#2500#2512#2513#2525#2528#2535`.
#2468 was Phase A2 **steps 1–2 only** — the differential
characterisation. No convergence PR exists.

## Why this is a PR and not another status message

You have asked this question three times. I have answered it three times
by grepping, and each answer was a number nobody could re-derive later —
including me, which is why I re-ran the audit from scratch each time.
That is exactly the shape this program keeps finding: a fact everyone
believes, maintained by nobody.

So the census is now a test. It **fails in both directions**,
deliberately:

- **A new read appears** → someone re-gated behaviour on a flag that is
`false` for every real project, so the feature behind it will not run.
That is the defect class U12 spent its length finding (the capacity
gate, the U5 guards, the move policies — all looked enforced, none
were).
- **The last read disappears** → U2b has landed, and the settings key
can finally go. The removal steps are written at the assertion.

The second case is the one that matters. It converts "remember to delete
the settings key someday" into a failing test at the exact moment that
becomes possible, instead of a note in a PR body that ages out.

## Verified in both directions, not assumed

- Adding a reference in `lifecycle-ops.ts` → fails with `+
"packages/core/src/task-store/lifecycle-ops.ts"`.
- Dropping `moves.ts` from the allowlist → fails with `+
"packages/core/src/task-store/moves.ts"`.

Equality rather than subset is what makes the second case possible; a
subset check would let the last reader vanish silently and leave the key
orphaned forever.

Two supporting assertions, both there because of failure modes this
program has already hit:

- **No production code WRITES the key.** That is the premise the entire
unit rests on — if a writer appears, every "this branch is unreachable"
conclusion in U12 needs revisiting.
- **The scan sees >200 files.** A broken path glob would otherwise make
every assertion vacuously green: a guard reporting success without
checking anything.

## Verification

`pnpm test:gate` (414 + 10 + 71), `pnpm lint`, `pnpm verify:fast`, core
typecheck green.

## Standing offer

If you want U12 actually closed rather than ratcheted, the remaining
work is U2b's convergence. I have the inventory and the divergence list
its characterisation suite does not yet cover (plugin column gates, the
`transitionPending` marker, `workflowId` in `task:move` run-audit,
move-policy preflight). I would want the current U2b worker stood down
from `moves.ts` first — two writers on the file this whole program
pivots on is the one hazard I would not take on my own authority.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Added a new automated Vitest “census ratchet” to ensure only an
approved, fixed set of production reads is made for the workflow columns
compatibility flag.
* Added checks that disallow hardcoded `workflowColumns: true/false`
assignments in production sources.
* Added allowlist validation, including per-file occurrence counts,
required rationale text length, and confirmation that referenced files
exist.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
gsxdsm added a commit that referenced this pull request Jul 30, 2026
…rlier PRs cited had no test of its own (#2543)

## U12 part 10 — the R7 sweep everything else leans on was itself
unpinned

`reconcileUndeclaredTaskColumns` re-homes a card resting in a column its
workflow no longer declares. It is the shipped answer to **R7**, and it
is the reason several earlier U12 deletions were safe — I cited it when
deleting the superseded `runWorkflowColumnsIntegrityPass` (#2500), and
again when arguing that a torn workflow switch leaves *recoverable*
state (#2512).

Its only coverage was **incidental**: two live PostgreSQL e2e suites
that exercise it in passing. A repair the rest of the unit leans on had
no test of its own — a guarantee everyone cites and nobody checks, which
is the exact shape this unit keeps finding.

### Six cases

The plan names three scenarios for U12; those are the three ways this
sweep can be wrong, plus I added the over-fire direction:

- repairs the stranded card to its workflow's **own** rebound target
(not a hardcoded legacy id)
- leaves a **user-paused** card alone
- leaves an **unresolvable-workflow** card alone
- is **idempotent** — a second run does not move the card again
- ignores a card already resting in a declared column
- repairs one stranded card **without disturbing** healthy or paused
neighbours

The leave-alone cases matter more than the repair. A sweep that
over-fires rewrites an operator's board, and this one runs at startup
against every task.

It also asserts `recoveryRehome: true` explicitly, because that flag is
load-bearing rather than incidental: the stranded card's *source* column
is undeclared too, so adjacency resolves to `[]` and every target is
rejected without it. Its absence once made this sweep a repair that
never repaired anything (#2462).

### Mechanism coverage — measured, and one case that isn't

Verified by mutation rather than asserted:

| mutation | result |
|---|---|
| delete the user-pause guard | **2 cases fail** |
| delete the already-declared short-circuit | **2 cases fail** |
| delete the unresolvable-workflow `continue` | still green |

That last row is stated at the assertion rather than hidden. The
unresolvable-workflow case pins the **outcome**, not the mechanism:
every mutation I could construct — dropping the `continue`, dropping the
try/catch so the throw reaches the outer handler — also ends in "no
move". So it is a regression guard on observable behaviour, not proof
the specific guard is reached, and I am not claiming otherwise.

### A decision I made

Store double rather than PostgreSQL. The sweep's decisions are pure
functions of the task list and the resolved IR, and a double makes the
"did **not** move" assertions exact rather than inferred from an absence
of change. It also keeps the suite off the slow lane, per the standing
rule against adding slow tests.

### Verification

`pnpm test:gate` (414 + 10 + 71), `pnpm lint`, engine typecheck green.
New suite: 6 passed.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Added coverage for automatically restoring tasks stranded in
undeclared workflow columns.
* Verified paused tasks, unresolved workflows, and tasks already in
valid columns remain unchanged.
  * Confirmed repairs are idempotent and affect only the intended task.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant