* test(#2893): add regression for append destroying prose below JSON ledger writeLedgerAtomic overwrites the entire file with renderLedger(ledger), dropping any prose below the JSON closing fence. The test creates a WINDOWS.md with prose sections below the ledger, appends an entry, and asserts the prose survives. * fix(#2893): preserve prose below the JSON ledger on append/waive/fixed writeLedgerAtomic was overwriting the entire WINDOWS.md with renderLedger(ledger), which reconstructs only the frontmatter + header + table + JSON block — silently destroying any prose a user wrote below the JSON closing fence. Now the writer reads the existing file before overwriting, extracts content after the closing fence, and appends it to the rendered ledger. First-write (no existing file) proceeds normally with no prose to preserve. * fix(#2893): address review — correct fence search + idempotency test BLOCKER from isolated adversarial review: indexOf(JSON_FENCE_CLOSE) matched the OPENING fence ('json' starts with ''), duplicating the entire JSON body as prose on every write. Now searches for the closing fence starting AFTER the opening fence, mirroring parseJsonBlock. Test hardened: non-empty initial ledger, second append (idempotency — prose appears exactly once, exactly one JSON fence open), parseLedger round-trip. * chore(#2893): add changeset fragment * chore(#2893): backfill changeset PR number 2975 --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -721,7 +721,33 @@ function writeLedgerAtomic(cwd: string, ledger: Ledger): void {
|
||||
ensurePlanningDir(cwd);
|
||||
const p = ledgerPath(cwd);
|
||||
const tmp = `${p}.${process.pid}.tmp`;
|
||||
fs.writeFileSync(tmp, renderLedger(ledger), 'utf8');
|
||||
|
||||
// #2893: preserve any prose below the JSON ledger block. renderLedger
|
||||
// reconstructs frontmatter + header + table + JSON — it does not include
|
||||
// trailing prose that users may have written below the closing fence.
|
||||
// Without this, every append/waive/fixed silently destroys that prose.
|
||||
let trailingProse = '';
|
||||
try {
|
||||
const existing = fs.readFileSync(p, 'utf8');
|
||||
// #2893: search for the CLOSING fence starting AFTER the opening fence,
|
||||
// mirroring parseJsonBlock — indexOf(JSON_FENCE_CLOSE) alone would match
|
||||
// the opening fence ('````json' starts with '````').
|
||||
const openIdx = existing.indexOf(JSON_FENCE_OPEN);
|
||||
if (openIdx !== -1) {
|
||||
const fenceEnd = existing.indexOf(JSON_FENCE_CLOSE, openIdx + JSON_FENCE_OPEN.length);
|
||||
if (fenceEnd !== -1) {
|
||||
const afterFence = existing.slice(fenceEnd + JSON_FENCE_CLOSE.length);
|
||||
// Drop leading newlines; keep the rest as prose.
|
||||
trailingProse = afterFence.replace(/^\n+/, '');
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// File doesn't exist yet (first write) — no prose to preserve.
|
||||
}
|
||||
|
||||
const rendered = renderLedger(ledger);
|
||||
const content = trailingProse ? `${rendered}${trailingProse}` : rendered;
|
||||
fs.writeFileSync(tmp, content, 'utf8');
|
||||
try {
|
||||
renameWithRetry(tmp, p);
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user