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:
gsxdsm
2026-07-30 08:48:21 -07:00
committed by GitHub
parent 9b61d795c9
commit fdd958efc9

View File

@@ -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) });
}