import type { AtomConstructorBinding, AtomId, AttachmentOwner, Binding, BoundDependency, Conformance, DependencyBinding, DependencyPort, EdgeCardinality, EdgeDefinition, EdgeEndpoint, EdgeEndpointConstraint, EdgePrimitive, EdgeProjectionId, EdgeTypeId, InterfaceMember, InterfaceOperation, InterfaceOperationMode, InterfaceRevision, InterfaceRevisionId, OperationId, OwnedAttachment, PackageExport, PackageOperationExport, PackageFunctionExport, PackageRevision, PackageRevisionId, PersistentAttachment, SlotId, SourceRevision, StatePrimitive, StateSlotDefinition, ValueType, WorkspaceRevision, } from "./types.js"; import { valueType } from "./types.js"; import { appliedInterfaceId, GenericTypeError } from "./generics.js"; import { specializePackageExport } from "./generic-packages.js"; import { isDeepStrictEqual } from "node:util"; export type CapabilityValidationIssueCode = | "invalid-semantic-major" | "duplicate-conformance-id" | "required-value" | "invalid-source" | "invalid-value-type" | "invalid-operation" | "duplicate-atom" | "duplicate-attachment" | "duplicate-projection" | "duplicate-interface-revision" | "duplicate-interface-member" | "duplicate-interface-operation" | "duplicate-package-revision" | "duplicate-package-export" | "duplicate-dependency-port" | "duplicate-conformance" | "duplicate-operation-binding" | "duplicate-relationship-materialization" | "duplicate-constructor" | "unresolved-reference" | "invalid-attachment" | "invalid-state-binding" | "invalid-edge-binding" | "invalid-package-binding" | "invalid-dependency-binding" | "private-attachment-access" | "incompatible-package-receiver" | "unsatisfied-interface" | "cyclic-conformance-requirement" | "missing-operation-binding" | "unknown-operation-binding" | "invalid-constructor" | "invalid-relationship-materialization"; export interface CapabilityValidationIssue { code: CapabilityValidationIssueCode; path: string; message: string; } interface InterfaceOperationEntry { operation: InterfaceOperation; member: InterfaceMember; } interface InterfaceIndexEntry { revision: InterfaceRevision; operations: Map; } interface PackageIndexEntry { revision: PackageRevision; exports: Map; } interface AttachmentIndexEntry extends OwnedAttachment { path: string; } interface ValidationIndexes { atoms: Map; interfaces: Map; packages: Map; conformances: Map; conformancePaths: Map; attachments: Map; projections: Map; constructors: Map; } const hasText = (value: string) => value.trim().length > 0; const conformanceKey = (atomId: AtomId, interfaceRevisionId: InterfaceRevisionId): string => `${atomId}\u0000${interfaceRevisionId}`; const attachmentKey = (attachment: PersistentAttachment): string => `${attachment.kind}\u0000${attachment.id}`; const issue = ( issues: CapabilityValidationIssue[], code: CapabilityValidationIssueCode, path: string, message: string, ) => issues.push({ code, path, message }); const requireText = (issues: CapabilityValidationIssue[], value: string, path: string, label: string) => { if (!hasText(value)) { issue(issues, "required-value", path, `${label} is required`); } }; const typeLabel = (type: ValueType): string => { switch (type.kind) { case "builtin": case "scalar": return type.name; case "message": return `message:${type.descriptorId}`; case "record": return `record{${Object.keys(type.fields) .sort() .map((key) => `${key}:${typeLabel(type.fields[key])}`) .join(";")}}`; case "object-ref": return type.expectation.kind === "atom" ? `object:atom:${type.expectation.atomId}` : `object:interface:${type.expectation.interfaceRevisionId}`; case "optional": return `optional<${typeLabel(type.value)}>`; case "list": return `list<${typeLabel(type.value)}>`; } }; export const valueTypesEqual = (left: ValueType, right: ValueType): boolean => { if (left.kind !== right.kind) { return false; } switch (left.kind) { case "builtin": case "scalar": return left.name === (right as typeof left).name; case "message": return left.descriptorId === (right as typeof left).descriptorId; case "record": { const other = (right as typeof left).fields; return ( Object.keys(left.fields).length === Object.keys(other).length && Object.entries(left.fields).every( ([key, value]) => Object.hasOwn(other, key) && valueTypesEqual(value, other[key]), ) ); } case "object-ref": { const other = (right as typeof left).expectation; if (left.expectation.kind !== other.kind) { return false; } return left.expectation.kind === "atom" ? left.expectation.atomId === (other as typeof left.expectation).atomId : left.expectation.interfaceRevisionId === (other as typeof left.expectation).interfaceRevisionId; } case "optional": case "list": return valueTypesEqual(left.value, (right as typeof left).value); } }; const constraintsEqual = (left: EdgeEndpointConstraint, right: EdgeEndpointConstraint) => left.kind === right.kind && (left.kind === "atom" ? left.atomId === (right as typeof left).atomId : left.interfaceRevisionId === (right as typeof left).interfaceRevisionId); const sourceRequiredFields: Array<[keyof SourceRevision, string]> = [ ["repository", "repository"], ["commit", "commit"], ]; const validateSource = (issues: CapabilityValidationIssue[], source: SourceRevision, path: string) => { for (const [key, label] of sourceRequiredFields) { if (!hasText(source[key])) { issue(issues, "invalid-source", `${path}.${key}`, `Source ${label} is required`); } } }; const validateValueType = ( issues: CapabilityValidationIssue[], type: ValueType, path: string, indexes: Pick, ) => { switch (type.kind) { case "record": for (const [name, field] of Object.entries(type.fields)) { if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) issue(issues, "invalid-value-type", path, "Invalid record field name"); validateValueType(issues, field, `${path}.fields.${name}`, indexes); } return; case "builtin": if (!["unit", "watch-handle"].includes(type.name)) issue(issues, "invalid-value-type", path, "Unknown builtin value type"); return; case "scalar": if (!["bool", "bytes", "double", "int32", "int64", "string", "uint32", "uint64"].includes(type.name)) issue(issues, "invalid-value-type", path, "Unknown scalar value type"); return; case "message": requireText(issues, type.descriptorId, `${path}.descriptorId`, "Message descriptor identity"); return; case "optional": case "list": validateValueType(issues, type.value, `${path}.value`, indexes); return; case "object-ref": if (type.expectation.kind === "atom") { if (!indexes.atoms.has(type.expectation.atomId)) { issue( issues, "unresolved-reference", `${path}.expectation.atomId`, `Unknown atom ${type.expectation.atomId}`, ); } } else if (!indexes.interfaces.has(type.expectation.interfaceRevisionId)) { issue( issues, "unresolved-reference", `${path}.expectation.interfaceRevisionId`, `Unknown interface revision ${type.expectation.interfaceRevisionId}`, ); } return; default: issue( issues, "invalid-value-type", path, "Installed contracts require closed value types; unresolved authoring expressions are not executable", ); } }; const validateConstraint = ( issues: CapabilityValidationIssue[], constraint: EdgeEndpointConstraint, path: string, indexes: Pick, ) => { if (constraint.kind === "atom") { if (!indexes.atoms.has(constraint.atomId)) { issue(issues, "unresolved-reference", `${path}.atomId`, `Unknown atom ${constraint.atomId}`); } } else if (!indexes.interfaces.has(constraint.interfaceRevisionId)) { issue( issues, "unresolved-reference", `${path}.interfaceRevisionId`, `Unknown interface revision ${constraint.interfaceRevisionId}`, ); } }; const emitsEvents = (mode: InterfaceOperationMode) => mode === "watch-start" || mode === "subscribe"; const validateOperationEventShape = ( issues: CapabilityValidationIssue[], operation: Pick, path: string, ) => { if (emitsEvents(operation.mode) && operation.eventType === undefined) { issue(issues, "invalid-operation", `${path}.eventType`, `${operation.mode} operations require an event type`); } if (!emitsEvents(operation.mode) && operation.eventType !== undefined) { issue( issues, "invalid-operation", `${path}.eventType`, `${operation.mode} operations must not declare an event type`, ); } }; interface OperationSignature { mode: InterfaceOperationMode; inputType: ValueType; outputType: ValueType; eventType?: ValueType; } const signaturesMatch = (operation: OperationSignature, implementation: OperationSignature) => operation.mode === implementation.mode && valueTypesEqual(operation.inputType, implementation.inputType) && valueTypesEqual(operation.outputType, implementation.outputType) && (operation.eventType === undefined ? implementation.eventType === undefined : implementation.eventType !== undefined && valueTypesEqual(operation.eventType, implementation.eventType)); const describeSignature = (signature: OperationSignature) => { const event = signature.eventType ? ` emits ${typeLabel(signature.eventType)}` : ""; return `${signature.mode} ${typeLabel(signature.inputType)} -> ${typeLabel(signature.outputType)}${event}`; }; export const statePrimitiveSignature = (slot: StateSlotDefinition, primitive: StatePrimitive): OperationSignature => { const writeType = slot.storagePolicy.kind === "crdt-document" ? slot.storagePolicy.updateType : slot.valueType; switch (primitive) { case "read": return { mode: "call", inputType: valueType.unit, outputType: slot.valueType, }; case "write": return { mode: "call", inputType: writeType, outputType: valueType.unit, }; case "watch-start": return { mode: "watch-start", inputType: valueType.unit, outputType: valueType.watchHandle, eventType: slot.valueType, }; case "watch-stop": return { mode: "watch-stop", inputType: valueType.watchHandle, outputType: valueType.unit, }; } }; const constraintValueType = (constraint: EdgeEndpointConstraint): ValueType => constraint.kind === "atom" ? valueType.atomRef(constraint.atomId) : valueType.interfaceRef(constraint.interfaceRevisionId); export const cardinalityValueType = (constraint: EdgeEndpointConstraint, cardinality: EdgeCardinality): ValueType => { const target = constraintValueType(constraint); switch (cardinality) { case "exactly-one": return target; case "optional-one": return valueType.optional(target); case "many": case "many-unique": return valueType.list(target); } }; const edgeProjection = (edge: EdgeDefinition, projectionId: EdgeProjectionId) => { const index = edge.endpoints.findIndex((endpoint) => endpoint.projectionId === projectionId); if (index !== 0 && index !== 1) { return undefined; } const endpointIndex = index as 0 | 1; return { endpoint: edge.endpoints[endpointIndex], target: edge.endpoints[endpointIndex === 0 ? 1 : 0], endpointIndex, }; }; export const edgePrimitiveSignature = ( edge: EdgeDefinition, projectionId: EdgeProjectionId, primitive: EdgePrimitive, ): OperationSignature | undefined => { const projection = edgeProjection(edge, projectionId); if (!projection) { return undefined; } const targetType = constraintValueType(projection.target.constraint); const resolvedType = cardinalityValueType(projection.target.constraint, projection.endpoint.cardinality); switch (primitive) { case "resolve": return { mode: "call", inputType: valueType.unit, outputType: resolvedType, }; case "connect": case "disconnect": return { mode: "call", inputType: targetType, outputType: valueType.unit, }; case "watch-start": return { mode: "watch-start", inputType: valueType.unit, outputType: valueType.watchHandle, eventType: resolvedType, }; case "watch-stop": return { mode: "watch-stop", inputType: valueType.watchHandle, outputType: valueType.unit, }; } }; const atomSatisfiesConstraint = ( atomId: AtomId, constraint: EdgeEndpointConstraint, conformances: ReadonlyMap, ) => constraint.kind === "atom" ? constraint.atomId === atomId : conformances.has(conformanceKey(atomId, constraint.interfaceRevisionId)); const constraintSatisfiesConstraint = ( actual: EdgeEndpointConstraint, required: EdgeEndpointConstraint, conformances: ReadonlyMap, ) => constraintsEqual(actual, required) || (actual.kind === "atom" && required.kind === "interface" && atomSatisfiesConstraint(actual.atomId, required, conformances)); const canAccessAttachment = ( owner: AttachmentOwner, conformance: Pick | undefined, ) => owner.kind === "workspace" || (conformance !== undefined && owner.atomId === conformance.atomId && owner.interfaceRevisionId === conformance.interfaceRevisionId); const collectIdentityIndexes = ( workspace: WorkspaceRevision, issues: CapabilityValidationIssue[], ): ValidationIndexes => { const atoms = new Map(); for (const [index, atom] of workspace.atoms.entries()) { const path = `atoms[${index}]`; requireText(issues, atom.id, `${path}.id`, "Atom ID"); requireText(issues, atom.displayName, `${path}.displayName`, "Atom name"); if (atoms.has(atom.id)) { issue(issues, "duplicate-atom", `${path}.id`, `Duplicate atom ${atom.id}`); } else { atoms.set(atom.id, atom); } } const interfaces = new Map(); for (const [interfaceIndex, revision] of workspace.interfaceImports.entries()) { const path = `interfaceImports[${interfaceIndex}]`; requireText(issues, revision.interfaceId, `${path}.interfaceId`, "Interface ID"); if (revision.template) issue(issues, "invalid-value-type", path, "Unapplied generic interface cannot enter an installed workspace"); requireText(issues, revision.revisionId, `${path}.revisionId`, "Interface revision ID"); requireText(issues, revision.displayName, `${path}.displayName`, "Interface name"); validateSource(issues, revision.source, `${path}.source`); if (revision.application) { try { if ( appliedInterfaceId(revision.application) !== revision.revisionId || revision.application.source.repository !== revision.source.repository || revision.application.source.commit !== revision.source.commit ) issue( issues, "invalid-value-type", path, "Applied interface identity does not match its exact provenance and arguments", ); } catch (error) { if (!(error instanceof GenericTypeError)) throw error; issue(issues, "invalid-value-type", path, error.message); } } const memberIds = new Set(); const operations = new Map(); for (const [memberIndex, member] of revision.members.entries()) { const memberPath = `${path}.members[${memberIndex}]`; requireText(issues, member.id, `${memberPath}.id`, "Member ID"); requireText(issues, member.displayName, `${memberPath}.displayName`, "Member name"); if (memberIds.has(member.id)) { issue(issues, "duplicate-interface-member", `${memberPath}.id`, `Duplicate member ${member.id}`); } memberIds.add(member.id); for (const [operationIndex, operation] of member.operations.entries()) { const operationPath = `${memberPath}.operations[${operationIndex}]`; requireText(issues, operation.id, `${operationPath}.id`, "Operation ID"); requireText(issues, operation.displayName, `${operationPath}.displayName`, "Operation name"); validateOperationEventShape(issues, operation, operationPath); if (operations.has(operation.id)) { issue(issues, "duplicate-interface-operation", `${operationPath}.id`, `Duplicate operation ${operation.id}`); } else { operations.set(operation.id, { operation, member }); } } } if (interfaces.has(revision.revisionId)) { issue( issues, "duplicate-interface-revision", `${path}.revisionId`, `Duplicate interface revision ${revision.revisionId}`, ); } else { interfaces.set(revision.revisionId, { revision, operations }); } } const packages = new Map(); for (const [packageIndex, revision] of workspace.packageImports.entries()) { const path = `packageImports[${packageIndex}]`; if ( revision.semanticMajor !== undefined && (!Number.isSafeInteger(revision.semanticMajor) || revision.semanticMajor < 1) ) { issue( issues, "invalid-semantic-major", `${path}.semanticMajor`, "Semantic major must be a positive safe integer", ); } requireText(issues, revision.packageId, `${path}.packageId`, "Package ID"); requireText(issues, revision.revisionId, `${path}.revisionId`, "Package revision ID"); requireText(issues, revision.displayName, `${path}.displayName`, "Package name"); validateSource(issues, revision.source, `${path}.source`); const exports = new Map(); for (const [exportIndex, entry] of revision.exports.entries()) { const exportPath = `${path}.exports[${exportIndex}]`; if (entry.application) { try { const definition = revision.genericExports?.find((candidate) => candidate.id === entry.application!.exportId); if (!definition) throw new Error("Missing generic export definition"); const expected = specializePackageExport(revision, definition, entry.application.arguments, { arguments: new Map(), self: entry.application.self, applyInterface: (id, args) => { const applied = workspace.interfaceImports.find( (contract) => contract.application?.definitionId === id && isDeepStrictEqual(contract.application.arguments, args), ); if (applied) return applied.revisionId; const concrete = workspace.interfaceImports.find((contract) => contract.revisionId === id); if (concrete && !concrete.template && args.length === 0) return id; throw new Error(`Missing closed interface application ${id}`); }, // Retained obligations are discharged against candidate conformances below. implementsInterface: (target, required) => (revision.argumentRequirements ?? []).some( (obligation) => obligation.required === required && isDeepStrictEqual(obligation.target, target), ), }); if (!isDeepStrictEqual(entry, expected)) throw new Error("Specialized export differs from its definition, source or arguments"); } catch (error) { issue(issues, "invalid-operation", exportPath, error instanceof Error ? error.message : String(error)); } } requireText(issues, entry.id, `${exportPath}.id`, "Package export ID"); requireText(issues, entry.displayName, `${exportPath}.displayName`, "Package export name"); if (exports.has(entry.id)) { issue(issues, "duplicate-package-export", `${exportPath}.id`, `Duplicate package export ${entry.id}`); } else { exports.set(entry.id, entry); } const portIds = new Set(); for (const [portIndex, port] of entry.dependencyPorts.entries()) { const portPath = `${exportPath}.dependencyPorts[${portIndex}]`; requireText(issues, port.id, `${portPath}.id`, "Dependency port ID"); requireText(issues, port.displayName, `${portPath}.displayName`, "Dependency port name"); if (portIds.has(port.id)) { issue(issues, "duplicate-dependency-port", `${portPath}.id`, `Duplicate dependency port ${port.id}`); } portIds.add(port.id); } } if (packages.has(revision.revisionId)) { issue( issues, "duplicate-package-revision", `${path}.revisionId`, `Duplicate package revision ${revision.revisionId}`, ); } else { packages.set(revision.revisionId, { revision, exports }); } } const conformances = new Map(); const conformancePaths = new Map(); const conformanceIds = new Set(); for (const [index, conformance] of workspace.conformances.entries()) { const path = `conformances[${index}]`; if ( conformance.semanticMajor !== undefined && (!Number.isSafeInteger(conformance.semanticMajor) || conformance.semanticMajor < 1) ) { issue( issues, "invalid-semantic-major", `${path}.semanticMajor`, "Semantic major must be a positive safe integer", ); } if (conformance.id !== undefined) { requireText(issues, conformance.id, `${path}.id`, "Conformance ID"); if (conformanceIds.has(conformance.id)) issue(issues, "duplicate-conformance-id", `${path}.id`, `Duplicate conformance ID ${conformance.id}`); conformanceIds.add(conformance.id); } const key = conformanceKey(conformance.atomId, conformance.interfaceRevisionId); if (conformances.has(key)) { issue( issues, "duplicate-conformance", path, `Duplicate conformance for ${conformance.atomId} as ${conformance.interfaceRevisionId}`, ); } else { conformances.set(key, conformance); conformancePaths.set(key, path); } } const constructors = new Map(); for (const [index, constructor] of workspace.constructors.entries()) { const path = `constructors[${index}]`; if (constructors.has(constructor.atomId)) { issue(issues, "duplicate-constructor", path, `Atom ${constructor.atomId} has more than one constructor`); } else { constructors.set(constructor.atomId, constructor); } } const attachments = new Map(); const projections = new Map(); const addAttachment = (attachment: PersistentAttachment, owner: AttachmentOwner, path: string) => { const key = attachmentKey(attachment); if (attachments.has(key)) { issue(issues, "duplicate-attachment", `${path}.id`, `Duplicate ${attachment.kind} attachment ${attachment.id}`); } else { attachments.set(key, { attachment, owner, path }); } if (attachment.kind === "edge") { for (const [endpointIndex, endpoint] of attachment.endpoints.entries()) { const endpointPath = `${path}.endpoints[${endpointIndex}]`; if (projections.has(endpoint.projectionId)) { issue( issues, "duplicate-projection", `${endpointPath}.projectionId`, `Duplicate edge projection ${endpoint.projectionId}`, ); } else { projections.set(endpoint.projectionId, { edge: attachment, endpoint, endpointIndex: endpointIndex as 0 | 1, }); } } } }; for (const [index, attachment] of workspace.sharedAttachments.entries()) { addAttachment(attachment, { kind: "workspace" }, `sharedAttachments[${index}]`); } for (const [conformanceIndex, conformance] of workspace.conformances.entries()) { for (const [attachmentIndex, attachment] of conformance.privateAttachments.entries()) { addAttachment( attachment, { kind: "conformance", atomId: conformance.atomId, interfaceRevisionId: conformance.interfaceRevisionId, }, `conformances[${conformanceIndex}].privateAttachments[${attachmentIndex}]`, ); } } return { atoms, interfaces, packages, conformances, conformancePaths, attachments, projections, constructors, }; }; const validateInterfaces = ( workspace: WorkspaceRevision, issues: CapabilityValidationIssue[], indexes: ValidationIndexes, ) => { const implies = ( actual: InterfaceRevisionId, required: InterfaceRevisionId, visited = new Set(), ): boolean => { if (actual === required) return true; if (visited.has(actual)) return false; visited.add(actual); return (indexes.interfaces.get(actual)?.revision.requiredInterfaces ?? []).some((entry) => implies(entry, required, visited), ); }; for (const [packageIndex, pkg] of workspace.packageImports.entries()) { for (const obligation of pkg.argumentRequirements ?? []) { const satisfied = obligation.target.kind === "atom" ? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required)) : implies(obligation.target.interfaceRevisionId, obligation.required); if (!satisfied) issue( issues, "unsatisfied-interface", `packageImports[${packageIndex}]`, `Generic argument does not implement ${obligation.required}`, ); } } for (const [interfaceIndex, revision] of workspace.interfaceImports.entries()) { const path = `interfaceImports[${interfaceIndex}]`; for (const required of revision.requiredInterfaces ?? []) { if (!indexes.interfaces.has(required)) issue(issues, "unresolved-reference", path, `Unknown prerequisite interface ${required}`); else if (implies(required, revision.revisionId)) issue( issues, "cyclic-conformance-requirement", path, `Cyclic prerequisite involving ${revision.revisionId} and ${required}`, ); } for (const obligation of revision.argumentRequirements ?? []) { const satisfied = obligation.target.kind === "atom" ? indexes.conformances.has(conformanceKey(obligation.target.atomId, obligation.required)) : implies(obligation.target.interfaceRevisionId, obligation.required); if (!satisfied) issue(issues, "unsatisfied-interface", path, `Generic argument does not implement ${obligation.required}`); } for (const [memberIndex, member] of revision.members.entries()) { const memberPath = `${path}.members[${memberIndex}]`; if (member.kind === "value") { validateValueType(issues, member.valueType, `${memberPath}.valueType`, indexes); } else if (member.kind === "relationship") { validateConstraint(issues, member.target, `${memberPath}.target`, indexes); } else { validateValueType(issues, member.inputType, `${memberPath}.inputType`, indexes); validateValueType(issues, member.outputType, `${memberPath}.outputType`, indexes); } for (const [operationIndex, operation] of member.operations.entries()) { const operationPath = `${memberPath}.operations[${operationIndex}]`; validateValueType(issues, operation.inputType, `${operationPath}.inputType`, indexes); validateValueType(issues, operation.outputType, `${operationPath}.outputType`, indexes); if (operation.eventType) { validateValueType(issues, operation.eventType, `${operationPath}.eventType`, indexes); } } } } }; const validateAttachments = (issues: CapabilityValidationIssue[], indexes: ValidationIndexes) => { for (const { attachment, owner, path } of indexes.attachments.values()) { requireText(issues, attachment.id, `${path}.id`, `${attachment.kind} attachment ID`); requireText(issues, attachment.displayName, `${path}.displayName`, `${attachment.kind} name`); if (attachment.kind === "state") { if (!indexes.atoms.has(attachment.attachedTo)) { issue(issues, "unresolved-reference", `${path}.attachedTo`, `Unknown atom ${attachment.attachedTo}`); } validateValueType(issues, attachment.valueType, `${path}.valueType`, indexes); const containsRpcType = (type: ValueType): boolean => type.kind === "object-ref" || type.kind === "record" || ((type.kind === "optional" || type.kind === "list") && containsRpcType(type.value)); if ( containsRpcType(attachment.valueType) || (attachment.storagePolicy.kind === "crdt-document" && containsRpcType(attachment.storagePolicy.updateType)) ) { issue( issues, "invalid-attachment", `${path}.valueType`, "Managed object references belong in graph relationships, not ordinary state; record types are RPC-only", ); } if (attachment.storagePolicy.kind === "crdt-document") { validateValueType(issues, attachment.storagePolicy.updateType, `${path}.storagePolicy.updateType`, indexes); } if (owner.kind === "conformance") { if (attachment.attachedTo !== owner.atomId) { issue( issues, "invalid-attachment", `${path}.attachedTo`, `Private state must attach to its conformance atom ${owner.atomId}`, ); } } continue; } if (attachment.endpoints.length !== 2) { issue(issues, "invalid-attachment", `${path}.endpoints`, `Edge ${attachment.id} must have exactly two endpoints`); } for (const [endpointIndex, endpoint] of attachment.endpoints.entries()) { const endpointPath = `${path}.endpoints[${endpointIndex}]`; if ( endpoint.keyType !== undefined && (!["string", "boolean", "int64"].includes(endpoint.keyType) || endpoint.ordered || !["many", "many-unique"].includes(endpoint.cardinality)) ) { issue( issues, "invalid-attachment", `${endpointPath}.keyType`, "Keyed projections require string/boolean/int64 keys, many cardinality, and no ordering", ); } requireText(issues, endpoint.projectionId, `${endpointPath}.projectionId`, "Projection ID"); if (endpoint.onDelete !== undefined && !["restrict", "detach", "cascade-other"].includes(endpoint.onDelete)) { issue( issues, "invalid-attachment", `${endpointPath}.onDelete`, "Deletion policy must be restrict, detach, or cascade-other", ); } requireText(issues, endpoint.displayName, `${endpointPath}.displayName`, "Projection name"); validateConstraint(issues, endpoint.constraint, `${endpointPath}.constraint`, indexes); } if (attachment.endpoints.every((endpoint) => endpoint.keyType)) issue( issues, "invalid-attachment", `${path}.endpoints`, "A v0 map has one keyed projection and one unkeyed inverse", ); if (owner.kind === "conformance") { if ( !attachment.endpoints.some((endpoint) => atomSatisfiesConstraint(owner.atomId, endpoint.constraint, indexes.conformances), ) ) { issue( issues, "invalid-attachment", `${path}.endpoints`, `Private edge has no endpoint compatible with conformance atom ${owner.atomId}`, ); } } } }; const uniquePrimitiveList = (issues: CapabilityValidationIssue[], primitives: readonly string[], path: string) => { if (primitives.length === 0) { issue(issues, "invalid-dependency-binding", path, "Dependency port requires at least one primitive"); } const seen = new Set(); for (const primitive of primitives) { if (seen.has(primitive)) { issue(issues, "invalid-dependency-binding", path, `Dependency primitive ${primitive} is listed more than once`); } seen.add(primitive); } }; const validatePackages = ( workspace: WorkspaceRevision, issues: CapabilityValidationIssue[], indexes: ValidationIndexes, ) => { for (const [packageIndex, revision] of workspace.packageImports.entries()) { const path = `packageImports[${packageIndex}]`; for (const [exportIndex, entry] of revision.exports.entries()) { const exportPath = `${path}.exports[${exportIndex}]`; validateValueType(issues, entry.inputType, `${exportPath}.inputType`, indexes); validateValueType(issues, entry.outputType, `${exportPath}.outputType`, indexes); if (entry.kind === "operation") { validateOperationEventShape(issues, entry, exportPath); if (entry.eventType) { validateValueType(issues, entry.eventType, `${exportPath}.eventType`, indexes); } if (entry.receiverRequirement.kind === "exact-atom") { if (!indexes.atoms.has(entry.receiverRequirement.atomId)) { issue( issues, "unresolved-reference", `${exportPath}.receiverRequirement.atomId`, `Unknown atom ${entry.receiverRequirement.atomId}`, ); } } else if (entry.receiverRequirement.kind === "all-interfaces") { const seen = new Set(); for (const [ requirementIndex, interfaceRevisionId, ] of entry.receiverRequirement.interfaceRevisionIds.entries()) { const requirementPath = `${exportPath}.receiverRequirement.interfaceRevisionIds[${requirementIndex}]`; if (!indexes.interfaces.has(interfaceRevisionId)) { issue( issues, "unresolved-reference", requirementPath, `Unknown interface revision ${interfaceRevisionId}`, ); } if (seen.has(interfaceRevisionId)) { issue( issues, "invalid-operation", requirementPath, `Receiver interface ${interfaceRevisionId} is duplicated`, ); } seen.add(interfaceRevisionId); } } } else if (entry.kind === "constructor") { if (!indexes.atoms.has(entry.constructsAtom)) { issue(issues, "unresolved-reference", `${exportPath}.constructsAtom`, `Unknown atom ${entry.constructsAtom}`); } const expected = valueType.atomRef(entry.constructsAtom); if (!valueTypesEqual(entry.outputType, expected)) { issue( issues, "invalid-constructor", `${exportPath}.outputType`, `Constructor must return ${typeLabel(expected)}, not ${typeLabel(entry.outputType)}`, ); } } for (const [portIndex, port] of entry.dependencyPorts.entries()) { const portPath = `${exportPath}.dependencyPorts[${portIndex}]`; switch (port.requirement.kind) { case "state": validateValueType(issues, port.requirement.valueType, `${portPath}.requirement.valueType`, indexes); uniquePrimitiveList(issues, port.requirement.primitives, `${portPath}.requirement.primitives`); break; case "edge": validateConstraint(issues, port.requirement.target, `${portPath}.requirement.target`, indexes); uniquePrimitiveList(issues, port.requirement.primitives, `${portPath}.requirement.primitives`); break; case "interface": if (!indexes.interfaces.has(port.requirement.interfaceRevisionId)) { issue( issues, "unresolved-reference", `${portPath}.requirement.interfaceRevisionId`, `Unknown interface revision ${port.requirement.interfaceRevisionId}`, ); } break; case "constructor": if (port.requirement.inputType) validateValueType(issues, port.requirement.inputType, `${portPath}.requirement.inputType`, indexes); if (!indexes.atoms.has(port.requirement.atomId)) { issue( issues, "unresolved-reference", `${portPath}.requirement.atomId`, `Unknown atom ${port.requirement.atomId}`, ); } } } } } }; const findAttachment = ( indexes: ValidationIndexes, kind: Kind, id: Kind extends "state" ? SlotId : EdgeTypeId, ): AttachmentIndexEntry | undefined => indexes.attachments.get(`${kind}\u0000${id}`); const validateAttachmentAccess = ( issues: CapabilityValidationIssue[], entry: AttachmentIndexEntry, conformance: Pick | undefined, path: string, ) => { if (!canAccessAttachment(entry.owner, conformance)) { issue( issues, "private-attachment-access", path, `Attachment ${entry.attachment.id} is private to conformance ${ entry.owner.kind === "conformance" ? `${entry.owner.atomId} as ${entry.owner.interfaceRevisionId}` : "" }`, ); return false; } return true; }; const validateTraversal = ( issues: CapabilityValidationIssue[], indexes: ValidationIndexes, atomId: AtomId, traversal: { edgeTypeId: EdgeTypeId; projectionId: EdgeProjectionId }, path: string, conformance?: Pick, ) => { const attachment = findAttachment(indexes, "edge", traversal.edgeTypeId); if (!attachment || attachment.attachment.kind !== "edge") { issue(issues, "invalid-dependency-binding", `${path}.edgeTypeId`, `Unknown traversal edge ${traversal.edgeTypeId}`); return undefined; } const projection = edgeProjection(attachment.attachment, traversal.projectionId); if (!projection) { issue( issues, "invalid-dependency-binding", `${path}.projectionId`, `Traversal projection ${traversal.projectionId} is not on edge ${traversal.edgeTypeId}`, ); return undefined; } if (!projection.endpoint.publicTraversal) validateAttachmentAccess(issues, attachment, conformance, `${path}.edgeTypeId`); if (!atomSatisfiesConstraint(atomId, projection.endpoint.constraint, indexes.conformances)) { issue( issues, "invalid-dependency-binding", `${path}.projectionId`, `Traversal projection ${traversal.projectionId} cannot originate at receiver atom ${atomId}`, ); } if (projection.endpoint.cardinality !== "exactly-one") { issue( issues, "invalid-dependency-binding", `${path}.projectionId`, "Dependency traversal must select exactly one object", ); } return projection; }; const validateBoundDependencies = ( issues: CapabilityValidationIssue[], params: { dependencies: BoundDependency[]; dependencyPorts: DependencyPort[]; atomId: AtomId; conformance?: Pick; path: string; indexes: ValidationIndexes; requirementGraph: Map>; graphSourceKey?: string; }, ) => { const ports = new Map(params.dependencyPorts.map((port) => [port.id, port])); const bindings = new Map(); for (const [index, dependency] of params.dependencies.entries()) { const path = `${params.path}.dependencies[${index}]`; if (bindings.has(dependency.portId)) { issue( issues, "invalid-dependency-binding", `${path}.portId`, `Dependency port ${dependency.portId} is bound more than once`, ); continue; } bindings.set(dependency.portId, dependency); const port = ports.get(dependency.portId); if (!port) { issue(issues, "invalid-dependency-binding", `${path}.portId`, `Unknown dependency port ${dependency.portId}`); continue; } const requirement = port.requirement; const binding = dependency.binding; if (requirement.kind !== binding.kind) { issue( issues, "invalid-dependency-binding", `${path}.binding`, `Port ${port.id} requires ${requirement.kind}, not ${binding.kind}`, ); continue; } switch (requirement.kind) { case "state": { const stateBinding = binding as Extract; const attachment = findAttachment(params.indexes, "state", stateBinding.slotId); if (!attachment || attachment.attachment.kind !== "state") { issue( issues, "invalid-dependency-binding", `${path}.binding.slotId`, `Unknown state attachment ${stateBinding.slotId}`, ); break; } validateAttachmentAccess(issues, attachment, params.conformance, `${path}.binding.slotId`); const traversal = stateBinding.via ? validateTraversal( issues, params.indexes, params.atomId, stateBinding.via, `${path}.binding.via`, params.conformance, ) : undefined; if ( stateBinding.via ? traversal && !atomSatisfiesConstraint( attachment.attachment.attachedTo, traversal.target.constraint, params.indexes.conformances, ) : attachment.attachment.attachedTo !== params.atomId ) { issue( issues, "invalid-dependency-binding", `${path}.binding.slotId`, stateBinding.via ? `State ${stateBinding.slotId} is not attached to the traversal target` : `State ${stateBinding.slotId} is not attached to receiver atom ${params.atomId}`, ); } if (!valueTypesEqual(requirement.valueType, attachment.attachment.valueType)) { issue( issues, "invalid-dependency-binding", `${path}.binding.slotId`, `State port expects ${typeLabel(requirement.valueType)}, not ${typeLabel(attachment.attachment.valueType)}`, ); } break; } case "edge": { const edgeBinding = binding as Extract; const attachment = findAttachment(params.indexes, "edge", edgeBinding.edgeTypeId); if (!attachment || attachment.attachment.kind !== "edge") { issue( issues, "invalid-dependency-binding", `${path}.binding.edgeTypeId`, `Unknown edge attachment ${edgeBinding.edgeTypeId}`, ); break; } validateAttachmentAccess(issues, attachment, params.conformance, `${path}.binding.edgeTypeId`); const projection = edgeProjection(attachment.attachment, edgeBinding.projectionId); if (!projection) { issue( issues, "invalid-dependency-binding", `${path}.binding.projectionId`, `Projection ${edgeBinding.projectionId} is not on edge ${edgeBinding.edgeTypeId}`, ); break; } const traversal = edgeBinding.via ? validateTraversal( issues, params.indexes, params.atomId, edgeBinding.via, `${path}.binding.via`, params.conformance, ) : undefined; const originMatches = edgeBinding.via ? traversal && (projection.endpoint.constraint.kind === "atom" ? atomSatisfiesConstraint( projection.endpoint.constraint.atomId, traversal.target.constraint, params.indexes.conformances, ) : constraintsEqual(projection.endpoint.constraint, traversal.target.constraint)) : atomSatisfiesConstraint(params.atomId, projection.endpoint.constraint, params.indexes.conformances); if (!originMatches) { issue( issues, "invalid-dependency-binding", `${path}.binding.projectionId`, edgeBinding.via ? `Projection ${edgeBinding.projectionId} cannot originate at the traversal target` : `Projection ${edgeBinding.projectionId} cannot originate at receiver atom ${params.atomId}`, ); } if ( !constraintSatisfiesConstraint( projection.target.constraint, requirement.target, params.indexes.conformances, ) || requirement.cardinality !== projection.endpoint.cardinality ) { issue( issues, "invalid-dependency-binding", `${path}.binding`, `Edge port target or cardinality does not match projection ${edgeBinding.projectionId}`, ); } break; } case "interface": { const interfaceBinding = binding as Extract; if (interfaceBinding.interfaceRevisionId !== requirement.interfaceRevisionId) { issue( issues, "invalid-dependency-binding", `${path}.binding.interfaceRevisionId`, `Interface port requires ${requirement.interfaceRevisionId}`, ); break; } const traversal = interfaceBinding.via ? validateTraversal( issues, params.indexes, params.atomId, interfaceBinding.via, `${path}.binding.via`, params.conformance, ) : undefined; const candidateAtoms = interfaceBinding.via ? traversal ? ([...params.indexes.atoms.keys()].filter((atomId) => atomSatisfiesConstraint(atomId as AtomId, traversal.target.constraint, params.indexes.conformances), ) as AtomId[]) : [] : [params.atomId]; for (const atomId of candidateAtoms) { const targetKey = conformanceKey(atomId, requirement.interfaceRevisionId); if (!params.indexes.conformances.has(targetKey)) { issue( issues, "unsatisfied-interface", `${path}.binding.interfaceRevisionId`, `${interfaceBinding.via ? "Related" : "Receiver"} atom ${atomId} does not conform to ${requirement.interfaceRevisionId}`, ); } else if (params.graphSourceKey) { params.requirementGraph.get(params.graphSourceKey)?.add(targetKey); } } break; } case "constructor": { const constructorBinding = binding as Extract; if (constructorBinding.atomId !== requirement.atomId) { issue( issues, "invalid-dependency-binding", `${path}.binding.atomId`, `Constructor port requires atom ${requirement.atomId}`, ); } if (!params.indexes.constructors.has(requirement.atomId)) { issue( issues, "invalid-dependency-binding", `${path}.binding.atomId`, `Atom ${requirement.atomId} has no constructor binding`, ); } if (requirement.inputType) { const selected = params.indexes.constructors.get(requirement.atomId); const implementation = selected ? params.indexes.packages.get(selected.packageRevisionId)?.exports.get(selected.exportId) : undefined; if (implementation && !valueTypesEqual(requirement.inputType, implementation.inputType)) { issue( issues, "invalid-dependency-binding", `${path}.binding.atomId`, `Constructor port requires input ${typeLabel(requirement.inputType)}, but selected constructor accepts ${typeLabel(implementation.inputType)}`, ); } } break; } } } for (const port of params.dependencyPorts) { if (!bindings.has(port.id)) { issue( issues, "invalid-dependency-binding", `${params.path}.dependencies`, `Missing binding for dependency port ${port.id}`, ); } } }; const validatePackageReceiver = ( issues: CapabilityValidationIssue[], params: { entry: PackageOperationExport; atomId: AtomId; path: string; indexes: ValidationIndexes; requirementGraph: Map>; graphSourceKey: string; }, ) => { const receiver = params.entry.receiverRequirement; if (receiver.kind === "exact-atom" && receiver.atomId !== params.atomId) { issue( issues, "incompatible-package-receiver", params.path, `Package export requires atom ${receiver.atomId}, not ${params.atomId}`, ); } if (receiver.kind === "all-interfaces") { for (const requiredInterface of receiver.interfaceRevisionIds) { const targetKey = conformanceKey(params.atomId, requiredInterface); if (!params.indexes.conformances.has(targetKey)) { issue( issues, "unsatisfied-interface", params.path, `Atom ${params.atomId} does not conform to ${requiredInterface}`, ); } else { params.requirementGraph.get(params.graphSourceKey)?.add(targetKey); } } } }; const validateConformances = ( workspace: WorkspaceRevision, issues: CapabilityValidationIssue[], indexes: ValidationIndexes, ) => { const requirementGraph = new Map>(); for (const key of indexes.conformances.keys()) { requirementGraph.set(key, new Set()); } for (const [index, conformance] of workspace.conformances.entries()) { const path = `conformances[${index}]`; const key = conformanceKey(conformance.atomId, conformance.interfaceRevisionId); if (!indexes.atoms.has(conformance.atomId)) { issue(issues, "unresolved-reference", `${path}.atomId`, `Unknown atom ${conformance.atomId}`); } const interfaceEntry = indexes.interfaces.get(conformance.interfaceRevisionId); if (!interfaceEntry) { issue( issues, "unresolved-reference", `${path}.interfaceRevisionId`, `Unknown interface revision ${conformance.interfaceRevisionId}`, ); continue; } const bindings = new Map(); for (const required of interfaceEntry.revision.requiredInterfaces ?? []) { const requiredKey = conformanceKey(conformance.atomId, required); if (!indexes.conformances.has(requiredKey)) issue(issues, "unsatisfied-interface", path, `Conformance requires ${conformance.atomId} as ${required}`); else requirementGraph.get(key)?.add(requiredKey); } for (const [bindingIndex, entry] of conformance.operationBindings.entries()) { const bindingPath = `${path}.operationBindings[${bindingIndex}]`; if (bindings.has(entry.operationId)) { issue( issues, "duplicate-operation-binding", `${bindingPath}.operationId`, `Operation ${entry.operationId} is bound more than once`, ); continue; } bindings.set(entry.operationId, entry.binding); const operationEntry = interfaceEntry.operations.get(entry.operationId); if (!operationEntry) { issue( issues, "unknown-operation-binding", `${bindingPath}.operationId`, `Operation ${entry.operationId} does not belong to interface ${conformance.interfaceRevisionId}`, ); continue; } const operation = operationEntry.operation; const binding = entry.binding; if (operation.scope === "class" && (!conformance.id || operation.mode !== "call")) { issue( issues, "invalid-package-binding", bindingPath, "Class capabilities require an explicit conformance ID and call mode", ); continue; } if (operation.scope === "class" && binding.kind !== "package") { issue( issues, "invalid-package-binding", bindingPath, "Class capabilities must bind a free package function, not instance storage", ); continue; } if (binding.kind === "state") { const attachment = findAttachment(indexes, "state", binding.slotId); if (!attachment || attachment.attachment.kind !== "state") { issue( issues, "invalid-state-binding", `${bindingPath}.binding.slotId`, `Unknown state attachment ${binding.slotId}`, ); continue; } validateAttachmentAccess(issues, attachment, conformance, `${bindingPath}.binding.slotId`); if (attachment.attachment.attachedTo !== conformance.atomId) { issue( issues, "invalid-state-binding", `${bindingPath}.binding.slotId`, `State ${binding.slotId} is not attached to atom ${conformance.atomId}`, ); } if (operationEntry.member.kind !== "value") { issue( issues, "invalid-state-binding", `${bindingPath}.binding`, "State primitives may only implement value members", ); } const expected = statePrimitiveSignature(attachment.attachment, binding.primitive); if (!signaturesMatch(operation, expected)) { issue( issues, "invalid-state-binding", `${bindingPath}.binding`, `State primitive provides ${describeSignature(expected)}, but operation requires ${describeSignature(operation)}`, ); } continue; } if (binding.kind === "edge") { const attachment = findAttachment(indexes, "edge", binding.edgeTypeId); if (!attachment || attachment.attachment.kind !== "edge") { issue( issues, "invalid-edge-binding", `${bindingPath}.binding.edgeTypeId`, `Unknown edge attachment ${binding.edgeTypeId}`, ); continue; } const projection = edgeProjection(attachment.attachment, binding.projectionId); // An owned endpoint may explicitly export read-only traversal, including // a native inverse relationship view. This never exports mutation rights. if (!(binding.primitive === "resolve" && projection?.endpoint.publicTraversal)) { validateAttachmentAccess(issues, attachment, conformance, `${bindingPath}.binding.edgeTypeId`); } const relationshipMember = operationEntry.member.kind === "relationship" ? operationEntry.member : undefined; const operationEdge = relationshipMember ? { ...attachment.attachment, endpoints: attachment.attachment.endpoints.map((endpoint, index) => index === (projection?.endpointIndex === 0 ? 1 : 0) ? { ...endpoint, constraint: relationshipMember.target } : endpoint, ) as [EdgeEndpoint, EdgeEndpoint], } : attachment.attachment; const expected = edgePrimitiveSignature(operationEdge, binding.projectionId, binding.primitive); if (!projection || !expected) { issue( issues, "invalid-edge-binding", `${bindingPath}.binding.projectionId`, `Projection ${binding.projectionId} is not on edge ${binding.edgeTypeId}`, ); continue; } if (!atomSatisfiesConstraint(conformance.atomId, projection.endpoint.constraint, indexes.conformances)) { issue( issues, "invalid-edge-binding", `${bindingPath}.binding.projectionId`, `Projection ${binding.projectionId} cannot originate at atom ${conformance.atomId}`, ); } if (operationEntry.member.kind !== "relationship") { issue( issues, "invalid-edge-binding", `${bindingPath}.binding`, "Edge primitives may only implement relationship members", ); } else if ( !constraintSatisfiesConstraint( projection.target.constraint, operationEntry.member.target, indexes.conformances, ) || operationEntry.member.cardinality !== projection.endpoint.cardinality ) { issue( issues, "invalid-edge-binding", `${bindingPath}.binding`, "Edge projection target or cardinality does not match relationship member", ); } if (!signaturesMatch(operation, expected)) { issue( issues, "invalid-edge-binding", `${bindingPath}.binding`, `Edge primitive provides ${describeSignature(expected)}, but operation requires ${describeSignature(operation)}`, ); } continue; } const packageEntry = indexes.packages.get(binding.packageRevisionId); const packageExport = packageEntry?.exports.get(binding.exportId); if (!packageEntry) { issue( issues, "invalid-package-binding", `${bindingPath}.binding.packageRevisionId`, `Unknown package revision ${binding.packageRevisionId}`, ); continue; } if (!packageExport) { issue( issues, "invalid-package-binding", `${bindingPath}.binding.exportId`, `Unknown export ${binding.exportId} in package ${binding.packageRevisionId}`, ); continue; } if (operation.scope === "class" ? packageExport.kind !== "function" : packageExport.kind !== "operation") { issue( issues, "invalid-package-binding", `${bindingPath}.binding.exportId`, `Package export ${binding.exportId} must be ${operation.scope === "class" ? "a free function" : "an instance operation"}`, ); continue; } if ( !signaturesMatch(operation, { ...packageExport, mode: packageExport.kind === "operation" ? packageExport.mode : "call", }) ) { issue( issues, "invalid-package-binding", `${bindingPath}.binding.exportId`, `Package export signature does not match ${describeSignature(operation)}`, ); } if (packageExport.kind === "operation") validatePackageReceiver(issues, { entry: packageExport, atomId: conformance.atomId, path: `${bindingPath}.binding.exportId`, indexes, requirementGraph, graphSourceKey: key, }); if ( operation.scope === "class" && (binding.dependencies.some((entry) => entry.binding.kind !== "constructor") || packageExport.dependencyPorts.some((entry) => entry.requirement.kind !== "constructor")) ) { issue( issues, "invalid-package-binding", bindingPath, "Class functions may inject constructors, not instance-dependent ports", ); } validateBoundDependencies(issues, { dependencies: binding.dependencies, dependencyPorts: packageExport.dependencyPorts, atomId: conformance.atomId, conformance, path: `${bindingPath}.binding`, indexes, requirementGraph, graphSourceKey: key, }); } const materializedMembers = new Set(); for (const [materializationIndex, materialization] of (conformance.relationshipMaterializations ?? []).entries()) { const materializationPath = `${path}.relationshipMaterializations[${materializationIndex}]`; if (materializedMembers.has(materialization.memberId)) { issue( issues, "duplicate-relationship-materialization", materializationPath, `Relationship ${materialization.memberId} has more than one materialization recipe`, ); continue; } materializedMembers.add(materialization.memberId); const member = interfaceEntry.revision.members.find((entry) => entry.id === materialization.memberId); if (!member || member.kind !== "relationship") { issue( issues, "invalid-relationship-materialization", `${materializationPath}.memberId`, `Member ${materialization.memberId} is not a relationship on ${conformance.interfaceRevisionId}`, ); continue; } if (member.cardinality !== "optional-one") { issue( issues, "invalid-relationship-materialization", `${materializationPath}.memberId`, "A lazily constructed relationship must be optional-one before construction", ); } const resolveOperation = member.operations.find((entry) => entry.displayName === "resolve"); const resolveBinding = resolveOperation ? bindings.get(resolveOperation.id) : undefined; if (!resolveBinding || resolveBinding.kind !== "edge") { issue( issues, "invalid-relationship-materialization", `${materializationPath}.memberId`, "A materialized relationship must bind its resolve operation to an edge", ); continue; } if (resolveBinding.edgeTypeId !== materialization.edgeTypeId) { issue( issues, "invalid-relationship-materialization", `${materializationPath}.edgeTypeId`, "Materialization and relationship resolution must use the same edge", ); } const attachment = findAttachment(indexes, "edge", materialization.edgeTypeId); if (!attachment || attachment.attachment.kind !== "edge") { issue( issues, "invalid-relationship-materialization", `${materializationPath}.edgeTypeId`, `Unknown materialization edge ${materialization.edgeTypeId}`, ); continue; } validateAttachmentAccess(issues, attachment, conformance, `${materializationPath}.edgeTypeId`); const hostProjection = edgeProjection(attachment.attachment, resolveBinding.projectionId); const constructedProjection = edgeProjection(attachment.attachment, materialization.constructedProjectionId); if ( !hostProjection || !constructedProjection || hostProjection.endpointIndex === constructedProjection.endpointIndex ) { issue( issues, "invalid-relationship-materialization", `${materializationPath}.constructedProjectionId`, "Materialization must connect through the opposite projection of the resolved edge", ); continue; } if (!atomSatisfiesConstraint(materialization.constructorAtomId, member.target, indexes.conformances)) { issue( issues, "invalid-relationship-materialization", `${materializationPath}.constructorAtomId`, `Constructed atom ${materialization.constructorAtomId} does not satisfy the relationship target`, ); } if ( !atomSatisfiesConstraint( materialization.constructorAtomId, constructedProjection.endpoint.constraint, indexes.conformances, ) || !atomSatisfiesConstraint(conformance.atomId, constructedProjection.target.constraint, indexes.conformances) ) { issue( issues, "invalid-relationship-materialization", `${materializationPath}.constructedProjectionId`, "Constructed projection does not connect the constructed atom back to the host atom", ); } const constructor = indexes.constructors.get(materialization.constructorAtomId); const constructorExport = constructor ? indexes.packages.get(constructor.packageRevisionId)?.exports.get(constructor.exportId) : undefined; if (!constructor || !constructorExport || constructorExport.kind !== "constructor") { issue( issues, "invalid-relationship-materialization", `${materializationPath}.constructorAtomId`, `Constructed atom ${materialization.constructorAtomId} has no valid constructor`, ); } else if (!valueTypesEqual(constructorExport.inputType, valueType.atomRef(conformance.atomId))) { issue( issues, "invalid-relationship-materialization", `${materializationPath}.constructorAtomId`, `Component constructor must accept ${typeLabel(valueType.atomRef(conformance.atomId))}`, ); } } for (const operation of interfaceEntry.operations.values()) { if (!bindings.has(operation.operation.id)) { issue( issues, "missing-operation-binding", `${path}.operationBindings`, `Missing binding for operation ${operation.operation.id}`, ); } } } const visitState = new Map(); const visit = (key: string, stack: string[]) => { const state = visitState.get(key); if (state === "visited") { return; } if (state === "visiting") { const cycleStart = stack.indexOf(key); const cycle = [...stack.slice(Math.max(0, cycleStart)), key]; issue( issues, "cyclic-conformance-requirement", indexes.conformancePaths.get(key) ?? "conformances", `Cyclic conformance requirement: ${cycle.join(" -> ")}`, ); return; } visitState.set(key, "visiting"); for (const dependency of requirementGraph.get(key) ?? []) { visit(dependency, [...stack, key]); } visitState.set(key, "visited"); }; for (const key of requirementGraph.keys()) { visit(key, []); } }; const validateConstructors = ( workspace: WorkspaceRevision, issues: CapabilityValidationIssue[], indexes: ValidationIndexes, ) => { const noGraph = new Map>(); for (const [index, constructor] of workspace.constructors.entries()) { const path = `constructors[${index}]`; if (!indexes.atoms.has(constructor.atomId)) { issue(issues, "invalid-constructor", `${path}.atomId`, `Unknown atom ${constructor.atomId}`); } const packageEntry = indexes.packages.get(constructor.packageRevisionId); const packageExport = packageEntry?.exports.get(constructor.exportId); if (!packageEntry) { issue( issues, "invalid-constructor", `${path}.packageRevisionId`, `Unknown package revision ${constructor.packageRevisionId}`, ); continue; } if (!packageExport) { issue(issues, "invalid-constructor", `${path}.exportId`, `Unknown export ${constructor.exportId}`); continue; } if (packageExport.kind !== "constructor") { issue( issues, "invalid-constructor", `${path}.exportId`, `Export ${constructor.exportId} is ${packageExport.kind}, not a constructor`, ); continue; } if (packageExport.constructsAtom !== constructor.atomId) { issue( issues, "invalid-constructor", `${path}.exportId`, `Export constructs ${packageExport.constructsAtom}, not ${constructor.atomId}`, ); } validateBoundDependencies(issues, { dependencies: constructor.dependencies, dependencyPorts: packageExport.dependencyPorts, atomId: constructor.atomId, path, indexes, requirementGraph: noGraph, }); } }; export const validateWorkspaceRevision = (workspace: WorkspaceRevision): CapabilityValidationIssue[] => { const issues: CapabilityValidationIssue[] = []; requireText(issues, workspace.id, "id", "Workspace revision ID"); requireText(issues, workspace.workspaceId, "workspaceId", "Workspace ID"); requireText(issues, workspace.sourceRootCommit, "sourceRootCommit", "Source root commit"); const indexes = collectIdentityIndexes(workspace, issues); validateInterfaces(workspace, issues, indexes); validateAttachments(issues, indexes); validatePackages(workspace, issues, indexes); validateConformances(workspace, issues, indexes); validateConstructors(workspace, issues, indexes); return issues; }; export interface CompiledConformance { source: Conformance; operationBindings: ReadonlyMap; } export interface CompiledWorkspaceRevision { source: WorkspaceRevision; atoms: ReadonlyMap; interfaces: ReadonlyMap; packages: ReadonlyMap; attachments: ReadonlyMap; conformances: ReadonlyMap; constructors: ReadonlyMap; } export type CompileWorkspaceRevisionResult = | { ok: true; plan: CompiledWorkspaceRevision } | { ok: false; issues: CapabilityValidationIssue[] }; export const compileWorkspaceRevision = (workspace: WorkspaceRevision): CompileWorkspaceRevisionResult => { const snapshot = structuredClone(workspace) as WorkspaceRevision; const issues = validateWorkspaceRevision(snapshot); if (issues.length > 0) { return { ok: false, issues }; } const attachments = new Map(); for (const attachment of snapshot.sharedAttachments) { attachments.set(attachmentKey(attachment), { attachment, owner: { kind: "workspace" }, }); } const conformances = new Map(); for (const conformance of snapshot.conformances) { for (const attachment of conformance.privateAttachments) { attachments.set(attachmentKey(attachment), { attachment, owner: { kind: "conformance", atomId: conformance.atomId, interfaceRevisionId: conformance.interfaceRevisionId, }, }); } conformances.set(conformanceKey(conformance.atomId, conformance.interfaceRevisionId), { source: conformance, operationBindings: new Map(conformance.operationBindings.map((entry) => [entry.operationId, entry.binding])), }); } return { ok: true, plan: { source: snapshot, atoms: new Map(snapshot.atoms.map((atom) => [atom.id, atom])), interfaces: new Map(snapshot.interfaceImports.map((revision) => [revision.revisionId, revision])), packages: new Map(snapshot.packageImports.map((revision) => [revision.revisionId, revision])), attachments, conformances, constructors: new Map(snapshot.constructors.map((entry) => [entry.atomId, entry])), }, }; }; export const resolveConformance = ( plan: CompiledWorkspaceRevision, atomId: AtomId, interfaceRevisionId: InterfaceRevisionId, ): CompiledConformance | undefined => plan.conformances.get(conformanceKey(atomId, interfaceRevisionId)); export const resolveOperationBinding = ( plan: CompiledWorkspaceRevision, atomId: AtomId, interfaceRevisionId: InterfaceRevisionId, operationId: OperationId, ): Binding | undefined => resolveConformance(plan, atomId, interfaceRevisionId)?.operationBindings.get(operationId); export type ResolvedOperationPlan = | { kind: "state"; binding: Extract; attachment: OwnedAttachment & { attachment: StateSlotDefinition }; } | { kind: "edge"; binding: Extract; attachment: OwnedAttachment & { attachment: EdgeDefinition }; } | { kind: "package"; binding: Extract; packageRevision: PackageRevision; packageExport: PackageOperationExport | PackageFunctionExport; dependencies: Array<{ port: DependencyPort; binding: DependencyBinding; }>; }; export const resolveOperationPlan = ( plan: CompiledWorkspaceRevision, atomId: AtomId, interfaceRevisionId: InterfaceRevisionId, operationId: OperationId, ): ResolvedOperationPlan | undefined => { const binding = resolveOperationBinding(plan, atomId, interfaceRevisionId, operationId); if (!binding) { return undefined; } if (binding.kind === "state") { const owned = plan.attachments.get(`state\u0000${binding.slotId}`); if (!owned || owned.attachment.kind !== "state") { return undefined; } return { kind: "state", binding, attachment: owned as OwnedAttachment & { attachment: StateSlotDefinition; }, }; } if (binding.kind === "edge") { const owned = plan.attachments.get(`edge\u0000${binding.edgeTypeId}`); if (!owned || owned.attachment.kind !== "edge") { return undefined; } return { kind: "edge", binding, attachment: owned as OwnedAttachment & { attachment: EdgeDefinition }, }; } const packageRevision = plan.packages.get(binding.packageRevisionId); const packageExport = packageRevision?.exports.find( (entry): entry is PackageOperationExport | PackageFunctionExport => entry.id === binding.exportId && (entry.kind === "operation" || entry.kind === "function"), ); if (!packageRevision || !packageExport) { return undefined; } const bound = new Map(binding.dependencies.map((entry) => [entry.portId, entry.binding])); return { kind: "package", binding, packageRevision, packageExport, dependencies: packageExport.dependencyPorts.map((port) => ({ port, binding: bound.get(port.id)!, })), }; }; export interface CapabilityClosure { conformances: Array<{ atomId: AtomId; interfaceRevisionId: InterfaceRevisionId; }>; packageRevisionIds: PackageRevisionId[]; attachmentIds: Array; constructorAtomIds: AtomId[]; } export const computeCapabilityClosure = ( plan: CompiledWorkspaceRevision, roots: Array<{ atomId: AtomId; interfaceRevisionId: InterfaceRevisionId }>, ): CapabilityClosure => { const conformances = new Map< string, { atomId: AtomId; interfaceRevisionId: InterfaceRevisionId; } >(); const packages = new Set(); const attachments = new Set(); const constructors = new Set(); const queued = [...roots]; const visited = new Set(); const sourceConformances = new Map([...plan.conformances].map(([key, value]) => [key, value.source])); const includeDependencies = (atomId: AtomId, dependencies: readonly BoundDependency[]) => { for (const dependency of dependencies) { switch (dependency.binding.kind) { case "state": attachments.add(dependency.binding.slotId); if (dependency.binding.via) { attachments.add(dependency.binding.via.edgeTypeId); } break; case "edge": attachments.add(dependency.binding.edgeTypeId); if (dependency.binding.via) { attachments.add(dependency.binding.via.edgeTypeId); } break; case "interface": { if (dependency.binding.via) { attachments.add(dependency.binding.via.edgeTypeId); } const targetAtoms = dependency.binding.via ? (() => { const attachment = plan.attachments.get(`edge\u0000${dependency.binding.via!.edgeTypeId}`)?.attachment; if (!attachment || attachment.kind !== "edge") return []; const projection = edgeProjection(attachment, dependency.binding.via!.projectionId); return projection ? [...plan.atoms.keys()].filter((candidate) => atomSatisfiesConstraint(candidate, projection.target.constraint, sourceConformances), ) : []; })() : [atomId]; for (const targetAtom of targetAtoms) { queued.push({ atomId: targetAtom, interfaceRevisionId: dependency.binding.interfaceRevisionId, }); } break; } case "constructor": { constructors.add(dependency.binding.atomId); const constructor = plan.constructors.get(dependency.binding.atomId); if (constructor) { packages.add(constructor.packageRevisionId); includeDependencies(dependency.binding.atomId, constructor.dependencies); } } } } }; while (queued.length > 0) { const root = queued.shift()!; const key = conformanceKey(root.atomId, root.interfaceRevisionId); if (visited.has(key)) { continue; } visited.add(key); const conformance = plan.conformances.get(key); if (!conformance) { continue; } conformances.set(key, { atomId: conformance.source.atomId, interfaceRevisionId: conformance.source.interfaceRevisionId, }); for (const interfaceRevisionId of plan.interfaces.get(root.interfaceRevisionId)?.requiredInterfaces ?? []) { queued.push({ atomId: root.atomId, interfaceRevisionId }); } for (const binding of conformance.operationBindings.values()) { if (binding.kind === "state") { attachments.add(binding.slotId); } else if (binding.kind === "edge") { attachments.add(binding.edgeTypeId); } else { packages.add(binding.packageRevisionId); includeDependencies(root.atomId, binding.dependencies); const packageRevision = plan.packages.get(binding.packageRevisionId); const operation = packageRevision?.exports.find( (entry): entry is PackageOperationExport => entry.id === binding.exportId && entry.kind === "operation", ); if (operation?.receiverRequirement.kind === "all-interfaces") { for (const interfaceRevisionId of operation.receiverRequirement.interfaceRevisionIds) { queued.push({ atomId: root.atomId, interfaceRevisionId }); } } } } for (const materialization of conformance.source.relationshipMaterializations ?? []) { attachments.add(materialization.edgeTypeId); constructors.add(materialization.constructorAtomId); const constructor = plan.constructors.get(materialization.constructorAtomId); if (constructor) { packages.add(constructor.packageRevisionId); includeDependencies(materialization.constructorAtomId, constructor.dependencies); } } } return { conformances: [...conformances.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([, conformance]) => conformance), packageRevisionIds: [...packages].sort(), attachmentIds: [...attachments].sort(), constructorAtomIds: [...constructors].sort(), }; };