fix(gate): the inert-seam ratchet never scanned plugins/, where real lane logic lives (#3002)

#3000 showed this gate never scanned `scripts/`. I went auditing my own
instrument after that, and the roots have a **second** hole: the walk
was rooted at `packages/` alone, so every lane parameter a plugin
declares or calls sat outside the ratchet entirely.

## Measured with a control

| probe | before | after |
|---|---|---|
| unwired seam under `packages/` | caught | caught |
| **identical** seam under `plugins/` | **missed** | caught |
| clean tree | exit 0 | exit 0 |

## The sibling gate already knew

`check-lane-wiring` lists `plugins` in its roots, and its header records
the incident that put it there: an unwired `completeColumnsByTaskId` sat
on `main` unreported because the glasses plugin wasn't scanned.

This gate re-opened the same hole rather than inheriting the lesson. Two
scope holes in one instrument is the actual finding — **the roots
deserve the same scrutiny as the matcher, and until now they had none.**
Every blind spot found in this gate so far has been in the matcher;
nobody, me included, thought to probe what it walks.

## Newly visible — audited, not blind-baselined

`plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx` calls
`isTaskStuck()` without the resolved flags, while **six of the seven**
other call sites supply them. That's exactly the partial-supply shape
this gate exists to catch, hidden purely by scope.

It's exempted rather than wired, and the reason is the interesting part.
The plugin has **no lane-trait source anywhere**: it's mounted as a
dashboard view through `PluginDashboardViewContext`, and
`DependencyGraph` receives `tasks: Task[]` and nothing else. Passing the
argument here would pass `undefined` — an unsupplied optional parameter,
which the learnings doc's first failure shape calls strictly worse than
the literal it replaces, because it reads as converted and answers
legacy forever.

Correct supply needs the plugin **view context** to carry per-task
flags: a published-API change. That's the same "needs a data change"
category as the existing `TaskDetailModal` entry, not the "awkward means
wire it" case the exemption rule refuses. I checked that distinction
against my own rule before taking the exemption, because the rule exists
to stop exactly this kind of convenient reading.

Filed for the plugin-API owner rather than bodged here.

## Measured

- seam population **22 → 23** with plugins in scope
- gate's own suite: **18/18 green**
- lint and the FNXC gate green

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-07-31 00:08:21 -07:00
committed by GitHub
parent 72f5f8e51a
commit d861923355

View File

@@ -36,7 +36,25 @@ import { fileURLToPath } from "node:url";
import ts from "typescript";
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const PACKAGES = join(REPO, "packages");
/*
FNXC:InertFlagSeams 2026-07-30-23:30:
`plugins/` IS SCANNED, because plugins hold real lane logic and this gate could not see any of it.
The walk was rooted at `packages/` alone. Measured with a control: the identical unwired seam is
caught under `packages/` and MISSED under `plugins/` — so every lane parameter a plugin declares or
calls was outside the ratchet entirely.
Not hypothetical. `plugins/fusion-plugin-even-realities-glasses` resolves workflow IRs and filters by
column in `src/cards.ts` and `src/routes/board-routes.ts`. The sibling `check-lane-wiring` already
lists `plugins` in its roots, and its header records the incident that put it there: an unwired
`completeColumnsByTaskId` sat on `main` unreported because that plugin was not scanned. This gate
re-opened the same hole rather than learning from it.
Found while auditing my own gates after #3000 showed this one never scanned `scripts/` either. Two
scope holes in one instrument is a pattern: the roots deserve the same scrutiny as the matcher, and
they had none.
*/
const SCAN_ROOTS = [join(REPO, "packages"), join(REPO, "plugins")];
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)$/;
@@ -121,6 +139,17 @@ An omission earns an entry only when supplying the argument would be WRONG, not
"Awkward" means wire it; the check exists to make that the cheaper path.
*/
const ALLOWED_OMISSIONS = new Map([
[
"plugins/fusion-plugin-dependency-graph/src/GraphTaskNode.tsx::isTaskStuck",
{ count: 1, reason: "Surfaced the moment this gate started scanning `plugins/` (2026-07-31). The plugin has NO "
+ "lane-trait source anywhere: it is mounted as a dashboard view through `PluginDashboardViewContext`, "
+ "and `DependencyGraph` receives `tasks: Task[]` and nothing else. Supplying the argument here would "
+ "pass `undefined` — an unsupplied optional parameter, which this document's first failure shape says "
+ "is strictly worse than the literal it replaces, because it reads as converted and answers legacy "
+ "forever. Correct supply needs the plugin VIEW CONTEXT to carry per-task flags: a published-API "
+ "change, not local wiring, which is the same 'needs a data change' category as the entry below. "
+ "Filed for the plugin-API owner rather than bodged here." },
],
[
"packages/dashboard/app/components/TaskDetailModal.tsx::isNearDuplicateCanonicalInactive",
{ count: 1, reason: "The flags in scope describe the MODAL'S task; the canonical is a different task on a column this "
@@ -177,7 +206,7 @@ missing suppliers in ListView and Column — review did.
Declarations still use the prefilter: a file DECLARING a flags parameter necessarily contains the
name, so that half is safe and keeps the scan quick.
*/
for (const file of walkAll(PACKAGES)) {
for (const file of SCAN_ROOTS.flatMap((root) => [...walkAll(root)])) {
const source = readFileSync(file, "utf8");
const fileIsTest = isTestFile(relative(REPO, file).split("\\").join("/"));
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);