/* RFC-0051 ai.txt generation. Pure function buildAiTxt produces the machine-readable AI policy file from a structured AiPolicy config. Do not read content files or validate input — the caller is responsible. Do not manage default policy fallback — caller decides skip vs fallback. RFC-0051: Initial implementation. */ export interface AiProviderOverride { name: string; policy: "allow" | "disallow" | "limited"; training?: "allow" | "disallow"; usage?: string[]; commercial?: "yes" | "no"; attribution?: "required" | "optional" | "none"; license?: string; contact?: string; url?: string; } export interface AiPolicy { policy: "allow" | "disallow" | "limited"; training?: "allow" | "disallow"; usage?: string[]; commercial?: "yes" | "no"; attribution?: "required" | "optional" | "none"; license?: string; contact?: string; url?: string; version?: string; updated?: string; providers?: AiProviderOverride[]; } function formatKeyValue(key: string, value: string | undefined): string | null { if (value === undefined || value === null) return null; return `${key}: ${value}`; } function _formatList(key: string, values: string[] | undefined): string | null { if (!values || values.length === 0) return null; return `${key}: ${values.join(", ")}`; } export function buildAiTxt(policy: AiPolicy, siteUrl: string): string { const lines: string[] = []; const headerComment = siteUrl ? `# ai.txt for ${siteUrl.replace(/^https?:\/\//, "")}` : "# ai.txt"; lines.push(headerComment); lines.push("# AI training and usage policy"); if (policy.url) lines.push(`# See: ${policy.url}`); if (policy.contact) lines.push(`# Contact: ${policy.contact}`); lines.push(""); const version = policy.version ?? "0.1"; const updated = policy.updated ?? new Date().toISOString().slice(0, 10); lines.push(`version: ${version}`); lines.push(`updated: ${updated}`); lines.push(""); lines.push("# Default policy for AI systems"); lines.push(`policy: ${policy.policy}`); const optionalGlobalFields: Array<[string, string | undefined]> = [ ["license", policy.license], ["url", policy.url], ["contact", policy.contact], ]; const pushLine = (key: string, val: string | undefined | string[]) => { if (Array.isArray(val) && val.length > 0) { lines.push(`${key}: ${val.join(", ")}`); } else if (typeof val === "string" && val.length > 0) { lines.push(`${key}: ${val}`); } }; pushLine("training", policy.training); pushLine("usage", policy.usage); pushLine("commercial", policy.commercial); pushLine("attribution", policy.attribution); for (const [key, val] of optionalGlobalFields) { const line = formatKeyValue(key, val); if (line) lines.push(line); } const unknownKeys = new Set(Object.keys(policy)); const knownKeys = new Set([ "policy", "training", "usage", "commercial", "attribution", "license", "contact", "url", "version", "updated", "providers", ]); for (const key of unknownKeys) { if (!knownKeys.has(key)) { const val = (policy as unknown as Record)[key]; if (typeof val === "string" && val.length > 0) { lines.push(`${key}: ${val}`); } } } if (policy.providers && policy.providers.length > 0) { lines.push(""); for (const provider of policy.providers) { lines.push(`[${provider.name}]`); lines.push(`policy: ${provider.policy}`); if (provider.training) lines.push(`training: ${provider.training}`); if (provider.usage && provider.usage.length > 0) { lines.push(`usage: ${provider.usage.join(", ")}`); } if (provider.commercial) lines.push(`commercial: ${provider.commercial}`); if (provider.attribution) lines.push(`attribution: ${provider.attribution}`); if (provider.license) lines.push(`license: ${provider.license}`); if (provider.contact) lines.push(`contact: ${provider.contact}`); if (provider.url) lines.push(`url: ${provider.url}`); lines.push(""); } } return lines.join("\n").trimEnd() + "\n"; }