fix(#3116): strip trailing CR per line in parseFrontmatterStrict for CRLF WINDOWS.md (#3137)

* test(#3116): failing-first — parseLedger throws on CRLF WINDOWS.md

On repos with core.autocrlf=true (Windows default), .planning/WINDOWS.md
is checked out CRLF. The \n--- close-fence scan leaves the last frontmatter
line's CR attached, and the key:value regex's . doesn't match CR, so the
parser throws WINDOWS_LEDGER_MALFORMED on the last key.

* fix(#3116): strip trailing CR per line in parseFrontmatterStrict

The \n--- close-fence scan lands on the LF of the last frontmatter line's
CRLF, so yamlBody ends with a bare \r. split(/\r?\n/) strips CR from
interior lines but the last line's \r survives. The key:value regex fails
because . doesn't match CR.

Fix: strip \r per line (rawLine.replace(/\r$/, '')) rather than normalizing
raw — the writer round-trips raw byte-exact.

* chore(#3116): add changeset fragment

* chore(#3116): backfill changeset PR number 3137

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-07 03:14:40 -04:00
committed by GitHub
parent 0e6fa2e2cf
commit a731a45cd6
3 changed files with 65 additions and 1 deletions

View File

@@ -401,7 +401,12 @@ function parseFrontmatterStrict(raw: string): Record<string, number | string> {
}
const yamlBody = raw.slice(headerEnd, closeIdx);
const out: Record<string, number | string> = {};
for (const line of yamlBody.split(/\r?\n/)) {
for (const rawLine of yamlBody.split(/\r?\n/)) {
// #3116: the `\n---` scan leaves the final line's CR attached on a CRLF
// ledger, and `.` never matches CR, so the key: value regex below fails on
// it. Strip the trailing CR per line so the rest of `raw` (which
// parseJsonBlock also slices by byte offset) is unaffected.
const line = rawLine.replace(/\r$/, '');
if (line.trim() === '') continue;
const m = line.match(/^([a-zA-Z0-9_]+):\s*(.*)$/);
if (!m) {