import type { BoundDependency, Conformance, DependencyPort, InitializationField, ValueType, WorkspaceRevision, } from "./types.js"; import type { CapabilityValidationIssue } from "./validation.js"; import { cardinalityValueType, valueTypesEqual } from "./validation.js"; /** Creation grants seed inputs on new objects, never general private-state authority. */ export function validateInitialization(workspace: WorkspaceRevision): CapabilityValidationIssue[] { const issues: CapabilityValidationIssue[] = []; const fail = (path: string, message: string) => issues.push({ code: "invalid-initialization", path, message }); const ownerKey = (owner: Conformance) => `${owner.atomId}/${owner.interfaceRevisionId}`; const shapes = new Map(); const fieldType = (owner: Conformance, field: InitializationField): ValueType | undefined => { const binding = field.binding; const attachment = owner.privateAttachments.find( (a) => a.kind === binding.kind && a.id === (binding.kind === "state" ? binding.slotId : binding.edgeTypeId), ); if (!attachment) { fail(ownerKey(owner), `Initialization input ${field.input} must name an attachment owned by this conformance`); return; } if (attachment.kind === "state") return attachment.valueType; if (binding.kind !== "edge") return; const index = attachment.endpoints.findIndex((e) => e.projectionId === binding.projectionId); const endpoint = attachment.endpoints[index]; const target = attachment.endpoints[1 - index]; if (!endpoint || !target || endpoint.constraint.kind !== "atom" || endpoint.constraint.atomId !== owner.atomId) { fail(ownerKey(owner), `Initialization input ${field.input} must name an edge projection on the newborn atom`); return; } if (target.keyType) { fail( ownerKey(owner), "Initialize a keyed relationship through its keyed projection; inverse seeds cannot supply a map key", ); return; } if (endpoint.cardinality === "many" || endpoint.cardinality === "many-unique") { const fields: Record = { key: { kind: "scalar", name: "string" }, target: cardinalityValueType(target.constraint, "exactly-one"), }; if (endpoint.keyType) fields.mapKey = { kind: "scalar", name: endpoint.keyType === "boolean" ? "bool" : endpoint.keyType }; return { kind: "list", value: { kind: "record", fields } }; } return cardinalityValueType(target.constraint, endpoint.cardinality); }; for (const owner of workspace.conformances) { if (!owner.initialization) continue; const fields: Record = Object.create(null); const destinations = new Set(); for (const field of owner.initialization) { const path = `${ownerKey(owner)}/${field.input}`; if (!field.input || Object.hasOwn(fields, field.input)) fail(path, "Initialization input names must be nonempty and distinct"); const destination = JSON.stringify(field.binding); if (destinations.has(destination)) fail(path, "An attachment may have only one initialization input"); destinations.add(destination); const type = fieldType(owner, field); if (type) fields[field.input] = type; } shapes.set(ownerKey(owner), { kind: "record", fields }); } const checkBindings = (ports: DependencyPort[], dependencies: BoundDependency[], path: string) => { for (const dependency of dependencies) { if (dependency.binding.kind !== "creation") continue; const binding = dependency.binding; const requirement = ports.find((p) => p.id === dependency.portId)?.requirement; if (requirement?.kind !== "creation") continue; // General binding validation reports the kind mismatch. const portPath = `${path}/${dependency.portId}`; if (binding.atomId !== requirement.atomId) fail(portPath, `Creation port requires atom ${requirement.atomId}`); const initialized = new Set(); const supplied = new Set(); for (const mapping of binding.owners) { const key = `${binding.atomId}/${mapping.interfaceRevisionId}`; if (supplied.has(key)) fail(portPath, `Duplicate initialization owner ${key}`); supplied.add(key); const owner = workspace.conformances.find((o) => ownerKey(o) === key); const shape = shapes.get(key); if (!owner || !shape) { fail(portPath, `Owner ${key} has no declared initialization mapping`); continue; } let seed: ValueType | undefined = requirement.inputType; for (const component of mapping.inputPath) seed = seed?.kind === "record" ? seed.fields[component] : undefined; if (!seed || !valueTypesEqual(seed, shape)) fail( portPath, `Seed input${mapping.inputPath.map((p) => `.${p}`).join("")} does not match initialization inputs of ${key}`, ); for (const field of owner.initialization!) if (field.binding.kind === "state") initialized.add(field.binding.slotId); } const states = [ ...workspace.sharedAttachments, ...workspace.conformances.flatMap((o) => o.privateAttachments), ].filter((a) => a.kind === "state" && a.attachedTo === binding.atomId); for (const state of states) if (state.kind === "state" && state.defaultValue === undefined && !initialized.has(state.id)) fail(portPath, `Required state ${state.id} has no default or declared seed mapping`); } }; const exports = new Map( workspace.packageImports.flatMap((p) => p.exports.map((e) => [`${p.revisionId}/${e.id}`, e] as const)), ); for (const owner of workspace.conformances) for (const operation of owner.operationBindings) { if (operation.binding.kind !== "package") continue; const entry = exports.get(`${operation.binding.packageRevisionId}/${operation.binding.exportId}`); if (entry) checkBindings( entry.dependencyPorts, operation.binding.dependencies, `${ownerKey(owner)}/${operation.operationId}`, ); } for (const constructor of workspace.constructors) { const entry = exports.get(`${constructor.packageRevisionId}/${constructor.exportId}`); if (entry) checkBindings(entry.dependencyPorts, constructor.dependencies, `constructor/${constructor.atomId}`); } return issues; }