/* RFC-1307: pure planner for sternsystem.register --intake — turns a validated werkstatt:intake/v1 envelope (plus pre-read asset bytes) into the exact files seeded into a new Sternsystem's cache clone: system.md, the primary-locale business-profile PBP documents, publishable page assets with RFC-0220 credit sidecars, and the system-state onboarding provenance block. Do not read or write files, spawn processes, or read the clock — the command owns IO; this function is called twice by the determinism test. Do not compose pages, sections or prose — composition is the ON-07 composer. Do not filter or repair entities — a PBP entity that fails its canonical schema throws (DNA-103). Locale directories and i18n keys use the primary subtag (route segment); the full BCP 47 tag rides in i18n.supported.<segment>.tag. Only rights states the spec makes publishable without an operator decision (cleared, generated, licensed-stock) are copied; client-supplied, claimed Every seeded file is serialized through canonical writers (sorted keys, RFC-0493 quoting) so the same intake yields byte-identical seeds (DNA-58). The seeded system.md carries the enforce gate triple (contractTerms, provenance, publication) plus legalJurisdiction — the born-valid contract, no downstream repair. Two entities resolving to one document path fail the plan Two locale tags sharing a primary subtag fail the plan rather than silently overwriting an i18n entry, Seeded assets and credit sidecars live in the pages domain (pages/<lang>/assets) so page image references match their credits RFC-1328/AMD-005: per-locale document variants (statementByLocale values + statementLang rewrite, localeNeutral clones); prose never crosses locales — description dropped everywhere, summary dropped on statement-bearing variants; envelope-only fields stripped before the strict pbp parse. RFC-1307: record review-fix decisions in compass headers REVIEW-CODE-2026-10-16-08 N3/N4: reject duplicate locale primary segments and non-2-letter primary subtags at plan time instead of seeding born-invalid i18n; credit target.domain names business-profile — the tree the seeded asset actually lands in. RFC-1307: review-08 N1-N7 — ref-scoped rollback, pre-existing content preservation, svg resolution, explicit io RFC-1307: add register --domain for intakes without a confirmed domain RFC-1307: seed intake assets into the pages domain so page credits match RFC-1307 */ import YAML from "yaml"; import { pbpSchemaById } from "@warpgogol/pbp/entities"; import type { IntakeEnvelope, IntakeAsset } from "@warpgogol/werkstatt-shared/onboarding/intake"; import { pbpDocumentPath, serializePbpDocument, } from "@warpgogol/werkstatt-shared/onboarding/pbp-layout"; import { materialCreditSchema } from "@warpgogol/werkstatt-shared/schemas/material-credit"; /** Rights states copied into the site at birth (INTAKE-CONTRACT § rights). */ export const SEEDABLE_RIGHTS_STATES = ["cleared", "generated", "licensed-stock"] as const; /** Asset kinds that are design references, not site content. */ export const NON_CONTENT_ASSET_KINDS = ["mood"] as const; export interface IntakeSeedInput { intake: IntakeEnvelope; /** sha256 (`sha256:`) of the bundle's intake.json bytes. */ intakeSha256: string; systemId: string; cosmicStar: string; biomeId: string; /** * Host seeded into identity.domain when the intake has no confirmed domain * (operator decision, e.g. a workers.dev host). Never overrides a domain * the intake confirms. */ domainFallback?: string; /** Asset id → file bytes, pre-read by the command for every seedable asset. */ assetBytes: ReadonlyMap; } export interface IntakeSeedFile { /** POSIX path relative to the cache clone root. */ path: string; content: string | Uint8Array; } export interface IntakeSeedPlan { files: IntakeSeedFile[]; systemState: { onboarding: { intakeId: string; intakeSha256: string; contractSnapshot: string } }; pbpDocuments: number; assets: number; } /** Primary subtag of a BCP 47 tag — the route segment and content directory. */ export function localeSegment(tag: string): string { return tag.split("-")[0]!; } /** * AMD-005/RFC-1328: the entity field a locale variant replaces — `statement` * wins over `summary`. Free prose never ships under another locale: * `description` has no locale channel and is dropped from every non-primary * document, and `summary` is dropped when `statement` carried the variant. */ const STATEMENT_FIELDS = ["statement", "summary"] as const; /** * Per-locale document for one entity, or undefined when the entity has no * facts for that locale (it surfaces in the primary tree until the producer * emits the locale — Q-4). A document is seeded when the entity carries a * `statementByLocale` value for the tag or is marked `localeNeutral` * (names, identifiers, addresses — verbatim clone minus prose). */ function localeVariantDocument( entity: IntakeEnvelope["pbp"][number], canonicalEntity: Record, tag: string, ): Record | undefined { const statementField = STATEMENT_FIELDS.find((f) => typeof canonicalEntity[f] === "string"); const variant = entity.statementByLocale?.[tag]; if (typeof variant === "string" && variant.length > 0 && statementField) { const clone = { ...canonicalEntity, [statementField]: variant }; delete clone["description"]; if (statementField === "statement") delete clone["summary"]; // The variant statement is authored in the variant locale — a declared // statementLang follows the doc's locale, not the primary one. if (typeof clone["statementLang"] === "string") clone["statementLang"] = tag; return clone; } if (entity.localeNeutral === true) { const clone = { ...canonicalEntity }; delete clone["description"]; return clone; } return undefined; } function sortDeep(value: unknown): unknown { if (Array.isArray(value)) return value.map(sortDeep); if (typeof value !== "object" || value === null) return value; const record = value as Record; const out: Record = {}; for (const key of Object.keys(record).sort()) { if (record[key] !== undefined) out[key] = sortDeep(record[key]); } return out; } function stringifyYaml(value: unknown): string { return YAML.stringify(value, { lineWidth: 0, singleQuote: false, defaultStringType: "PLAIN", defaultKeyType: "PLAIN", }); } /** Asset ids from the intake are `AST-NN`; site file names are lowercase. */ function assetBaseName(asset: IntakeAsset): string { return asset.id.toLowerCase(); } function assetExtension(asset: IntakeAsset): string { const dot = asset.file.lastIndexOf("."); return asset.file.slice(dot + 1); } function materialKindFor(mediaType: string): "image" | "video" | "document" { if (mediaType.startsWith("image/")) return "image"; if (mediaType.startsWith("video/")) return "video"; return "document"; } /** Assets copied at birth: publishable rights state and real site content. */ export function isSeedableAsset(asset: IntakeAsset): boolean { return ( (SEEDABLE_RIGHTS_STATES as readonly string[]).includes(asset.rights.state) && !(NON_CONTENT_ASSET_KINDS as readonly string[]).includes(asset.kind) ); } function creditFor(asset: IntakeAsset, lang: string): Record { const id = assetBaseName(asset); // Seeded assets land under src/content/pages//assets/ — site imagery is // consumed by pages, and the credit target domain must name the tree the file // actually lives in (REVIEW-08 N5) so page references match their credit. const target = { kind: materialKindFor(asset.mediaType), id, domain: "pages", lang, }; if (asset.rights.state === "generated") { const generator = asset.generator; return { id, target, sourceType: "ai-generated", status: "needs-review", aiUsage: { kind: "ai-generated", humanContribution: `Prompted and selected by the studio (concept ${generator?.conceptId ?? "unknown"}).`, copyrightClaimed: false, ...(generator?.promptSha256 ? { generationRecordRef: generator.promptSha256 } : {}), }, parties: [ { role: "aiModel", name: generator?.model ?? "unknown", kind: "AIModel", ...(generator?.generatedAt ? { generatedAt: generator.generatedAt } : {}), }, ], license: { label: "AI-generated — no copyright claimed" }, }; } const holder = asset.rights.holder ?? "unknown"; return { id, target, sourceType: asset.rights.state === "licensed-stock" ? "licensed-third-party" : "commissioned", status: "needs-review", usageBasis: { type: asset.rights.state === "licensed-stock" ? "license" : "express-permission", }, parties: [{ role: "rightsHolder", name: holder, kind: "Organization" }], license: { label: asset.rights.licence ?? "Rights cleared for this site", ...(asset.rights.licenceUrl ? { url: asset.rights.licenceUrl } : {}), }, }; } function buildSystemMd(input: IntakeSeedInput, primarySegment: string): string { const { intake } = input; const supported: Record> = {}; const seenSegments = new Set(); for (const locale of intake.classification.locales) { const segment = localeSegment(locale.tag); // i18n.config.validate requires ^[a-z]{2}(-[A-Z]{2})?$ keys — a 3-letter // primary subtag would birth a site that fails its own i18n validation. if (!/^[a-z]{2}$/.test(segment)) { throw new Error( `[seed-from-intake] locale '${locale.tag}' primary subtag '${segment}' is not a 2-letter i18n segment — restrict the intake tag upstream`, ); } // Two distinct tags sharing a primary segment would silently overwrite — // one locale lost, the survivor claiming the wrong hreflang. if (seenSegments.has(segment)) { throw new Error( `[seed-from-intake] locales share primary segment '${segment}' — disambiguate the intake tags before registering`, ); } seenSegments.add(segment); supported[segment] = segment === locale.tag ? { name: segment, hreflang: locale.tag } : { name: segment, hreflang: locale.tag, tag: locale.tag }; } const identity: Record = { systemStar: input.cosmicStar, biome: input.biomeId, }; const domain = intake.classification.domain ?? input.domainFallback; if (typeof domain === "string") identity.domain = domain; const frontmatter = { app: input.systemId, version: "1.0.0", identity, i18n: { default: primarySegment, supported }, legalJurisdiction: intake.classification.jurisdiction, publication: { gate: "enforce" }, contractTerms: { gate: "enforce" }, provenance: { gate: "enforce" }, }; return `---\n${stringifyYaml(frontmatter)}---\n`; } /** * Plan every file seeded at birth. Pure: the same input always yields the * same files in the same order with the same bytes. */ export function planSeedFromIntake(input: IntakeSeedInput): IntakeSeedPlan { const { intake } = input; const primary = intake.classification.locales.find((l) => l.role === "primary"); if (!primary) throw new Error("[seed-from-intake] intake has no locale with role: primary"); const primarySegment = localeSegment(primary.tag); const files: IntakeSeedFile[] = [ { path: "src/content/system.md", content: buildSystemMd(input, primarySegment) }, ]; let pbpDocuments = 0; const nonPrimaryLocales = intake.classification.locales.filter((l) => l.role !== "primary"); for (const entity of intake.pbp) { const schema = pbpSchemaById[entity.schema]; if (!schema) { throw new Error( `[seed-from-intake] no canonical PBP schema for ${entity.schema} (${entity.id})`, ); } // statementByLocale/localeNeutral are envelope-only (AMD-005) — the // frozen pbp/*@1 strict schemas never see them. const { statementByLocale: _statementByLocale, localeNeutral: _localeNeutral, ...canonicalEntity } = entity as Record; const parsed = schema.parse(canonicalEntity) as Record; const rel = pbpDocumentPath(primarySegment, { type: entity.type, id: entity.id }); files.push({ path: `src/content/business-profile/${rel}`, content: serializePbpDocument(parsed), }); pbpDocuments++; // RFC-1328 (AMD-005): locale variants carry statementByLocale values — // the document schema is unchanged, only the statement value differs. for (const locale of nonPrimaryLocales) { const variant = localeVariantDocument(entity, canonicalEntity, locale.tag); if (!variant) continue; const parsedVariant = schema.parse(variant) as Record; const variantRel = pbpDocumentPath(localeSegment(locale.tag), { type: entity.type, id: entity.id, }); files.push({ path: `src/content/business-profile/${variantRel}`, content: serializePbpDocument(parsedVariant), }); pbpDocuments++; } } let assets = 0; for (const asset of intake.assets.filter(isSeedableAsset)) { const bytes = input.assetBytes.get(asset.id); if (!bytes) throw new Error(`[seed-from-intake] missing bytes for seedable asset ${asset.id}`); const base = `src/content/pages/${primarySegment}/assets/${assetBaseName(asset)}`; files.push({ path: `${base}.${assetExtension(asset)}`, content: bytes }); const credit = materialCreditSchema.parse(creditFor(asset, primarySegment)); files.push({ path: `${base}.credits.yaml`, content: stringifyYaml(sortDeep(credit)) }); assets++; } files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)); // Two entities or assets resolving to one file would silently overwrite — // fail at plan time so the defect is fixed in the intake, not masked. const seenPaths = new Set(); for (const file of files) { if (seenPaths.has(file.path)) { throw new Error(`[seed-from-intake] duplicate seed path ${file.path} — check entity ids`); } seenPaths.add(file.path); } return { files, systemState: { onboarding: { intakeId: intake.intakeId, intakeSha256: input.intakeSha256, contractSnapshot: intake.provenance.contractSnapshot, }, }, pbpDocuments, assets, }; }