fix(test): the archived-gate parity guard could not see an aliased table (red on main) (#2768)
`archived-column-gate-parity` is red on main after #2745 converted three TypeScript sites. Fixing the stale inventory is the small half. **The guard had a hole, and it is the interesting part.** ## The hole The SQL scan required the predicate's receiver to be literally `<x>.tasks`: ```ts ne(schema.project.tasks.column, "archived") // seen const table = schema.project.tasks; ne(table.column, "archived") // INVISIBLE ``` So `branch-group-ops.ts:58` was **never audited**. The inventory claimed six files; seven exist. A parity guard that cannot see one of the encodings reports agreement it never checked — the exact failure mode this file was written to prevent, occurring inside the file itself. ## And that site is not hypothetical `#2745` converted `branch-group-ops`'s **TypeScript** half to the resolved role. Its **SQL** half still compares the raw string. On a board whose archived lane is renamed, the two disagree — one says a task is archived, the other returns it as live. That is the split-brain described in the guard's own failure message, and the guard could not see it. ## The fix Alias bindings (`const <id> = <...>.tasks`) are collected per file in a first pass — first pass because the binding can appear *after* its uses inside nested closures — and accepted as the receiver. `branch-group-ops.ts` joins `AUDITED_SQL_SITES` as **newly visible, not newly written**. Also drops the three TypeScript entries #2745 converted (`blocker-fanout`, `branch-group-ops`, `task-store-helpers`) — that is the red itself. ## Measured, both directions | mutation | result | |---|---| | alias set emptied (the old, alias-blind scan) | **1 failed** — "Drizzle encoding changed" | | product SQL half converted to a resolved lane | **1 failed** — "Drizzle encoding changed" | | as shipped | **2 passed** | The first proves the scanner fix is load-bearing. The second proves the newly-audited site is genuinely *counted*, not merely listed in an inventory. Two earlier mutation attempts produced no output and I discarded them rather than reading them as passes — they had broken the file's syntax, so nothing ran. A mutation that fails to compile proves nothing, and looks identical to a clean run when output is filtered. Gate **726**, core `tsc` clean, lint clean. ## Left for the owner of #2745 Whether `branch-group-ops.ts:58`'s SQL half should now be converted too. The guard's own header explains why the SQL halves cannot simply be converted (`ne(tasks.column, ...)` needs the resolved id as a value, which the call sites do not all have), so this is a real design question rather than a mechanical follow-up — and it is now *visible* and *audited* instead of silently absent.
This commit is contained in:
@@ -59,7 +59,6 @@ const AUDITED_TS_SITES: Readonly<Record<string, number>> = {
|
||||
"packages/core/src/assigned-task-ranking.ts": 1,
|
||||
"packages/core/src/async-mission-store-queries.ts": 2,
|
||||
"packages/core/src/async-mission-store.ts": 2,
|
||||
"packages/core/src/blocker-fanout.ts": 1,
|
||||
"packages/core/src/duplicate-intake.ts": 1,
|
||||
"packages/core/src/eval-signal-collector.ts": 1,
|
||||
"packages/core/src/live-agent-count.ts": 1,
|
||||
@@ -71,12 +70,10 @@ const AUDITED_TS_SITES: Readonly<Record<string, number>> = {
|
||||
"packages/core/src/task-store/async-comments-attachments.ts": 8,
|
||||
"packages/core/src/task-store/audit-ops.ts": 1,
|
||||
"packages/core/src/task-store/branch-and-pr-entities.ts": 1,
|
||||
"packages/core/src/task-store/branch-group-ops.ts": 1,
|
||||
"packages/core/src/task-store/lifecycle-ops.ts": 1,
|
||||
"packages/core/src/task-store/moves.ts": 1,
|
||||
"packages/core/src/task-store/symbol-locks.ts": 1,
|
||||
"packages/core/src/task-store/task-id-integrity.ts": 1,
|
||||
"packages/core/src/task-store/task-store-helpers.ts": 1,
|
||||
"packages/core/src/task-store/update-task-deps.ts": 1,
|
||||
};
|
||||
|
||||
@@ -102,6 +99,15 @@ const AUDITED_SQL_SITES: Readonly<Record<string, number>> = {
|
||||
"packages/core/src/task-store/async-lifecycle.ts": 1,
|
||||
"packages/core/src/task-store/async-search.ts": 1,
|
||||
"packages/core/src/task-store/async-self-healing.ts": 1,
|
||||
/*
|
||||
FNXC:ArchivedGateParity 2026-07-30-16:20:
|
||||
Newly VISIBLE, not newly written. `branch-group-ops.ts:58` binds the table first
|
||||
(`const table = schema.project.tasks`) and the scan previously required a literal `<x>.tasks`
|
||||
receiver, so this predicate was never audited — the inventory claimed six files while seven
|
||||
existed. Its TypeScript half was converted by #2745; this SQL half still compares the raw string,
|
||||
which is precisely the split-brain this file exists to catch and could not see.
|
||||
*/
|
||||
"packages/core/src/task-store/branch-group-ops.ts": 1,
|
||||
"packages/core/src/task-store/branch-and-pr-entities.ts": 2,
|
||||
"packages/core/src/task-store/task-mutation-ops.ts": 1,
|
||||
};
|
||||
@@ -192,6 +198,25 @@ describe("the archived-state gate is enforced in TypeScript AND in SQL", () => {
|
||||
const lineOf = (node: import("typescript").Node): number =>
|
||||
sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
||||
|
||||
/*
|
||||
FNXC:ArchivedGateParity 2026-07-30-16:20:
|
||||
Locals bound to the tasks table — `const table = schema.project.tasks` — so an aliased Drizzle
|
||||
predicate is not invisible to the SQL scan below. Collected in a first pass because the binding
|
||||
can appear after its uses inside nested closures.
|
||||
*/
|
||||
const tasksAliases = new Set<string>();
|
||||
const collectAliases = (node: import("typescript").Node): void => {
|
||||
if (ts.isVariableDeclaration(node)
|
||||
&& ts.isIdentifier(node.name)
|
||||
&& node.initializer
|
||||
&& ts.isPropertyAccessExpression(node.initializer)
|
||||
&& node.initializer.name.text === "tasks") {
|
||||
tasksAliases.add(node.name.text);
|
||||
}
|
||||
ts.forEachChild(node, collectAliases);
|
||||
};
|
||||
collectAliases(sf);
|
||||
|
||||
const visit = (node: import("typescript").Node): void => {
|
||||
/*
|
||||
TS half: `<something>.column === "archived"` (or `!==`). Keyed on the PROPERTY being named
|
||||
@@ -238,16 +263,30 @@ describe("the archived-state gate is enforced in TypeScript AND in SQL", () => {
|
||||
SQL half: `eq(<...>.tasks.column, "archived")` / `ne(...)`. Drizzle builds the predicate as a
|
||||
call, so this is a CallExpression whose first argument is the tasks.column Column object and
|
||||
whose second is the literal.
|
||||
|
||||
FNXC:ArchivedGateParity 2026-07-30-16:20:
|
||||
ALIASED TABLES COUNT TOO. This required the receiver to be literally `<x>.tasks`, so the very
|
||||
common Drizzle shape
|
||||
|
||||
const table = schema.project.tasks;
|
||||
ne(table.column, "archived")
|
||||
|
||||
was INVISIBLE to this scan — `branch-group-ops.ts:58` sat unaudited while the inventory
|
||||
claimed six files. A parity guard that cannot see one of the encodings reports agreement it
|
||||
never checked, which is the failure mode this whole file exists to prevent. Alias bindings are
|
||||
now collected per file and accepted as the receiver.
|
||||
*/
|
||||
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
|
||||
const fn = node.expression.text;
|
||||
if ((fn === "eq" || fn === "ne") && node.arguments.length === 2) {
|
||||
const [columnArg, valueArg] = node.arguments;
|
||||
const receiverIsTasks = (expr: ts.Expression): boolean =>
|
||||
(ts.isPropertyAccessExpression(expr) && expr.name.text === "tasks")
|
||||
|| (ts.isIdentifier(expr) && tasksAliases.has(expr.text));
|
||||
const isTasksColumn = columnArg
|
||||
&& ts.isPropertyAccessExpression(columnArg)
|
||||
&& columnArg.name.text === "column"
|
||||
&& ts.isPropertyAccessExpression(columnArg.expression)
|
||||
&& columnArg.expression.name.text === "tasks";
|
||||
&& receiverIsTasks(columnArg.expression);
|
||||
if (isTasksColumn && valueArg && ts.isStringLiteral(valueArg) && valueArg.text === "archived") {
|
||||
sqlSites.push({ file, line: lineOf(node) });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user