From a6138abeffbf7ca67ef5e86f8468b84bf0a375c5 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 30 Jul 2026 01:02:25 -0700 Subject: [PATCH] =?UTF-8?q?U12:=20DELIBERATE-LITERAL=20counts=20key=20on?= =?UTF-8?q?=20file=20AND=20column=20=E2=80=94=20closing=20the=20P1=20left?= =?UTF-8?q?=20on=20merged=20#2661=20(#2666)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the P1 that was still open when #2661 merged. ## The hole A per-file integer is offset **within a single file**: remove one reviewed `todo` exemption, add an `in-review` one beside it, and the number never moves. The fresh guard is invisible to the column counts too, because deliberate findings are excluded from them — so `--strict` goes green with a new lifecycle-column guard hiding inside an existing marker. Now keyed on **file AND column id**. **Proven with the exact scenario:** swapping a marked `triage` for `done` inside `TaskCard.tsx` leaves the per-file total unchanged and now fails with ``` packages/dashboard/app/components/TaskCard.tsx (DELIBERATE-LITERAL: done): 0 -> 1 ``` ## The pattern worth naming This is the **third** time this instrument has been defeated by an aggregate: | version | defeated by | |---|---| | repo-wide `totals.deliberate` | an addition in file A offset by a removal in file B | | per-file integer | an addition offset by a removal **in the same file** | | per-file per-column | — | Each step narrows what can offset silently, and I walked into the next one twice by fixing the *reported case* rather than the *shape*. Writing it down because the same reflex will produce a fourth if someone adds another aggregate here. **The residual is deliberate, not an oversight:** a same-file **same-column** swap still offsets. Two `todo` exemptions in one file are interchangeable by definition, so there is nothing a reviewer could act on. That is recorded at the site so the next person doesn't rediscover it as a bug. ## Migration, again The key **shape** changed (`file` → `file\0columnId`), which is the same hazard as a missing field: comparing new keys against old reports every existing marker as a fresh rise and pushes people to convert already-reviewed literals. I hit it on the first run here — `TaskCard (DELIBERATE-LITERAL: triage): 0 -> 2` — exactly as I did one shape earlier in #2661. Detected by the delimiter rather than a version field, since old keys have none, and re-seeded on the next `--update-baseline`. 15 file+column entries recorded. ## Verification `pnpm lint` clean. `pnpm test:gate` green (10 / 158 / 487 / 71). `pnpm check:lifecycle-columns` exits 0. Independent of #2655; either order merges. Co-authored-by: Claude Opus 5 (1M context) --- scripts/lib/lifecycle-column-census-ast.mjs | 14 ++++++++++- .../lib/lifecycle-column-census-baseline.json | 23 ++++++++++++------- scripts/lifecycle-column-census.mjs | 23 +++++++++++++++---- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/scripts/lib/lifecycle-column-census-ast.mjs b/scripts/lib/lifecycle-column-census-ast.mjs index be5adafba8..54bbb3d4a7 100644 --- a/scripts/lib/lifecycle-column-census-ast.mjs +++ b/scripts/lib/lifecycle-column-census-ast.mjs @@ -303,7 +303,19 @@ export function summarize(findings) { } totals[finding.kind] += 1; if (finding.kind === "deliberate") { - deliberateByFile.set(finding.file, (deliberateByFile.get(finding.file) ?? 0) + 1); + /* + FNXC:WorkflowLifecycleColumns 2026-07-31-10:00 (PR #2661 review — greptile P1, same class again): + Keyed by FILE **and COLUMN ID**, not a per-file integer. A per-file aggregate is offset within a + single file: remove one reviewed `todo` exemption, add a `in-review` one beside it, and the + number never moves — so a fresh guard hides inside an existing marker. + + That is the third time this instrument has been defeated by an aggregate (repo total -> per file + -> per file per column). Each step narrows what can offset silently. The residual is a same-file + SAME-COLUMN swap, and that one is deliberate: two `todo` exemptions in one file are + interchangeable by definition, so there is nothing a reviewer could act on. + */ + const key = `${finding.file}\u0000${finding.columnId}`; + deliberateByFile.set(key, (deliberateByFile.get(key) ?? 0) + 1); } if (finding.kind !== "column") continue; byColumnId[finding.columnId] = (byColumnId[finding.columnId] ?? 0) + 1; diff --git a/scripts/lib/lifecycle-column-census-baseline.json b/scripts/lib/lifecycle-column-census-baseline.json index 5256bba8de..1530af8980 100644 --- a/scripts/lib/lifecycle-column-census-baseline.json +++ b/scripts/lib/lifecycle-column-census-baseline.json @@ -155,14 +155,21 @@ "plugins/fusion-plugin-reports/src/store/report-types.ts": 1 }, "deliberateByFile": { - "packages/dashboard/app/components/command-center/MissionControlPanel.tsx": 3, - "packages/dashboard/app/components/TaskCard.tsx": 3, - "packages/dashboard/app/components/TaskDetailModal.tsx": 3, - "packages/engine/src/hold-release.ts": 3, - "packages/dashboard/src/routes/register-task-workflow-routes.ts": 2, - "packages/dashboard/app/components/TaskContextMenu.tsx": 1, - "packages/dashboard/app/utils/columnRoles.ts": 1, - "packages/engine/src/triage.ts": 1 + "packages/dashboard/app/components/TaskCard.tsx\u0000triage": 2, + "packages/dashboard/app/components/TaskDetailModal.tsx\u0000triage": 2, + "packages/dashboard/app/components/command-center/MissionControlPanel.tsx\u0000done": 1, + "packages/dashboard/app/components/command-center/MissionControlPanel.tsx\u0000in-review": 1, + "packages/dashboard/app/components/command-center/MissionControlPanel.tsx\u0000todo": 1, + "packages/dashboard/app/components/TaskCard.tsx\u0000todo": 1, + "packages/dashboard/app/components/TaskContextMenu.tsx\u0000triage": 1, + "packages/dashboard/app/components/TaskDetailModal.tsx\u0000todo": 1, + "packages/dashboard/app/utils/columnRoles.ts\u0000todo": 1, + "packages/dashboard/src/routes/register-task-workflow-routes.ts\u0000todo": 1, + "packages/dashboard/src/routes/register-task-workflow-routes.ts\u0000triage": 1, + "packages/engine/src/hold-release.ts\u0000archived": 1, + "packages/engine/src/hold-release.ts\u0000done": 1, + "packages/engine/src/hold-release.ts\u0000in-review": 1, + "packages/engine/src/triage.ts\u0000triage": 1 }, "properties": { "query": 83, diff --git a/scripts/lifecycle-column-census.mjs b/scripts/lifecycle-column-census.mjs index c566090f6b..838534075a 100644 --- a/scripts/lifecycle-column-census.mjs +++ b/scripts/lifecycle-column-census.mjs @@ -179,17 +179,32 @@ which is NOT the same as "every marked file had zero" — comparing against an a every existing marker as a fresh rise and demand people convert literals that were already reviewed. Seed it on the next `--update-baseline` instead, and start comparing once it is present. */ -const deliberateTracked = baseline.deliberateByFile !== undefined; +/* +FNXC:WorkflowLifecycleColumns 2026-07-31-10:05: +The key SHAPE changed (file -> file\u0000columnId), and a shape change is the same migration hazard +as a missing field: comparing new keys against old ones reports every existing marker as a fresh +rise and demands people convert already-reviewed literals. I hit exactly that on the first run here +(`TaskCard (DELIBERATE-LITERAL: triage): 0 -> 2`), and hit the same wall one shape earlier in #2661. + +Detect by the delimiter rather than by a version field: old keys have none. Re-seeds on the next +`--update-baseline`, then compares normally. +*/ +const deliberateKeysAreCurrentShape = Object.keys(baseline.deliberateByFile ?? {}).every((k) => k.includes("\u0000")); +const deliberateTracked = baseline.deliberateByFile !== undefined && deliberateKeysAreCurrentShape; const baselineDeliberateByFile = new Map(Object.entries(baseline.deliberateByFile ?? {})); const currentDeliberateByFile = new Map(summary.deliberateByFile ?? []); for (const [file, count] of deliberateTracked ? currentDeliberateByFile : []) { const allowed = baselineDeliberateByFile.get(file) ?? 0; - if (count > allowed) regressions.push({ file: `${file} (DELIBERATE-LITERAL)`, count, allowed }); - else if (count < allowed) stale.push({ file: `${file} (DELIBERATE-LITERAL)`, count, allowed }); + // Keys are `file\u0000columnId`; render them readably in the report. + const [f, columnId] = file.split("\u0000"); + const label = `${f} (DELIBERATE-LITERAL: ${columnId})`; + if (count > allowed) regressions.push({ file: label, count, allowed }); + else if (count < allowed) stale.push({ file: label, count, allowed }); } for (const [file, allowed] of deliberateTracked ? baselineDeliberateByFile : []) { if (!currentDeliberateByFile.has(file) && allowed > 0) { - stale.push({ file: `${file} (DELIBERATE-LITERAL)`, count: 0, allowed }); + const [f, columnId] = file.split("\u0000"); + stale.push({ file: `${f} (DELIBERATE-LITERAL: ${columnId})`, count: 0, allowed }); } }