Add factory creation ports and owner initialization contracts

This commit is contained in:
Timothy J. Aveni
2026-09-19 22:20:55 -07:00
parent 03ac7f8557
commit 0555c929b1
25 changed files with 3899 additions and 3012 deletions
+7
View File
@@ -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}`);
+2
View File
@@ -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`);
}
+3 -1
View File
@@ -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),
},
+121
View File
@@ -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;
}
+14 -1
View File
@@ -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;
+12 -4
View File
@@ -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);