* 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:
5
.changeset/eager-lynx-squeak.md
Normal file
5
.changeset/eager-lynx-squeak.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3137
|
||||
---
|
||||
**`gsd-tools windows` no longer crashes on CRLF ledgers** — on repos with `core.autocrlf=true` (Windows default), the frontmatter parser threw on the last key of a CRLF `WINDOWS.md`, making the broken-windows status/waive/fixed subcommands unusable. (#3116)
|
||||
@@ -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) {
|
||||
|
||||
@@ -783,3 +783,57 @@ describe('broken-windows CLI: lifecycle', () => {
|
||||
assert.equal(status.ledger.total_count, 2);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// #3116: parseFrontmatterStrict 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 line's CR
|
||||
// attached, and `.` doesn't match CR, so the key:value regex fails.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('#3116: parseLedger handles CRLF ledgers', () => {
|
||||
// Build ledgers via renderLedger (the real writer) so the JSON fence
|
||||
// format (4-backtick) and structure always match what production emits.
|
||||
// parseLedger validates that frontmatter counts match the entries array,
|
||||
// so non-zero counts require real entries (appendWindow).
|
||||
|
||||
test('CRLF empty ledger parses without throwing', () => {
|
||||
const ledger = emptyLedger();
|
||||
ledger.last_updated = '2026-08-06T09:43:08.354Z';
|
||||
const lfLedger = renderLedger(ledger);
|
||||
const crlfLedger = lfLedger.replace(/\n/g, '\r\n');
|
||||
|
||||
// Must not throw — before the fix this throws WINDOWS_LEDGER_MALFORMED
|
||||
// on the last frontmatter key ("last_updated: ...\r")
|
||||
const parsed = parseLedger(crlfLedger);
|
||||
assert.equal(parsed.schema_version, 1);
|
||||
assert.equal(parsed.open_count, 0);
|
||||
assert.equal(parsed.last_updated, '2026-08-06T09:43:08.354Z');
|
||||
});
|
||||
|
||||
test('CRLF ledger with entries parses correctly', () => {
|
||||
let ledger = emptyLedger();
|
||||
const { ledger: led1 } = appendWindow(ledger, makeEntry(), { now: '2026-08-06T12:00:00Z' });
|
||||
const { ledger: led2 } = appendWindow(led1, makeEntry({ description: 'second' }), { now: '2026-08-06T12:01:00Z' });
|
||||
ledger = led2;
|
||||
const lfLedger = renderLedger(ledger);
|
||||
const crlfLedger = lfLedger.replace(/\n/g, '\r\n');
|
||||
|
||||
const parsed = parseLedger(crlfLedger);
|
||||
assert.equal(parsed.open_count, 2);
|
||||
assert.equal(parsed.total_count, 2);
|
||||
assert.equal(parsed.entries.length, 2);
|
||||
});
|
||||
|
||||
test('CRLF and LF ledgers produce identical parse results', () => {
|
||||
let ledger = emptyLedger();
|
||||
const { ledger: led1 } = appendWindow(ledger, makeEntry(), { now: '2026-08-06T09:43:08Z' });
|
||||
ledger = led1;
|
||||
const lfLedger = renderLedger(ledger);
|
||||
|
||||
const lfParsed = parseLedger(lfLedger);
|
||||
const crlfParsed = parseLedger(lfLedger.replace(/\n/g, '\r\n'));
|
||||
|
||||
assert.deepEqual(crlfParsed, lfParsed);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user