483bc68a94
Enable evolution by default for source-backed workspaces. Add stable conformance ownership, semantic-major review, candidate typechecking, and durable fenced cutover with explicit migrations and forward recovery. Independently supervise package runtimes so unchanged resource owners keep their processes and connections across cutover. Add scoped invocation authority, resource sessions, and typed callback rebinding. Wire opaque object references through generated bindings and RPCs. Add canonical relationship sets, keyed maps, and ordered lists with scoped transactional mutations, revision checks, and inverse consistency. Support planned cascade deletion, protection, tombstones, and lifecycle foundations. Add journaled structural edits, package/function/migration scaffolding, managed repository creation, and resumable bottom-up dependency pin publication. Document lifetime boundaries, revision pinning, prototype compatibility policy, commands, and deferred work. Validate with 210 tests, user-systemd process/connection continuity, generated-package TypeScript checks, and Nix host/protocol checks. TTL handoff, physical reclamation, general multi-step migrations, and root-systemd migration isolation acceptance remain deferred.
2157 lines
72 KiB
TypeScript
2157 lines
72 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,
|
|
PackageRevision,
|
|
PackageRevisionId,
|
|
PersistentAttachment,
|
|
SlotId,
|
|
SourceRevision,
|
|
StatePrimitive,
|
|
StateSlotDefinition,
|
|
ValueType,
|
|
WorkspaceRevision,
|
|
} from "./types.js";
|
|
import { valueType } from "./types.js";
|
|
|
|
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<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 "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 "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 "builtin":
|
|
case "scalar":
|
|
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}`,
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
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");
|
|
requireText(
|
|
issues,
|
|
revision.revisionId,
|
|
`${path}.revisionId`,
|
|
"Interface revision ID",
|
|
);
|
|
requireText(issues, revision.displayName, `${path}.displayName`, "Interface name");
|
|
validateSource(issues, revision.source, `${path}.source`);
|
|
const memberIds = new Set<string>();
|
|
const operations = new Map<OperationId, InterfaceOperationEntry>();
|
|
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<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}]`;
|
|
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,
|
|
) => {
|
|
for (const [interfaceIndex, revision] of workspace.interfaceImports.entries()) {
|
|
const path = `interfaceImports[${interfaceIndex}]`;
|
|
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 containsReference = (type: ValueType): boolean => type.kind === "object-ref" || ((type.kind === "optional" || type.kind === "list") && containsReference(type.value));
|
|
if (containsReference(attachment.valueType) || (attachment.storagePolicy.kind === "crdt-document" && containsReference(attachment.storagePolicy.updateType))) {
|
|
issue(issues, "invalid-attachment", `${path}.valueType`, "Managed object references belong in graph relationships, not ordinary state");
|
|
}
|
|
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 "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 "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 [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 (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;
|
|
}
|
|
validateAttachmentAccess(issues, attachment, conformance, `${bindingPath}.binding.edgeTypeId`);
|
|
const projection = edgeProjection(attachment.attachment, binding.projectionId);
|
|
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 (packageExport.kind !== "operation") {
|
|
issue(
|
|
issues,
|
|
"invalid-package-binding",
|
|
`${bindingPath}.binding.exportId`,
|
|
`Package export ${binding.exportId} is ${packageExport.kind}, not an operation`,
|
|
);
|
|
continue;
|
|
}
|
|
if (!signaturesMatch(operation, packageExport)) {
|
|
issue(
|
|
issues,
|
|
"invalid-package-binding",
|
|
`${bindingPath}.binding.exportId`,
|
|
`Package export provides ${describeSignature(packageExport)}, but operation requires ${describeSignature(operation)}`,
|
|
);
|
|
}
|
|
validatePackageReceiver(issues, {
|
|
entry: packageExport,
|
|
atomId: conformance.atomId,
|
|
path: `${bindingPath}.binding.exportId`,
|
|
indexes,
|
|
requirementGraph,
|
|
graphSourceKey: key,
|
|
});
|
|
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;
|
|
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 =>
|
|
entry.id === binding.exportId && entry.kind === "operation",
|
|
);
|
|
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 "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 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(),
|
|
};
|
|
};
|