From 24116997561ad330dd2f6bb63fa28db1e73a742c Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 30 Jul 2026 23:08:08 -0700 Subject: [PATCH] fix(gate): passing `undefined` for the lane answer read as supplying it (#2981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) --- .../__tests__/check-inert-flag-seams.test.mjs | 49 ++++++++++++++++++- scripts/check-inert-flag-seams.mjs | 35 ++++++++++++- 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/scripts/__tests__/check-inert-flag-seams.test.mjs b/scripts/__tests__/check-inert-flag-seams.test.mjs index 4f69700581..238e63355f 100644 --- a/scripts/__tests__/check-inert-flag-seams.test.mjs +++ b/scripts/__tests__/check-inert-flag-seams.test.mjs @@ -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); +}); diff --git a/scripts/check-inert-flag-seams.mjs b/scripts/check-inert-flag-seams.mjs index 0c55c512c7..d50e5411af 100644 --- a/scripts/check-inert-flag-seams.mjs +++ b/scripts/check-inert-flag-seams.mjs @@ -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),