E2E: table-driven converted-sweep coverage, two new sites, and an honest unproven-sites ledger (#2485)

Follow-up to #2475 (merged). Test-only, plus one test-utility seam.

## Why a table

#2475 proved one converted sweep. The count has since gone to **three**,
twice while this work was open — `surfaceStalePausedTodos` appeared
during #2475's review, and #2478 landed `recovery-reconciler.ts` while
this branch was open. A suite with a bespoke `describe` per sweep is a
coverage claim that quietly becomes false.

Replaced with a table of `(seed, run, acted, roles, observability)`. The
driver derives four assertions per entry:

| | positive | negative |
|---|---|---|
| **renamed vocabulary** | acts on the card | inert in a non-target
column |
| **default vocabulary** | acts (regression floor) | inert |

Adding a converted sweep is **one entry** — the #2478 site proved that
in practice, not in principle. `actsOnRole`/`inertRole` are keys of
`Vocabulary`, not column strings, so an entry cannot hardcode `todo` and
pass for the wrong reason.

## Two findings, both from mutation rather than reading

**1. The census was wrong about `recovery-reconciler.ts:198.`** It was
flagged as a `resolveLifecycleColumns` site, so the row was first
labelled as covering it. **Destroying that role resolution leaves all 18
tests green** — `decideRecovery` looks policy up by *column id* and
never consults a role. The row is relabelled to what it actually proves,
and mutation-verified against that instead: keying the reconciler's
policy lookup on the `todo` literal fails exactly its renamed test.

**2. `resolveRoleRecovery` is an unreachable export.** It is the only
use of `resolveLifecycleColumns` in that file and has **no production
caller anywhere** in engine, core, or dashboard. So that census line is
not a live converted site — it is a helper written ahead of its
consumer. **Not fixed here:** it is production code owned by the U4
slice, and whether the consumer is still to land or it should be deleted
is its author's call.

## Observability is now explicit in the type

`persisted-row` is the strong form. `returned-decision` is recorded as
**weaker evidence** and the reconciler row uses it, because
`reconcileRecovery` decides and does not apply — there is no row to
read. Naming it in the type is what stops a return-value assertion from
quietly passing as observed state, and it is what keeps the ledger
truthful per site.

## Harness seam

`PgTestHarness` now exposes its raw admin SQL client. The store
**stamps** `updatedAt`/`columnMovedAt` on every write, so `updateTask`
cannot express an aged row at all — the patch is accepted and the value
silently replaced with `now`. **Found by the new case failing on BOTH
vocabularies**, which is what distinguishes a broken fixture from a
broken guard. Seeding only; assertions still read back through the real
`getTask` path.

## Mutation verification

| Mutation | Result |
|---|---|
| revert **only** `recoverStrandedCompletedTodoTasks`'s resolution |
**exactly** that row's renamed test fails |
| revert **only** `surfaceStalePausedTodos`'s resolution | **exactly**
its own renamed test fails |
| reconciler policy lookup keyed on `todo` | exactly the reconciler
row's renamed test fails |
| `resolveRoleRecovery` role resolution destroyed | **nothing fails** →
finding #2 |
| `hold-release` `isHeldTask` keyed on `todo` | 5 of 18 fail; default
spine survives |
| `markMoveInFlight` dropped | both spine tests fail |

Per-site verification matters here: three rows could all be riding one
guard. They are not.

## The honest number

**Proven end to end: 5** (two self-healing sweeps, the reconciler's
policy lookup at the weaker observability, hold-release's capacity
release, and the graph boundary + `moveTask` + post-commit bus).

**Not proven: 11 call sites** — `merger.ts:324-326`,
`merger-ai.ts:1022,1039`, `auto-merge-finalization.ts:20-22`,
`executor.ts:1763,6339,6341`, `self-healing.ts:713,6732`,
`mesh-lease-manager.ts:61`, `task-agent-sync.ts:59`,
`core/task-store/reads.ts:130`, `core/live-agent-count.ts:63-75`, and
four dashboard route sites.

The ledger lives in the file, not just here, so it stays with the code.

## Where the table does not fit — reported, not papered over

The **merge/rebound family** cannot be a table row: those sweeps have no
observable persisted effect without a real git repository, so `acted`
cannot be written against the row at all. They need an engine-slow
real-git lane. The dashboard sites need an HTTP route test with a live
store. Both are different lanes, not missing entries.

## Verification

- 18/18 green; engine + core `tsc --noEmit` clean; `pnpm test:gate`
green (299 + 10 + 71)
- full core PG suite run (the harness is shared): 1036 passed, 3 failed
in `central-archive-secrets` and `workflow-settings-project-identity` —
**reproduce identically with this change stashed**, pre-existing

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-27 15:25:17 -07:00
committed by GitHub
parent 2dfac47917
commit 89284df85e
2 changed files with 290 additions and 80 deletions

View File

@@ -45,7 +45,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { basename, join } from "node:path";
import { tmpdir } from "node:os";
import { describe as vitestDescribe } from "vitest";
import postgres from "postgres";
import postgres, { type Sql } from "postgres";
import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js";
import { sql } from "drizzle-orm";
import type { ResolvedBackend } from "../postgres/backend-resolver.js";
@@ -209,6 +209,17 @@ export interface PgTestHarness {
readonly layer: AsyncDataLayer;
/** A separate admin Drizzle connection for direct row inspection/seeding. */
readonly adminDb: PostgresJsDatabase;
/*
FNXC:PgTestHarness 2026-07-27-18:55:
The RAW tagged-template client behind `adminDb`. Needed because several persisted fields are
stamped by the store on every write (`updatedAt`, `columnMovedAt`), so a fixture that must
present an AGED row — anything testing a staleness threshold — cannot express it through
`updateTask` at all: the patch is accepted and the value silently replaced with `now`. Consumers
outside `@fusion/core` also cannot reach `adminDb` usefully, since driving it needs `drizzle-orm`
and the table schema, neither of which is a dependency of the engine package.
Seeding only — never a substitute for asserting through the real read path.
*/
readonly adminSql: Sql;
/** The temp rootDir used for filesystem-backed operations. */
readonly rootDir: string;
/** The unique test database name (for diagnostics). */
@@ -812,6 +823,7 @@ export async function createTaskStoreForTest(options?: {
store,
layer,
adminDb,
adminSql,
rootDir,
dbName,
testUrl,
@@ -892,6 +904,8 @@ export interface SharedPgTaskStoreHarness {
readonly store: () => TaskStore;
readonly layer: () => AsyncDataLayer;
readonly adminDb: () => PostgresJsDatabase;
/** Raw admin SQL client — see the note on {@link PgTestHarness.adminSql}. */
readonly adminSql: () => Sql;
readonly beforeAll: () => Promise<void>;
readonly beforeEach: () => Promise<void>;
readonly afterEach: () => Promise<void>;
@@ -968,6 +982,10 @@ export function createSharedPgTaskStoreTestHarness(options?: {
if (!harness) throw new Error("SharedPgTaskStoreHarness: beforeAll not called yet");
return harness.adminDb;
},
adminSql: () => {
if (!harness) throw new Error("SharedPgTaskStoreHarness: beforeAll not called yet");
return harness.adminSql;
},
beforeAll: async () => {
if (harness) return;
// FNXC:PgTestTemplateDb 2026-07-19-17:20:

View File

@@ -36,7 +36,7 @@ throwaway per-file database; never port 4040; no temp-root walk.
*/
import { beforeAll, beforeEach, afterEach, afterAll, expect, it, describe } from "vitest";
import "@fusion/core"; // registers the built-in column traits into the shared registry
import type { Settings, Task, TaskDetail, WorkflowIr } from "@fusion/core";
import type { Settings, Task, TaskDetail, TaskStore, WorkflowIr } from "@fusion/core";
import {
getWorkflowEventBus,
resetWorkflowEventBusForTesting,
@@ -53,6 +53,7 @@ import { WorkflowGraphTaskRunner, type WorkflowColumnBoundaryHooks } from "../wo
import { createExecutorColumnBoundaryHooks } from "../workflow-column-boundary-hooks.js";
import { runHoldReleaseSweep } from "../hold-release.js";
import { SelfHealingManager } from "../self-healing.js";
import { reconcileRecovery } from "../recovery-reconciler.js";
/** The four lifecycle roles this program's guards are supposed to resolve by TRAIT, not by id. */
interface Vocabulary {
@@ -95,6 +96,10 @@ function lifecycleIr(v: Vocabulary, id: string): WorkflowIr {
id: v.hold,
name: "Hold",
traits: [{ trait: "hold", config: { release: "capacity" } }],
/* U4 workflow-declared recovery policy (#2478). Declared on the HOLD column of both
vocabularies from the one builder, so the reconciler's role resolution is exercised
against a renamed column with nothing else differing. */
recovery: { stalenessMs: HOLD_STALENESS_MS, onStale: { action: "surface", code: "e2e-stale-hold" } },
},
{
id: v.wip,
@@ -131,6 +136,8 @@ function lifecycleIr(v: Vocabulary, id: string): WorkflowIr {
} as WorkflowIr;
}
const HOLD_STALENESS_MS = 60 * 60_000;
const OK = { outcome: "success" as const };
/** Records which seams actually ran, so "exactly once" is asserted on real invocations. */
@@ -455,99 +462,284 @@ pgDescribe("live lifecycle E2E: real graph + real PostgreSQL store", () => {
});
/*
The one converted lifecycle-mutating sweep on this tip (slice B3.1 — U4) run against a REAL
store and a REAL renamed workflow. Its conversion claim is that the sweep now resolves the hold
column from the card's own workflow instead of the `todo` literal in BOTH halves (query and
guard). Nothing so far has run it against a workflow that has no `todo` column at all.
FNXC:WorkflowLifecycleColumns 2026-07-27-18:30 (converted-sweep table):
The recovery callback performs a REAL `moveTask`, so the assertion is on the card's persisted
column afterwards — not on whether the callback was invoked.
A TABLE, not a scenario per sweep. The U4 conversion keeps ADDING callers of the lifecycle-role
resolution — the count went from one to two while PR #2475 was in review — so a suite with a
bespoke `describe` per sweep is a coverage claim that quietly goes stale. Adding a converted
sweep here is one entry.
Each entry declares four things and the shared driver derives four assertions from them:
seed — put a card in a given column in whatever state the sweep keys on
run — invoke the REAL sweep on a REAL store
acted — did the sweep act on this card? READ FROM THE PERSISTED ROW, never from a callback
roles — the lifecycle ROLE it must act on, and one it must stay inert in
Derived per entry: {default vocabulary, renamed vocabulary} x {positive, negative}. The default
half is the regression floor; the renamed half is the claim the conversion exists for; the
negative half is what stops "resolve the column per task" from degrading into "act on everything".
DELIBERATELY ROLE-TYPED. `actsOnRole`/`inertRole` are keys of `Vocabulary`, not column strings, so
an entry cannot accidentally hardcode `todo` and pass for the wrong reason.
IF A SWEEP DOES NOT FIT. Report it rather than hand-rolling a one-off beside the table — a sweep
whose outcome cannot be observed on the persisted row is itself a finding about that sweep. See
the UNPROVEN SITES ledger at the foot of this file for what is not covered and why.
*/
describe("converted self-healing sweep — recoverStrandedCompletedTodoTasks", () => {
async function seedStrandedTask(taskId: string, v: Vocabulary, workflowId: string, column: string) {
interface SweepCaseContext {
readonly store: TaskStore;
readonly taskId: string;
readonly vocab: Vocabulary;
readonly column: string;
readonly workflowId: string;
}
interface ConvertedSweepCase {
/** The sweep method under test. */
readonly sweep: string;
/** Where the converted lifecycle-role resolution lives, for the ledger. */
readonly site: string;
/** Put a card in `ctx.column` in whatever state this sweep keys on. */
readonly seed: (ctx: SweepCaseContext) => Promise<void>;
/** Project settings this sweep needs in order to run at all. */
readonly settings?: Record<string, unknown>;
/** Invoke the REAL sweep. Returns whatever it produces, for decision-only entries. */
readonly run: (store: TaskStore, vocab: Vocabulary) => Promise<unknown>;
/*
WHERE THE OUTCOME IS OBSERVED. `persisted-row` is the strong form and the default expectation:
the sweep changed the row, and `acted` reads it back through `getTask`. `returned-decision` is
WEAKER EVIDENCE and is recorded as such — the sweep produces a decision it does not apply, so
there is no row to read. An entry must not silently use the weak form: naming it here is what
keeps the ledger at the foot of this file truthful about what "covered" means per site.
*/
readonly observability: "persisted-row" | "returned-decision";
/** Did the sweep act? For `persisted-row` entries, read the row and ignore `runResult`. */
readonly acted: (task: TaskDetail, vocab: Vocabulary, runResult: unknown) => boolean;
/** The lifecycle role the sweep must act on. */
readonly actsOnRole: keyof Vocabulary;
/** A role of the SAME workflow the sweep must leave alone. */
readonly inertRole: keyof Vocabulary;
}
/** Promote through the store's real transition policy: hold → wip → review.
* A DIRECT hold → review move is refused ("Invalid transition: 'backlog' → 'checking'. Valid
* targets: building") and that refusal is itself workflow-resolved — the renamed board's only
* legal target is its own `building` — so it is honored rather than bypassed. */
async function promoteThroughPolicy(store: TaskStore, taskId: string, v: Vocabulary): Promise<boolean> {
for (const target of [v.wip, v.review]) {
await store.moveTask(taskId, target, {
moveSource: "engine",
bypassGuards: true,
preserveProgress: true,
allowDirectInReviewMove: true,
skipMergeBlocker: true,
} as never);
}
return true;
}
const STALE_PAUSED_THRESHOLD_MS = 24 * 60 * 60_000;
const STALE_PAUSED_MARKER = "Stale paused todo surfaced [";
const CONVERTED_SWEEPS: ConvertedSweepCase[] = [
{
sweep: "recoverStrandedCompletedTodoTasks",
observability: "persisted-row",
site: "self-healing.ts — resolveLifecycleColumns(...).hold (slice B3.1, U4)",
actsOnRole: "hold",
inertRole: "wip",
seed: async ({ store, taskId }) => {
// Fully-complete implementation steps are this sweep's entry condition.
await store.updateTask(taskId, { steps: [{ name: "only step", status: "done" }] } as never);
},
run: async (store, vocab) => {
const manager = new SelfHealingManager(store, {
recoverCompletedTask: async (task: Task) => promoteThroughPolicy(store, task.id, vocab),
} as never);
await manager.recoverStrandedCompletedTodoTasks();
},
// Acted iff the card actually left the hold column for the review column.
acted: (task, vocab) => task.column === vocab.review,
},
{
sweep: "surfaceStalePausedTodos",
observability: "persisted-row",
site: "self-healing.ts — resolveLifecycleColumns(...).hold (PR #2470 review, P1)",
actsOnRole: "hold",
inertRole: "wip",
settings: { stalePausedTodoThresholdMs: STALE_PAUSED_THRESHOLD_MS },
seed: async ({ store, taskId }) => {
/* The store STAMPS `updatedAt`/`columnMovedAt` on every write, so `updateTask` cannot
express an aged row at all — the patch is accepted and the value silently replaced with
`now`, and the sweep then correctly finds nothing. Discovered by this case failing on
BOTH vocabularies, which is what distinguishes a broken fixture from a broken guard.
The age is therefore written through the harness's raw admin client. Seeding only: the
assertion still reads back through the real `getTask` path. */
await store.updateTask(taskId, { paused: true, pausedReason: "e2e-hold" } as never);
const aged = new Date(Date.now() - STALE_PAUSED_THRESHOLD_MS * 3).toISOString();
await h.adminSql()`
UPDATE project.tasks
SET column_moved_at = ${aged}, updated_at = ${aged}
WHERE id = ${taskId}
`;
store.taskCache.delete(taskId);
},
run: async (store) => {
await new SelfHealingManager(store, {} as never).surfaceStalePausedTodos();
},
/* This sweep does not move the card — it writes an operator-facing log entry. That entry IS
persisted state (`task.log`), so it is still an observed-outcome assertion rather than a
spy; a sweep whose only effect were in-memory would not belong in this table at all. */
acted: (task) => (task.log ?? []).some((e) => (e.action ?? "").startsWith(STALE_PAUSED_MARKER)),
},
{
/*
Landed in #2478 AFTER the ledger below was first written — which is precisely the drift a
table exists to absorb: covering it was one entry, not a new describe block.
WHAT THIS ROW ACTUALLY PROVES, established by mutation rather than by reading the code. The
census flagged `recovery-reconciler.ts:198` as a `resolveLifecycleColumns` call site, so the
row was first labelled as covering it. It does not: destroying the role resolution in
`resolveRoleRecovery` leaves all 18 tests GREEN, because `decideRecovery` looks the policy up
by COLUMN ID (`resolveColumnRecovery(ir, task.column)`) and never consults a role.
`resolveRoleRecovery` turns out to have NO production caller at all — see the ledger.
What the row does prove, and what it is mutation-verified against: the reconciler resolves and
matches a column-declared recovery policy on a REAL renamed board — real store, real persisted
workflow definition, real `resolveWorkflowIrForTask`. Keying that lookup on the `todo` literal
fails exactly this row's renamed test.
OBSERVABILITY CAVEAT, stated rather than hidden: `reconcileRecovery` DECIDES and does not
APPLY. The slice deliberately stops before the writer, so there is no persisted effect to read
and `acted` must inspect the returned decision. That is weaker evidence than every other row
here. Switch this to `persisted-row` the moment the applier lands.
*/
sweep: "reconcileRecovery (recovery-reconciler.ts)",
observability: "returned-decision",
site: "recovery-reconciler.ts — resolveColumnRecovery policy lookup on a renamed board (U4, #2478)",
actsOnRole: "hold",
inertRole: "wip",
seed: async ({ store, taskId }) => {
// Rest the card in its column long enough to pass the policy's stalenessMs.
const aged = new Date(Date.now() - HOLD_STALENESS_MS * 3).toISOString();
await h.adminSql()`
UPDATE project.tasks
SET column_moved_at = ${aged}, updated_at = ${aged}
WHERE id = ${taskId}
`;
store.taskCache.delete(taskId);
},
run: async (store) => {
const tasks = await store.listTasks({ includeArchived: false });
return reconcileRecovery(store, tasks, { now: () => Date.now() });
},
acted: (task, _vocab, runResult) =>
(runResult as Array<{ taskId: string; code: string }>).some(
(d) => d.taskId === task.id && d.code === "e2e-stale-hold",
),
},
];
describe.each(CONVERTED_SWEEPS)("converted sweep — $sweep", (testCase) => {
/** Seed one card under `vocab` resting in `column`, then run the real sweep and report the
* PERSISTED outcome. */
async function driveCase(taskId: string, vocab: Vocabulary, roleKey: keyof Vocabulary, key: string) {
const store = h.store();
if (testCase.settings) await store.updateSettings(testCase.settings as never);
const { workflowId } = await seedWorkflow(vocab, key);
const column = vocab[roleKey];
await store.createTaskWithReservedId(
{ description: `stranded ${taskId}`, column } as never,
{ description: `${testCase.sweep} ${taskId}`, column } as never,
{ taskId, applyDefaultWorkflowSteps: false } as never,
);
await store.writeTaskWorkflowSelection(taskId, workflowId, []);
// Fully-complete implementation steps are the sweep's entry condition.
await store.updateTask(taskId, {
steps: [{ name: "only step", status: "done" }],
} as never);
await testCase.seed({ store, taskId, vocab, column, workflowId });
store.taskCache.delete(taskId);
const runResult = await testCase.run(store, vocab);
store.taskCache.delete(taskId);
const persisted = (await store.getTask(taskId)) as TaskDetail;
return { persisted, acted: testCase.acted(persisted, vocab, runResult) };
}
/** Run the real sweep with a recovery callback that performs REAL column moves.
* The move goes hold → wip → review because the store's transition policy REFUSES a direct
* hold → review move ("Invalid transition: 'backlog' → 'checking'. Valid targets: building").
* That refusal is itself workflow-resolved — the renamed board's only legal target is its own
* `building`, not `in-progress` — so it is left in place rather than bypassed. */
async function runSweep(v: Vocabulary): Promise<{ recovered: number; promoted: string[]; moveErrors: string[] }> {
const store = h.store();
const promoted: string[] = [];
const moveErrors: string[] = [];
const manager = new SelfHealingManager(store, {
recoverCompletedTask: async (task: Task) => {
promoted.push(task.id);
try {
for (const target of [v.wip, v.review]) {
await store.moveTask(task.id, target, {
moveSource: "engine",
bypassGuards: true,
preserveProgress: true,
allowDirectInReviewMove: true,
skipMergeBlocker: true,
} as never);
}
} catch (e) {
moveErrors.push(e instanceof Error ? e.message : String(e));
return false;
}
return true;
},
} as never);
const recovered = await manager.recoverStrandedCompletedTodoTasks();
return { recovered, promoted, moveErrors };
}
it("promotes a completed card stranded in a RENAMED hold column", async () => {
const v = RENAMED_VOCAB;
const { workflowId } = await seedWorkflow(v, "stranded-renamed");
await seedStrandedTask("FN-E2E-8", v, workflowId, v.hold);
const { recovered, promoted, moveErrors } = await runSweep(v);
expect(moveErrors).toEqual([]);
expect(promoted).toContain("FN-E2E-8");
expect(recovered).toBe(1);
// Observed state, not the callback: the card actually left the hold column.
expect(await persistedColumn("FN-E2E-8")).toBe(v.review);
it("acts on a card in the RENAMED lifecycle column", async () => {
const r = await driveCase("FN-SW-1", RENAMED_VOCAB, testCase.actsOnRole, "sweep-renamed");
expect(r.acted).toBe(true);
});
it("does NOT promote a completed card resting in a non-hold column of the same renamed workflow", async () => {
/* The negative half. Dropping the column filter without a correct per-task hold resolution
turns this sweep into "promote every completed card anywhere", which is a louder failure
than the silence it replaces. */
const v = RENAMED_VOCAB;
const { workflowId } = await seedWorkflow(v, "stranded-renamed-neg");
await seedStrandedTask("FN-E2E-9", v, workflowId, v.wip);
const { promoted } = await runSweep(v);
expect(promoted).not.toContain("FN-E2E-9");
expect(await persistedColumn("FN-E2E-9")).toBe(v.wip);
it("stays INERT for a card in a non-target column of the same renamed workflow", async () => {
const r = await driveCase("FN-SW-2", RENAMED_VOCAB, testCase.inertRole, "sweep-renamed-neg");
expect(r.acted).toBe(false);
// and the card is untouched where it stands
expect(r.persisted.column).toBe(RENAMED_VOCAB[testCase.inertRole]);
});
it("still promotes a default-vocabulary card in `todo` (regression floor)", async () => {
const v = DEFAULT_VOCAB;
const { workflowId } = await seedWorkflow(v, "stranded-default");
await seedStrandedTask("FN-E2E-10", v, workflowId, v.hold);
it("still acts on a DEFAULT-vocabulary card (regression floor)", async () => {
const r = await driveCase("FN-SW-3", DEFAULT_VOCAB, testCase.actsOnRole, "sweep-default");
expect(r.acted).toBe(true);
});
const { promoted } = await runSweep(v);
expect(promoted).toContain("FN-E2E-10");
expect(await persistedColumn("FN-E2E-10")).toBe(v.review);
it("stays INERT for a default-vocabulary card in a non-target column", async () => {
const r = await driveCase("FN-SW-4", DEFAULT_VOCAB, testCase.inertRole, "sweep-default-neg");
expect(r.acted).toBe(false);
});
});
});
/*
FNXC:WorkflowLifecycleColumns 2026-07-28-16:10 — UNPROVEN SITES LEDGER.
Kept deliberately, and kept HONEST: the difference between "the E2E covers the conversion" and
"the E2E covers N of M sites" is this list. Census re-taken against main at b133d521c4 (it had
already drifted once — #2478 landed a new site between the first census and this one, which is why
the coverage above is a table).
Census method: every caller of `resolveLifecycleColumns`, `resolveCompleteColumn`,
`resolveMergeOrchestrationColumn`, `resolveReboundTarget`, `resolveTaskLifecycleColumns`,
`columnHasFlag`, `columnsWithFlag` outside tests and the barrel re-exports.
PROVEN end to end against a live renamed workflow by this file (each mutation-verified, and for the
two self-healing sweeps verified PER SITE — reverting one fails exactly its own row):
- self-healing.ts recoverStrandedCompletedTodoTasks (table row, persisted-row)
- self-healing.ts surfaceStalePausedTodos (table row, persisted-row)
- recovery-reconciler.ts column-declared policy lookup (table row, returned-decision — WEAKER)
- hold-release.ts isHeldTask / the capacity release (spine)
- the graph column boundary + store.moveTask + the post-commit bus (spine)
FINDING — AN UNREACHABLE EXPORT. `resolveRoleRecovery` (recovery-reconciler.ts:194) is the ONLY
use of `resolveLifecycleColumns` in that file, and it has NO production caller: `decideRecovery`
looks policy up by column id. Established by mutation — destroying the role resolution leaves all
18 tests here green, and a repo-wide grep finds no caller outside this file. So that census line is
not a live converted site; it is an exported helper written ahead of its consumer. Either its
consumer is still to land, or it should be deleted. Not resolved here: it is production code owned
by the U4 slice, and guessing which is a decision for its author.
NOT PROVEN end to end — real callers this suite does not reach:
- merger.ts:324-326 resolveCompleteColumn / resolveMergeOrchestrationColumn / resolveReboundTarget
- merger-ai.ts:1022,1039 resolveReboundTarget, resolveLifecycleColumns
- auto-merge-finalization.ts:20-22 completeColumn / mergeColumn / isCompleteColumn
- executor.ts:1763,6339,6341 rebound target, merge-orchestration probe, complete column
- self-healing.ts:713,6732 resolveReboundTarget (two distinct rebound paths)
- mesh-lease-manager.ts:61 resolveReboundTarget
- task-agent-sync.ts:59 resolveTaskLifecycleColumns
- core/task-store/reads.ts:130 listTasks hydration
- core/live-agent-count.ts:63-75 five columnHasFlag classifications
- dashboard register-task-workflow-routes.ts:151,166,175,1797
WHY, and what each would take:
- The merge/rebound family (merger, merger-ai, auto-merge-finalization, the executor rebound path,
mesh-lease-manager) needs a REAL git worktree, branch, and squash. This suite deliberately has
none — `merge-gate` is pure policy and the `merge` seam is scripted. They need an engine-slow
real-git lane, not another table row.
- The dashboard sites need an HTTP route test with a live store: reachable, different lane.
- `reads.ts:130` and `live-agent-count.ts` are read/hydration paths already covered at store level
by core's `store-stale-paused-renamed-hold.pg.test.ts`; what is missing is the end-to-end claim,
not the unit one.
TABLE FIT. Three rows fit. The merge/rebound family does NOT — not because the table is too rigid,
but because those sweeps have no observable persisted effect without a real repository, so `acted`
cannot be written against the row at all. That is a finding about the lane they need, not a reason
to hand-roll a scenario beside the table.
*/