/* [RFC-0195] Dual SEO + GEO projection for the Programmatic Surface. Renders a baked page into a clean Markdown twin (the AI-readable artifact, RFC-0184) and decides per-entry GEO inclusion. The same dataset × axes that target Google long-tail also feed generative answer engines, with no separate authoring. Suppressed (noindex / non-live) pages are excluded by the caller. Pure. Do not write files or read the network (the kernel command does I/O). RFC-0195: initial GEO projection. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ import type { PageEntry } from "./types.ts"; import type { VirtualRouteEntry } from "./types.ts"; function str(value: unknown): string | undefined { return typeof value === "string" && value.trim() ? value : undefined; } /** A twin renderer for one block type: takes the block's props and returns Markdown lines. */ export type BlockTwinRenderer = (props: Record) => string[]; const heroTwinRenderer: BlockTwinRenderer = (props) => { const header = props.header as Record | undefined; const heading = str(props.heading) ?? str(header?.heading); const lead = str(props.lead) ?? str(props.description) ?? str(props.tagline); const lines: string[] = []; if (heading) lines.push(`## ${heading}`); if (lead) lines.push("", lead); return lines; }; const markdownTwinRenderer: BlockTwinRenderer = (props) => { const heading = str(props.heading); const lead = str(props.lead) ?? str(props.description) ?? str(props.tagline); const lines: string[] = []; if (heading) lines.push(`## ${heading}`); if (lead) lines.push("", lead); return lines; }; const cardGridTwinRenderer: BlockTwinRenderer = (props) => { const heading = str(props.heading); const cards = (props.body as { cards?: Array> } | undefined)?.cards; const lines: string[] = []; if (heading) lines.push(`## ${heading}`); for (const card of cards ?? []) { const title = str(card.title); const description = str(card.description); if (title || description) { lines.push( "", title ? `- **${title}**${description ? `: ${description}` : ""}` : `- ${description}`, ); } } return lines; }; const linkedCardGridTwinRenderer: BlockTwinRenderer = (props) => { const heading = str(props.heading); const cards = (props.cards as Array> | undefined) ?? (props.body as { cards?: Array> } | undefined)?.cards; const lines: string[] = []; if (heading) lines.push(`## ${heading}`); for (const card of cards ?? []) { const title = str(card.title); const description = str(card.description); if (title || description) { lines.push( "", title ? `- **${title}**${description ? `: ${description}` : ""}` : `- ${description}`, ); } } return lines; }; const listCardsTwinRenderer: BlockTwinRenderer = (props) => { const heading = str(props.heading); const items = (props.items as Array> | undefined) ?? (props.body as { items?: Array> } | undefined)?.items; const lines: string[] = []; if (heading) lines.push(`## ${heading}`); for (const item of items ?? []) { const title = str(item.title); const description = str(item.description) ?? str(item.body); if (title || description) { lines.push( "", title ? `- **${title}**${description ? `: ${description}` : ""}` : `- ${description}`, ); } } return lines; }; const ctaTwinRenderer: BlockTwinRenderer = (props) => { const heading = str(props.heading); const lines: string[] = []; if (heading) lines.push(`## ${heading}`); return lines; }; const blockTwinRegistry: Readonly> = { hero: heroTwinRenderer, markdown: markdownTwinRenderer, "card-grid": cardGridTwinRenderer, "linked-card-grid": linkedCardGridTwinRenderer, "list-cards": listCardsTwinRenderer, cta: ctaTwinRenderer, }; /** Fallback: extract any visible string-valued props as bullet lines. */ function fallbackTwinRenderer(props: Record): string[] { const lines: string[] = []; const heading = str(props.heading); if (heading) lines.push(`## ${heading}`); const lead = str(props.lead) ?? str(props.description); if (lead) lines.push("", lead); return lines; } /** * Render a baked page into a Markdown twin. Each block type has a registered twin renderer in * {@link blockTwinRegistry}; unknown block types fall back to a generic visible-text extractor so * they are never silently dropped. New block types register a renderer instead of patching this * function. */ export function renderTwin(page: PageEntry, pathname: string): string { const lines: string[] = []; lines.push(`# ${page.title}`); lines.push(""); if (page.description) { lines.push(page.description); lines.push(""); } lines.push(`Source: ${pathname}`); lines.push(""); for (const block of page.blocks) { const props = (block.props ?? {}) as Record; const renderer = blockTwinRegistry[block.type ?? ""] ?? fallbackTwinRenderer; const blockLines = renderer(props); if (blockLines.length > 0) { lines.push(...blockLines); lines.push(""); } } return lines.join("\n"); } /** A live, non-suppressed entry whose GEO depth is full or twin-only gets a Markdown twin. */ export function includeInTwins(entry: VirtualRouteEntry): boolean { if (!entry.indexable || entry.noindex || !entry.page) return false; return entry.geo !== "off"; } /** A live, non-suppressed entry whose GEO depth is full contributes to llms.txt. */ export function includeInLlms(entry: VirtualRouteEntry): boolean { if (!entry.indexable || entry.noindex) return false; return entry.geo === "full"; }