fix(gate): passing undefined for the lane answer read as supplying it (#2981)

## What

Continuing the #2979 discipline — probe a ratchet with shapes its author
did *not* have in mind — applied to my own inert-seam gate. Four probes,
three got through. Two turned out to be the sibling gate's job. This is
the one that's nobody's:

```ts
resolveSomething("KB-1", undefined)
```

The seam is a trailing optional parameter, so the gate asked how many
**arguments** a call site passes. Spelling the omission out satisfies
that count while the callee receives exactly what it received before:
nothing. The parameter is still inert, the board still reads the legacy
vocabulary — the gate just stops saying so.

Not an exotic spelling. It's what a partial wiring-up produces when
flags are threaded through an intermediate that has none to pass, and
what a mechanical positional edit produces when it fills argument slots.

## Missed by both gates — checked before touching anything

`check-lane-wiring` (#2966) covers the default-valued and options-object
shapes this gate is structurally blind to. I probed it first, and it
caught **both**, so the two remain genuinely complementary rather than
overlapping. But it counts arguments the same way here, so this shape
was uncovered by either.

| probe | inert-seam (before) | lane-wiring |
|---|---|---|
| omitted entirely | caught | — |
| default-valued param | missed | **caught** |
| options-object flags | missed | **caught** |
| explicit `undefined` | missed | **missed** ← this PR |

## The trim is trailing-only

A **middle** `undefined` still positions the arguments after it, so
those are real answers. That's the case that keeps the trim honest, and
it's pinned as a test.

## Measured

| check | result |
|---|---|
| clean `main` | exit 0, unchanged |
| now caught | explicit `undefined` · `void 0` · several trailing
undefineds |
| correctly **not** flagged | a real trailing value · a middle
`undefined` with a real value after it |
| gate's own suite | **12 → 18 tests**, all green |
| reverting to the raw argument count | fails **exactly** the 3
positives; the negatives hold |

## One note on the fourth gate

`check-fnxc-future-dates` went red on this branch — on my own comments.
I'd stamped them `2026-07-31`, which is tomorrow. Fixed by correcting
the stamps to today, not by re-recording the baseline; the baseline
already tolerates some pre-existing future stamps and adding mine to it
would have been appeasement. Worth noting that the gate earned its keep
against the person who has been writing the other gates.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-30 23:08:08 -07:00
committed by GitHub
parent 968af0822c
commit 2411699756
2 changed files with 82 additions and 2 deletions

View File

@@ -26,7 +26,7 @@ import assert from "node:assert/strict";
import ts from "typescript";
import { collectImportBindings, isRelevantCallSite } from "../check-inert-flag-seams.mjs";
import { collectImportBindings, isRelevantCallSite, effectiveArgCount } from "../check-inert-flag-seams.mjs";
const DECLARING = "packages/core/src/near-duplicate-canonical.ts";
@@ -160,3 +160,50 @@ test("a plain import records no alias", () => {
assert.equal(localAlias.size, 0);
assert.equal(importedFrom.get("enqueueMergeQueue"), "@fusion/core");
});
/*
FNXC:LifecycleColumnCensus 2026-07-30-23:55:
`undefined` IS NOT AN ANSWER, and counting raw arguments treated it as one.
`f("KB-1", undefined)` passes the arity check while the callee receives exactly what it received
before. The seam stays inert and the board keeps reading the legacy vocabulary — the gate just stops
saying so. This is what a partial wiring-up produces when the flags are threaded through an
intermediate that has none, and what a mechanical positional edit produces.
The negatives are the half that keeps this honest: a MIDDLE undefined still positions the real
argument after it, so trimming must stop at the first non-undefined from the right.
*/
function argsOf(source) {
const sf = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
let found = null;
const visit = (node) => {
if (!found && ts.isCallExpression(node)) found = node.arguments;
ts.forEachChild(node, visit);
};
visit(sf);
return found;
}
test("a trailing `undefined` argument supplies nothing", () => {
assert.equal(effectiveArgCount(argsOf('f("KB-1", undefined);')), 1);
});
test("`void 0` is the same omission spelled differently", () => {
assert.equal(effectiveArgCount(argsOf('f("KB-1", void 0);')), 1);
});
test("several trailing undefineds all collapse", () => {
assert.equal(effectiveArgCount(argsOf('f("KB-1", undefined, undefined);')), 1);
});
test("a real trailing argument still counts", () => {
assert.equal(effectiveArgCount(argsOf('f("KB-1", flags);')), 2);
});
test("a MIDDLE undefined positions the argument after it, which is real", () => {
assert.equal(effectiveArgCount(argsOf('f("KB-1", undefined, flags);')), 3);
});
test("a call with no arguments is unchanged", () => {
assert.equal(effectiveArgCount(argsOf("f();")), 0);
});

View File

@@ -39,6 +39,39 @@ const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const PACKAGES = join(REPO, "packages");
const SKIP_DIRS = new Set(["node_modules", "dist", "__tests__", "__mocks__", "e2e", ".gate-bundle", "coverage"]);
const TRAILING_FLAG_PARAM = /([Cc]olumnFlags|[Ll]ifecycleColumns|[Rr]eviewColumns|[Tt]erminalColumns|[Pp]lannerLanes)$/;
/*
FNXC:LifecycleColumnCensus 2026-07-30-23:40:
PASSING `undefined` FOR THE LANE ANSWER IS NOT SUPPLYING IT.
The seam is a trailing optional parameter, so this gate asked how many ARGUMENTS each call site
passes. A call that spells the omission out —
resolveSomething("KB-1", undefined)
— satisfies that count while the callee receives exactly what it received before: nothing. The
parameter is still inert and the board still reads the legacy vocabulary.
That spelling is not exotic. It is what a partial wiring-up produces when the flags are threaded
through an intermediate that has none to pass, and what a mechanical edit produces when it fills
argument slots positionally. Either way the gate reported the seam as answered.
Missed by BOTH gates: check-lane-wiring (#2966) covers the default-valued and options-object shapes
this one is structurally blind to, but it counts arguments the same way here. Found by probing my own
gate with shapes I had not designed it for — the discipline argued for in #2979.
TRAILING ONLY. A middle `undefined` still positions the arguments after it, so those are real answers.
*/
const isUndefinedArgument = (arg) =>
(ts.isIdentifier(arg) && arg.text === "undefined") || ts.isVoidExpression(arg);
/** Arguments that actually carry a value, ignoring trailing `undefined` / `void 0` placeholders. */
export function effectiveArgCount(args) {
let count = args.length;
while (count > 0 && isUndefinedArgument(args[count - 1])) count -= 1;
return count;
}
/** Unanchored twin used only to skip files fast; see the note at the call site. */
const PREFILTER = /(olumnFlags|ifecycleColumns|eviewColumns|erminalColumns|lannerLanes)/;
@@ -220,7 +253,7 @@ for (const file of walkAll(PACKAGES)) {
if (!callSites.has(target)) callSites.set(target, []);
callSites.get(target).push({
file: relative(REPO, file),
args: node.arguments.length,
args: effectiveArgCount(node.arguments),
shadowed: locallyDeclared.has(callee),
from: importedFrom.get(callee),
viaProperty: ts.isPropertyAccessExpression(node.expression),