122 lines
6.1 KiB
TypeScript
122 lines
6.1 KiB
TypeScript
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 (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;
|
||
|
|
}
|