/* [RFC-0192/0193] The Blueprint contract type (the declarative spec that configures one surface) plus the pure helpers that turn a Blueprint + loaded datasets into the eligibility matrix and the materialized VirtualRouteEntry[] (routes + redirect decisions). Framework-free. The runtime Zod schema and the YAML files live in @warpgogol/werkstatt-shared/ontology (RFC-0193); block baking is added there. Do not parse YAML or validate (ontology owns the Zod schema). Do not read content or bake page blocks (the kernel command / RFC-0193 provider does). RFC-0496: add BlueprintServiceConfig + ServicePublicationGate for website-service depth-1 service dossier configuration; add BlueprintLinkingParent for cross-surface parent linking. RFC-0497: add BlueprintIntersectionConfig + IntersectionGate + IntersectionSimilarity for website-local depth-5 intersection gate configuration. RFC-0500: add BlueprintHubConfig for depth-0 editorial knowledge hub configuration; add BlueprintStatusGate for record status filtering. RFC-1106: step 5 — merge blueprint triplet surface/blueprint.ts now holds the blueprintSchema (sole declaration), all ~30 contract type names derived via z.infer, parseBlueprint, and the pure helpers. Deleted satellites blueprint-types.ts + blueprint-schema.ts, their package.json export entries, and the zero-consumer site shims (domain/surface/blueprint{,-types,-schema}.ts + 2 load-verification tests). All barrel consumers unchanged. RFC-1257: structured CtaTarget + scenario-plumbed contact links RFC-0192, RFC-0199, warpgogol-m000185 */ import { buildEligibilityMatrix, nearestLiveAncestor, normalizeSegment, pathKey, type AxisFieldMap, type EligibilityMatrix, type MatrixEntry, } from "./eligibility.ts"; import type { AxisTuple, EligibilityPolicy, LocalizedUniverse, SurfaceAxis, SurfaceRecord, VirtualRouteEntry, } from "./types.ts"; import { z } from "zod"; import { ctaTargetSchema } from "../schemas/section-cta.ts"; // --------------------------------------------------------------------------- // Blueprint schema — RFC-1106: the sole declaration of the Blueprint contract. // All Blueprint types below derive from blueprintSchema via z.infer; the former // blueprint-types.ts / blueprint-schema.ts satellites are merged here. // --------------------------------------------------------------------------- const localizedString = z.record(z.string(), z.string()); const intRecord = z.record(z.string(), z.number().int()); // RFC-0490: pillar-hub configuration for depth-0 hub pages. // RFC-1257: pillar CTA targets use the canonical CtaTarget union — the same // grammar the baked section props carry, so no string→struct conversion ever // happens at the bake seam. const pillarCtaSchema = z.object({ label: localizedString, target: ctaTargetSchema, }); const pillarHeroSchema = z.object({ eyebrow: localizedString, heading: localizedString, lead: localizedString, primaryCta: pillarCtaSchema, secondaryCta: pillarCtaSchema, }); const pillarAdaptationDimensionSchema = z.object({ heading: localizedString, body: localizedString, }); const pillarAdaptationSchema = z.object({ heading: localizedString, dimensions: z.array(pillarAdaptationDimensionSchema).min(1), }); const pillarProductPriceSchema = z.object({ heading: localizedString, body: localizedString, priceRef: z.string().min(1).optional(), }); const pillarFinalCtaSchema = z.object({ heading: localizedString, body: localizedString, primaryCta: pillarCtaSchema, secondaryCta: pillarCtaSchema, }); const pillarFaqItemSchema = z.object({ question: localizedString, answer: localizedString, }); const pillarSchema = z.object({ hero: pillarHeroSchema, adaptation: pillarAdaptationSchema, productPrice: pillarProductPriceSchema, finalCta: pillarFinalCtaSchema, catalogHeading: localizedString.optional(), faq: z.array(pillarFaqItemSchema).optional(), }); // RFC-0492: dossier configuration for depth-1 industry pages. const industryPublicationGateSchema = z.object({ minServiceCategories: z.number().int().min(0), minCustomerJourneys: z.number().int().min(0), minTrustSignals: z.number().int().min(0), minArchitectureEntries: z.number().int().min(0), minModuleMappings: z.number().int().min(0), minUniqueFaq: z.number().int().min(0), }); const dossierSchema = z.object({ gate: industryPublicationGateSchema, claimRestrictions: z.array(z.string().min(1)), doorwayMaxFlaggedShare: z.number().min(0).max(1), duplicateMaxSimilarity: z.number().min(0).max(1), mode: z.enum(["warn", "fail"]), }); // RFC-0496: service configuration for depth-1 service dossier pages. const servicePublicationGateSchema = z.object({ minServiceVariants: z.number().int().min(0), minCustomerQuestions: z.number().int().min(0), minPriceModels: z.number().int().min(0), minFaq: z.number().int().min(0), minPageStructure: z.number().int().min(0), }); const serviceSchema = z.object({ gate: servicePublicationGateSchema, claimRestrictions: z.array(z.string().min(1)), mode: z.enum(["warn", "fail"]), }); const intersectionGateSchema = z.object({ minLocalServiceQuestions: z.number().int().min(0), minScenarios: z.number().int().min(0), minLocalEvidence: z.number().int().min(0), minUniqueContentBlocks: z.number().int().min(0), minUniqueFaq: z.number().int().min(0), minSources: z.number().int().min(0), }); const intersectionSimilaritySchema = z.object({ similarityToIndustryPage: z.number().min(0).max(1), similarityToCityPage: z.number().min(0).max(1), similarityToServicePage: z.number().min(0).max(1), similarityToOtherIntersections: z.number().min(0).max(1), }); const intersectionSchema = z.object({ gate: intersectionGateSchema, similarity: intersectionSimilaritySchema, substanceIndependenceThreshold: z.number().min(0).max(1), mode: z.enum(["warn", "fail"]), }); // RFC-0500: hub configuration for depth-0 editorial knowledge hubs (ratgeber). const hubSchema = z.object({ cardFields: z.array(z.string().min(1)).min(1), reservedSlugs: z.array(z.string().min(1)), }); // RFC-0500: status gate — only records with allowed statuses are emitted as surface entries. const statusGateSchema = z.object({ allowedStatuses: z.array(z.string().min(1)), excludedStatuses: z.array(z.string().min(1)), }); // RFC-0496: cross-surface parent linking. const linkingParentSchema = z.object({ surface: z.string().min(1), depth: z.number().int().min(0), joinField: z.string().min(1), }); // RFC-0325: static article metadata for a level with no per-record binding (depth-0 hubs). const blueprintLevelArticleSchema = z.object({ publishedAt: z.string().min(1), updatedAt: z.string().min(1).optional(), author: z.string().min(1).optional(), tags: z.array(z.string().min(1)).optional(), }); const evidencePolicySchema = z.object({ approvedNarrative: z.enum(["required", "optional"]).optional(), requiredRecordFields: z.array(z.string().min(1)).optional(), preferredEvidenceSources: z.array(z.string().min(1)).optional(), minTupleSpecificFacts: z.number().int().min(0).optional(), minWerkEvidence: z.number().int().min(0).optional(), existenceSource: z.enum(["records", "works"]).optional(), freshness: z.enum(["valid-and-current", "valid"]).optional(), duplicate: z .object({ method: z.enum(["shingle", "simhash"]).optional(), maxSimilarityWithinCluster: z.number().min(0).max(1).optional(), }) .optional(), leadImage: z.enum(["required", "warning", "optional"]).optional(), mode: z.enum(["error", "warning"]).optional(), }); const demandPolicySchema = z.object({ minVolume: z.number().min(0).optional(), allowIntents: z .array(z.enum(["informational", "commercial", "transactional", "navigational"])) .optional(), missing: z.enum(["noindex", "do-not-emit"]).optional(), staleAfterDays: z.number().int().min(0).optional(), }); const depthRolePolicySchema = z.object({ indexability: z.enum(["index", "navigation-noindex", "evidence-gated"]), canonicalTarget: z.union([z.literal("tradeHub"), z.number().int().min(0)]).optional(), follow: z.boolean().optional(), includeInSitemap: z.boolean().optional(), geo: z.enum(["full", "twin-only", "off"]).optional(), localEvidence: z .object({ minVerifiedFacts: z.number().int().min(0).optional(), minCitySpecificQa: z.number().int().min(0).optional(), minUniqueTokenShare: z.number().min(0).max(1).optional(), maxBodySimilarityWithinBranch: z.number().min(0).max(1).optional(), }) .optional(), }); export const blueprintSchema = z.object({ id: z.string().min(1), entitlement: z.string().min(1), dataset: z.object({ collection: z.string().min(1), status: z.string().optional() }), axes: z .array( z.object({ id: z.string().min(1), universe: z.union([ z.object({ collection: z.string().min(1), field: z.string().min(1) }), z.object({ provider: z.string().min(1) }), ]), match: z.object({ recordField: z.string().min(1) }), }), ) .min(1), levels: z .array( z.object({ depth: z.number().int().min(0), slug: localizedString, redirectToPageId: z.string().min(1).optional(), constellation: z.string().min(1), geo: z.enum(["full", "twin-only", "off"]).optional(), titleTemplate: localizedString.optional(), descriptionTemplate: localizedString.optional(), intro: localizedString.optional(), semanticType: z.string().min(1).optional(), article: blueprintLevelArticleSchema.optional(), pillar: pillarSchema.optional(), dossier: dossierSchema.optional(), service: serviceSchema.optional(), intersection: intersectionSchema.optional(), hub: hubSchema.optional(), }), ) .min(1), policy: z.object({ minRecordsPerDepth: intRecord, noindexBelowPerDepth: intRecord.optional(), redirectPolicy: z.enum(["nearest-ancestor", "root"]).optional(), trailingSlash: z.boolean().optional(), maxStubDepth: z.number().int().optional(), substanceMin: z.number().min(0).max(100).optional(), substanceMinPerDepth: intRecord.optional(), evidencePerDepth: z.record(z.string(), evidencePolicySchema).optional(), demandPerDepth: z.record(z.string(), demandPolicySchema).optional(), depthRoles: z.record(z.string(), depthRolePolicySchema).optional(), sitemapBudget: z.number().int().min(0).optional(), maxThinShare: z.number().min(0).max(1).optional(), regionalGateDepths: z.array(z.number().int().min(0)).optional(), bake: z.enum(["inline", "lazy"]).optional(), statusGate: statusGateSchema.optional(), }), linking: z .object({ children: z.object({ limit: z.number().int() }).optional(), siblings: z.object({ limit: z.number().int() }).optional(), teasers: z .object({ relevance: z.array(z.object({ sharedAxis: z.string(), weight: z.number() })).optional(), }) .optional(), parent: linkingParentSchema.optional(), }) .optional(), rotation: z.object({ variantsByTupleHash: z.boolean() }).optional(), projection: z .object({ title: localizedString.optional(), description: z.union([localizedString, z.object({ ref: z.string() })]).optional(), }) .optional(), freshness: z .object({ slaDaysPerDepth: intRecord, field: z.string().min(1), mode: z.enum(["any", "all", "median"]).optional(), }) .optional(), enrichedFields: z .array( z.object({ field: z.string().min(1), promptId: z.string().min(1), scopeDepth: z.number().int(), maxTokens: z.number().int(), // RFC-0207: output shape + generation granularity (both default to the original behavior). kind: z.enum(["field", "narrative"]).optional(), scope: z.enum(["tuple", "record"]).optional(), axis: z.string().min(1).optional(), }), ) .optional(), }); // --------------------------------------------------------------------------- // Derived contract types — every name the deleted blueprint-types.ts exported, // now inferred from the schema above. // --------------------------------------------------------------------------- /** A localized string map: lang → value. */ export type LocalizedString = z.infer; export type Blueprint = z.infer; export type BlueprintAxis = Blueprint["axes"][number]; export type BlueprintLevel = Blueprint["levels"][number]; export type BlueprintPolicy = Blueprint["policy"]; export type BlueprintLinking = NonNullable; export type BlueprintProjection = NonNullable; export type EnrichedFieldSpec = NonNullable[number]; export type GeoDepth = NonNullable; export type BlueprintLevelArticle = NonNullable; export type BlueprintPillar = NonNullable; export type BlueprintPillarHero = BlueprintPillar["hero"]; export type BlueprintPillarAdaptation = BlueprintPillar["adaptation"]; export type BlueprintPillarAdaptationDimension = BlueprintPillarAdaptation["dimensions"][number]; export type BlueprintPillarProductPrice = BlueprintPillar["productPrice"]; export type BlueprintPillarFinalCta = BlueprintPillar["finalCta"]; export type BlueprintDossier = NonNullable; export type IndustryPublicationGate = BlueprintDossier["gate"]; export type BlueprintServiceConfig = NonNullable; export type ServicePublicationGate = BlueprintServiceConfig["gate"]; export type BlueprintIntersectionConfig = NonNullable; export type IntersectionGate = BlueprintIntersectionConfig["gate"]; export type IntersectionSimilarity = BlueprintIntersectionConfig["similarity"]; export type BlueprintHubConfig = NonNullable; export type BlueprintStatusGate = NonNullable; export type BlueprintLinkingParent = NonNullable; export type BlueprintEvidenceDepthPolicy = NonNullable[string]; export type BlueprintDemandDepthPolicy = NonNullable[string]; export type BlueprintDepthRolePolicy = NonNullable[string]; export type BlueprintDuplicatePolicy = NonNullable; export interface ParseBlueprintResult { ok: boolean; blueprint?: Blueprint; errors: string[]; } /** Validate an unknown value (parsed YAML) into a typed Blueprint. */ export function parseBlueprint(value: unknown): ParseBlueprintResult { const result = blueprintSchema.safeParse(value); if (result.success) { return { ok: true, blueprint: result.data, errors: [] }; } return { ok: false, errors: result.error.issues.map( (issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`, ), }; } const DEFAULT_SEGMENT_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/; /** Resolve the Blueprint policy into a concrete EligibilityPolicy. */ export function resolvePolicy(bp: Blueprint): EligibilityPolicy { return { minRecordsPerDepth: bp.policy.minRecordsPerDepth, noindexBelowPerDepth: bp.policy.noindexBelowPerDepth ?? {}, redirectPolicy: bp.policy.redirectPolicy ?? "nearest-ancestor", trailingSlash: bp.policy.trailingSlash ?? true, segmentPattern: DEFAULT_SEGMENT_PATTERN, }; } export function buildAxisFieldMap(bp: Blueprint): AxisFieldMap { const map: Record = {}; for (const axis of bp.axes) map[axis.id] = axis.match.recordField; return map; } /** Build SurfaceAxis[] from a Blueprint and the loaded value universes (axisId → slugs). */ export function buildAxes( bp: Blueprint, universes: Record, ): SurfaceAxis[] { return bp.axes.map((axis) => ({ id: axis.id, universe: universes[axis.id] ?? [] })); } /** Synthetic stable pageId for a (blueprint, depth, tuple). */ export function pageIdFor( bp: Blueprint, depth: number, tuple: AxisTuple, axisOrder: readonly string[], ): string { if (depth === 0) return `${bp.id}:_root`; const values: string[] = []; for (let i = 0; i < depth; i += 1) values.push(tuple[axisOrder[i]!] ?? ""); return `${bp.id}:${values.join(":")}`; } function levelByDepth(bp: Blueprint, depth: number): BlueprintLevel | undefined { return bp.levels.find((level) => level.depth === depth); } function tradeHubDepth(bp: Blueprint): number { for (const [depth, role] of Object.entries(bp.policy.depthRoles ?? {})) { if (role.indexability === "index") return Number(depth); } return 1; } function canonicalTargetPageId( bp: Blueprint, entry: MatrixEntry, axisOrder: readonly string[], ): string | undefined { const role = bp.policy.depthRoles?.[entry.depth]; if (role?.indexability !== "navigation-noindex") return undefined; const targetDepth = role.canonicalTarget === "tradeHub" ? tradeHubDepth(bp) : typeof role.canonicalTarget === "number" ? role.canonicalTarget : undefined; if (targetDepth === undefined || targetDepth >= entry.depth) return undefined; return pageIdFor(bp, targetDepth, entry.tuple, axisOrder); } /** * Resolve a slug template ("website/{industry}/{city}") against a tuple for one language, * normalizing each value. RFC-0199: each token's value is the per-language localized slug from * `universe` when present, falling back to the neutral tuple value otherwise — so the identity key * stays neutral while the emitted URL segment localizes. */ export function resolveSlug( template: string, tuple: AxisTuple, lang: string, universe: LocalizedUniverse, pattern: RegExp, ): string { return template.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, axisId: string) => { const value = tuple[axisId]; if (value === undefined) return ""; const localized = universe[axisId]?.get(value)?.byLang?.[lang] ?? value; return normalizeSegment(localized, pattern); }); } /** Localized routes (lang → slug) for one matrix entry. */ function routesFor( bp: Blueprint, entry: MatrixEntry, langs: readonly string[], pattern: RegExp, universe: LocalizedUniverse, ): Record { const level = levelByDepth(bp, entry.depth); const routes: Record = {}; if (!level) return routes; for (const lang of langs) { const template = level.slug[lang] ?? level.slug[langs[0]!] ?? ""; routes[lang] = resolveSlug(template, entry.tuple, lang, universe, pattern); } return routes; } /** * Turn the eligibility matrix into materialized VirtualRouteEntry[] (routes + redirect targets). * Block baking (entry.page) is layered on by the kernel command / RFC-0193 provider afterwards. */ export function assembleEntries( bp: Blueprint, matrix: EligibilityMatrix, langs: readonly string[], policy: EligibilityPolicy, localizedUniverse: LocalizedUniverse = {}, ): VirtualRouteEntry[] { const result: VirtualRouteEntry[] = []; for (const entry of matrix.entries) { const pageId = pageIdFor(bp, entry.depth, entry.tuple, matrix.axisOrder); const routes = routesFor(bp, entry, langs, policy.segmentPattern, localizedUniverse); const level = levelByDepth(bp, entry.depth); const role = bp.policy.depthRoles?.[entry.depth]; const navigationNoindex = role?.indexability === "navigation-noindex"; const geo = navigationNoindex ? (role.geo ?? "off") : (level?.geo ?? "full"); const forcedRedirectToPageId = level?.redirectToPageId; const indexable = forcedRedirectToPageId ? false : entry.indexable; const noindex = forcedRedirectToPageId || navigationNoindex ? true : entry.noindex; const canonicalPageId = canonicalTargetPageId(bp, entry, matrix.axisOrder); let redirectToPageId: string | undefined = forcedRedirectToPageId; if (!indexable && !redirectToPageId) { const ancestor = nearestLiveAncestor(entry, matrix, policy); if (ancestor) { redirectToPageId = pageIdFor(bp, ancestor.depth, ancestor.tuple, matrix.axisOrder); } } result.push({ surfaceId: bp.id, pageId, routes, axes: entry.tuple, depth: entry.depth, recordCount: entry.recordCount, indexable, noindex, geo, ...(redirectToPageId ? { redirectToPageId } : {}), ...(canonicalPageId ? { canonicalPageId } : {}), decision: forcedRedirectToPageId ? { ...(entry.decision ?? { recordGate: true, indexable: false, noindex: true }), indexable: false, noindex: true, reason: "redirect-stub", } : navigationNoindex ? { ...(entry.decision ?? { recordGate: true, indexable: true, noindex: true }), indexable, noindex: true, reason: "navigation-noindex", } : entry.decision, }); } return result; } /** Full pipeline: Blueprint + loaded data → VirtualRouteEntry[] (no baked pages). */ export function generateEntries( bp: Blueprint, data: { records: readonly SurfaceRecord[]; universes: Record; langs: readonly string[]; /** RFC-0199: per-language slug segments. Absent ⇒ neutral slugs only (legacy behavior). */ localizedUniverse?: LocalizedUniverse; /** RFC-0240: whether the resolved entitlements unlock the regional-hub-or-higher `pseo` tier. */ regionalUnlocked?: boolean; }, ): VirtualRouteEntry[] { const policy = resolvePolicy(bp); const axes = buildAxes(bp, data.universes); const axisFieldMap = buildAxisFieldMap(bp); const matrix = buildEligibilityMatrix(axes, axisFieldMap, data.records, policy, { maxDepth: Math.max(0, ...bp.levels.map((level) => level.depth)), maxStubDepth: bp.policy.maxStubDepth ?? 1, forceNonIndexableDepths: data.regionalUnlocked ? [] : (bp.policy.regionalGateDepths ?? []), }); return assembleEntries(bp, matrix, data.langs, policy, data.localizedUniverse ?? {}); } export { pathKey };