fix(core): extend atomicWriteFileSync to milestone, phase, and frontmatter (#1972)
Replace 11 fs.writeFileSync calls with atomicWriteFileSync in three files that write to .planning/ artifacts (ROADMAP.md, REQUIREMENTS.md, MILESTONES.md, and frontmatter updates). This prevents partial writes from corrupting planning files on crash or power loss. Skipped low-risk writes: .gitkeep (empty files) and archive directory writes (new files, not read-modify-write). Files changed: - milestone.cjs: 5 sites (REQUIREMENTS.md, MILESTONES.md) - phase.cjs: 5 sites (ROADMAP.md, REQUIREMENTS.md) - frontmatter.cjs: 2 sites (arbitrary .planning/ files) Closes #1972 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { safeReadFile, normalizeMd, output, error } = require('./core.cjs');
|
||||
const { safeReadFile, normalizeMd, output, error, atomicWriteFileSync } = require('./core.cjs');
|
||||
|
||||
// ─── Parsing engine ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -337,7 +337,7 @@ function cmdFrontmatterSet(cwd, filePath, field, value, raw) {
|
||||
try { parsedValue = JSON.parse(value); } catch { parsedValue = value; }
|
||||
fm[field] = parsedValue;
|
||||
const newContent = spliceFrontmatter(content, fm);
|
||||
fs.writeFileSync(fullPath, normalizeMd(newContent), 'utf-8');
|
||||
atomicWriteFileSync(fullPath, normalizeMd(newContent));
|
||||
output({ updated: true, field, value: parsedValue }, raw, 'true');
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ function cmdFrontmatterMerge(cwd, filePath, data, raw) {
|
||||
try { mergeData = JSON.parse(data); } catch { error('Invalid JSON for --data'); return; }
|
||||
Object.assign(fm, mergeData);
|
||||
const newContent = spliceFrontmatter(content, fm);
|
||||
fs.writeFileSync(fullPath, normalizeMd(newContent), 'utf-8');
|
||||
atomicWriteFileSync(fullPath, normalizeMd(newContent));
|
||||
output({ merged: true, fields: Object.keys(mergeData) }, raw, 'true');
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { escapeRegex, getMilestonePhaseFilter, extractOneLinerFromBody, normalizeMd, planningPaths, output, error } = require('./core.cjs');
|
||||
const { escapeRegex, getMilestonePhaseFilter, extractOneLinerFromBody, normalizeMd, planningPaths, output, error, atomicWriteFileSync } = require('./core.cjs');
|
||||
const { extractFrontmatter } = require('./frontmatter.cjs');
|
||||
const { writeStateMd, stateReplaceFieldWithFallback } = require('./state.cjs');
|
||||
|
||||
@@ -74,7 +74,7 @@ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
|
||||
}
|
||||
|
||||
if (updated.length > 0) {
|
||||
fs.writeFileSync(reqPath, reqContent, 'utf-8');
|
||||
atomicWriteFileSync(reqPath, reqContent);
|
||||
}
|
||||
|
||||
output({
|
||||
@@ -178,21 +178,21 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
const existing = fs.readFileSync(milestonesPath, 'utf-8');
|
||||
if (!existing.trim()) {
|
||||
// Empty file — treat like new
|
||||
fs.writeFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`), 'utf-8');
|
||||
atomicWriteFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`));
|
||||
} else {
|
||||
// Insert after the header line(s) for reverse chronological order (newest first)
|
||||
const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
|
||||
if (headerMatch) {
|
||||
const header = headerMatch[1];
|
||||
const rest = existing.slice(header.length);
|
||||
fs.writeFileSync(milestonesPath, normalizeMd(header + milestoneEntry + rest), 'utf-8');
|
||||
atomicWriteFileSync(milestonesPath, normalizeMd(header + milestoneEntry + rest));
|
||||
} else {
|
||||
// No recognizable header — prepend the entry
|
||||
fs.writeFileSync(milestonesPath, normalizeMd(milestoneEntry + existing), 'utf-8');
|
||||
atomicWriteFileSync(milestonesPath, normalizeMd(milestoneEntry + existing));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fs.writeFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`), 'utf-8');
|
||||
atomicWriteFileSync(milestonesPath, normalizeMd(`# Milestones\n\n${milestoneEntry}`));
|
||||
}
|
||||
|
||||
// Update STATE.md — use shared helpers that handle both **bold:** and plain Field: formats
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, planningDir, withPlanningLock, output, error, readSubdirectories, phaseTokenMatches } = require('./core.cjs');
|
||||
const { escapeRegex, loadConfig, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, planningDir, withPlanningLock, output, error, readSubdirectories, phaseTokenMatches, atomicWriteFileSync } = require('./core.cjs');
|
||||
const { extractFrontmatter } = require('./frontmatter.cjs');
|
||||
const { writeStateMd, readModifyWriteStateMd, stateExtractField, stateReplaceField, stateReplaceFieldWithFallback, updatePerformanceMetricsSection } = require('./state.cjs');
|
||||
|
||||
@@ -392,7 +392,7 @@ function cmdPhaseAdd(cwd, description, raw, customId) {
|
||||
updatedContent = rawContent + phaseEntry;
|
||||
}
|
||||
|
||||
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
||||
atomicWriteFileSync(roadmapPath, updatedContent);
|
||||
return { newPhaseId: _newPhaseId, dirName: _dirName };
|
||||
});
|
||||
|
||||
@@ -493,7 +493,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
||||
}
|
||||
|
||||
const updatedContent = rawContent.slice(0, insertIdx) + phaseEntry + rawContent.slice(insertIdx);
|
||||
fs.writeFileSync(roadmapPath, updatedContent, 'utf-8');
|
||||
atomicWriteFileSync(roadmapPath, updatedContent);
|
||||
return { decimalPhase: _decimalPhase, dirName: _dirName };
|
||||
});
|
||||
|
||||
@@ -607,7 +607,7 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(roadmapPath, content, 'utf-8');
|
||||
atomicWriteFileSync(roadmapPath, content);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -783,7 +783,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2');
|
||||
}
|
||||
|
||||
fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8');
|
||||
atomicWriteFileSync(roadmapPath, roadmapContent);
|
||||
|
||||
// Update REQUIREMENTS.md traceability for this phase's requirements
|
||||
const reqPath = path.join(planningDir(cwd), 'REQUIREMENTS.md');
|
||||
@@ -816,7 +816,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
);
|
||||
}
|
||||
|
||||
fs.writeFileSync(reqPath, reqContent, 'utf-8');
|
||||
atomicWriteFileSync(reqPath, reqContent);
|
||||
requirementsUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user