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