fix(gate): detect ternary move-target literals, which #3246's ratchet could not see (#3250)

## 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."
This commit is contained in:
gsxdsm
2026-07-31 15:19:11 -07:00
committed by GitHub
parent 1e50b71255
commit 0b30eb4146

View File

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