From 0b30eb41465d7a36a13f664944f0766ade5a200e Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 15:19:11 -0700 Subject: [PATCH] fix(gate): detect ternary move-target literals, which #3246's ratchet could not see (#3250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What #3246 landed a gate holding `moveTask` legacy-literal destinations at zero, printing **"POPULATION EMPTY … keep it empty."** I probed that claim the way #3247 probed the census. It held for two spellings and not a third. | form | before | after | |---|---|---| | `moveTask(id, "done")` | ✅ | ✅ | | `` moveTask(id, `todo`) `` | ✅ | ✅ | | `moveTask(id, ok ? "done" : "in-review")` | ❌ **invisible** | ✅ | The check required `arguments[1]` to *be* a literal. A ternary over two lanes is a natural way to write exactly the destination this gate exists to prevent — and per the gate's own header, a wrong target **throws** at runtime rather than no-opping. ## Measured ``` real repo, before and after: 0 targets, --strict passes (no false positives) ternary probe: 0 on HEAD~1 -> 1 after direct + backtick forms: unchanged DELIBERATE-LITERAL marker: still suppresses (canonical placement) lint clean; fnxc-future-dates: none added ``` ## Two scoping decisions, both probed rather than assumed **Not descending into `??` / `||`.** `moveTask(id, lanes.complete ?? "done")` is the documented degraded arm this program writes deliberately — the shape the lifecycle census classifies as `traitFallback` rather than backlog. Counting it would report correct code as debt. Measured at 0 both before and after. **`const t = "archived"; moveTask(id, t)` is still undetected**, and the comment says so at the site. Resolving it needs symbol/dataflow analysis rather than a shape test, which is a different tool than this file is. Flagged so the next person extends deliberately instead of assuming coverage. ## No unit test, and why The script has no export seam and executes at import, so testing it means extracting one — a refactor of a one-commit-old file, which belongs in its own change rather than folded into a behaviour fix. The revert-proof is the measurement above: the ternary probe reads 0 against `HEAD~1` and 1 against this commit. ## Note to #3246's author I raised these gaps on your PR first and offered to send this rather than assume. Two traps that cost me time on the census extension, in case you take it further: - **A count-unless-excluded rule backfires on this vocabulary.** My first census extension counted `switch (x)` unless the receiver looked like a role/status and reported 7 guards — 6 were `switch (eventName)` / `switch (state)` / `switch (event)`, since event enums routinely carry `case "done"`. Requiring a *positive* column signal was the fix. - **Verify the probe file is git-tracked.** My first probe measured nothing because the scanner enumerates tracked files; the scanned-file count stayed flat and I nearly read that as "no gap." --- scripts/check-move-target-literals.mjs | 33 +++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/scripts/check-move-target-literals.mjs b/scripts/check-move-target-literals.mjs index 6f72500db7..4d4095bdf5 100644 --- a/scripts/check-move-target-literals.mjs +++ b/scripts/check-move-target-literals.mjs @@ -63,6 +63,34 @@ if (files.length === 0) { } /** True when the statement enclosing `node` carries a DELIBERATE-LITERAL in its LEADING comments. */ +/* +FNXC:MoveTargetRatchet 2026-07-31-21:55 (probe of this gate's own claim): +TERNARY DESTINATIONS. The gate prints "POPULATION EMPTY ... keep it empty", and a staged probe showed +that claim held for two spellings and not a third: `moveTask(id, ok ? "done" : "in-review")` was +invisible, because the check required arguments[1] to BE a literal. A ternary over two lanes is a +natural way to write exactly the destination this gate exists to prevent. + +ONE hit per call site, not per branch — a two-branch ternary is one move, and counting both would +inflate a population the ratchet holds at zero. + +DELIBERATELY NOT DESCENDING into `??` or `||`. `moveTask(id, lanes.complete ?? "done")` is the +documented degraded arm this program writes on purpose — a resolved value with a legacy fallback, +which the lifecycle census classifies as `traitFallback` rather than backlog. Counting those would +report correct code as debt. Probed and left alone rather than assumed: both forms measure 0. + +STILL NOT DETECTED, stated so nobody assumes coverage: a destination bound to a local first +(`const t = "archived"; moveTask(id, t)`). Resolving that needs symbol/dataflow analysis rather than a +shape test, which is a different tool than this file is. +*/ +function destinationLiterals(expr) { + if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) return [expr.text]; + if (ts.isConditionalExpression(expr)) { + return [...destinationLiterals(expr.whenTrue), ...destinationLiterals(expr.whenFalse)]; + } + if (ts.isParenthesizedExpression(expr)) return destinationLiterals(expr.expression); + return []; +} + function hasDeliberateMarker(node, source) { for (let cur = node; cur; cur = cur.parent) { const ranges = ts.getLeadingCommentRanges(source, cur.getFullStart()) ?? []; @@ -101,9 +129,8 @@ for (const file of files) { `{ reason: "done" }` in an options bag as a move target — work that does not exist and that no real fix could clear. Backtick form included. */ const destination = node.arguments[1]; - const isLiteral = destination - && (ts.isStringLiteral(destination) || ts.isNoSubstitutionTemplateLiteral(destination)); - if (isLiteral && LEGACY_COLUMN_IDS.has(destination.text) && !hasDeliberateMarker(node, source)) { + const hit = destination && destinationLiterals(destination).find((text) => LEGACY_COLUMN_IDS.has(text)); + if (hit && !hasDeliberateMarker(node, source)) { count += 1; } }