test(core): the sync IR path is blocked TWICE, not once — every note in the repo undercounts it (#3103)
Every remaining census cluster I could not convert — `executor.ts` (4),
`scheduler.ts` (2), `triage.ts` (1), and the four-guard fan-out I
withdrew from my own PR — is waiting on the same thing. So I went to
unblock it, and found the record is wrong.
## The repo says one blocker. There are two, plus a constraint
The call-site allow-list header, the live-PG proof, and a dozen FNXC
notes across engine and core — **several of which I wrote** — all say:
`resolveTaskWorkflowIrSync` is inert because the sync selection reader
returns `undefined`, and the fix is "a sync-capable workflow-selection
reader".
That understates the work by half, and the undercount is load-bearing:
it makes the unblock read like a caching job, so the next person ships a
selection cache and finds the rest at integration time.
### Blocker 2 — the IR read is dead too
`resolveTaskWorkflowIrSyncImpl` loads a **custom** workflow's IR through
`store.db.prepare("SELECT ir FROM workflows WHERE id = ?")`.
`TaskStore.db` is not "SQLite-only". Its implementation (`dbImpl`,
`task-id-integrity.ts`) is an **unconditional throw with no mode branch
at all**. That read always throws into the surrounding `catch`, which
always returns the default IR.
The consequence is precisely the one this program cares about:
| workflow kind | after a perfect selection reader |
|---|---|
| built-in | resolves — that branch never touches `store.db` |
| **custom** | **still the default IR, always** |
**A renamed lane is by definition a custom workflow.** So the sync path
cannot serve the renamed-board case *at all* until this second read is
replaced. Fixing the selection reader alone would produce a change that
looks like it works — on default boards.
### Blocker 3 — a node-local cache is unsafe here
Not a bug; a constraint that bounds the fix's shape. Multiple Fusion
nodes run their own engines against **one shared PostgreSQL**
(`docs/multi-project.md` → "Shared Postgres multi-node runbook").
A node-local synchronous cache of `task_workflow_selection` therefore
goes stale whenever *another node* rewrites a selection — and answers
with full confidence. That is **worse than today's default**, which is
at least uniformly wrong rather than intermittently wrong. Any sync
reader needs an invalidation story that survives a writer on a different
host.
## Why a test rather than a comment
A comment saying "db always throws" decays the moment someone adds a
mode branch, and the whole argument silently inverts — which is the same
decay mode this conversion program keeps hitting with allow-list entries
and stale notes.
The assertions are deliberately about `dbImpl`'s **source** rather than
a call. Calling it proves one construction path throws; the fix depends
on the stronger claim that **no mode returns a database**. Reintroducing
an `if`/`return` there fails the test, which is the correct outcome: the
premise really has changed and the file must be re-read.
## Measured
- 4 new cases pass; the allow-list file's own 7 still pass with its
corrected header.
- **MUTATION**: adding a mode branch to `dbImpl` fails the first case.
- An **anti-vacuity** case pins that the resolver is still live and
still allow-listed, so these source assertions cannot keep passing after
the concern is deleted.
- `tsc --noEmit -p packages/core` clean; census `--strict`,
`check-inert-sync-lane-conversions`, `check-fnxc-future-dates` clean.
## Census
**No movement — this converts nothing.** It corrects the record about
what the remaining conversions are waiting on, and it corrects notes I
authored. I would rather spend a PR making the next attempt cheap than
leave a half-true blocker in place that costs someone a full cycle to
rediscover.
## What I did not do
I did not build the sync reader. With blockers 2 and 3 in view it is a
store-substrate change — a second read to replace, and an invalidation
story that survives a writer on another host — not a fleet conversion,
and starting it mid-sweep on a shared file would repeat the collision
pattern that has already cost this branch three rebuilds. It remains
unclaimed, and now it is fully specified.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,19 @@ FNXC:WorkflowLifecycleColumns 2026-07-31-18:00 (fleet — stop the inert-convers
|
||||
PostgreSQL-cutover stub that answers `undefined` unconditionally, so the resolver always takes its
|
||||
`!workflowId` branch. Its return type is non-optional, so no caller can detect the substitution.
|
||||
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-23:10 (CORRECTION — this note named ONE blocker; there are
|
||||
TWO, and I am the author of several of the notes elsewhere that repeat the same undercount):
|
||||
Fixing the selection reader alone would NOT un-inert this path for the boards this program exists
|
||||
for. `resolveTaskWorkflowIrSyncImpl` loads a CUSTOM workflow's IR through `store.db.prepare(...)`,
|
||||
and `TaskStore.db` (`dbImpl`, task-id-integrity.ts) is an UNCONDITIONAL throw with no mode branch —
|
||||
so that read always throws into the surrounding `catch` and always yields the default IR. A built-in
|
||||
workflow could resolve once the selection reader works; a CUSTOM one never can, and a renamed lane
|
||||
is by definition a custom workflow.
|
||||
A third constraint bounds the fix's shape: multiple nodes run their own engines against ONE shared
|
||||
PostgreSQL (`docs/multi-project.md`), so a node-local sync cache of the selection goes stale when
|
||||
another node writes one — confidently wrong, which is worse than today's uniformly-wrong default.
|
||||
All three are proved and kept honest by `sync-workflow-ir-second-blocker.test.ts`.
|
||||
|
||||
That makes it the most dangerous tool in this conversion program. A guard written as
|
||||
|
||||
resolveLifecycleColumns(store.resolveTaskWorkflowIrSync(id))?.hold
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
FNXC:WorkflowLifecycleColumns 2026-07-31-23:05:
|
||||
THE SYNC IR PATH HAS TWO INDEPENDENT BLOCKERS, AND EVERY NOTE IN THIS REPO NAMES ONLY ONE.
|
||||
|
||||
The existing record — this file's sibling `sync-workflow-ir-callsite-allowlist.test.ts`, the
|
||||
`postgres/sync-workflow-ir-is-always-default.pg.test.ts` proof, and a dozen FNXC notes across engine
|
||||
and core, several of which I wrote — all say the same thing: `resolveTaskWorkflowIrSync` is inert
|
||||
because `getTaskWorkflowSelectionImpl` returns `undefined` unconditionally, and the fix is "a
|
||||
sync-capable workflow-selection reader".
|
||||
|
||||
That understates the work by half. Fixing the selection reader alone would NOT un-inert the sync
|
||||
path for the boards this program cares about.
|
||||
|
||||
BLOCKER 2, proved below: `resolveTaskWorkflowIrSyncImpl` loads a CUSTOM workflow's IR with
|
||||
|
||||
store.db.prepare("SELECT ir FROM workflows WHERE id = ?")
|
||||
|
||||
and `TaskStore.db` is not "SQLite-only" — its implementation (`dbImpl`, task-id-integrity.ts) is an
|
||||
UNCONDITIONAL throw with no mode branch at all. So that read always throws, is always swallowed by
|
||||
the surrounding `catch`, and always returns the default IR.
|
||||
|
||||
Consequence, and it is the one that matters here: a task bound to a BUILT-IN workflow could resolve
|
||||
through the sync path today (that branch never touches `store.db`), but a task bound to a CUSTOM
|
||||
workflow can never resolve, whatever the selection reader returns. Renamed lanes are by definition a
|
||||
custom workflow. So the sync path cannot serve the renamed-board case at all until this read is
|
||||
replaced too.
|
||||
|
||||
BLOCKER 3 is a design constraint rather than a bug, recorded here because it bounds the shape of any
|
||||
fix: Fusion supports multiple nodes running their own engines against ONE shared PostgreSQL
|
||||
(`docs/multi-project.md` → "Shared Postgres multi-node runbook"). A node-local synchronous cache of
|
||||
`task_workflow_selection` therefore goes stale whenever ANOTHER node rewrites a selection, and it
|
||||
would answer with full confidence. That is strictly worse than today's honest default, which is at
|
||||
least uniformly wrong rather than intermittently wrong. Any sync reader needs an invalidation story
|
||||
that survives a writer on a different host.
|
||||
|
||||
This file exists so the next person to attempt the unblock reads all three before starting, instead
|
||||
of shipping a selection cache and discovering the second read at integration time.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
const REPO_ROOT = resolve(__dirname, "../../../..");
|
||||
|
||||
function source(relPath: string): string {
|
||||
return readFileSync(join(REPO_ROOT, relPath), "utf8");
|
||||
}
|
||||
|
||||
describe("the sync workflow-IR path is blocked twice, not once", () => {
|
||||
/*
|
||||
Asserted against `dbImpl`'s SOURCE rather than by calling it, because calling it proves only that
|
||||
one construction path throws. The claim is stronger and is what the fix depends on: there is no
|
||||
mode in which this accessor returns a database, so no selection reader can rescue the read that
|
||||
uses it.
|
||||
*/
|
||||
it("TaskStore.db is an unconditional throw — no SQLite branch survives", () => {
|
||||
const body = source("packages/core/src/task-store/task-id-integrity.ts");
|
||||
const start = body.indexOf("export function dbImpl(");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const fn = body.slice(start, body.indexOf("\n}", start));
|
||||
|
||||
expect(fn).toContain("throw new Error");
|
||||
/* If a mode branch is ever reintroduced, this file's premise changes and it must be re-read. */
|
||||
expect(fn).not.toMatch(/\bif\s*\(/);
|
||||
expect(fn).not.toMatch(/\breturn\b/);
|
||||
});
|
||||
|
||||
it("the sync IR resolver still loads custom workflows through that dead accessor", () => {
|
||||
const body = source("packages/core/src/task-store/workflow-definitions.ts");
|
||||
const start = body.indexOf("export function resolveTaskWorkflowIrSyncImpl(");
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
const fn = body.slice(start, body.indexOf("\n}", start));
|
||||
|
||||
/* The custom-workflow branch. Reaching it guarantees the catch, hence the default IR. */
|
||||
expect(fn).toContain("store.db");
|
||||
expect(fn).toContain("SELECT ir FROM workflows");
|
||||
expect(fn).toContain("catch");
|
||||
});
|
||||
|
||||
/*
|
||||
ANTI-VACUITY. The two assertions above are about source text, so they would keep passing if the
|
||||
sync resolver were deleted or renamed and the whole concern became moot. This pins that the
|
||||
resolver is still exported and still the thing the allow-list guards.
|
||||
*/
|
||||
it("the resolver this file is about is still live", () => {
|
||||
expect(source("packages/core/src/store.ts")).toContain("resolveTaskWorkflowIrSync");
|
||||
expect(source("packages/core/src/__tests__/sync-workflow-ir-callsite-allowlist.test.ts"))
|
||||
.toContain("resolveTaskWorkflowIrSync");
|
||||
});
|
||||
|
||||
/*
|
||||
The multi-node constraint is a documented product fact, not an inference. If that runbook ever goes
|
||||
away, blocker 3 goes with it and a node-local cache becomes viable — so the fix's shape depends on
|
||||
this line still being true.
|
||||
*/
|
||||
it("multiple nodes still share one PostgreSQL, which is what makes a node-local cache unsafe", () => {
|
||||
expect(source("docs/multi-project.md")).toContain("Shared Postgres multi-node runbook");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user