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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user