Skip to content

fix: normalise YAML block scalars in LLM-generated frontmatter - #561

Open
AndrewDongminYoo wants to merge 3 commits into
nashsu:mainfrom
AndrewDongminYoo:fix/normalise-yaml-block-scalars
Open

fix: normalise YAML block scalars in LLM-generated frontmatter#561
AndrewDongminYoo wants to merge 3 commits into
nashsu:mainfrom
AndrewDongminYoo:fix/normalise-yaml-block-scalars

Conversation

@AndrewDongminYoo

@AndrewDongminYoo AndrewDongminYoo commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

What

Normalize top-level YAML block scalars in LLM-generated frontmatter without reserializing unrelated YAML.

Why

LLMs sometimes emit folded or literal block scalars that downstream rendering does not support. Parsing and dumping the entire frontmatter fixed the scalar but also removed comments and rewrote quoting, arrays, line endings, and other author formatting.

How

  • Fast-exit when no top-level block scalar indicator exists.
  • Parse each affected scalar in isolation and replace only its lines with the equivalent inline value.
  • Preserve unrelated comments, quoting, key order, inline arrays, CRLF, and whitespace-padded fences byte-for-byte.

Validation

  • npm run test:mocks -- --run src/lib/ingest-sanitize.test.ts (28 passed)
  • npm run typecheck
  • npm run test:mocks (118 files, 1743 tests passed)
  • npm run build

@chuenchen309 chuenchen309 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is clean — nice that it round-trips through js-yaml with a fast-exit guard instead of regex-munging the values. One correctness issue I think is worth fixing before merge:

The + 4 offsets in the reconstruction assume the opening fence is exactly ---\n, but fmRe matches more than that. ^---\s*\r?\n also matches ---\r\n (CRLF) and --- \n (trailing space), where the opening delimiter is 5+ chars. In those cases:

content.slice(0, m.index! + 4) + newPayload + content.slice(m.index! + 4 + payload.length)

slices mid-delimiter — for CRLF input the prefix becomes ---\r (the \n is dropped) and the suffix start is off by one, so the last payload character gets duplicated. Net result is corrupted frontmatter on any Windows-line-ending or trailing-whitespace source. Repro is the existing folded-scalar test with \r\n line endings.

Since the exact fence text is already in the match, deriving its length (or capturing the opening/closing fences as regex groups) avoids the hardcoded 4. A CRLF case in the test suite would lock it in — the current tests all use \n so this slips through.

Minor / by design: collapsing |- literal scalars to spaces (per the test) is lossy for fields where line breaks are meaningful. Fine for description; just flagging in case any field is meant to preserve them.

@AndrewDongminYoo
AndrewDongminYoo force-pushed the fix/normalise-yaml-block-scalars branch from 50699b2 to 52b5633 Compare July 16, 2026 05:39
@AndrewDongminYoo

Copy link
Copy Markdown
Contributor Author

Fixed — the reconstruction now captures and reuses the exact opening and closing fence text instead of assuming ---\n is four characters.

I also added a regression test covering CRLF line endings and whitespace-padded fences, which verifies the frontmatter boundary and body remain intact.

@chuenchen309

Copy link
Copy Markdown
Contributor

Looks great — reusing m[1] / m[3] for the exact fences means CRLF and trailing-space delimiters round-trip cleanly, and tightening the padding to [ \t]* (from \s*) is a nice touch: it stops the opening match from greedily reaching into the payload. The new CRLF/whitespace test covers exactly the case I was worried about. Thanks for the quick turnaround!

@AndrewDongminYoo

Copy link
Copy Markdown
Contributor Author

Circling back on the second point, which I left unanswered earlier — the |- collapse is by design, and the constraint is broader than description.

FrontmatterValue is string | string[] (frontmatter.ts:3), so the frontmatter layer has no multi-line representation at all — there's no field a line break could survive into. None of the schema templates in templates.ts define one either; they're title / tags: [] / related: [] plus single-line scalars, and the panel renders each value on one line. So collapsing to spaces matches the invariant the rest of the layer already assumes rather than trading against it.

If a genuinely multi-line field is ever added, this normalizer would need a per-key exemption — that's the right moment to add one, not now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants