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 00:12:39 -07:00
parent 9358b5ed0e
commit 52803dda05
26 changed files with 6009 additions and 2193 deletions
+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);