Skip to content

Commit 4c7ebf0

Browse files
committed
fix(memory-wiki): wiki_apply errors when another page has bad frontmatter
wiki_apply and wiki_lint recompile the whole vault after their own write, which parses every page's frontmatter. One page with unparsable YAML (e.g. a sourceIds list item containing an inline colon) threw out of the scan and failed the entire operation, even though the requested write or lint had already succeeded. toWikiPageSummary now degrades a page with bad frontmatter to an empty frontmatter instead of throwing, recording the parse failure on the summary. Lint surfaces it as an invalid-frontmatter issue on that page instead of crashing the whole vault scan. Callers that parse one specific page they're about to mutate still throw as before. Closes #96125
1 parent d96ac02 commit 4c7ebf0

4 files changed

Lines changed: 111 additions & 4 deletions

File tree

extensions/memory-wiki/src/lint.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,4 +430,43 @@ describe("lintMemoryWikiVault", () => {
430430
await expect(fs.readFile(result.reportPath, "utf8")).resolves.toContain("### Contradictions");
431431
await expect(fs.readFile(result.reportPath, "utf8")).resolves.toContain("### Open Questions");
432432
});
433+
434+
it("reports unparsable frontmatter as a lint issue instead of failing the whole vault (#96125)", async () => {
435+
const { rootDir, config } = await createVault({
436+
prefix: "memory-wiki-lint-invalid-frontmatter-",
437+
});
438+
await fs.mkdir(path.join(rootDir, "syntheses"), { recursive: true });
439+
await fs.writeFile(
440+
path.join(rootDir, "syntheses", "broken.md"),
441+
[
442+
"---",
443+
"pageType: synthesis",
444+
"id: synthesis.broken",
445+
"sourceIds:",
446+
' - **MEMORY.md line 235**:"some quoted, value"',
447+
"---",
448+
"",
449+
"# Broken\n",
450+
].join("\n"),
451+
"utf8",
452+
);
453+
await fs.writeFile(
454+
path.join(rootDir, "syntheses", "healthy.md"),
455+
renderWikiMarkdown({
456+
frontmatter: {
457+
pageType: "synthesis",
458+
id: "synthesis.healthy",
459+
title: "Healthy",
460+
sourceIds: ["source.alpha"],
461+
},
462+
body: "# Healthy\n",
463+
}),
464+
"utf8",
465+
);
466+
467+
const result = await lintMemoryWikiVault(config);
468+
469+
expect(issueCodesForPath(result, "syntheses/broken.md")).toEqual(["invalid-frontmatter"]);
470+
expect(issueCodesForPath(result, "syntheses/healthy.md")).not.toContain("invalid-frontmatter");
471+
});
433472
});

extensions/memory-wiki/src/lint.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type MemoryWikiLintIssue = {
2424
severity: "error" | "warning";
2525
category: "structure" | "provenance" | "links" | "contradictions" | "open-questions" | "quality";
2626
code:
27+
| "invalid-frontmatter"
2728
| "missing-id"
2829
| "duplicate-id"
2930
| "missing-page-type"
@@ -100,6 +101,20 @@ function collectPageIssues(
100101
const claimHealth = collectWikiClaimHealth(pages);
101102

102103
for (const page of pages) {
104+
if (page.frontmatterError) {
105+
issues.push({
106+
severity: "error",
107+
category: "structure",
108+
code: "invalid-frontmatter",
109+
path: page.relativePath,
110+
message: `Frontmatter failed to parse: ${page.frontmatterError}`,
111+
});
112+
// Frontmatter-derived fields below are empty as a result of the parse
113+
// failure; skip checks that would otherwise pile on misleading
114+
// "missing field" issues for the same root cause.
115+
continue;
116+
}
117+
103118
const requiresStructuredPageMetadata = !isUnmanagedRawSourcePage(
104119
page,
105120
managedImportedSourcePagePaths,

extensions/memory-wiki/src/markdown.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,4 +421,32 @@ describe("toWikiPageSummary", () => {
421421
},
422422
]);
423423
});
424+
425+
it("degrades to empty frontmatter instead of throwing on unparsable YAML (#96125)", () => {
426+
const raw = [
427+
"---",
428+
"pageType: synthesis",
429+
"id: synthesis.test",
430+
"sourceIds:",
431+
' - **MEMORY.md line 235**:"some quoted, value"',
432+
"---",
433+
"",
434+
"# Test\n",
435+
].join("\n");
436+
437+
const summary = toWikiPageSummary({
438+
absolutePath: "/tmp/wiki/syntheses/test.md",
439+
relativePath: "syntheses/test.md",
440+
raw,
441+
});
442+
if (!summary) {
443+
throw new Error("expected wiki summary");
444+
}
445+
446+
expect(summary.frontmatterError).toBeTruthy();
447+
expect(summary.hasFrontmatter).toBe(true);
448+
expect(summary.id).toBeUndefined();
449+
expect(summary.sourceIds).toEqual([]);
450+
expect(summary.title).toBe("Test");
451+
});
424452
});

extensions/memory-wiki/src/markdown.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ export type WikiPageSummary = {
7979
kind: WikiPageKind;
8080
title: string;
8181
hasFrontmatter: boolean;
82+
/** Set when frontmatter YAML failed to parse; frontmatter-derived fields fall back to empty. */
83+
frontmatterError?: string;
8284
id?: string;
8385
pageType?: string;
8486
entityType?: string;
@@ -173,19 +175,27 @@ export function createWikiPageFilename(stem: string, extension = ".md"): string
173175
return `${capWikiValueWithHash(stem, maxStemBytes, "page")}${normalizedExtension}`;
174176
}
175177

176-
export function parseWikiMarkdown(content: string): ParsedWikiMarkdown {
178+
function splitWikiFrontmatterBlock(content: string): { yamlSource: string; body: string } | null {
177179
const match = content.match(FRONTMATTER_PATTERN);
178180
if (!match) {
181+
return null;
182+
}
183+
return { yamlSource: match[1] ?? "", body: content.slice(match[0].length) };
184+
}
185+
186+
export function parseWikiMarkdown(content: string): ParsedWikiMarkdown {
187+
const split = splitWikiFrontmatterBlock(content);
188+
if (!split) {
179189
return { hasFrontmatter: false, frontmatter: {}, body: content };
180190
}
181-
const parsed = YAML.parse(match[1]) as unknown;
191+
const parsed = YAML.parse(split.yamlSource) as unknown;
182192
return {
183193
hasFrontmatter: true,
184194
frontmatter:
185195
parsed && typeof parsed === "object" && !Array.isArray(parsed)
186196
? (parsed as Record<string, unknown>)
187197
: {},
188-
body: content.slice(match[0].length),
198+
body: split.body,
189199
};
190200
}
191201

@@ -571,7 +581,21 @@ export function toWikiPageSummary(params: {
571581
if (!kind) {
572582
return null;
573583
}
574-
const parsed = parseWikiMarkdown(params.raw);
584+
// A page with unparsable YAML frontmatter must not crash vault-wide scans
585+
// (compile/lint/query) over unrelated pages; degrade to empty frontmatter
586+
// and surface the failure via WikiPageSummary.frontmatterError instead.
587+
let parsed: ParsedWikiMarkdown;
588+
let frontmatterError: string | undefined;
589+
try {
590+
parsed = parseWikiMarkdown(params.raw);
591+
} catch (error) {
592+
frontmatterError = error instanceof Error ? error.message : String(error);
593+
parsed = {
594+
hasFrontmatter: true,
595+
frontmatter: {},
596+
body: splitWikiFrontmatterBlock(params.raw)?.body ?? params.raw,
597+
};
598+
}
575599
const title =
576600
(typeof parsed.frontmatter.title === "string" && parsed.frontmatter.title.trim()) ||
577601
extractTitleFromMarkdown(parsed.body) ||
@@ -592,6 +616,7 @@ export function toWikiPageSummary(params: {
592616
kind,
593617
title,
594618
hasFrontmatter: parsed.hasFrontmatter,
619+
...(frontmatterError ? { frontmatterError } : {}),
595620
id: normalizeOptionalString(parsed.frontmatter.id),
596621
pageType: normalizeOptionalString(parsed.frontmatter.pageType),
597622
entityType: normalizeOptionalString(parsed.frontmatter.entityType),

0 commit comments

Comments
 (0)