* fix(#1161): preserve existing roadmap completion date on repeat phase complete Guard the Completed cell write in cmdRoadmapUpdatePlanProgress so that a non-empty, non-placeholder date is never overwritten on repeat invocations. Also routes the date source through realClock.today() so GSD_NOW_MS pins the written date deterministically in tests (clock seam parity with the rest of the codebase). Regression tests added to 4-phase-complete-cjs-regression.test.cjs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1161): preserve completion date in cmdPhaseComplete (real handler) + drive regression via phase complete CLI - Import realClock in phase.cts; switch cmdPhaseComplete from bare `new Date()` to `realClock.today()` so the clock seam is honoured in tests - Apply preserve-or-stamp guard to both 5-col (cells[4]) and 4-col (cells[3]) paths in cmdPhaseComplete: keep existing non-empty, non-dash date; only stamp today on first completion - Rewrite the #1161 describe block in 4-phase-complete-cjs-regression.test.cjs to drive the real handler via runGsdTools('phase complete 1') end-to-end; cases (a)/(b)/(c)/(d) all FAIL against unfixed build and pass post-fix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1161): preserve only date-shaped completion cells (self-heal garbage); correct test helper for 5-col Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1161): add changeset fragment for completion-date idempotence Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import stateMod = require('./state.cjs');
|
||||
import { platformWriteSync, platformReadSync, platformEnsureDir } from './shell-command-projection.cjs';
|
||||
import { formatGsdSlash, resolveRuntime } from './runtime-slash.cjs';
|
||||
import { deriveProgressFromRoadmap, clampPercent } from './phase-lifecycle.cjs';
|
||||
import { realClock } from './clock.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- uat-predicate.cjs is an export= CommonJS module
|
||||
import uatPredicate = require('./uat-predicate.cjs');
|
||||
const { evaluateUatPassed } = uatPredicate;
|
||||
@@ -1339,7 +1340,7 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
const roadmapPath = path.join(planningDir(cwd), 'ROADMAP.md');
|
||||
const statePath = path.join(planningDir(cwd), 'STATE.md');
|
||||
const phasesDir = path.join(planningDir(cwd), 'phases');
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = realClock.today();
|
||||
|
||||
const phaseInfoRaw = findPhaseInternal(cwd, phaseNum);
|
||||
if (!phaseInfoRaw) {
|
||||
@@ -1408,14 +1409,19 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
|
||||
);
|
||||
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
||||
const cells = fullRow.split('|').slice(1, -1);
|
||||
const dateShape = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (cells.length === 5) {
|
||||
cells[2] = ` ${summaryCount}/${planCount} `;
|
||||
cells[3] = ' Complete ';
|
||||
cells[4] = ` ${today} `;
|
||||
// Preserve only a valid ISO date (#1161: idempotent; self-heal garbage)
|
||||
const existingDate5 = cells[4].trim();
|
||||
cells[4] = dateShape.test(existingDate5) ? cells[4] : ` ${today} `;
|
||||
} else if (cells.length === 4) {
|
||||
cells[1] = ` ${summaryCount}/${planCount} `;
|
||||
cells[2] = ' Complete ';
|
||||
cells[3] = ` ${today} `;
|
||||
// Preserve only a valid ISO date (#1161: idempotent; self-heal garbage)
|
||||
const existingDate4 = cells[3].trim();
|
||||
cells[3] = dateShape.test(existingDate4) ? cells[3] : ` ${today} `;
|
||||
}
|
||||
return '|' + cells.join('|') + '|';
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { realClock } from './clock.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
import core = require('./core.cjs');
|
||||
const { escapeRegex, normalizePhaseName, phaseMarkdownRegexSource, phaseMarkdownRegexSourceExact, output, error, findPhaseInternal, phaseTokenMatches } = core;
|
||||
@@ -470,7 +471,7 @@ function cmdRoadmapUpdatePlanProgress(cwd: string, phaseNum: string | null | und
|
||||
|
||||
const isComplete = summaryCount >= planCount;
|
||||
const status = isComplete ? 'Complete' : summaryCount > 0 ? 'In Progress' : 'Planned';
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const today = realClock.today();
|
||||
|
||||
if (!fs.existsSync(roadmapPath)) {
|
||||
output({ updated: false, reason: 'ROADMAP.md not found', plan_count: planCount, summary_count: summaryCount }, raw, 'no roadmap');
|
||||
@@ -487,19 +488,27 @@ function cmdRoadmapUpdatePlanProgress(cwd: string, phaseNum: string | null | und
|
||||
`^(\\|\\s*${phasePattern}\\.?\\s[^|]*(?:\\|[^\\n]*))$`,
|
||||
'im'
|
||||
);
|
||||
const dateField = isComplete ? ` ${today} ` : ' ';
|
||||
roadmapContent = roadmapContent.replace(tableRowPattern, (fullRow) => {
|
||||
const cells = fullRow.split('|').slice(1, -1); // drop leading/trailing empty from split
|
||||
const dateShape = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (cells.length === 5) {
|
||||
// 5-col: Phase | Milestone | Plans | Status | Completed
|
||||
cells[2] = ` ${summaryCount}/${planCount} `;
|
||||
cells[3] = ` ${status.padEnd(11)}`;
|
||||
cells[4] = dateField;
|
||||
// Preserve only a valid ISO date (#1161: idempotent; self-heal garbage)
|
||||
const existingDate5 = cells[4].trim();
|
||||
cells[4] = isComplete
|
||||
? (dateShape.test(existingDate5) ? cells[4] : ` ${today} `)
|
||||
: ' ';
|
||||
} else if (cells.length === 4) {
|
||||
// 4-col: Phase | Plans | Status | Completed
|
||||
cells[1] = ` ${summaryCount}/${planCount} `;
|
||||
cells[2] = ` ${status.padEnd(11)}`;
|
||||
cells[3] = dateField;
|
||||
// Preserve only a valid ISO date (#1161: idempotent; self-heal garbage)
|
||||
const existingDate4 = cells[3].trim();
|
||||
cells[3] = isComplete
|
||||
? (dateShape.test(existingDate4) ? cells[3] : ` ${today} `)
|
||||
: ' ';
|
||||
}
|
||||
return '|' + cells.join('|') + '|';
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user