fix(gate): the lane-wiring census could not see three of the shapes it asks for (incl. a false positive it started reporting) (#3013)

**The gate could not see three of the shapes it exists to check** —
including the wiring I landed this week in #2990 and #3004. Found by
using it: the baseline still listed `useBlockerFanout.ts`,
`ExecutorStatusBar.tsx`, and `TaskDetailModal.tsx` as unwired *after*
those PRs merged.

### 1. Conditional shapes

Passing lanes only when they resolved is the **correct** way to write
these — an empty trait index means "not loaded yet", not "nothing is
terminal", so the caller must fall through to the documented legacy
default rather than fabricate one. Both idioms that produces were
invisible:

```ts
computeBlockerFanoutMap(tasks, flags ? { columnFlagsByTaskId: flags } : {})   // ConditionalExpression
computeBlockerFanoutMapCore(tasks, N, { ...(flags ? { classify } : {}) })     // SpreadAssignment, name === undefined
```

Either branch supplying the lane now counts. **Neither branch supplying
it is still unwired** — that negative is tested.

### 2. Vocabulary

`columnFlagsByTaskId`, the per-task trait index the dashboard threads,
was never added. It answers every lane question at once, so a call site
dropping it reverts to the legacy vocabulary wholesale — and the gate
would have stayed silent.

### 3. Name collisions — the false positive

Declarations were `set` by name, so the **last one parsed won**. Core's
`computeBlockerFanoutMap(tasks, n, opts)` and the dashboard wrapper
`computeBlockerFanoutMap(tasks, opts)` put their lane options at
**different argument indices**, so core's callers were checked against
the wrapper's signature: `task-priority.ts:141` passes `terminalColumns`
at index 2 and was reported unwired.

I caught this because adding the vocabulary entry in (2) made it appear.
A ratchet that reports a correctly-wired site is worse than one that
misses it — the first person to open one learns the number is noise.
Both shapes are now merged; a call satisfying either counts.

### Measured

| | |
|---|---|
| new tests | **6** — every positive paired with its negative |
| against the old census | **3 of 6 fail** — exactly the three
positives; the negatives pass either way, which is why they exist |
| baseline | **23 → 19** — four sites recognized as *already* wired; no
site newly excused |
| new flags | none |

### Verification

`node --test scripts/__tests__/check-lane-wiring.test.mjs` **16 passed**
· `pnpm test:gate` 161 + 13 + 487 + 71 · lint · lifecycle census
`--strict` · fnxc-dates (TZ=UTC) · changesets — green.

Seven other `scripts/__tests__` files fail on main independently of this
change (`verify-fast`, `dependency-security-floor`,
`engine-vitest-gate-policy`, `plugin-authoring-docs`,
`release-prompt-gate`, `ci-test-shard-timings`,
`workflow-reliability-release-check`). None are in the merge gate and
none are touched here — noting them because I looked, not because this
PR affects them.
This commit is contained in:
gsxdsm
2026-07-31 00:33:13 -07:00
committed by GitHub
parent 86c5a89169
commit ffe9898710
4 changed files with 151 additions and 10 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Internal gate fix; no user-visible change.
category: internal
dev: The lane-wiring census now sees lane arguments passed via a ternary or a conditional spread, adds `columnFlagsByTaskId` to its vocabulary, and merges same-named declarations instead of letting the last one clobber the rest.

View File

@@ -56,6 +56,12 @@ function callArgs(source) {
return found;
}
/** Run the whole census over one throwaway source and return its unwired sites. */
function findUnwiredCallSitesIn(source) {
const files = fixture(source);
return findUnwiredCallSites(files, findLaneAcceptingFunctions(files));
}
test("a property spelled `undefined` supplies nothing", () => {
assert.equal(suppliesAValue(firstObjectProperty("f({ reviewColumns: undefined });")), false);
});
@@ -121,3 +127,61 @@ test("the census does NOT report the same call once the property carries a value
assert.deepEqual(unwired.map((u) => u.fn), []);
});
/*
FNXC:LaneWiring 2026-07-31-01:15:
CONDITIONAL shapes and NAME COLLISIONS — the third and fourth ways a wired call site stayed invisible.
Both were found the same way the earlier holes were: by using the gate and noticing it disagreed with
what the code plainly said. Three call sites wired in #2990/#3004 still counted as unwired, and a
correctly-wired call in task-priority.ts started being reported as unwired the moment a second
same-named function entered the vocabulary.
Every positive below is paired with its NEGATIVE. A shape the census merely stops flagging is not
progress — the question is whether deleting the argument still fails.
*/
const LANE_FN = "export function f(t: unknown, o?: { reviewColumns?: ReadonlySet<string> }) { return [t, o]; }";
test("a lane passed only in a ternary's true branch counts as wired", () => {
const unwired = findUnwiredCallSitesIn(`${LANE_FN}\nf(1, cond ? { reviewColumns: r } : {});`);
assert.equal(unwired.length, 0);
});
test("...and a ternary supplying it in NEITHER branch is still unwired", () => {
const unwired = findUnwiredCallSitesIn(`${LANE_FN}\nf(1, cond ? { nowMs: 1 } : {});`);
assert.equal(unwired.length, 1);
});
test("a lane passed through a conditional spread counts as wired", () => {
const unwired = findUnwiredCallSitesIn(`${LANE_FN}\nf(1, { ...(cond ? { reviewColumns: r } : {}) });`);
assert.equal(unwired.length, 0);
});
test("...and a conditional spread carrying no lane is still unwired", () => {
const unwired = findUnwiredCallSitesIn(`${LANE_FN}\nf(1, { ...(cond ? { nowMs: 1 } : {}) });`);
assert.equal(unwired.length, 1);
});
test("two same-named lane functions MERGE rather than clobber", () => {
/*
The real pair: core's `computeBlockerFanoutMap(tasks, n, opts)` and the dashboard wrapper
`computeBlockerFanoutMap(tasks, opts)`. Their lane options sit at different indices, so the last
declaration parsed used to replace the first and every caller of the other shape was misreported.
*/
const source = [
"export function g(t: unknown, n: number, o?: { reviewColumns?: ReadonlySet<string> }) { return [t, n, o]; }",
"export function g(t: unknown, o?: { columnFlagsByTaskId?: ReadonlyMap<string, unknown> }) { return [t, o]; }",
"g(1, 2, { reviewColumns: r });",
"g(1, { columnFlagsByTaskId: m });",
].join("\n");
assert.equal(findUnwiredCallSitesIn(source).length, 0);
});
test("...and the merge does not excuse a call passing neither shape", () => {
const source = [
"export function g(t: unknown, n: number, o?: { reviewColumns?: ReadonlySet<string> }) { return [t, n, o]; }",
"export function g(t: unknown, o?: { columnFlagsByTaskId?: ReadonlyMap<string, unknown> }) { return [t, o]; }",
"g(1, 2, { nowMs: 1 });",
].join("\n");
assert.equal(findUnwiredCallSitesIn(source).length, 1);
});

View File

@@ -2,18 +2,16 @@
"counts": {
"packages/core/src/task-merge.ts": 1,
"packages/core/src/task-store/branch-and-pr-entities.ts": 1,
"packages/core/src/task-store/moves.ts": 2,
"packages/core/src/task-store/moves.ts": 1,
"packages/core/src/task-store/task-update.ts": 1,
"packages/engine/src/auto-merge-finalization.ts": 1,
"packages/engine/src/project-engine.ts": 1,
"packages/engine/src/runtimes/in-process-runtime.ts": 1,
"packages/engine/src/scheduler.ts": 1,
"packages/dashboard/src/routes/register-task-workflow-routes.ts": 1,
"packages/dashboard/app/components/ExecutorStatusBar.tsx": 1,
"packages/dashboard/app/components/Lane.tsx": 1,
"packages/dashboard/app/components/ListView.tsx": 1,
"packages/dashboard/app/components/ModelSelectorTab.tsx": 2,
"packages/dashboard/app/components/TaskDetailModal.tsx": 2,
"packages/dashboard/app/components/TaskDetailModal.tsx": 1,
"packages/dashboard/app/hooks/useBlockerFanout.ts": 1,
"packages/cli/src/commands/dashboard-tui/app.tsx": 1,
"packages/cli/src/commands/dashboard-tui/bucket-mapping.ts": 1,

View File

@@ -45,6 +45,15 @@ export const LANE_ARGUMENT_NAMES = new Set([
/* FNXC:WorkflowLifecycleColumns 2026-07-30-22:00: the MEMBERSHIP form, added with the surfacing
family's split-role fix — without it the gate cannot see a dropped `holdColumns`. */
"holdColumns",
/*
FNXC:LaneWiring 2026-07-31-00:50:
The per-TASK trait index, the shape the dashboard threads (`columnFlagsByTaskId`). It is how three
dashboard surfaces answer every lane question at once, so a call site that drops it goes back to
the legacy vocabulary wholesale — exactly what this census exists to notice. It was missing, so all
three of those newly-wired sites still counted as unwired and the gate could not have caught a
regression at any of them.
*/
"columnFlagsByTaskId",
]);
function parse(file) {
@@ -179,7 +188,30 @@ export function findLaneAcceptingFunctions(files) {
}
});
if (namesByIndex.size > 0 || positions.size > 0) {
accepting.set(node.name.text, { namesByIndex, positions });
/*
FNXC:LaneWiring 2026-07-31-01:05:
MERGE on a name collision; a plain `set` let the last declaration win.
Two different functions legitimately share a name here — core's `computeBlockerFanoutMap` and
the dashboard wrapper that calls it — and their lane options sit at DIFFERENT argument
indices (2 and 1). Overwriting meant core's callers were checked against the wrapper's
signature, so a call passing `terminalColumns` at index 2 was reported unwired. That is the
false positive that makes a ratchet's number noise, and the first person to open one of these
sites learns not to trust it.
The census keys by name because it has no type resolution, so a collision cannot be told
apart from an overload — union both, and a call satisfying EITHER shape counts.
*/
const existing = accepting.get(node.name.text);
if (existing) {
for (const [index, names] of namesByIndex) {
if (!existing.namesByIndex.has(index)) existing.namesByIndex.set(index, new Set());
for (const name of names) existing.namesByIndex.get(index).add(name);
}
for (const position of positions) existing.positions.add(position);
} else {
accepting.set(node.name.text, { namesByIndex, positions });
}
}
});
}
@@ -214,6 +246,50 @@ function unwrapObjectLiteral(node) {
return ts.isObjectLiteralExpression(current) ? current : null;
}
/*
FNXC:LaneWiring 2026-07-31-00:55:
CONDITIONAL shapes, the third way a wired call site stayed invisible.
Passing lanes only when they resolved is the CORRECT way to write these — an empty trait index means
"not loaded yet", not "nothing is terminal", so the caller must fall through to the documented legacy
default rather than fabricate one. Both idioms it produces were invisible:
computeBlockerFanoutMap(tasks, flags ? { columnFlagsByTaskId: flags } : {}) // ternary argument
computeBlockerFanoutMapCore(tasks, N, { ...(flags ? { classify } : {}) }) // conditional spread
The first is a ConditionalExpression, not an object literal. The second is a SpreadAssignment, whose
`p.name` is undefined. So the census scored three call sites I had just wired as unwired — and would
equally have stayed silent had someone deleted the argument.
Answer YES if EITHER branch supplies the lane. A branch that omits it is the deliberate
fall-through-to-legacy path, not an unwired call; a call site that supplies it in neither branch is
still counted.
*/
function objectLiteralsFrom(node) {
let current = node;
while (
ts.isSatisfiesExpression(current)
|| ts.isAsExpression(current)
|| ts.isParenthesizedExpression(current)
) {
current = current.expression;
}
if (ts.isConditionalExpression(current)) {
return [...objectLiteralsFrom(current.whenTrue), ...objectLiteralsFrom(current.whenFalse)];
}
return ts.isObjectLiteralExpression(current) ? [current] : [];
}
/** Does this object literal supply one of `wanted`, looking through conditional spreads? */
function suppliesLane(bag, wanted) {
return bag.properties.some((p) => {
if (ts.isSpreadAssignment(p)) {
return objectLiteralsFrom(p.expression).some((inner) => suppliesLane(inner, wanted));
}
return p.name && ts.isIdentifier(p.name) && wanted.has(p.name.text) && suppliesAValue(p);
});
}
/*
FNXC:LaneWiring 2026-07-30-23:40:
`undefined` IS NOT AN ANSWER, in either position this census checks.
@@ -265,11 +341,7 @@ export function findUnwiredCallSites(files, accepting) {
const passesOption = node.arguments.some((arg, index) => {
const wanted = accepted.namesByIndex.get(index);
if (wanted === undefined) return false;
const bag = unwrapObjectLiteral(arg);
return bag !== null
&& bag.properties.some(
(p) => p.name && ts.isIdentifier(p.name) && wanted.has(p.name.text) && suppliesAValue(p),
);
return objectLiteralsFrom(arg).some((bag) => suppliesLane(bag, wanted));
});
const passesPositional = [...accepted.positions].some(
(index) => effectiveArgCount(node.arguments) > index,