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) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user