* refactor(shell-projection): migrate roadmap.cjs writes to platformWriteSync (#3467) 2 atomicWriteFileSync calls → platformWriteSync. The seam owns markdown normalization, so the explicit utf-8 encoding arg is no longer needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate config.cjs writes to platformWriteSync (#3467) - 3 atomicWriteFileSync calls → platformWriteSync - 1 raw fs.writeFileSync (depth→granularity migration) → platformWriteSync - 2 fs.mkdirSync(planningBase, { recursive: true }) → platformEnsureDir Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate docs.cjs reads to platformReadSync (#3467) 6 try { fs.readFileSync } catch {} patterns → platformReadSync(path) with explicit null guards. detectProjectType now reads package.json once and shares it across has_cli_bin/is_monorepo/has_tests checks. JSON.parse is still wrapped in a try (parsing is a separate failure mode from missing file). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate audit.cjs reads to platformReadSync (#3467) 8 try { fs.readFileSync(safeFilePath, 'utf-8') } catch { continue } patterns → const content = platformReadSync(safeFilePath); if (content === null) continue; The single safeSum case (where catch set status='unreadable' rather than continue) maps to an if/else that preserves the same semantics. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate planning-workspace.cjs to platform* seam (#3467) - 2 try { fs.readFileSync } catch {} → platformReadSync (null on missing) - 2 fs.writeFileSync (workstream pointer writes) → platformWriteSync - 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir The .lock file write at withPlanningLock is intentionally NOT migrated. That call uses { flag: 'wx' } for atomic exclusive-create, which is the correct lock-acquisition primitive. platformWriteSync's atomic-rename pattern would silently overwrite an existing lock file and break the locking guarantee. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate milestone.cjs writes to platform* seam (#3467) - 5 atomicWriteFileSync calls → platformWriteSync (4 dropped normalizeMd wrapper; seam handles .md normalization automatically) - 2 raw fs.writeFileSync (archive ROADMAP.md / REQUIREMENTS.md) → platformWriteSync - 2 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir - Dropped normalizeMd import (only used as write pre-call here) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate intel.cjs to platform* seam (#3467) - 7 fs.readFileSync (existsSync+readFileSync patterns and try/catch) → platformReadSync - 2 fs.writeFileSync → platformWriteSync - 1 fs.mkdirSync(intelPath, { recursive: true }) → platformEnsureDir - Consolidated dual-check (existsSync + readFileSync) into single platformReadSync call returning null on missing file Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate workstream.cjs to platform* seam (#3467) - 5 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir - 1 fs.writeFileSync (STATE.md initial scaffold) → platformWriteSync Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate init.cjs reads/writes to platform* seam (#3467) - 11 try/readFileSync and existsSync+readFileSync patterns → platformReadSync - 1 fs.writeFileSync (skill-manifest.json) → platformWriteSync Three bare fs.readFileSync calls remain (ROADMAP/STATE reads in code paths where the file is required to exist) — these are not "Done when" violations (no try/catch wrapping, no inline existsSync guard). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate commands.cjs reads/writes to platform* seam (#3467) - 6 try/readFileSync and existsSync+readFileSync patterns → platformReadSync - 2 fs.writeFileSync → platformWriteSync - 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir - Removed unused safeReadFile import (zero call sites in this file) Three bare fs.readFileSync calls remain (sourcePath at line 752, fullPath at 443, roadmapPath in cmdAuditOpen) — preceded by existsSync guards or in code paths where file presence is required; not "Done when" violations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate profile-output.cjs to platform* seam (#3467) - 6 safeReadFile (from core.cjs) calls preserved by aliasing platformReadSync as safeReadFile in the import — same semantics, zero call-site changes - 3 try/JSON.parse(readFileSync) patterns → platformReadSync + try/JSON.parse - 1 existsSync+readFileSync pattern (claude.md update) → platformReadSync - 5 fs.writeFileSync → platformWriteSync - 4 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir Two bare fs.readFileSync calls remain (template reads where file must exist or fail loudly) — not "Done when" violations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate state.cjs to platform* seam (#3467) - 4 atomicWriteFileSync calls → platformWriteSync (3 dropped normalizeMd wrapper; seam handles .md normalization) - 4 try/readFileSync and existsSync+readFileSync patterns → platformReadSync - 1 fs.writeFileSync (WAITING.json) → platformWriteSync - 1 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir - Dropped normalizeMd and atomicWriteFileSync imports (only used as write pre-calls here) Bare fs.readFileSync calls remain in code paths where STATE.md is required to exist (statePath reads in cmd handlers, dry-run prune) — not "Done when" violations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate core.cjs to platform* seam (#3467) - 7 try/readFileSync and existsSync+readFileSync patterns → platformReadSync - 3 fs.writeFileSync (config writes + large-payload temp file) → platformWriteSync - 1 fs.mkdirSync (GSD_TEMP_DIR) → platformEnsureDir Three fs calls remain — they are the internal implementations of the safeReadFile and atomicWriteFileSync wrappers that core.cjs exports for backward compatibility. The wrappers are scheduled for removal in Phase 4 (#3468) and will not be migrated here. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate phase.cjs writes to platform* seam (#3467) - 6 atomicWriteFileSync calls → platformWriteSync - 3 fs.writeFileSync(path.join(dirPath, '.gitkeep'), '') → platformWriteSync - 3 fs.mkdirSync(..., { recursive: true }) → platformEnsureDir Bare fs.readFileSync calls remain for roadmapPath/planPath reads where the file is required to exist; these are not "Done when" violations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate verify.cjs to platform* seam (#3467) - 8 safeReadFile (from core.cjs) calls preserved by aliasing platformReadSync as safeReadFile in the import — same semantics, zero call-site changes - 1 existsSync+readFileSync inline ternary → safeReadFile (returns null) - 5 fs.writeFileSync (config writes + milestones writes) → platformWriteSync Bare fs.readFileSync calls remain for code paths where the file is required to exist (roadmap/state/config full reads); these are not "Done when" violations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(shell-projection): migrate frontmatter.cjs + update atomic-write test (#3467) - frontmatter.cjs: 2 atomicWriteFileSync calls → platformWriteSync. The legacy normalizeMd wrapper is dropped because the seam handles markdown normalization. safeReadFile preserved by aliasing platformReadSync. - atomic-write-coverage.test.cjs: update the #1972 structural invariant to assert on platformWriteSync. platformWriteSync uses the same tmp-file + atomic-rename primitive that atomicWriteFileSync did — the no-partial-write guarantee is preserved across the migration. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(changeset): add entry for shell-projection Phase 3 migration (#3467) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(coderabbit): disable ESLint tool (repo uses custom lint scripts) CodeRabbit's review surface emits a "skipped: no ESLint configuration" warning because the repo doesn't ship ESLint config. The repo intentionally does not use ESLint — it ships its own targeted lint scripts (scripts/lint-no-source-grep.cjs, npm run lint:tests) that enforce repo-specific test-quality invariants. Adding ESLint config purely to satisfy CR would add an external dependency (CONTRIBUTING.md: "No external dependencies in core") and overlap with the existing custom lint surface. Disable the ESLint tool in CR's tools config so the skip warning stops appearing on every PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
98 lines
3.4 KiB
JavaScript
98 lines
3.4 KiB
JavaScript
/**
|
|
* Structural regression guard for atomic write usage (#1972).
|
|
*
|
|
* Ensures that milestone.cjs, phase.cjs, and frontmatter.cjs do NOT
|
|
* contain bare fs.writeFileSync calls targeting .planning/ files. All
|
|
* such writes must go through platformWriteSync (the shell-projection
|
|
* seam's atomic writer) to prevent partial writes from corrupting planning
|
|
* artifacts on crash. platformWriteSync uses the same tmp-file + rename
|
|
* primitive as the legacy atomicWriteFileSync — migrated in #3467.
|
|
*
|
|
* Allowed exceptions:
|
|
* - Writes to .gitkeep (empty files, no corruption risk)
|
|
* - Writes to archive directories (new files, not read-modify-write)
|
|
*
|
|
* This test is structural — it reads the source files and parses for
|
|
* bare writeFileSync patterns. It complements functional tests in
|
|
* atomic-write.test.cjs which verify the helper itself.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const libDir = path.resolve(__dirname, '..', 'get-shit-done', 'bin', 'lib');
|
|
|
|
/**
|
|
* Find all fs.writeFileSync(...) call sites in a file.
|
|
* Returns array of { line: number, text: string }.
|
|
*/
|
|
function findBareWrites(filePath) {
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const lines = content.split('\n');
|
|
const hits = [];
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (/\bfs\.writeFileSync\s*\(/.test(lines[i])) {
|
|
hits.push({ line: i + 1, text: lines[i].trim() });
|
|
}
|
|
}
|
|
return hits;
|
|
}
|
|
|
|
/**
|
|
* Classify a bare write as allowed (archive, .gitkeep) or disallowed.
|
|
*/
|
|
function isAllowedException(lineText) {
|
|
// .gitkeep writes (empty file, no corruption risk)
|
|
if (/\.gitkeep/.test(lineText)) return true;
|
|
// Archive directory writes (new files, not read-modify-write)
|
|
if (/archiveDir/.test(lineText)) return true;
|
|
return false;
|
|
}
|
|
|
|
describe('atomic write coverage (#1972)', () => {
|
|
const targetFiles = ['milestone.cjs', 'phase.cjs', 'frontmatter.cjs'];
|
|
|
|
for (const file of targetFiles) {
|
|
test(`${file}: all fs.writeFileSync calls target allowed exceptions`, () => {
|
|
const filePath = path.join(libDir, file);
|
|
assert.ok(fs.existsSync(filePath), `${file} must exist at ${filePath}`);
|
|
|
|
const hits = findBareWrites(filePath);
|
|
const violations = hits.filter(h => !isAllowedException(h.text));
|
|
|
|
if (violations.length > 0) {
|
|
const report = violations.map(v => ` line ${v.line}: ${v.text}`).join('\n');
|
|
assert.fail(
|
|
`${file} contains ${violations.length} bare fs.writeFileSync call(s) targeting planning files.\n` +
|
|
`These should use platformWriteSync instead:\n${report}`
|
|
);
|
|
}
|
|
});
|
|
|
|
test(`${file}: imports platformWriteSync from shell-command-projection.cjs`, () => {
|
|
const filePath = path.join(libDir, file);
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
assert.match(
|
|
content,
|
|
/platformWriteSync[^)]*\}\s*=\s*require\(['"]\.\/shell-command-projection\.cjs['"]\)/s,
|
|
`${file} must import platformWriteSync from shell-command-projection.cjs`
|
|
);
|
|
});
|
|
}
|
|
|
|
test('all three files use platformWriteSync at least once', () => {
|
|
for (const file of targetFiles) {
|
|
const content = fs.readFileSync(path.join(libDir, file), 'utf-8');
|
|
assert.match(
|
|
content,
|
|
/platformWriteSync\s*\(/,
|
|
`${file} must contain at least one platformWriteSync call`
|
|
);
|
|
}
|
|
});
|
|
});
|