Seven fixes that were sitting on separate handoff branches with no owner while `main` moved. Consolidated, rebased onto current `main`, and verified **together** rather than only per-branch. The individual branches remain if a subset is preferred. This is the same consolidation that got `batch-core` and #2787 adopted. **Close it if it breaks queue policy** — the branch keeps the work safe either way. ## Where these came from #2787's review found an optional parameter whose production caller never passed it. That is a class, so I ran it against everything I had landed and found five more. **All five turned out to have their real defect in the CALLER, not the parameter** — in four of them the parameter was unreachable: | unwired parameter | what was actually wrong | |---|---| | `blocker-fanout.escalationColumns` | the hold default made the count zero — **no bottleneck warning was emitted at all** | | analytics `columnFlagsByName` | routes never built a map — **0 in-progress / 0 in-review beside correct cost totals** | | `isLegacyAutoMergeStampCandidate` | the read **queried a column a renamed board does not have**, so the backfill iterated nothing | | `rankAssignedTasksForWakeDelta` | `getTasksByAssignedAgent`'s `excludeArchived` used the literal — **archived cards returned as open work** | | `duplicate-intake.columnFlagsByColumnId` | intake could **archive or soft-delete a newly created task** as a duplicate of finished work | The heuristic worth keeping: **an optional parameter no production caller fills is a marker pointing at an unexamined caller.** The census cannot see any of these five — every gate is a `Set`/array literal or a query filter, i.e. a definition rather than a comparison. ## Also included - **`executor.ts`** — the stale-spec guard did the exact thing its own comment forbids: on a renamed board it ran on a LIVE task and pulled it out of execution into replan. `activeMergeStatuses` protected merging cards *by accident*, which is why the symptom looked arbitrary. - **`register-project-routes.ts`** — project health reported **0 active tasks**; its list also still contained `triage`, dead since U11. - **`dashboard/app/utils/taskTiming.ts`** — a **second copy** of `getTotalAgentActiveMs`. Core's was converted; the card chip imports this one, so the census counted the site as done while the rendered number stayed keyed on `"in-progress"`. ## Verification Verified as a set: `pnpm test:gate` **161 / 13 / 487 / 71** · core suites **15 passed** · engine **7** · dashboard **12** · four `tsc` targets clean · lint clean · census `--strict` exits 0. Each fix is revert-proven individually; the specific case that fails is named in each test header. ## Two honesty notes **Three guards here are structural, not behavioural, and say so in their headers.** `sanitizeAgentTaskLinks` is a closure inside `createApiRoutes`; the analytics aggregators need a live `AsyncDataLayer`; the stale-spec guard sits deep inside `execute()`. Each ratchet fails on revert — verified — but none is an end-to-end proof, and the headers state which half they cover. **One of my behavioural test sets would have lied.** The intake-dedup cases drive `findSameAgentDuplicates` directly; I removed the wiring to measure the revert and **they stayed green**, because they pin the predicate and not the caller. That is the exact illusion this audit was chasing, reproduced in my own file. The forward now has its own structural check. ## Deliberately not included `worktree-pool.ts:1205` — it **fails safe** (a missed match protects a branch from cleanup rather than deleting it) and sits in the merger's branch-reaping path where the opposite error destroys work. That deserves its owner's judgement, not a drive-by conversion. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
127 lines
5.4 KiB
JavaScript
127 lines
5.4 KiB
JavaScript
/*
|
|
FNXC:WorkflowLifecycleColumns 2026-07-31-16:40:
|
|
Find lane-resolution parameters that NO production caller supplies.
|
|
|
|
WHY THIS EXISTS. The lifecycle-column program repeatedly shipped a conversion shaped like this:
|
|
|
|
export function isSomething(task, reviewColumns?: ReadonlySet<string>) {
|
|
return reviewColumns ? reviewColumns.has(task.column) : task.column === "in-review";
|
|
}
|
|
|
|
…and then never passed `reviewColumns` from the production caller. The census counts the site as
|
|
converted (the literal is behind a documented fallback), every test passes (they inject the value by
|
|
hand), and production keeps the legacy behaviour. Measured: FIVE such parameters were live on `main`
|
|
at once, and auditing them found that in FOUR the parameter was unreachable because the CALLER held a
|
|
larger defect — a query for a column the board does not have, a count that was always zero, a store
|
|
read returning archived rows as open work.
|
|
|
|
Two workers found this class independently (#2787's review and #2799), which is the argument for
|
|
detecting it mechanically instead of by sweep. Unlike the census, the shape IS statically decidable:
|
|
an exported declaration has an optional parameter whose name is lane-shaped, and no file anywhere
|
|
mentions that name as an argument.
|
|
|
|
DELIBERATELY CONSERVATIVE. It only reports a parameter when:
|
|
- the declaration is exported (an internal helper's callers are all in-file and easy to see);
|
|
- the parameter is optional (a required one cannot be silently skipped);
|
|
- the name matches the lane vocabulary this program actually uses;
|
|
- and NO file in the scanned set mentions that name outside the declaring file.
|
|
|
|
The last condition is deliberately loose — a mention is enough. A guard that argues about how a value
|
|
reaches a call site would produce false positives, and a false positive here costs more than a miss:
|
|
it teaches people to disable the check.
|
|
*/
|
|
|
|
import { createRequire } from "node:module";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const ts = require("typescript");
|
|
|
|
/**
|
|
* Parameter names this program uses for a resolved lane answer.
|
|
*
|
|
* A NAME list rather than a type check on purpose: the same fact is spelled `ReadonlySet<string>`,
|
|
* `ColumnRoleFlags`, `boolean` and `(task) => boolean` across the packages, so the type tells you
|
|
* less than the name does. Adding a name here is how a new convention opts into the guard.
|
|
*/
|
|
export const LANE_PARAMETER_NAMES = [
|
|
"activeColumns",
|
|
"columnFlags",
|
|
"columnFlagsByColumnId",
|
|
"columnFlagsByName",
|
|
"completeColumnsByTaskId",
|
|
"escalationColumns",
|
|
"flagsByColumnId",
|
|
"holdColumn",
|
|
"isReviewColumn",
|
|
"isWipColumn",
|
|
"reviewColumns",
|
|
"satisfactionColumnsByTaskId",
|
|
"terminalColumns",
|
|
"terminalColumnsByTaskId",
|
|
];
|
|
|
|
const LANE_PARAMETER_SET = new Set(LANE_PARAMETER_NAMES);
|
|
|
|
function isExported(node) {
|
|
const modifiers = node.modifiers ?? [];
|
|
return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
}
|
|
|
|
/** Declarations whose parameters are worth checking: exported functions and exported interfaces. */
|
|
function collectLaneParameters(filePath, source) {
|
|
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
const found = [];
|
|
|
|
const recordParam = (param, ownerName) => {
|
|
if (!param.name || !ts.isIdentifier(param.name)) return;
|
|
if (!LANE_PARAMETER_SET.has(param.name.text)) return;
|
|
/* Only OPTIONAL parameters can be silently skipped; a required one fails to compile. */
|
|
if (!param.questionToken && !param.initializer) return;
|
|
const { line } = sourceFile.getLineAndCharacterOfPosition(param.getStart(sourceFile));
|
|
found.push({ file: filePath, line: line + 1, parameter: param.name.text, owner: ownerName });
|
|
};
|
|
|
|
const visit = (node) => {
|
|
if (ts.isFunctionDeclaration(node) && isExported(node) && node.name) {
|
|
for (const param of node.parameters) recordParam(param, node.name.text);
|
|
}
|
|
/* An options-object property is the same fact wearing a different shape. */
|
|
if (ts.isInterfaceDeclaration(node) && isExported(node)) {
|
|
for (const member of node.members) {
|
|
if (!ts.isPropertySignature(member) || !member.name || !ts.isIdentifier(member.name)) continue;
|
|
if (!LANE_PARAMETER_SET.has(member.name.text)) continue;
|
|
if (!member.questionToken) continue;
|
|
const { line } = sourceFile.getLineAndCharacterOfPosition(member.getStart(sourceFile));
|
|
found.push({ file: filePath, line: line + 1, parameter: member.name.text, owner: node.name.text });
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
|
|
visit(sourceFile);
|
|
return found;
|
|
}
|
|
|
|
/**
|
|
* @param files absolute paths to scan (production sources; callers exclude tests)
|
|
* @param readFile injected for testability
|
|
* @returns declarations whose lane parameter is mentioned in no other file
|
|
*/
|
|
export function findUnwiredLaneParameters(files, readFile = (f) => readFileSync(f, "utf8")) {
|
|
const sources = new Map();
|
|
for (const file of files) sources.set(file, readFile(file));
|
|
|
|
const declarations = [];
|
|
for (const [file, source] of sources) declarations.push(...collectLaneParameters(file, source));
|
|
|
|
return declarations.filter((declaration) => {
|
|
for (const [file, source] of sources) {
|
|
if (file === declaration.file) continue;
|
|
/* A mention anywhere else counts as wired. Deliberately loose — see the header. */
|
|
if (source.includes(declaration.parameter)) return false;
|
|
}
|
|
return true;
|
|
});
|
|
}
|