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:
7
.changeset/lane-census-conditional-shapes.md
Normal file
7
.changeset/lane-census-conditional-shapes.md
Normal 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.
|
||||||
@@ -56,6 +56,12 @@ function callArgs(source) {
|
|||||||
return found;
|
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", () => {
|
test("a property spelled `undefined` supplies nothing", () => {
|
||||||
assert.equal(suppliesAValue(firstObjectProperty("f({ reviewColumns: undefined });")), false);
|
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), []);
|
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);
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,18 +2,16 @@
|
|||||||
"counts": {
|
"counts": {
|
||||||
"packages/core/src/task-merge.ts": 1,
|
"packages/core/src/task-merge.ts": 1,
|
||||||
"packages/core/src/task-store/branch-and-pr-entities.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/core/src/task-store/task-update.ts": 1,
|
||||||
"packages/engine/src/auto-merge-finalization.ts": 1,
|
"packages/engine/src/auto-merge-finalization.ts": 1,
|
||||||
"packages/engine/src/project-engine.ts": 1,
|
"packages/engine/src/project-engine.ts": 1,
|
||||||
"packages/engine/src/runtimes/in-process-runtime.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/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/Lane.tsx": 1,
|
||||||
"packages/dashboard/app/components/ListView.tsx": 1,
|
"packages/dashboard/app/components/ListView.tsx": 1,
|
||||||
"packages/dashboard/app/components/ModelSelectorTab.tsx": 2,
|
"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/dashboard/app/hooks/useBlockerFanout.ts": 1,
|
||||||
"packages/cli/src/commands/dashboard-tui/app.tsx": 1,
|
"packages/cli/src/commands/dashboard-tui/app.tsx": 1,
|
||||||
"packages/cli/src/commands/dashboard-tui/bucket-mapping.ts": 1,
|
"packages/cli/src/commands/dashboard-tui/bucket-mapping.ts": 1,
|
||||||
|
|||||||
@@ -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
|
/* 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`. */
|
family's split-role fix — without it the gate cannot see a dropped `holdColumns`. */
|
||||||
"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) {
|
function parse(file) {
|
||||||
@@ -179,7 +188,30 @@ export function findLaneAcceptingFunctions(files) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (namesByIndex.size > 0 || positions.size > 0) {
|
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;
|
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:
|
FNXC:LaneWiring 2026-07-30-23:40:
|
||||||
`undefined` IS NOT AN ANSWER, in either position this census checks.
|
`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 passesOption = node.arguments.some((arg, index) => {
|
||||||
const wanted = accepted.namesByIndex.get(index);
|
const wanted = accepted.namesByIndex.get(index);
|
||||||
if (wanted === undefined) return false;
|
if (wanted === undefined) return false;
|
||||||
const bag = unwrapObjectLiteral(arg);
|
return objectLiteralsFrom(arg).some((bag) => suppliesLane(bag, wanted));
|
||||||
return bag !== null
|
|
||||||
&& bag.properties.some(
|
|
||||||
(p) => p.name && ts.isIdentifier(p.name) && wanted.has(p.name.text) && suppliesAValue(p),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
const passesPositional = [...accepted.positions].some(
|
const passesPositional = [...accepted.positions].some(
|
||||||
(index) => effectiveArgCount(node.arguments) > index,
|
(index) => effectiveArgCount(node.arguments) > index,
|
||||||
|
|||||||
Reference in New Issue
Block a user