mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* 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:
+34
-18
@@ -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}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user