/* RFC-0143: pure resolution of the per-page `output` projection container. Single resolution point shared by every per-page output generator (sitemap, llms) so the static disk loader, the Astro runtime path, and the sitemap command cannot drift. Do not read files — pure function only. Do not own site-wide policy blocks (ai:, robots:) — those are separate top-level surfaces. Do not own per-generator value semantics beyond defaulting (llms depth lives in llms-policy.ts). RFC-0143: Initial implementation. Folds legacy sitemapExclude + per-page sitemap block into output.sitemap. RFC-0328: Apply legal-page default sitemap category when semanticType is "legal". RFC-0143 cleanup: Removed deprecated sitemapExclude and top-level sitemap alias handling. */ import type { PageOutputProjection, RobotsProjection, SemanticImage, SitemapProjection, } from "./models.ts"; import { resolveLlmsPolicy, type RawLlmsPolicy } from "./llms-policy.ts"; /** Raw `output.image` as authored in system.md. RFC-0165/0167. */ export type RawImageProjection = { url: string; alt?: string; width?: number; height?: number; }; /** * Resolve the author-declared per-page content image. Returns a SemanticImage * tagged `contentImage: true` (eligible for the image sitemap), or undefined * when no image is declared or the url is empty. */ function resolveImage(raw: RawImageProjection | undefined): SemanticImage | undefined { if (!raw || typeof raw.url !== "string" || raw.url.trim() === "") return undefined; return { url: raw.url, ...(raw.alt !== undefined ? { alt: raw.alt } : {}), ...(raw.width !== undefined ? { width: raw.width } : {}), ...(raw.height !== undefined ? { height: raw.height } : {}), contentImage: true, }; } /** Raw `output.robots` as authored in system.md. RFC-0165. */ export type RawRobotsProjection = | boolean | { index?: boolean; follow?: boolean; }; function resolveRobots(raw: RawRobotsProjection | undefined): RobotsProjection { if (typeof raw === "boolean") { // `output.robots: false` is shorthand for noindex,nofollow. return { index: raw, follow: raw }; } const obj = raw ?? {}; return { index: obj.index ?? true, follow: obj.follow ?? true }; } /** Raw `output.sitemap` as authored in system.md (boolean shorthand or object). */ export type RawSitemapProjection = | boolean | { include?: boolean; category?: string; lastmod?: string; includeLastmod?: boolean; }; /** Raw `output` block as authored in system.md. Closed: only known generator keys. */ export type RawPageOutput = { sitemap?: RawSitemapProjection; llms?: RawLlmsPolicy; /** RFC-0165: per-page indexability. */ robots?: RawRobotsProjection; /** RFC-0165/0167: author-declared content illustration (absolute https url). */ image?: RawImageProjection; }; export interface ResolvePageOutputOptions { semanticType?: string; } const DEFAULT_SITEMAP_CATEGORY = "content"; function resolveSitemap( raw: RawSitemapProjection | undefined, defaultCategory: string = DEFAULT_SITEMAP_CATEGORY, ): SitemapProjection { // Boolean shorthand: `output.sitemap: false` if (typeof raw === "boolean") { return { include: raw, category: defaultCategory, lastmod: undefined, includeLastmod: true, }; } const obj = raw ?? {}; return { include: obj.include ?? true, category: obj.category ?? defaultCategory, lastmod: obj.lastmod, includeLastmod: obj.includeLastmod ?? true, }; } /** * RFC-0143: resolve every per-page projection from the raw `output` block. * The single resolution point for all per-page output generators. */ export function resolvePageOutput( raw: RawPageOutput | undefined, options: ResolvePageOutputOptions = {}, ): PageOutputProjection { const output = raw ?? {}; const defaultCategory = options.semanticType === "legal" ? "legal" : DEFAULT_SITEMAP_CATEGORY; return { sitemap: resolveSitemap(output.sitemap, defaultCategory), llms: resolveLlmsPolicy(output.llms, options.semanticType ?? ""), robots: resolveRobots(output.robots), ...(resolveImage(output.image) ? { image: resolveImage(output.image) } : {}), }; }