Files
fusion/packages/engine
gsxdsm dcf9900d61 test(engine): live-PG differential between resolvePlannerLanes and its async twin (#2791)
## What

One new live-PostgreSQL E2E suite, 5 tests. **No production file is
touched** — evidence, per the E2E worker's remit. Follows #2789 (same
defect class, different site).


`packages/engine/src/__tests__/workflow-planner-lanes-sync-vs-async-live-e2e.pg.test.ts`

## The finding

`replan-target.ts` exports two functions with identical logic and
identical fallbacks, differing only in how they obtain the task's IR:

```
resolvePlannerLanes(store, taskId)              -> store.resolveTaskWorkflowIrSync(taskId)
resolvePlannerLanesForTaskAsync(store, taskId)  -> await resolveWorkflowIrForTask(store, taskId)
```

Under PostgreSQL the sync selection reader answers `undefined` for every
task, so the sync twin resolves the **default** workflow for every card
regardless of the board it is on. **Nine production call sites use it**
(2 × `executor.ts`, 7 × `triage.ts`); one uses the async twin.

The module's own doc comment argues this, and the store-level fact is
proven in `sync-workflow-ir-is-always-default.pg.test.ts`. What had no
executable evidence is the consequence **at this seam** against a real
store with a real persisted workflow. That is this file — a pure
differential: both twins, same store, same task, same call.

### Two harms, different severity

1. **Wrong lanes, labelled authoritative.** `resolvedFromWorkflow`
exists to tell a caller "these came from the workflow, not the
fallback". The sync twin sets it `true` — an IR did come back — while
handing over the default board's ids. A caller that correctly checks the
flag before trusting the lanes is misled *precisely by checking it*,
which is strictly worse than the honest `false` an unresolvable store
would give.
2. **Invented forward lanes.** `wip`/`review`/`complete` are optional so
a caller *refuses* rather than moving a card into a column the board
does not declare (PR #2628's review). The sync twin defeats that
contract without touching it: never having seen the real board, it
reports the default board's forward lanes as present. The optionality is
intact in the type and unreachable in practice. The sharpest arm: a
board declaring **no** review lane gets `undefined` from the async twin
and `"in-review"` from the sync twin.

### A correction worth carrying forward

"It falls back to the legacy lanes" is the wrong mental model **twice
over**. `LEGACY_PLANNER_LANES` (`hold: "todo", intake: "triage"`) is
reached only when no IR resolves at all — under PostgreSQL, never. What
a caller actually receives is the **post-U11 merged default**, whose
intake and hold are one `todo` lane. So the sync twin does not return
`triage` for intake; it returns `todo`, and a caller reading `intake`
gets not merely a wrong id but a lane that is not a dedicated intake at
all.

This is also why the control arm uses `MERGED_VOCAB`: the shape the
twins agree on is the merged one. `DEFAULT_VOCAB`, which splits intake
out as `triage`, already separates them.

## Evidence discipline

- **Observed state.** These are exported pure functions over a live
store; the observation is their return value against a persisted
workflow definition. No spies, no mock IR anywhere in the file. Contrast
the unit coverage in `planner-lanes-async-resolution.test.ts`, which
must supply a mock `resolveTaskWorkflowIrSync` and therefore cannot see
this divergence at all.
- **Control arm.** On the post-U11 default shape the twins agree exactly
— which is why this survived: every default-board test passes and only a
renamed board separates them.
- **Characterization, not endorsement.** The four renamed arms assert
the wrong-but-current values deliberately; they flip when the call sites
move to the async twin, and that flip is the point.

### Mutation-verified, including a round that found weak arms

Replacing the sync twin's whole body with `return LEGACY_PLANNER_LANES`:

| | arms failing |
|---|---|
| first draft | **3 of 5** |
| after strengthening | **5 of 5** |

Two arms originally asserted only `wip`/`review`/`complete`, which are
identical in the merged default IR and in `LEGACY_PLANNER_LANES` — so
they proved the lanes were wrong without proving *why*, and survived the
mutation. Each now also pins `intake` (`todo` merged vs `triage`
legacy), the single field that separates "resolved the wrong board" from
"took the fallback". Recorded in the file next to the assertions.

## Not done, and why

**No fix.** The async twin already exists and is documented as a drop-in
("identical logic and identical fallbacks — the ONLY difference is
awaiting the authoritative resolver"), so the migration is mechanical
*where the caller is already async*. It is not universally so: several
`triage.ts` sites are inside synchronous paths, and `triage.ts:831`
calls `resolvePlannerLanes(this.store, "")` with an empty task id — a
sweep-wide lane read that has no single task to resolve against and
needs a decision, not a mechanical swap. Both are behaviour calls in
files another worker owns; flagging, not smuggling.

## Verification

- new suite — **5/5 passed**, mutation-verified 5/5
- full live-PG E2E surface, 20 suites — **126/126 passed** (was 121/121)
- `pnpm lint` — clean
- `pnpm check:lifecycle-columns` — exit 0

Lane: `.pg.test.ts`, skipped via `pgDescribe` when no PostgreSQL is
reachable, so the merge gate is unaffected. Throwaway per-file database;
never port 4040.

🤖 Generated with [Claude Code](https://claude.com/claude-code)


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

## Summary by CodeRabbit

* **Tests**
* Added end-to-end coverage comparing synchronous and asynchronous
workflow lane resolution.
* Validated lane consistency for renamed, non-default, and custom boards
using persisted workflow data.
* Added checks for incorrect fallback lanes, workflow resolution
indicators, and absent review lanes.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 10:32:24 -07:00
..
2026-07-26 18:11:47 -07:00