/* RFC-0271: typed, schema-validated operating contexts for Programmatic Surface modules. A module context declares which Blueprints it owns, which entitlement gates them, and which locale is the canonical generation/review source before any derived public translation. Do not read system.md or entitlements; kernel commands own I/O. Do not redefine Blueprint axes, levels, or route policy. RFC-0271: add module context contracts for masterLocale, publishedLocales, entitlement, glossary refs, translator-note refs, and generation/approval policy. RFC-0277: add stage and urlPolicy fields for PSEO lifecycle governance. warpgogol-m000185: add noindexLocales/noindexDepths module policies — locale-scoped and depth-scoped index suppression for generated pages (pages stay live, robots noindex + sitemap/llms exclusion). */ import { z } from "zod"; export const moduleSiteModeSchema = z.enum(["bodenstation", "sternsystem"]); export const pseoStageSchema = z.enum(["internalCapability", "managedVisibility", "productModule"]); export const urlPolicySchema = z.enum(["nonDestruction"]); export const moduleReviewPolicySchema = z.object({ firstNPerTemplateField: z.number().int().min(0).optional(), sampleAfterStabilization: z.number().min(0).max(1).optional(), claims: z.enum(["human", "sample", "machine"]).optional(), }); export const surfaceModuleContextSchema = z.object({ id: z.string().min(1).optional(), entitlement: z.string().min(1), blueprints: z.array(z.string().min(1)).min(1), masterLocale: z.string().min(2), publishedLocales: z.array(z.string().min(2)).default([]), /** * Per-Blueprint locale-scoped index suppression: the rendered page stays live * for users but carries robots noindex and drops out of sitemap/llms for the * listed locales (e.g. operator-language copies of generated city/service * pages). Keyed by Blueprint id. The default locale is never suppressed. */ noindexLocales: z.record(z.string().min(1), z.array(z.string().min(2))).optional(), /** * Per-Blueprint depth-scoped index suppression: entries at the listed depths * are marked noindex in every locale (live for users, out of sitemap/llms). * Keyed by Blueprint id. For locale-scoped suppression use noindexLocales. */ noindexDepths: z.record(z.string().min(1), z.array(z.number().int().min(0))).optional(), stage: pseoStageSchema.optional(), urlPolicy: urlPolicySchema.optional(), context: z .object({ siteMode: moduleSiteModeSchema.optional(), operatorLanguage: z.string().min(2).optional(), audience: z.string().min(1).optional(), forbiddenClaims: z.array(z.string().min(1)).optional(), sourceBoundaries: z.array(z.string().min(1)).optional(), voice: z.record(z.string(), z.unknown()).optional(), }) .default({}), indexBudget: z .object({ publicName: z.literal("managedCoverage").optional(), maxIndexable: z.number().int().min(0).optional(), regionalGateDepths: z.array(z.number().int().min(0)).optional(), }) .optional(), generation: z .object({ provider: z.string().min(1).optional(), modelPolicy: z.string().min(1).optional(), normalBuildCallsLlm: z.literal(false).default(false), }) .default({ normalBuildCallsLlm: false }), approval: z .object({ requireHumanApproval: z.boolean().default(true), requireReadyForTranslation: z.boolean().default(true), }) .default({ requireHumanApproval: true, requireReadyForTranslation: true }), localization: z .object({ glossaryRefs: z.record(z.string(), z.string()).optional(), translatorNoteRefs: z.record(z.string(), z.string()).optional(), reviewPolicy: z.record(z.string(), moduleReviewPolicySchema).optional(), }) .optional(), }); export const surfaceModulesSchema = z.record(z.string(), surfaceModuleContextSchema); export type SurfaceModuleContext = z.infer & { id: string }; export type SurfaceModules = Record; export type PseoStage = z.infer; export type UrlPolicy = z.infer; export interface BlueprintModuleClaim { blueprint: string; modules: string[]; } export function normalizeSurfaceModules(value: unknown): SurfaceModules { const parsed = surfaceModulesSchema.parse(value ?? {}); const modules: SurfaceModules = {}; for (const [id, context] of Object.entries(parsed)) { modules[id] = { ...context, id }; } return modules; } export function findModuleForBlueprint( modules: SurfaceModules, blueprintId: string, ): SurfaceModuleContext | undefined { return Object.values(modules).find((module) => module.blueprints.includes(blueprintId)); } export function findDuplicateBlueprintClaims(modules: SurfaceModules): BlueprintModuleClaim[] { const claims = new Map(); for (const module of Object.values(modules)) { for (const blueprint of module.blueprints) { const owners = claims.get(blueprint) ?? []; owners.push(module.id); claims.set(blueprint, owners); } } return [...claims.entries()] .filter(([, modulesForBlueprint]) => modulesForBlueprint.length > 1) .map(([blueprint, modulesForBlueprint]) => ({ blueprint, modules: modulesForBlueprint })); }