From 0a003763541a3f685ccbf5269c274a9dbfd41f5a Mon Sep 17 00:00:00 2001 From: Jacob Weyer Date: Fri, 17 Jul 2026 17:56:46 -0400 Subject: [PATCH] fix: auto-frontmatter legacy wiki pages --- src/agent/index-middleware.ts | 81 ++++++++++++++++++++++++++++++++++- test/index-middleware.test.ts | 61 ++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/src/agent/index-middleware.ts b/src/agent/index-middleware.ts index cb3b64c8..72bdf44a 100644 --- a/src/agent/index-middleware.ts +++ b/src/agent/index-middleware.ts @@ -101,10 +101,12 @@ async function synchronizeDirectory( } const filePath = path.posix.join(directory.path, name); - const metadata = parseFrontmatter( - await readText(backend, filePath), + const content = await ensureFrontmatter( + backend, filePath, + await readText(backend, filePath), ); + const metadata = parseFrontmatter(content, filePath); files.push({ description: metadata.description, href: encodeURIComponent(name), @@ -133,6 +135,81 @@ async function synchronizeDirectory( } } +/** Adds minimal OKF front matter to legacy Markdown so indexing is zero-touch. */ +async function ensureFrontmatter( + backend: BackendProtocolV2, + filePath: string, + content: string, +): Promise { + if (hasFrontmatterOpening(content)) return content; + + const title = + firstHeading(content) ?? + titleFromSlug(path.posix.basename(filePath, ".md")); + const description = firstParagraph(content) ?? `OpenWiki page for ${title}.`; + const repaired = `${renderFallbackFrontmatter(title, description)}${content}`; + const result = await backend.edit(filePath, content, repaired); + if (result.error) { + throw new Error(`Unable to add front matter to ${filePath}: ${result.error}`); + } + return repaired; +} + +/** Checks only for a leading delimiter; malformed YAML still fails in parsing. */ +function hasFrontmatterOpening(content: string): boolean { + return /^---\r?\n/u.test(content); +} + +/** Renders minimal supported OpenWiki front matter for legacy pages. */ +function renderFallbackFrontmatter(title: string, description: string): string { + return `---\ntype: Reference\ntitle: ${JSON.stringify(title)}\ndescription: ${JSON.stringify(description)}\n---\n\n`; +} + +/** Extracts the first Markdown heading as display title. */ +function firstHeading(content: string): string | undefined { + const match = /^#\s+(.+?)\s*#*\s*$/mu.exec(content); + return match?.[1]?.trim() || undefined; +} + +/** Extracts a compact first prose paragraph for index descriptions. */ +function firstParagraph(content: string): string | undefined { + const blocks = content.split(/\r?\n\s*\r?\n/u); + for (const block of blocks) { + const normalized = block + .split(/\r?\n/u) + .map((line) => line.trim()) + .filter(Boolean) + .join(" "); + if ( + !normalized || + normalized.startsWith("#") || + normalized.startsWith("```") || + normalized.startsWith("|") + ) { + continue; + } + + return truncateDescription(stripMarkdownSyntax(normalized)); + } + return undefined; +} + +/** Removes lightweight inline Markdown markers from synthesized descriptions. */ +function stripMarkdownSyntax(value: string): string { + return value + .replace(/!\[([^\]]*)\]\([^)]+\)/gu, "$1") + .replace(/\[([^\]]+)\]\([^)]+\)/gu, "$1") + .replace(/[`*_~]+/gu, "") + .trim(); +} + +/** Keeps generated descriptions readable in indexes and front matter. */ +function truncateDescription(value: string): string { + const maxLength = 180; + if (value.length <= maxLength) return value; + return `${value.slice(0, maxLength - 3).trimEnd()}...`; +} + /** Renders a complete deterministic index document. */ function renderIndex( title: string, diff --git a/test/index-middleware.test.ts b/test/index-middleware.test.ts index 03fe0b32..4d36bd2a 100644 --- a/test/index-middleware.test.ts +++ b/test/index-middleware.test.ts @@ -95,6 +95,67 @@ describe("synchronizeWikiIndexes", () => { expect(repaired).not.toContain("_plan.md"); }); + test("adds front matter to legacy Markdown before indexing", async () => { + const { backend, rootDir } = await setup(); + await backend.write( + "/openwiki/runtime-provisioning.md", + [ + "# Runtime Provisioning", + "", + "Explains AMI build, boot-time setup, and PAM hooks.", + "", + "## Details", + "", + "More content.", + ].join("\n"), + ); + + await synchronizeWikiIndexes(backend, "repository"); + + const page = await readFile( + path.join(rootDir, "openwiki/runtime-provisioning.md"), + "utf8", + ); + const index = await readFile( + path.join(rootDir, "openwiki/index.md"), + "utf8", + ); + + expect(page).toMatch(/^---\ntype: Reference\n/); + expect(page).toContain("title: \"Runtime Provisioning\""); + expect(page).toContain( + 'description: "Explains AMI build, boot-time setup, and PAM hooks."', + ); + expect(page).toContain("\n---\n\n# Runtime Provisioning"); + expect(index).toContain( + "- [Runtime Provisioning](runtime-provisioning.md) - Explains AMI build, boot-time setup, and PAM hooks.", + ); + }); + + test("uses filename and generic description for empty legacy Markdown", async () => { + const { backend, rootDir } = await setup(); + await backend.write("/openwiki/source-map.md", ""); + + await synchronizeWikiIndexes(backend, "repository"); + + const page = await readFile( + path.join(rootDir, "openwiki/source-map.md"), + "utf8", + ); + const index = await readFile( + path.join(rootDir, "openwiki/index.md"), + "utf8", + ); + + expect(page).toContain("title: \"Source Map\""); + expect(page).toContain( + 'description: "OpenWiki page for Source Map."', + ); + expect(index).toContain( + "- [Source Map](source-map.md) - OpenWiki page for Source Map.", + ); + }); + test("indexes a valid OKF file without an optional description", async () => { const { backend, rootDir } = await setup(); await backend.write(