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