Implement capability generics, checked package specializations and CRUD scaffolding

Add kinded parameters, capability bounds, Self, aliases and closed application identities. Check generic implementations universally and build candidate-specific codecs and descriptors from immutable schemas. Preserve lexical aliases and exact dispatch identities in package and host bindings.

Add an imperative CRUD+index domain scaffold with explicit soft-deletion semantics, source/codegen regression coverage, installed CLI tests and an authoring guide. Existing Web Studio opaque props and class-level create-menu migration are separate from the implemented language core.
This commit is contained in:
Timothy J. Aveni
2026-09-16 10:42:12 -07:00
parent 9358b5ed0e
commit 52803dda05
26 changed files with 6009 additions and 2193 deletions
+3
View File
@@ -184,6 +184,9 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
materializations: sorted(conformance.relationshipMaterializations, (entry) => entry.memberId),
});
parent.dependencies.add(`interface:${conformance.interfaceRevisionId}`);
const contract = workspace.interfaceImports.find((entry) => entry.revisionId === conformance.interfaceRevisionId);
for (const required of contract?.requiredInterfaces ?? [])
parent.dependencies.add(conformanceKey(conformance.atomId, required));
for (const attachment of conformance.privateAttachments) parent.dependencies.add(`attachment:${attachment.id}`);
for (const operation of conformance.operationBindings)
binding(parent, operation.binding, conformance.atomId, {
+141
View File
@@ -0,0 +1,141 @@
import { createHash } from "node:crypto";
import {
capabilityId,
type PackageExport,
type PackageExportId,
type DependencyPort,
type PackageRevision,
} from "./types.js";
import {
TypeSubstitution,
GenericTypeError,
bindTypeParameters,
canonicalTypeArgument,
type TypeParameter,
type ValueTypeExpression,
type ObjectTypeExpression,
type InterfaceApplicationExpression,
type GenericTypeEnvironment,
type ClosedTypeArgument,
type ValueAliasDefinition,
} from "./generics.js";
export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
requirement:
| {
kind: "state";
valueType: ValueTypeExpression;
primitives: Extract<DependencyPort["requirement"], { kind: "state" }>["primitives"];
}
| {
kind: "edge";
target: ObjectTypeExpression;
cardinality: Extract<DependencyPort["requirement"], { kind: "edge" }>["cardinality"];
primitives: Extract<DependencyPort["requirement"], { kind: "edge" }>["primitives"];
}
| { kind: "interface"; application: InterfaceApplicationExpression }
| { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression };
};
export interface GenericPackageExport {
id: PackageExportId;
displayName: string;
parameters: TypeParameter[];
kind: "operation" | "function";
inputType: ValueTypeExpression;
outputType: ValueTypeExpression;
eventType?: ValueTypeExpression;
mode?: Extract<PackageExport, { kind: "operation" }>["mode"];
receiverRequirement:
| { kind: "any-object" }
| { kind: "target"; target: ObjectTypeExpression }
| { kind: "interfaces"; interfaces: InterfaceApplicationExpression[] };
dependencyPorts: GenericDependencyPort[];
aliases: ValueAliasDefinition[];
}
/** A closed manifest is a Nix build input, never a mutable source checkout. */
export const specializePackageExport = (
pkg: PackageRevision,
definition: GenericPackageExport,
arguments_: ClosedTypeArgument[],
environment: GenericTypeEnvironment,
): PackageExport => {
const lexical = { ...environment, aliases: new Map(definition.aliases.map((alias) => [alias.id, alias])) };
const bindings = bindTypeParameters(definition.parameters, arguments_, lexical, definition.displayName);
const substitution = new TypeSubstitution({ ...lexical, arguments: bindings });
const digest = createHash("sha256")
.update(
JSON.stringify([
"quixos-package-specialization-v1",
pkg.revisionId,
[pkg.source.repository, pkg.source.commit],
definition.id,
arguments_.map(canonicalTypeArgument),
environment.self ?? null,
]),
)
.digest("hex");
const ports: DependencyPort[] = definition.dependencyPorts.map((port) => {
const requirement = port.requirement;
switch (requirement.kind) {
case "state":
return { ...port, requirement: { ...requirement, valueType: substitution.value(requirement.valueType) } };
case "edge":
return { ...port, requirement: { ...requirement, target: substitution.object(requirement.target) } };
case "interface":
return {
...port,
requirement: { kind: "interface", interfaceRevisionId: substitution.application(requirement.application) },
};
case "constructor": {
const target = substitution.object(requirement.target);
if (target.kind !== "atom")
throw new GenericTypeError(
"constructor-target",
port.displayName,
"Constructor ports require a concrete atom argument, not an interface view",
);
return {
...port,
requirement: {
kind: "constructor",
atomId: target.atomId,
inputType: substitution.value(requirement.inputType),
},
};
}
}
});
const common = {
id: capabilityId.packageExport(`export-application:sha256:${digest}`),
displayName: `${definition.displayName}$${digest}`,
inputType: substitution.value(definition.inputType),
outputType: substitution.value(definition.outputType),
dependencyPorts: ports,
application: {
exportId: definition.id,
arguments: structuredClone(arguments_),
...(environment.self ? { self: environment.self } : {}),
},
};
if (definition.kind === "function") return { ...common, kind: "function" };
const receiver = definition.receiverRequirement;
const target = receiver.kind === "target" ? substitution.object(receiver.target) : undefined;
return {
...common,
kind: "operation",
mode: definition.mode!,
...(definition.eventType ? { eventType: substitution.value(definition.eventType) } : {}),
receiverRequirement: target
? target.kind === "atom"
? { kind: "exact-atom", atomId: target.atomId }
: { kind: "all-interfaces", interfaceRevisionIds: [target.interfaceRevisionId] }
: receiver.kind === "interfaces"
? {
kind: "all-interfaces",
interfaceRevisionIds: receiver.interfaces.map((value) => substitution.application(value)),
}
: { kind: "any-object" },
};
};
+420
View File
@@ -0,0 +1,420 @@
import { createHash } from "node:crypto";
import {
capabilityId,
type AtomId,
type InterfaceMember,
type InterfaceOperation,
type InterfaceRevision,
type InterfaceRevisionId,
type ObjectExpectation,
type SourceRevision,
type ValueType,
} from "./types.js";
/** Authoring-only expressions. Installed ValueType deliberately has no variable case. */
export type ValueTypeExpression =
| Extract<ValueType, { kind: "builtin" | "scalar" | "message" }>
| { kind: "parameter"; parameterId: string }
| { kind: "record"; fields: Record<string, ValueTypeExpression> }
| { kind: "list" | "optional"; value: ValueTypeExpression }
| { kind: "object-ref"; expectation: ObjectTypeExpression }
| { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] };
export type ObjectTypeExpression =
| { kind: "atom"; atomId: AtomId }
| { kind: "interface"; interfaceRevisionId: InterfaceRevisionId }
| { kind: "parameter"; parameterId: string }
| { kind: "self" }
| { kind: "application"; application: InterfaceApplicationExpression };
export type TypeArgumentExpression =
| { kind: "value"; type: ValueTypeExpression }
| { kind: "object"; target: ObjectTypeExpression };
export type ClosedTypeArgument = { kind: "value"; type: ValueType } | { kind: "object"; target: ObjectExpectation };
export interface InterfaceApplicationExpression {
definitionId: InterfaceRevisionId;
arguments: TypeArgumentExpression[];
}
export type TypeParameter =
| { id: string; name: string; kind: "value"; storable?: boolean }
| { id: string; name: string; kind: "object"; implements: InterfaceApplicationExpression[] };
export interface ValueAliasDefinition {
id: string;
parameters: TypeParameter[];
body: ValueTypeExpression;
}
export interface GenericInterfaceTemplate {
parameters: TypeParameter[];
members: InterfaceMember<ValueTypeExpression, ObjectTypeExpression>[];
requires: InterfaceApplicationExpression[];
usesSelf: boolean;
aliases?: ValueAliasDefinition[];
}
/** Instantiation produces the existing closed runtime IR, with explicit provenance. */
export const instantiateInterface = (
definition: InterfaceRevision,
arguments_: readonly ClosedTypeArgument[],
environment: GenericTypeEnvironment,
): InterfaceRevision => {
const template = definition.template;
if (!template) {
if (arguments_.length) fail("type-arity", definition.displayName, "Non-generic interface takes no type arguments");
return definition;
}
const lexicalEnvironment = {
...environment,
aliases: new Map((template.aliases ?? []).map((alias) => [alias.id, alias])),
};
const argumentsMap = bindTypeParameters(template.parameters, arguments_, lexicalEnvironment, definition.displayName);
if (template.usesSelf && !environment.self)
fail("unbound-self", definition.displayName, "Self requires an implementing atom");
const substitution = new TypeSubstitution({ ...lexicalEnvironment, arguments: argumentsMap });
const operation = (entry: InterfaceOperation<ValueTypeExpression>): InterfaceOperation => ({
...entry,
inputType: substitution.value(entry.inputType, `${definition.displayName}.${entry.displayName}.input`),
outputType: substitution.value(entry.outputType, `${definition.displayName}.${entry.displayName}.output`),
eventType: entry.eventType
? substitution.value(entry.eventType, `${definition.displayName}.${entry.displayName}.event`)
: undefined,
});
const members: InterfaceMember[] = template.members.map((member) => {
const common = { id: member.id, displayName: member.displayName, operations: member.operations.map(operation) };
switch (member.kind) {
case "value":
return { ...common, kind: "value", valueType: substitution.value(member.valueType, member.displayName) };
case "relationship":
return {
...common,
kind: "relationship",
target: substitution.object(member.target, member.displayName),
cardinality: member.cardinality,
ordered: member.ordered,
};
case "operation":
return {
...common,
kind: "operation",
inputType: substitution.value(member.inputType, member.displayName),
outputType: substitution.value(member.outputType, member.displayName),
};
}
});
const application: AppliedInterfaceIdentity = {
definitionId: definition.revisionId,
source: definition.source,
arguments: structuredClone([...arguments_]),
...(template.usesSelf ? { self: environment.self } : {}),
};
return {
interfaceId: definition.interfaceId,
revisionId: appliedInterfaceId(application),
displayName: definition.displayName,
source: definition.source,
members,
application,
requiredInterfaces: template.requires.map((required) => substitution.application(required)),
};
};
export interface GenericTypeEnvironment {
arguments: ReadonlyMap<string, ClosedTypeArgument>;
self?: AtomId;
aliases?: ReadonlyMap<string, ValueAliasDefinition>;
/** Resolves only checked declarations, never a caller-supplied runtime type string. */
applyInterface: (definitionId: InterfaceRevisionId, arguments_: readonly ClosedTypeArgument[]) => InterfaceRevisionId;
/** Proof in the candidate, not an authorization grant. */
implementsInterface: (target: ObjectExpectation, required: InterfaceRevisionId) => boolean;
/** External codecs must explicitly declare reference-free persistence support. */
storableMessage?: (descriptorId: string) => boolean;
}
export class GenericTypeError extends Error {
constructor(
readonly code: string,
readonly path: string,
message: string,
) {
super(`${path}: ${message}`);
this.name = "GenericTypeError";
}
}
const fail = (code: string, path: string, message: string): never => {
throw new GenericTypeError(code, path, message);
};
/** One budget across nested aliases/substitutions, including concrete arguments. */
class Budget {
private remaining = 10000;
enter(path: string, depth: number) {
if (depth > 128 || --this.remaining < 0)
fail("type-complexity-limit", path, "Type exceeds the depth or expansion budget");
}
}
export const isStorableType = (
type: ValueType,
storableMessage: (descriptorId: string) => boolean = () => false,
): boolean => {
const budget = new Budget();
const visit = (value: ValueType, depth: number): boolean => {
budget.enter("storable", depth);
switch (value.kind) {
case "scalar":
return true;
case "builtin":
return value.name === "unit";
case "message":
return storableMessage(value.descriptorId);
case "object-ref":
return false;
case "list":
case "optional":
return visit(value.value, depth + 1);
// Records currently have RPC codecs, not ordinary-state persistence codecs.
// A generic bound must not promise storage that the installed model rejects.
case "record":
return false;
}
};
return visit(type, 0);
};
/** Canonical closed-type encoding, independent of record insertion order. */
export const canonicalTypeArgument = (argument: ClosedTypeArgument): string => {
const budget = new Budget();
const target = (value: ObjectExpectation): unknown => {
switch (value.kind) {
case "atom":
return ["atom", value.atomId];
case "interface":
return ["interface", value.interfaceRevisionId];
default:
return fail("unresolved-type", "argument", "Expected a closed object target");
}
};
const type = (value: ValueType, depth: number): unknown => {
budget.enter("argument", depth);
switch (value.kind) {
case "builtin":
case "scalar":
return [value.kind, value.name];
case "message":
return ["message", value.descriptorId];
case "object-ref":
return ["object-ref", target(value.expectation)];
case "list":
case "optional":
return [value.kind, type(value.value, depth + 1)];
case "record":
return [
"record",
Object.keys(value.fields)
.sort()
.map((key) => [key, type(value.fields[key], depth + 1)]),
];
default:
return fail("unresolved-type", "argument", "Expected a closed value type");
}
};
switch (argument.kind) {
case "value":
return JSON.stringify(["value", type(argument.type, 0)]);
case "object":
return JSON.stringify(["object", target(argument.target)]);
default:
return fail("invalid-kind", "argument", "Expected a value or object type argument");
}
};
export interface AppliedInterfaceIdentity {
definitionId: InterfaceRevisionId;
source: SourceRevision;
arguments: ClosedTypeArgument[];
/** Only include Self when it is actually part of the closed contract. */
self?: AtomId;
}
export const appliedInterfaceId = (application: AppliedInterfaceIdentity): InterfaceRevisionId => {
const encoding = JSON.stringify([
"quixos-applied-interface-v1",
application.definitionId,
application.source.repository,
application.source.commit.toLowerCase(),
application.arguments.map(canonicalTypeArgument),
application.self ?? null,
]);
return capabilityId.interfaceRevision(
`interface-application:sha256:${createHash("sha256").update(encoding).digest("hex")}`,
);
};
export class TypeSubstitution {
private readonly budget = new Budget();
private readonly aliases: string[] = [];
constructor(private environment: GenericTypeEnvironment) {}
argument(expression: TypeArgumentExpression, path = "argument", depth = 0): ClosedTypeArgument {
this.budget.enter(path, depth);
switch (expression.kind) {
case "value":
return { kind: "value", type: this.value(expression.type, path, depth + 1) };
case "object":
return { kind: "object", target: this.object(expression.target, path, depth + 1) };
default:
return fail("invalid-kind", path, "Expected a value or object type argument");
}
}
application(expression: InterfaceApplicationExpression, path = "interface", depth = 0): InterfaceRevisionId {
this.budget.enter(path, depth);
return this.environment.applyInterface(
expression.definitionId,
expression.arguments.map((argument, index) => this.argument(argument, `${path}.arguments[${index}]`, depth + 1)),
);
}
object(expression: ObjectTypeExpression, path = "target", depth = 0): ObjectExpectation {
this.budget.enter(path, depth);
switch (expression.kind) {
case "atom":
return { kind: "atom", atomId: expression.atomId };
case "interface":
return { kind: "interface", interfaceRevisionId: expression.interfaceRevisionId };
case "self":
return this.environment.self
? { kind: "atom", atomId: this.environment.self }
: fail("unbound-self", path, "Self requires an implementing atom");
case "parameter": {
const argument = this.environment.arguments.get(expression.parameterId);
if (!argument) return fail("unbound-parameter", path, `Unbound parameter ${expression.parameterId}`);
if (argument.kind !== "object")
return fail("parameter-kind", path, `Parameter ${expression.parameterId} is a value, not an object target`);
canonicalTypeArgument(argument);
return structuredClone(argument.target);
}
case "application":
return { kind: "interface", interfaceRevisionId: this.application(expression.application, path, depth + 1) };
default:
return fail("invalid-type", path, "Unknown object type expression");
}
}
value(expression: ValueTypeExpression, path = "type", depth = 0): ValueType {
this.budget.enter(path, depth);
switch (expression.kind) {
case "builtin":
return { kind: "builtin", name: expression.name };
case "scalar":
return { kind: "scalar", name: expression.name };
case "message":
return { kind: "message", descriptorId: expression.descriptorId };
case "list":
case "optional":
return { kind: expression.kind, value: this.value(expression.value, `${path}.${expression.kind}`, depth + 1) };
case "record":
return {
kind: "record",
fields: Object.fromEntries(
Object.entries(expression.fields).map(([name, field]) => [
name,
this.value(field, `${path}.${name}`, depth + 1),
]),
),
};
case "object-ref":
return { kind: "object-ref", expectation: this.object(expression.expectation, `${path}.ref`, depth + 1) };
case "parameter": {
const argument = this.environment.arguments.get(expression.parameterId);
if (!argument) return fail("unbound-parameter", path, `Unbound parameter ${expression.parameterId}`);
if (argument.kind !== "value")
return fail("parameter-kind", path, `Parameter ${expression.parameterId} is an object target; use ref<T>`);
canonicalTypeArgument(argument);
return this.value(argument.type, path, depth + 1);
}
case "alias": {
const alias = this.environment.aliases?.get(expression.definitionId);
if (!alias) return fail("unknown-alias", path, `Unknown type alias ${expression.definitionId}`);
if (this.aliases.includes(alias.id))
return fail("recursive-alias", path, `Recursive value alias: ${[...this.aliases, alias.id].join(" -> ")}`);
const arguments_ = expression.arguments.map((argument, index) =>
this.argument(argument, `${path}.arguments[${index}]`, depth + 1),
);
const bindings = bindTypeParameters(alias.parameters, arguments_, this.environment, path);
this.aliases.push(alias.id);
try {
// Reuse this expansion budget/stack; lexical parameter maps are restored.
const previous = this.environment;
this.environment = { ...previous, arguments: bindings };
try {
return this.value(alias.body, `${path}.${alias.id}`, depth + 1);
} finally {
this.environment = previous;
}
} finally {
this.aliases.pop();
}
}
default:
return fail("invalid-type", path, "Unknown value type expression");
}
}
}
export const bindTypeParameters = (
parameters: readonly TypeParameter[],
arguments_: readonly ClosedTypeArgument[],
environment: GenericTypeEnvironment,
path = "parameters",
): ReadonlyMap<string, ClosedTypeArgument> => {
if (parameters.length !== arguments_.length)
fail("type-arity", path, `Expected ${parameters.length} type arguments, received ${arguments_.length}`);
const bindings = new Map<string, ClosedTypeArgument>();
const names = new Set<string>();
for (const [index, parameter] of parameters.entries()) {
if (
!parameter.id ||
!parameter.name ||
parameter.name === "Self" ||
bindings.has(parameter.id) ||
names.has(parameter.name)
)
fail("duplicate-parameter", path, `Invalid or duplicate parameter ${parameter.name}`);
names.add(parameter.name);
const argument = arguments_[index];
if (parameter.kind !== argument.kind)
fail("parameter-kind", `${path}.${parameter.name}`, `Expected ${parameter.kind}, received ${argument.kind}`);
canonicalTypeArgument(argument);
bindings.set(parameter.id, structuredClone(argument));
}
const substitution = new TypeSubstitution({ ...environment, arguments: bindings });
for (const parameter of parameters) {
const argument = bindings.get(parameter.id)!;
if (
parameter.kind === "value" &&
argument.kind === "value" &&
parameter.storable &&
!isStorableType(argument.type, environment.storableMessage)
)
fail(
"non-storable-argument",
`${path}.${parameter.name}`,
"State values cannot contain managed references or unsupported transport values",
);
if (parameter.kind === "object" && argument.kind === "object") {
for (const bound of parameter.implements) {
const required = substitution.application(bound, `${path}.${parameter.name}.implements`);
if (!environment.implementsInterface(argument.target, required))
fail("unsatisfied-bound", `${path}.${parameter.name}`, `Object target does not implement ${required}`);
}
}
}
return bindings;
};
+2
View File
@@ -2,3 +2,5 @@ export * from "./types.js";
export * from "./validation.js";
export * from "./evolution.js";
export * from "./migrations.js";
export * from "./generics.js";
export * from "./generic-packages.js";
+30 -14
View File
@@ -104,25 +104,25 @@ export interface AtomDefinition {
export type InterfaceOperationMode = "call" | "watch-start" | "watch-stop" | "subscribe" | "unsubscribe";
export interface InterfaceOperation {
export interface InterfaceOperation<Type = ValueType> {
id: OperationId;
displayName: string;
inputType: ValueType;
outputType: ValueType;
inputType: Type;
outputType: Type;
mode: InterfaceOperationMode;
/** Required for watch-start/subscribe and absent for other modes. */
eventType?: ValueType;
eventType?: Type;
}
interface InterfaceMemberBase {
interface InterfaceMemberBase<Type = ValueType> {
id: MemberId;
displayName: string;
operations: InterfaceOperation[];
operations: InterfaceOperation<Type>[];
}
export interface ValueInterfaceMember extends InterfaceMemberBase {
export interface ValueInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
kind: "value";
valueType: ValueType;
valueType: Type;
}
export type EdgeCardinality = "optional-one" | "exactly-one" | "many" | "many-unique";
@@ -134,21 +134,27 @@ export type EdgeEndpointConstraint =
interfaceRevisionId: InterfaceRevisionId;
};
export interface RelationshipInterfaceMember extends InterfaceMemberBase {
export interface RelationshipInterfaceMember<
Type = ValueType,
Target = EdgeEndpointConstraint,
> extends InterfaceMemberBase<Type> {
kind: "relationship";
target: EdgeEndpointConstraint;
target: Target;
cardinality: EdgeCardinality;
ordered: boolean;
}
/** A named callable capability that is not value or relationship sugar. */
export interface OperationInterfaceMember extends InterfaceMemberBase {
export interface OperationInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
kind: "operation";
inputType: ValueType;
outputType: ValueType;
inputType: Type;
outputType: Type;
}
export type InterfaceMember = ValueInterfaceMember | RelationshipInterfaceMember | OperationInterfaceMember;
export type InterfaceMember<Type = ValueType, Target = EdgeEndpointConstraint> =
| ValueInterfaceMember<Type>
| RelationshipInterfaceMember<Type, Target>
| OperationInterfaceMember<Type>;
export interface InterfaceRevision {
interfaceId: InterfaceId;
@@ -156,6 +162,12 @@ export interface InterfaceRevision {
displayName: string;
source: SourceRevision;
members: InterfaceMember[];
/** Authored generic definition; never an executable contract by itself. */
template?: import("./generics.js").GenericInterfaceTemplate;
/** Immutable provenance of a closed generic application. */
application?: import("./generics.js").AppliedInterfaceIdentity;
requiredInterfaces?: InterfaceRevisionId[];
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
}
export type StoragePolicy = { kind: "optimistic-register" } | { kind: "crdt-document"; updateType: ValueType };
@@ -233,6 +245,8 @@ interface PackageExportBase {
inputType: ValueType;
outputType: ValueType;
dependencyPorts: DependencyPort[];
/** Closed adapter served by the same immutable package build. */
application?: { exportId: PackageExportId; arguments: import("./generics.js").ClosedTypeArgument[]; self?: AtomId };
}
export interface PackageOperationExport extends PackageExportBase {
@@ -254,6 +268,8 @@ export interface PackageConstructorExport extends PackageExportBase {
export type PackageExport = PackageOperationExport | PackageFunctionExport | PackageConstructorExport;
export interface PackageRevision {
genericExports?: import("./generic-packages.js").GenericPackageExport[];
argumentRequirements?: { target: ObjectExpectation; required: InterfaceRevisionId }[];
migrationCatalog?: import("./migrations.js").MigrationCatalog;
packageId: PackageId;
revisionId: PackageRevisionId;
+120
View File
@@ -34,6 +34,9 @@ import type {
WorkspaceRevision,
} from "./types.js";
import { valueType } from "./types.js";
import { appliedInterfaceId, GenericTypeError } from "./generics.js";
import { specializePackageExport } from "./generic-packages.js";
import { isDeepStrictEqual } from "node:util";
export type CapabilityValidationIssueCode =
| "invalid-semantic-major"
@@ -217,7 +220,12 @@ const validateValueType = (
}
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");
@@ -244,6 +252,14 @@ const validateValueType = (
`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",
);
}
};
@@ -458,9 +474,29 @@ const collectIdentityIndexes = (
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()) {
@@ -516,6 +552,35 @@ const collectIdentityIndexes = (
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)) {
@@ -655,8 +720,54 @@ const validateInterfaces = (
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") {
@@ -1282,6 +1393,12 @@ const validateConformances = (
}
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)) {
@@ -1957,6 +2074,9 @@ export const computeCapabilityClosure = (
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);