Files
quixos-protocol/src/capability-model/initialization.ts
T

129 lines
6.3 KiB
TypeScript
Raw Normal View History

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<string, ValueType>();
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<string, ValueType> = {
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<string, ValueType> = Object.create(null);
const destinations = new Set<string>();
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<string>();
const supplied = new Set<string>();
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;
}