Files
quixos-protocol/src/capability-model/validation.ts
T

2277 lines
84 KiB
TypeScript

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-query-contract"
| "query-provider-reason-required"
| "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<OperationId, InterfaceOperationEntry>;
}
interface PackageIndexEntry {
revision: PackageRevision;
exports: Map<string, PackageExport>;
}
interface AttachmentIndexEntry extends OwnedAttachment {
path: string;
}
interface ValidationIndexes {
atoms: Map<string, WorkspaceRevision["atoms"][number]>;
interfaces: Map<string, InterfaceIndexEntry>;
packages: Map<string, PackageIndexEntry>;
conformances: Map<string, Conformance>;
conformancePaths: Map<string, string>;
attachments: Map<string, AttachmentIndexEntry>;
projections: Map<string, { edge: EdgeDefinition; endpoint: EdgeEndpoint; endpointIndex: 0 | 1 }>;
constructors: Map<AtomId, AtomConstructorBinding>;
}
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<ValidationIndexes, "atoms" | "interfaces">,
) => {
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<ValidationIndexes, "atoms" | "interfaces">,
) => {
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<InterfaceOperation, "mode" | "eventType">,
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<string, Conformance>,
) =>
constraint.kind === "atom"
? constraint.atomId === atomId
: conformances.has(conformanceKey(atomId, constraint.interfaceRevisionId));
const constraintSatisfiesConstraint = (
actual: EdgeEndpointConstraint,
required: EdgeEndpointConstraint,
conformances: ReadonlyMap<string, Conformance>,
) =>
constraintsEqual(actual, required) ||
(actual.kind === "atom" &&
required.kind === "interface" &&
atomSatisfiesConstraint(actual.atomId, required, conformances));
const canAccessAttachment = (
owner: AttachmentOwner,
conformance: Pick<Conformance, "atomId" | "interfaceRevisionId"> | 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<string, WorkspaceRevision["atoms"][number]>();
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<string, InterfaceIndexEntry>();
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<string>();
const operations = new Map<OperationId, InterfaceOperationEntry>();
for (const [memberIndex, member] of revision.members.entries()) {
const memberPath = `${path}.members[${memberIndex}]`;
if (
member.kind === "relationship" &&
member.keyType !== undefined &&
(!["string", "boolean", "int64"].includes(member.keyType) ||
!["many", "many-unique"].includes(member.cardinality))
)
issue(
issues,
"invalid-query-contract",
memberPath,
"Keyed relationships require many cardinality and string, boolean or int64 keys",
);
if (member.kind !== "operation" && member.queryRead) {
const contract = member.queryRead.execution;
const getter = member.kind === "value" ? "get" : "resolve";
if (contract !== "native" && contract !== "rpc-permitted")
issue(issues, "invalid-query-contract", memberPath, "Unknown query execution contract");
if (member.operations.filter((operation) => operation.displayName === getter).length !== 1)
issue(issues, "invalid-query-contract", memberPath, `Queryable members require exactly one ${getter}`);
if (member.kind === "relationship" && contract === "rpc-permitted")
issue(issues, "invalid-query-contract", memberPath, "RPC query relationships are not supported");
if (member.kind === "value") {
const type = member.valueType.kind === "optional" ? member.valueType.value : member.valueType;
if (type.kind !== "scalar")
issue(issues, "invalid-query-contract", memberPath, "Queryable values must be scalar or optional scalar");
}
}
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<string, PackageIndexEntry>();
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<string, PackageExport>();
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<string>();
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<string, Conformance>();
const conformancePaths = new Map<string, string>();
const conformanceIds = new Set<string>();
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<AtomId, AtomConstructorBinding>();
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<string, AttachmentIndexEntry>();
const projections = new Map<string, { edge: EdgeDefinition; endpoint: EdgeEndpoint; endpointIndex: 0 | 1 }>();
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<string>(),
): 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<string>();
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<string>();
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 "query":
if (
!indexes.packages
.get(port.requirement.packageRevisionId)
?.revision.queries?.some(
(query) => port.requirement.kind === "query" && query.id === port.requirement.queryId,
)
)
issue(
issues,
"unresolved-reference",
portPath,
`Unknown query ${port.requirement.packageRevisionId}:${port.requirement.queryId}`,
);
break;
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 = <Kind extends PersistentAttachment["kind"]>(
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<Conformance, "atomId" | "interfaceRevisionId"> | 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<Conformance, "atomId" | "interfaceRevisionId">,
) => {
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<Conformance, "atomId" | "interfaceRevisionId">;
path: string;
indexes: ValidationIndexes;
requirementGraph: Map<string, Set<string>>;
graphSourceKey?: string;
},
) => {
const ports = new Map(params.dependencyPorts.map((port) => [port.id, port]));
const bindings = new Map<string, BoundDependency>();
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 "query": {
const selected = binding as Extract<DependencyBinding, { kind: "query" }>;
const query = params.indexes.packages
.get(requirement.packageRevisionId)
?.revision.queries?.find((query) => query.id === requirement.queryId);
if (
!query ||
selected.queryId !== requirement.queryId ||
selected.packageRevisionId !== requirement.packageRevisionId
) {
issue(issues, "invalid-dependency-binding", path, "Query port must bind its exact checked query export");
break;
}
const traversal = selected.via
? validateTraversal(
issues,
params.indexes,
params.atomId,
selected.via,
`${path}.binding.via`,
params.conformance,
)
: undefined;
const targets = selected.via
? traversal
? [...params.indexes.atoms.keys()].filter((atom) =>
atomSatisfiesConstraint(atom as AtomId, traversal.target.constraint, params.indexes.conformances),
)
: []
: [params.atomId];
for (const atom of targets)
if (!params.indexes.conformances.has(conformanceKey(atom as AtomId, query.root)))
issue(issues, "unsatisfied-interface", path, `Query root ${atom} does not conform to ${query.root}`);
break;
}
case "state": {
const stateBinding = binding as Extract<DependencyBinding, { kind: "state" }>;
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<DependencyBinding, { kind: "edge" }>;
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<DependencyBinding, { kind: "interface" }>;
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<DependencyBinding, { kind: "constructor" }>;
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<string, Set<string>>;
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<string, Set<string>>();
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<OperationId, Binding>();
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;
const member = operationEntry.member;
if (
member.kind !== "operation" &&
member.queryRead &&
operation.displayName === (member.kind === "value" ? "get" : "resolve")
) {
const native =
member.kind === "value"
? binding.kind === "state" && binding.primitive === "read"
: binding.kind === "edge" && binding.primitive === "resolve";
if (!native && (member.queryRead.execution === "native" || binding.kind !== "package"))
issue(
issues,
"invalid-query-contract",
bindingPath,
"This queryable contract requires a native read binding; a justification cannot weaken it",
);
else if (!native && !entry.queryReason?.trim())
issue(
issues,
"query-provider-reason-required",
bindingPath,
"Package-backed query reads require a concrete query-reason justification",
);
}
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 ||
(operationEntry.member.keyType !== undefined && operationEntry.member.keyType !== projection.endpoint.keyType)
) {
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<string>();
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<string, "visiting" | "visited">();
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<string, Set<string>>();
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<OperationId, Binding>;
}
export interface CompiledWorkspaceRevision {
source: WorkspaceRevision;
atoms: ReadonlyMap<AtomId, WorkspaceRevision["atoms"][number]>;
interfaces: ReadonlyMap<InterfaceRevisionId, InterfaceRevision>;
packages: ReadonlyMap<PackageRevisionId, PackageRevision>;
attachments: ReadonlyMap<string, OwnedAttachment>;
conformances: ReadonlyMap<string, CompiledConformance>;
constructors: ReadonlyMap<AtomId, AtomConstructorBinding>;
}
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<string, OwnedAttachment>();
for (const attachment of snapshot.sharedAttachments) {
attachments.set(attachmentKey(attachment), {
attachment,
owner: { kind: "workspace" },
});
}
const conformances = new Map<string, CompiledConformance>();
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<Binding, { kind: "state" }>;
attachment: OwnedAttachment & { attachment: StateSlotDefinition };
}
| {
kind: "edge";
binding: Extract<Binding, { kind: "edge" }>;
attachment: OwnedAttachment & { attachment: EdgeDefinition };
}
| {
kind: "package";
binding: Extract<Binding, { kind: "package" }>;
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<SlotId | EdgeTypeId>;
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<PackageRevisionId>();
const attachments = new Set<SlotId | EdgeTypeId>();
const constructors = new Set<AtomId>();
const queued = [...roots];
const visited = new Set<string>();
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 "query": {
packages.add(dependency.binding.packageRevisionId);
const query = plan.packages
.get(dependency.binding.packageRevisionId)
?.checkedQueries?.find(
(query) => dependency.binding.kind === "query" && query.declaration.id === dependency.binding.queryId,
);
if (dependency.binding.via) attachments.add(dependency.binding.via.edgeTypeId);
for (const effect of query?.effects ?? []) {
for (const conformance of sourceConformances.values())
if (conformance.interfaceRevisionId === effect.interfaceRevisionId)
queued.push({ atomId: conformance.atomId, interfaceRevisionId: conformance.interfaceRevisionId });
}
break;
}
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(),
};
};