Add factory creation ports and owner initialization contracts
This commit is contained in:
@@ -149,6 +149,10 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
|
||||
}
|
||||
if (binding.kind === "state") parent.dependencies.add(`attachment:${binding.slotId}`);
|
||||
if (binding.kind === "edge") parent.dependencies.add(`attachment:${binding.edgeTypeId}`);
|
||||
if (binding.kind === "creation") {
|
||||
for (const owner of binding.owners)
|
||||
parent.dependencies.add(conformanceKey(binding.atomId, owner.interfaceRevisionId));
|
||||
}
|
||||
if (binding.kind === "constructor") {
|
||||
parent.dependencies.add(`constructor:${binding.atomId}`);
|
||||
const ctor = workspace.constructors.find((entry) => entry.atomId === binding.atomId);
|
||||
@@ -210,6 +214,9 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
|
||||
atomId: conformance.atomId,
|
||||
interfaceRevisionId: conformance.interfaceRevisionId,
|
||||
operations: sorted(conformance.operationBindings, (entry) => entry.operationId),
|
||||
...(conformance.initialization
|
||||
? { initialization: sorted(conformance.initialization, (entry) => entry.input) }
|
||||
: {}),
|
||||
materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId),
|
||||
});
|
||||
parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`);
|
||||
|
||||
@@ -89,6 +89,8 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
|
||||
const query = packages.get(requirement.packageRevisionId)?.queries?.find((q) => q.id === requirement.queryId);
|
||||
if (query?.placement?.kind !== "local")
|
||||
fail(portPath, "Portable operations require a local query dependency");
|
||||
} else if (requirement.kind === "creation") {
|
||||
if (entry.execution.kind !== "transaction") fail(portPath, "Object creation requires transaction execution");
|
||||
} else if (requirement.kind !== "interface")
|
||||
fail(portPath, `Portable execution does not support ${requirement.kind} dependencies`);
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
|
||||
application: InterfaceApplicationExpression;
|
||||
operations?: import("./types.js").OperationId[];
|
||||
}
|
||||
| { kind: "creation"; target: ObjectTypeExpression; inputType: ValueTypeExpression }
|
||||
| { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression };
|
||||
};
|
||||
|
||||
@@ -101,6 +102,7 @@ export const specializePackageExport = (
|
||||
...(requirement.operations ? { operations: requirement.operations } : {}),
|
||||
},
|
||||
};
|
||||
case "creation":
|
||||
case "constructor": {
|
||||
const target = substitution.object(requirement.target);
|
||||
if (target.kind !== "atom")
|
||||
@@ -112,7 +114,7 @@ export const specializePackageExport = (
|
||||
return {
|
||||
...port,
|
||||
requirement: {
|
||||
kind: "constructor",
|
||||
kind: requirement.kind,
|
||||
atomId: target.atomId,
|
||||
inputType: substitution.value(requirement.inputType),
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -181,7 +181,7 @@ export interface InterfaceRevision {
|
||||
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
|
||||
}
|
||||
|
||||
export type StoragePolicy = { kind: "optimistic-register" } | { kind: "crdt-document"; updateType: ValueType };
|
||||
export type StoragePolicy = { kind: "register" } | { kind: "crdt-document"; updateType: ValueType };
|
||||
|
||||
export interface StateSlotDefinition {
|
||||
kind: "state";
|
||||
@@ -246,6 +246,7 @@ export type DependencyPortRequirement =
|
||||
/** Exact public operation identities, not implementation export names. */
|
||||
operations?: OperationId[];
|
||||
}
|
||||
| { kind: "creation"; atomId: AtomId; inputType: ValueType }
|
||||
| { kind: "constructor"; atomId: AtomId; inputType?: ValueType };
|
||||
|
||||
export interface DependencyPort {
|
||||
@@ -320,6 +321,12 @@ export type DependencyBinding =
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
via?: EdgeTraversal;
|
||||
}
|
||||
| {
|
||||
kind: "creation";
|
||||
atomId: AtomId;
|
||||
owners: { interfaceRevisionId: InterfaceRevisionId; inputPath: string[] }[];
|
||||
via?: never;
|
||||
}
|
||||
| { kind: "constructor"; atomId: AtomId };
|
||||
|
||||
export interface BoundDependency {
|
||||
@@ -376,7 +383,13 @@ export interface OperationBinding {
|
||||
queryReason?: string;
|
||||
}
|
||||
|
||||
export interface InitializationField {
|
||||
input: string;
|
||||
binding: { kind: "state"; slotId: SlotId } | { kind: "edge"; edgeTypeId: EdgeTypeId; projectionId: EdgeProjectionId };
|
||||
}
|
||||
|
||||
export interface Conformance {
|
||||
initialization?: InitializationField[];
|
||||
/** Absent only for legacy assemblies awaiting explicit ownership enrollment. */
|
||||
id?: ConformanceId;
|
||||
semanticMajor?: number;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { validateInitialization } from "./initialization.js";
|
||||
import { validateExecutionContracts } from "./execution.js";
|
||||
import { resolveForwardedBinding, type ConcreteBinding } from "./forwarding.js";
|
||||
import type {
|
||||
@@ -42,6 +43,7 @@ import { specializePackageExport } from "./generic-packages.js";
|
||||
import { isDeepStrictEqual } from "node:util";
|
||||
|
||||
export type CapabilityValidationIssueCode =
|
||||
| "invalid-initialization"
|
||||
| "invalid-execution-contract"
|
||||
| "invalid-query-contract"
|
||||
| "query-provider-reason-required"
|
||||
@@ -1043,6 +1045,7 @@ const validatePackages = (
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "creation":
|
||||
case "constructor":
|
||||
if (port.requirement.inputType)
|
||||
validateValueType(issues, port.requirement.inputType, `${portPath}.requirement.inputType`, indexes);
|
||||
@@ -1794,14 +1797,14 @@ const validateConformances = (
|
||||
});
|
||||
if (
|
||||
operation.scope === "class" &&
|
||||
(binding.dependencies.some((entry) => entry.binding.kind !== "constructor") ||
|
||||
packageExport.dependencyPorts.some((entry) => entry.requirement.kind !== "constructor"))
|
||||
(binding.dependencies.some((entry) => !["constructor", "creation"].includes(entry.binding.kind)) ||
|
||||
packageExport.dependencyPorts.some((entry) => !["constructor", "creation"].includes(entry.requirement.kind)))
|
||||
) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-package-binding",
|
||||
bindingPath,
|
||||
"Class functions may inject constructors, not instance-dependent ports",
|
||||
"Class functions require receiverless dependencies such as creation ports",
|
||||
);
|
||||
}
|
||||
validateBoundDependencies(issues, {
|
||||
@@ -2084,7 +2087,7 @@ export const validateWorkspaceRevision = (workspace: WorkspaceRevision): Capabil
|
||||
validatePackages(workspace, issues, indexes);
|
||||
validateConformances(workspace, issues, indexes);
|
||||
validateConstructors(workspace, issues, indexes);
|
||||
issues.push(...validateExecutionContracts(workspace));
|
||||
issues.push(...validateExecutionContracts(workspace), ...validateInitialization(workspace));
|
||||
return issues;
|
||||
};
|
||||
|
||||
@@ -2327,6 +2330,11 @@ export const computeCapabilityClosure = (
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "creation": {
|
||||
for (const owner of dependency.binding.owners)
|
||||
queued.push({ atomId: dependency.binding.atomId, interfaceRevisionId: owner.interfaceRevisionId });
|
||||
break;
|
||||
}
|
||||
case "constructor": {
|
||||
constructors.add(dependency.binding.atomId);
|
||||
const constructor = plan.constructors.get(dependency.binding.atomId);
|
||||
|
||||
Reference in New Issue
Block a user