gate: the SQL column-literal ratchet never scanned scripts/, where the raw SQL actually is (#3000)

## The gate could not see the one place raw SQL is actually written by
hand

`check-sql-column-literals` walked `packages/` only and took `.tsx?`.
Every operator script is a repo-root `.mjs`.

I found it by removing a raw-SQL lane literal in #2999 and watching this
gate report:

```
[check-sql-column-literals] 22 known SQL column literal(s), none added.
```

Unchanged, and green. Its own header promises the opposite — *"a LOWER
count fails too so the baseline is ratcheted down"* — so the silence was
the tell.

**Two changes, and either alone still sees nothing:** the root and the
extension. Adding one without the other scans nothing new and reports a
reassuring zero — the same trap #2978 hit when widening the lane-wiring
census.

## Newly visible: 6 sites, audited not blind-baselined

| site | verdict |
| --- | --- |
| `audit-branch-cross-contamination.mjs:182` — `"column" IN
('triage','todo','in-progress','in-review')` | **real** — the
contamination audit scans only the legacy active lanes, so on a renamed
board it scans nothing and reports no contamination. Read-only, and it
does print its `scannedColumns`, which is the one thing keeping that
from being fully silent. |
| `reconcile-leaked-soft-deletes.mjs:53, :73` | already fixed by
**#2999** — the PR that exposed this gap |

## Proven able to fail, not just to count

A guard that has only ever printed a number is a number. A temporary
`.mjs` holding one forbidden comparison:

```
scripts/zz-probe-tmp.mjs: 1 SQL column literal(s), baseline allows 0
```

and the gate returned to green once removed.

## One claim I withdrew

I initially wrote that the `ScriptKind` move to `JS` for `.mjs` was
needed because *"TSX treats `<` as JSX and would misparse an ordinary
comparison"*. I could not demonstrate it. I tried three JSX-ambiguous
shapes — `x <div> y`, `f<b, c>(d)`, and a literal sandwiched between `<`
and `>` comparisons — and TSX recovered from all three with counts
identical to JS.

So `JS` is used because it is the correct kind for the file, **not**
because a miss was observed, and the code now says exactly that. The
opposite claim would have been easy to make and wrong, and this gate's
whole value is that its statements about its own coverage are true.

## Merge order

**#2999 removes both literals in `reconcile-leaked-soft-deletes.mjs`.**
Landing it *after* this PR drops the count, and this gate fails on
DECREASE (by design), needing a re-record. Merge #2999 first, or say the
word and I will re-record here.

Note the widening is self-protecting afterwards: if someone narrows the
walk back to `packages/`, the recorded `scripts/` entries vanish from
the scan and the gate goes red on decrease.

## Verification (measured)

- gate — green, **28 known / none added** (was 22 across `packages/`
only)
- its own suite — **32 passed**
- `eslint` — clean
- `lifecycle-column-census --strict`, `check-lane-wiring`,
`check-fnxc-future-dates` — green

Gate/tooling only; no product file touched.
This commit is contained in:
gsxdsm
2026-07-31 00:13:55 -07:00
committed by GitHub
parent 1de0141ab8
commit cd237ae760
2 changed files with 31 additions and 4 deletions

View File

@@ -37,6 +37,30 @@ import ts from "typescript";
const REPO = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const PACKAGES = join(REPO, "packages");
/*
FNXC:LifecycleColumnCensus 2026-07-30-23:58:
`scripts/` is scanned, and `.mjs` counts — the operator scripts were the one place raw SQL actually
lived, and this gate could not see any of it.
Found by removing a raw-SQL lane literal from `scripts/reconcile-leaked-soft-deletes.mjs` and watching
this gate report "22 known, none added" — unchanged and green. Its own header promises the opposite
("a LOWER count fails too so the baseline is ratcheted down"), so the silence was the tell.
TWO changes, and either alone still sees nothing: the walk started at `packages` only, and the filter
took `.tsx?`, while every operator script is a repo-root `.mjs`. Adding one without the other scans
nothing new and reports a reassuring zero — the same trap #2978 hit widening the lane-wiring census.
This does NOT contradict the `.sql` note below. That reasoning is about feeding DDL to a TypeScript
parser; `.mjs` IS JavaScript, so the existing AST walk applies unchanged.
The ScriptKind move is DEFENSIVE, and I could not demonstrate it was necessary — stated plainly
because the opposite claim would be easy to make and wrong. TSX reads `<` as JSX, so an ordinary
comparison in a plain script is a plausible misparse; I tried three shapes for it
(`x <div> y`, `f<b, c>(d)`, a literal between `<` and `>` comparisons) and TSX recovered from all of
them, returning the same count as JS. So JS is used for `.mjs` because it is the correct kind for the
file, not because a miss was observed.
*/
const SCRIPTS = join(REPO, "scripts");
const BASELINE = join(REPO, "scripts", "lib", "sql-column-literals-baseline.json");
const SKIP_DIRS = new Set(["node_modules", "dist", "__tests__", "__mocks__", "e2e", ".gate-bundle", "coverage"]);
@@ -132,7 +156,7 @@ function* walk(dir) {
data backfill (`UPDATE tasks SET column = ...`) landing in a migration. If one ever does, this
needs a separate raw-text matcher against `COMPARISON`, not an entry in the filter below.
*/
else if (/\.tsx?$/.test(full) && !/\.d\.ts$/.test(full)) yield full;
else if (/\.(tsx?|mjs)$/.test(full) && !/\.d\.ts$/.test(full)) yield full;
}
}
@@ -326,9 +350,11 @@ const matches = [];
function scan() {
const counts = {};
for (const file of walk(PACKAGES)) {
for (const file of [...walk(PACKAGES), ...walk(SCRIPTS)]) {
const source = readFileSync(file, "utf8");
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
/* The correct kind for the file. Defensive rather than a proven fix — see the header. */
const kind = file.endsWith(".mjs") ? ts.ScriptKind.JS : ts.ScriptKind.TSX;
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, kind);
const consts = collectStringConsts(sf);
let hits = 0;
const visit = (node) => {

View File

@@ -12,5 +12,6 @@
"packages/core/src/task-store/task-artifacts-ops.ts": 1,
"packages/core/src/task-store/workflow-definitions.ts": 2,
"packages/core/src/team-analytics.ts": 3,
"packages/core/src/workflow-analytics.ts": 3
"packages/core/src/workflow-analytics.ts": 3,
"scripts/audit-branch-cross-contamination.mjs": 4
}