2169 lines
72 KiB
TypeScript
2169 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 "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":
|
|
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 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 "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(),
|
|
};
|
|
};
|