cherry-pick(state): atomic-writes for guided-flow-queue and reports

Cherry-pick of gsd-build/gsd-2 53babec29 (Jeremy <jeremy@fluxlabs.net>)
— atomic-write half only. Eliminates torn-write risk on PROJECT.md
queue sync and reports.json/HTML index regeneration by switching
writeFileSync → atomicWriteSync (tmp+rename).

The companion lock-wrapped-append changes (journal.ts, uok/audit.ts,
workflow-logger.ts) are deferred — they need proper-lockfile +
withFileLockSync helper introduced first.

Co-Authored-By: Jeremy <jeremy@fluxlabs.net>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-04-28 05:16:39 +02:00
parent 8e827147c9
commit 3df56cb94f
2 changed files with 9 additions and 7 deletions

View file

@ -17,7 +17,8 @@ import {
sfRoot, resolveMilestoneFile, resolveSliceFile,
resolveSfRootFile, relSfRootFile, relSliceFile,
} from "./paths.js";
import { readFileSync, writeFileSync, existsSync } from "node:fs";
import { readFileSync, existsSync } from "node:fs";
import { atomicWriteSync } from "./atomic-write.js";
import { nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
import { loadEffectiveSFPreferences } from "./preferences.js";
import { loadQueueOrder, sortByQueueOrder, saveQueueOrder } from "./queue-order.js";
@ -387,7 +388,7 @@ function removeDependsOnFromContextFiles(
const newContent = newFm.trim()
? `---\n${newFm}\n---${body}`
: body.replace(/^\n+/, "");
writeFileSync(contextFile, newContent, "utf-8");
atomicWriteSync(contextFile, newContent, "utf-8");
}
}
@ -435,5 +436,5 @@ function syncProjectMdSequence(
const separatorLine = lines[tableStart + 1];
const newTable = [headerLine, separatorLine, ...newRows];
lines.splice(tableStart, tableEnd - tableStart, ...newTable);
writeFileSync(projectPath, lines.join("\n"), "utf-8");
atomicWriteSync(projectPath, lines.join("\n"), "utf-8");
}

View file

@ -14,11 +14,12 @@
* Manual: /sf export --html
*/
import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'node:fs';
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
import { join, basename } from 'node:path';
import { sfRoot } from './paths.js';
import { formatCost, formatTokenCount } from './metrics.js';
import { formatDateShort, formatDuration } from '../shared/format-utils.js';
import { atomicWriteSync } from './atomic-write.js';
// ─── Types ────────────────────────────────────────────────────────────────────
@ -83,7 +84,7 @@ export function loadReportsIndex(basePath: string): ReportsIndex | null {
function saveReportsIndex(basePath: string, index: ReportsIndex): void {
const dir = reportsDir(basePath);
mkdirSync(dir, { recursive: true });
writeFileSync(reportsIndexPath(basePath), JSON.stringify(index, null, 2) + '\n', 'utf-8');
atomicWriteSync(reportsIndexPath(basePath), JSON.stringify(index, null, 2) + '\n', 'utf-8');
}
// ─── Write a report snapshot ──────────────────────────────────────────────────
@ -121,7 +122,7 @@ export function writeReportSnapshot(args: WriteReportSnapshotArgs): string {
const filename = `${prefix}-${timestamp}.html`;
const filePath = join(dir, filename);
writeFileSync(filePath, args.html, 'utf-8');
atomicWriteSync(filePath, args.html, 'utf-8');
// Load or init registry
const existing = loadReportsIndex(args.basePath);
@ -170,7 +171,7 @@ export function writeReportSnapshot(args: WriteReportSnapshotArgs): string {
export function regenerateHtmlIndex(basePath: string, index: ReportsIndex): void {
const html = buildIndexHtml(index);
writeFileSync(reportsHtmlIndexPath(basePath), html, 'utf-8');
atomicWriteSync(reportsHtmlIndexPath(basePath), html, 'utf-8');
}
function buildIndexHtml(index: ReportsIndex): string {