* fix(2118): honor --dry-run in milestone complete with zero-mutation preview milestone complete treated --dry-run as a no-op: the flag was neither parsed nor rejected, so a caller who expected a preview instead triggered the full destructive mutation (archive phases → move audit artifacts → rewrite STATE.md) with no way to back out. Fix (option 2 from the issue): add dryRun to MilestoneCompleteOptions, parse --dry-run in the dispatcher, and return a JSON preview plan (would_archive, would_update) after the read-only stats gathering but before any mutations. Also gated platformEnsureDir on !dryRun so the archive directory is not created during preview. 3 regression tests: no-mutation happy path, --no-archive-phases combo, and --force bypass combo. * docs(changeset): backfill PR number (#2155) * chore(#2118): regenerate pi/qwen golden fixtures after next merge The milestone --dry-run fix changes gsd-core/bin/gsd-tools.cjs; qwen.json (missed at authoring) and pi.json (added on next, never carried the fix) were the only two runtime fixtures still holding the pre-fix hash. All other runtimes already reflect the change. Regenerated via the sanctioned gen-golden-install-parity script. * fix(#2118): surface accomplishments in dry-run preview; fix --dry-run --raw Orthogonal review findings on the --dry-run preview: - The preview omitted the already-computed accomplishments (the primary MILESTONES.md content a real run writes); surface it as a top-level field, mirroring the real-run result. - `--dry-run --raw` discarded the structured payload and printed the literal string "dry-run"; drop the raw-value arg so --raw emits the full preview JSON, matching the real-run output() call. Adds tests: --dry-run --raw is parseable JSON, preview includes accomplishments, and --dry-run --force is proven zero-mutation. 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:
@@ -38,6 +38,7 @@ interface MilestoneCompleteOptions {
|
||||
name?: string;
|
||||
force?: boolean;
|
||||
archivePhases?: boolean;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
function cmdRequirementsMarkComplete(cwd: string, reqIdsRaw: string[], raw: boolean): void {
|
||||
@@ -161,8 +162,10 @@ function cmdMilestoneComplete(cwd: string, version: string, options: MilestoneCo
|
||||
const today = realClock.localToday();
|
||||
const milestoneName = options.name || version;
|
||||
|
||||
// Ensure archive directory exists
|
||||
platformEnsureDir(archiveDir);
|
||||
// Ensure archive directory exists (skipped in dry-run — no mutations)
|
||||
if (!options.dryRun) {
|
||||
platformEnsureDir(archiveDir);
|
||||
}
|
||||
|
||||
// Scope stats and accomplishments to only the phases belonging to the
|
||||
// current milestone's ROADMAP. Uses the shared filter from roadmap-parser.cjs
|
||||
@@ -295,6 +298,47 @@ function cmdMilestoneComplete(cwd: string, version: string, options: MilestoneCo
|
||||
/* intentionally empty */
|
||||
}
|
||||
|
||||
// #2118: --dry-run preview — compute what WOULD happen without mutating.
|
||||
// The stats above are read-only; all mutations start at the archive section below.
|
||||
if (options.dryRun) {
|
||||
const phaseDirsToArchive: string[] = [];
|
||||
if (options.archivePhases !== false) {
|
||||
try {
|
||||
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
||||
for (const e of entries) {
|
||||
if (e.isDirectory() && isDirInMilestone(e.name)) {
|
||||
phaseDirsToArchive.push(e.name);
|
||||
}
|
||||
}
|
||||
} catch { /* phasesDir missing — nothing to archive */ }
|
||||
}
|
||||
const dryRunResult = {
|
||||
dry_run: true,
|
||||
version,
|
||||
name: milestoneName,
|
||||
stats: { phases: phaseCount, plans: totalPlans, tasks: totalTasks },
|
||||
accomplishments,
|
||||
would_archive: {
|
||||
roadmap: fs.existsSync(roadmapPath)
|
||||
? { source: path.relative(cwd, roadmapPath).split(path.sep).join('/'), target: path.relative(cwd, path.join(archiveDir, `${version}-ROADMAP.md`)).split(path.sep).join('/') }
|
||||
: null,
|
||||
requirements: fs.existsSync(reqPath)
|
||||
? { source: path.relative(cwd, reqPath).split(path.sep).join('/'), target: path.relative(cwd, path.join(archiveDir, `${version}-REQUIREMENTS.md`)).split(path.sep).join('/') }
|
||||
: null,
|
||||
audit: fs.existsSync(path.join(planningBase, `${version}-MILESTONE-AUDIT.md`))
|
||||
? { source: path.relative(cwd, path.join(planningBase, `${version}-MILESTONE-AUDIT.md`)).split(path.sep).join('/'), target: path.relative(cwd, path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)).split(path.sep).join('/') }
|
||||
: null,
|
||||
phases: phaseDirsToArchive,
|
||||
},
|
||||
would_update: {
|
||||
milestones_md: path.relative(cwd, milestonesPath).split(path.sep).join('/'),
|
||||
state_md: fs.existsSync(statePath) ? path.relative(cwd, statePath).split(path.sep).join('/') : null,
|
||||
},
|
||||
};
|
||||
output(dryRunResult, raw);
|
||||
return;
|
||||
}
|
||||
|
||||
// Archive ROADMAP.md
|
||||
if (fs.existsSync(roadmapPath)) {
|
||||
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
||||
|
||||
Reference in New Issue
Block a user