/*
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,
};
}