fix(#3657): tolerate commonmark fence widths in ledger readers (#3733)

* test(#3657): fence-width tolerance regression rows

* test(#3657): fix pure-row fixtures to use appendWindow result shape

* fix(#3657): tolerate commonmark fence widths in ledger readers

* fix(#3657): restore throw-block indentation in parseJsonBlock

* chore(#3657): backfill changeset pr number

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-21 03:45:06 -04:00
committed by GitHub
parent 72819a4616
commit 1f76861202
5 changed files with 380 additions and 31 deletions

View File

@@ -874,6 +874,29 @@ export function reanchorBaseline(
const PROPOSAL_JSON_FENCE_OPEN = '````json';
const PROPOSAL_JSON_FENCE_CLOSE = '````';
// Reader-side fence tolerance (#3657 — same defect class as the WINDOWS.md
// ledger): CommonMark formatters (Prettier et al.) narrow the written
// 4-backtick fence to the shortest legal width (3) whenever the body holds no
// backtick run, and a canonical-JSON candidates array never does. Locate the
// block by a line-anchored 3+ fence and close on a run at least as wide
// (CommonMark: a shorter run does not close). The writer above is unchanged.
// This module is a leaf (CONTEXT.md — imports only node:fs/node:path), so the
// span logic is local rather than imported from broken-windows.
const PROPOSAL_FENCE_OPEN_RE = /^(`{3,})json[ \t]*\r?$/m;
function locateProposalJsonBlock(text: string): { jsonText: string } | null {
const open = text.match(PROPOSAL_FENCE_OPEN_RE);
if (!open || open.index === undefined) return null;
const width = open[1].length;
const bodyStart = open.index + open[0].length;
for (const close of text.slice(bodyStart).matchAll(/^(`{3,})[ \t]*\r?$/gm)) {
if (close[1].length < width) continue;
const bodyEnd = close.index ?? 0;
return { jsonText: text.slice(bodyStart, bodyStart + bodyEnd).trim() };
}
return null;
}
export function renderProposal(p: Proposal): string {
const fm = [
'---',
@@ -931,11 +954,9 @@ export function parseProposal(text: string): Proposal | null {
fm[m[1]] = m[2].trim();
}
const jsonStart = text.indexOf(PROPOSAL_JSON_FENCE_OPEN);
if (jsonStart === -1) return null;
const jsonEnd = text.indexOf(PROPOSAL_JSON_FENCE_CLOSE, jsonStart + PROPOSAL_JSON_FENCE_OPEN.length);
if (jsonEnd === -1) return null;
const jsonText = text.slice(jsonStart + PROPOSAL_JSON_FENCE_OPEN.length, jsonEnd).trim();
const span = locateProposalJsonBlock(text);
if (span === null) return null;
const jsonText = span.jsonText;
let candidates: unknown;
try {
candidates = JSON.parse(jsonText);