fix(frontmatter): stop treating YAML comments inside the fence as markdown headings (#3225) (#3247)

* fix(frontmatter): stop treating YAML comments inside the fence as markdown headings

autoFixFrontmatter's MISSING_CLOSE repair walked lines from the opening
`---` and broke out of the scan on the first `#`-prefixed line, treating
it as a markdown heading before it ever reached the real closing fence.
A `#` line inside a closed YAML block is a comment, not a heading — but
the scan never got that far, so it inserted a spurious `---` right
before the comment and split valid frontmatter in two, pushing the real
keys (title, pubDate, ...) into the document body.

This is the same bug PR #2153 fixed in the parseMarkdown validator, but
autoFixFrontmatter in brain-writer.ts is a separate reimplementation of
the same MISSING_CLOSE logic that PR never touched. Because
parseMarkdown's validator now parses this shape cleanly, autoFixFrontmatter
is only reachable when some other fixable error (SLUG_MISMATCH, NULL_BYTES,
etc.) also fires on the same file — a common real-world case (e.g. a
renamed file with a stale slug: field) that still corrupts otherwise-valid
frontmatter today.

Fix: scan the full zone for the closing `---` first; only fall back to
the heading-shaped-line heuristic when no closer is found at all.

Addresses the report in #3225. Thanks to @WilliamCourterWelch for the
clear repro and for catching this via git diff before it reached a live
site.

Tests: 4 new regression cases in test/brain-writer.test.ts covering a
YAML comment before the close, comment-only frontmatter, a `#` inside a
quoted string value, and a comment co-occurring with an unrelated real
fix (SLUG_MISMATCH) — confirmed all 3 corruption-covering cases fail
against the pre-fix code (stash/red/restore) and pass after the fix.
The pre-existing genuinely-missing-closer case is unchanged.

bun test test/brain-writer.test.ts test/markdown-validation.test.ts
test/markdown.test.ts test/lint-frontmatter.test.ts
test/doctor-frontmatter-partial.test.ts test/frontmatter-cli.test.ts
-> 122 pass / 0 fail. bun run typecheck -> clean. Full suite intentionally
not run locally (targeted scope per contribution norms); CI covers it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(frontmatter): swap non-exercising regression case per codex review

The quoted-string test (title: "Chapter #1 recap") never exercised the
fixed branch — the heading regex is line-anchored on the trimmed line,
so a `#` mid-string never matched before or after the fix. Replace it
with an indented `#` line inside a YAML block scalar, which does hit
the same closer-first-scan code path as the other regression cases
with a different real-world shape.

bun test test/brain-writer.test.ts -> 27 pass / 0 fail. bun run
typecheck -> clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Masa
2026-07-23 14:21:39 -07:00
committed by GitHub
co-authored by Claude Fable 5
parent b82f520314
commit 4be9d112cb
2 changed files with 71 additions and 18 deletions
+34 -18
View File
@@ -139,8 +139,21 @@ export function autoFixFrontmatter(
fixes.push({ code: 'NULL_BYTES', description: 'Stripped null bytes' });
}
// 2. MISSING_CLOSE — if there's an opener but no closer before a heading,
// insert `---` immediately before the heading. Walk lines once.
// 2. MISSING_CLOSE — if there's an opener but no closer at all, insert
// `---` immediately before the first heading-shaped line (best-effort
// guess at where the frontmatter was meant to end).
//
// Find the closer FIRST, scanning the full zone — do not stop at the
// first `#`-prefixed line. A `#` line between the opening and closing
// `---` is a YAML comment (comments are valid anywhere in a YAML
// document), not a markdown heading; only the genuine absence of a
// closing `---` counts as MISSING_CLOSE. Mirrors the fix applied to
// the parseMarkdown validator in #2153 — this is the sibling
// reimplementation in the auto-fixer and had the same bug (it broke
// out of the scan on the first heading-shaped line, so a `#` comment
// appearing before a real closing fence was misdetected as
// MISSING_CLOSE and the fix inserted a spurious `---` that split
// valid frontmatter in two, pushing the real keys into the body).
{
const lines = working.split('\n');
let firstNonEmpty = -1;
@@ -149,24 +162,27 @@ export function autoFixFrontmatter(
}
if (firstNonEmpty >= 0 && lines[firstNonEmpty].trim() === '---') {
let closeIdx = -1;
let headingIdx = -1;
for (let i = firstNonEmpty + 1; i < lines.length; i++) {
const t = lines[i].trim();
if (t === '---') { closeIdx = i; break; }
if (/^#{1,6}\s/.test(t)) { headingIdx = i; break; }
if (lines[i].trim() === '---') { closeIdx = i; break; }
}
if (closeIdx === -1 && headingIdx >= 0) {
const fixed = [
...lines.slice(0, headingIdx),
'---',
'',
...lines.slice(headingIdx),
];
working = fixed.join('\n');
fixes.push({
code: 'MISSING_CLOSE',
description: `Inserted closing --- before heading at line ${headingIdx + 1}`,
});
if (closeIdx === -1) {
let headingIdx = -1;
for (let i = firstNonEmpty + 1; i < lines.length; i++) {
if (/^#{1,6}\s/.test(lines[i].trim())) { headingIdx = i; break; }
}
if (headingIdx >= 0) {
const fixed = [
...lines.slice(0, headingIdx),
'---',
'',
...lines.slice(headingIdx),
];
working = fixed.join('\n');
fixes.push({
code: 'MISSING_CLOSE',
description: `Inserted closing --- before heading at line ${headingIdx + 1}`,
});
}
}
}
}
+37
View File
@@ -32,6 +32,43 @@ describe('autoFixFrontmatter', () => {
expect(idxClose).toBeLessThan(idxHeading);
});
// Regression for #3225: a `#`-prefixed line inside an already-closed
// frontmatter fence is a YAML comment, not a markdown heading. The old
// MISSING_CLOSE scan broke out on the first heading-shaped line without
// continuing to look for the real closer, so it inserted a spurious
// `---` before the comment and split valid frontmatter in two — pushing
// the real keys (title, pubDate, ...) into the document body.
test('does not corrupt closed frontmatter containing a YAML comment line', () => {
const input = `${fence}\n# a YAML comment inside the frontmatter block\ntitle: "Real Title"\npubDate: 2026-06-29\n${fence}\nBody...`;
const { content, fixes } = autoFixFrontmatter(input);
expect(content).toBe(input);
expect(fixes).toEqual([]);
});
test('does not corrupt closed frontmatter that is comment-only', () => {
const input = `${fence}\n# just a comment\n# another comment\n${fence}\nBody`;
const { content, fixes } = autoFixFrontmatter(input);
expect(content).toBe(input);
expect(fixes).toEqual([]);
});
test('does not corrupt closed frontmatter with an indented `#` line inside a YAML block scalar', () => {
const input = `${fence}\ndescription: |\n # not a heading, just literal block-scalar text\ntitle: ok\n${fence}\nBody`;
const { content, fixes } = autoFixFrontmatter(input);
expect(content).toBe(input);
expect(fixes).toEqual([]);
});
test('YAML comment before close does not suppress an unrelated real fix (SLUG_MISMATCH)', () => {
const input = `${fence}\n# a YAML comment\ntitle: hi\nslug: wrong-slug\n${fence}\nBody`;
const { content, fixes } = autoFixFrontmatter(input, { filePath: 'people/jane-doe.md' });
expect(fixes.some(f => f.code === 'MISSING_CLOSE')).toBe(false);
expect(fixes.some(f => f.code === 'SLUG_MISMATCH')).toBe(true);
// The frontmatter fence itself must stay intact — only the slug line
// is removed, the comment/title/close survive unchanged.
expect(content).toBe(`${fence}\n# a YAML comment\ntitle: hi\n\n${fence}\nBody`);
});
test('rewrites nested-quote title to single-quoted', () => {
const input = `${fence}\ntype: concept\ntitle: "Phil "Nick" Last"\n${fence}\n\nbody`;
const { content, fixes } = autoFixFrontmatter(input);