fix(#1582): derive phase-complete velocity from By-Phase table (idempotent) (#1655)

* fix(#1582): derive phase-complete velocity from By-Phase table (idempotent)

updatePerformanceMetricsSection blind-added summaryCount onto the prior velocity
total on every phase complete, so re-running phase complete on an already-complete
phase incremented the total each time (the sibling of #4, which fixed the Completed
Phases counter the same way). The velocity total is now derived as the sum of the
By-Phase table's Plans column AFTER the row upsert — re-completing a phase upserts
the same row, so the sum is stable; a hand-edited inflated total self-heals downward
to the true sum on the next completion. When the By-Phase table is absent the total
is left unchanged (no crash). Strengthens the misnamed 'idempotent' test (its comment
explicitly declined to assert velocity idempotency — the latent gap) and adds a
self-heal regression; corrects the #320 behavior-lock velocity assertion which had
encoded the blind-add (3 = 1+2 double-count) — the derived value is 2.

* chore(#1582): backfill changeset pr ref to 1655

* fix(#1582): velocity sum tolerates indented By-Phase rows (codex review)

Adversarial review (codex, gpt-5.5/high) flagged that byPhaseTablePattern's
data-row capture allows leading whitespace ([ \t]*\|), but the derive sum was
anchored at ^\| and would skip indented hand-edited/legacy rows — capturing them
in the table but silently undercounting. Align the sum regex (^\s*\|) with the
table capture's tolerance. Adds an indented-row regression. Two other codex
findings are pre-existing and out of scope: padded/unpadded phase dedup
(phaseRowPattern, identical in old code — derive yields the same value as the old
blind-add) and CRLF tables (the shared byPhaseTablePattern header requires bare
\n, so the upsert was already broken on CRLF; the fix changes stale-vs-
double-count, does not worsen it).

* fix(#1582): add verification fixtures to velocity tests under #1522 gate

Post-rebase onto next+#1548, the #1582 velocity tests (self-heal, indented-row) use
phase complete, which now fail-closes under #1522's canonical verification gate without a
passed *-VERIFICATION.md. Add writePassedVerification(tmpDir,'02-next','02') to both.
This commit is contained in:
Tom Boucher
2026-06-24 14:36:01 -04:00
committed by GitHub
parent f202d243cb
commit ff161f2281
3 changed files with 151 additions and 15 deletions

View File

@@ -2417,16 +2417,11 @@ function cmdSignalResume(cwd: string, raw: boolean): void {
* Returns modified content string.
*/
function updatePerformanceMetricsSection(content: string, cwd: string, phaseNum: string | number, planCount: number, summaryCount: number): string {
// Update Velocity: Total plans completed
const totalMatch = content.match(/Total plans completed:\s*(\d+|\[N\])/);
const prevTotal = totalMatch && totalMatch[1] !== '[N]' ? parseInt(totalMatch[1], 10) : 0;
const newTotal = prevTotal + summaryCount;
content = content.replace(
/Total plans completed:\s*(\d+|\[N\])/,
`Total plans completed: ${newTotal}`
);
// Update By Phase table — upsert row for this phase
// By Phase table — upsert the row for THIS phase FIRST. The velocity total is then
// DERIVED from the table's Plans column so it stays idempotent on re-run: completing
// the same phase again upserts the same row, so the column sum is stable. The previous
// blind-add (prevTotal + summaryCount) re-read the cumulative total each call and
// double-counted on every re-run. (#1582)
const byPhaseMatch = content.match(byPhaseTablePattern);
if (byPhaseMatch) {
let tableBody = byPhaseMatch[2].trim();
@@ -2445,6 +2440,31 @@ function updatePerformanceMetricsSection(content: string, cwd: string, phaseNum:
content = content.replace(byPhaseTablePattern, (_match, tableHeader: string) => `${tableHeader}${tableBody}\n`);
}
// Velocity: Total plans completed — DERIVED as the sum of the By-Phase Plans column
// (the second cell) across all data rows. Idempotent by construction (re-running phase
// complete upserts the same row → same sum) and self-healing (a hand-edited inflated
// total is corrected to the true sum on the next completion). When the By-Phase table
// is absent, leave the velocity total unchanged rather than guess. (#1582)
if (/Total plans completed:\s*(\d+|\[N\])/.test(content)) {
const tableForSum = content.match(byPhaseTablePattern);
if (tableForSum) {
let sum = 0;
for (const row of tableForSum[2].split(/\r?\n/)) {
// Data rows look like `| <phase> | <plans> | … |`, optionally indented (the
// byPhaseTablePattern data-row capture allows `[ \t]*` leading whitespace, so the
// sum must too or hand-edited/legacy indented rows are silently skipped — #1582
// codex review). Header (`| Phase | Plans | …`) and separator (`| --- | --- | …`)
// rows have a non-numeric second cell and are skipped; non-numeric cells → 0.
const cellMatch = row.match(/^\s*\|\s*[^|]+\s*\|\s*(\d+)\s*\|/);
if (cellMatch) sum += parseInt(cellMatch[1], 10);
}
content = content.replace(
/Total plans completed:\s*(\d+|\[N\])/,
`Total plans completed: ${sum}`,
);
}
}
return content;
}