* 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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user