Add factory creation ports and owner initialization contracts
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user