Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions src/agent/index-middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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<string> {
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,
Expand Down
61 changes: 61 additions & 0 deletions test/index-middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down