From cd237ae7604fd97838280ed6be707f4a08dafba8 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 00:13:55 -0700 Subject: [PATCH] gate: the SQL column-literal ratchet never scanned scripts/, where the raw SQL actually is (#3000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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
y`, `f(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. --- scripts/check-sql-column-literals.mjs | 32 +++++++++++++++++-- scripts/lib/sql-column-literals-baseline.json | 3 +- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/scripts/check-sql-column-literals.mjs b/scripts/check-sql-column-literals.mjs index a4a8129e01..77f6cd75b0 100644 --- a/scripts/check-sql-column-literals.mjs +++ b/scripts/check-sql-column-literals.mjs @@ -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
y`, `f(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) => { diff --git a/scripts/lib/sql-column-literals-baseline.json b/scripts/lib/sql-column-literals-baseline.json index 5afd8218b7..c393aa8b0f 100644 --- a/scripts/lib/sql-column-literals-baseline.json +++ b/scripts/lib/sql-column-literals-baseline.json @@ -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 }