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
+78 -13
View File
@@ -1,21 +1,74 @@
import type { InterfaceRevision, PackageRevision, ValueType, DependencyPort } from "../capability-model/types.js";
import type {
InterfaceRevision,
PackageRevision,
ValueType,
DependencyPort,
WorkspaceRevision,
} from "../capability-model/types.js";
import type { CompiledCapabilityResourceRepository } from "../capability-language/assembly.js";
import { genericImplementationType } from "./generics.js";
/** Portable generator input. New backends consume this instead of the parser or TS runtime. */
export type BindingSchema = {
format: "quixos-bindings";
version: 1;
interfaces: InterfaceRevision[];
interfaceTemplates?: InterfaceRevision[];
packages: PackageRevision[];
};
export const bindingSchema = (compiled: CompiledCapabilityResourceRepository): BindingSchema => ({
export const bindingSchema = (compiled: Pick<CompiledCapabilityResourceRepository, "resources">): BindingSchema => ({
format: "quixos-bindings",
version: 1,
interfaces: compiled.resources.flatMap((node) =>
node.resource.kind === "interface" ? [node.resource.revision] : [],
interfaceTemplates: compiled.resources.flatMap((node) =>
node.resource.kind === "interface" && node.resource.revision.template ? [node.resource.revision] : [],
),
interfaces: [
...new Map(
compiled.resources
.flatMap((node) => [
...(node.resource.kind === "interface" ? [node.resource.revision] : []),
...(node.resource.specializations ?? []),
])
.filter((entry) => !entry.template)
.map((entry) => [entry.revisionId, entry]),
).values(),
],
packages: compiled.resources.flatMap((node) => (node.resource.kind === "package" ? [node.resource.revision] : [])),
});
export const specializeBindingSchema = (
base: BindingSchema,
workspace: WorkspaceRevision,
revisionId: string,
): BindingSchema => {
const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId);
if (!pkg) throw new Error(`Missing candidate package ${revisionId}`);
const needed = new Set<string>();
const visit = (value: unknown): void => {
if (!value || typeof value !== "object") return;
if ("interfaceRevisionId" in value && typeof value.interfaceRevisionId === "string")
needed.add(value.interfaceRevisionId);
Object.values(value).forEach(visit);
};
visit(pkg.exports);
const interfaces = new Map<string, InterfaceRevision>(base.interfaces.map((entry) => [entry.revisionId, entry]));
for (const id of needed) {
const iface = workspace.interfaceImports.find((entry) => entry.revisionId === id);
if (iface) {
interfaces.set(id, iface);
visit(iface.members);
}
}
return {
...base,
interfaces: [...interfaces.values()],
packages: base.packages.map((entry) => {
const candidate = workspace.packageImports.find((value) => value.revisionId === entry.revisionId);
if (!candidate) throw new Error(`Missing dependency package ${entry.revisionId}`);
return candidate;
}),
};
};
export type TypeScriptBindingOptions = {
runtimeModule?: string;
/** Each export must implement MessageBinding<T>, providing both TS type and wire codec. */
@@ -46,6 +99,8 @@ export const generateTypeScriptBindings = (
) => {
if (schema.format !== "quixos-bindings" || schema.version !== 1)
throw new Error("Unsupported binding schema version");
if (schema.interfaces.some((entry) => entry.template))
throw new Error("Package bindings require closed interface applications, not generic definitions");
const pkg = schema.packages.find((entry) => entry.revisionId === packageRevisionId);
if (!pkg) throw new Error(`Unknown package revision ${packageRevisionId}`);
const messages = new Map<string, string>();
@@ -174,6 +229,7 @@ export const generateTypeScriptBindings = (
const specs: Record<string, unknown> = {};
const contexts: [string, string][] = [];
const handlers: [string, string][] = [];
const results: [string, string][] = [];
for (const entry of exports) {
if (names.has(entry.displayName)) throw new Error(`Duplicate export name ${entry.displayName}`);
names.add(entry.displayName);
@@ -200,13 +256,15 @@ export const generateTypeScriptBindings = (
const event = entry.kind === "operation" ? entry.eventType : undefined;
const contextType = `Contexts[${q(entry.displayName)}]`;
const outputType = type(event ?? entry.outputType);
results.push([entry.displayName, outputType]);
// Watch-start handlers produce events through the runtime's derived stream protocol.
handlers.push([
entry.displayName,
event
? `QxDerived<${contextType}, ${outputType}>`
: `QxHandler<${contextType}, ${outputType}>${entry.kind === "operation" && entry.mode === "call" ? ` | QxDerived<${contextType}, ${outputType}>` : ""}`,
]);
if (!entry.application)
handlers.push([
entry.displayName,
event
? `QxDerived<${contextType}, ${outputType}>`
: `QxHandler<${contextType}, ${outputType}>${entry.kind === "operation" && entry.mode === "call" ? ` | QxDerived<${contextType}, ${outputType}>` : ""}`,
]);
specs[entry.displayName] = {
inputType: entry.inputType,
outputType: entry.outputType,
@@ -214,13 +272,19 @@ export const generateTypeScriptBindings = (
ports: Object.fromEntries(ports.map((port) => [port.name, port.spec])),
};
}
for (const definition of pkg.genericExports ?? []) {
handlers.push([
definition.displayName,
genericImplementationType(definition, [...schema.interfaces, ...(schema.interfaceTemplates ?? [])], type),
]);
}
const imports = [...messages].map(([id, alias]) => {
const binding = options.messages![id]!;
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(binding.export))
throw new Error(`Invalid message binding export ${binding.export}`);
return `import { ${binding.export} as ${alias} } from ${q(binding.module)};`;
});
const signatures = `${object(contexts)} ${object(handlers)}`;
const signatures = `${object(contexts)} ${object(handlers)} ${object(results)}`;
const typeImports = [
"BindingValue",
"QxObjectRef",
@@ -237,14 +301,15 @@ export const generateTypeScriptBindings = (
`import { ${exports.length ? "bindQxHandler, " : ""}${[...typeImports, "QxHandlerSpec", "QxMessages"].map((name) => `type ${name}`).join(", ")} } from ${q(options.runtimeModule ?? "@quixos/camino-package-runtime")};\n` +
imports.join("\n") +
`\nexport const packageRevisionId = ${q(pkg.revisionId)};\n` +
`export type Contexts = ${object(contexts)};\nexport type Implementation = ${object(handlers)};\n` +
`declare const appliedType: unique symbol;\ntype QxApplied<Definition extends string, Arguments extends readonly unknown[]> = string & {readonly [appliedType]: (value: [Definition, Arguments]) => [Definition, Arguments]};\n` +
`export type Contexts = ${object(contexts)};\nexport type Results = ${object(results)};\nexport type Implementation = ${object(handlers)};\n` +
`const messages = { ${[...messages].map(([id, alias]) => `${q(id)}: ${alias}`).join(", ")} } satisfies QxMessages;\n` +
`const specs = ${JSON.stringify(specs, null, 2)} satisfies Record<string, QxHandlerSpec>;\n` +
`export const createRuntime = (implementation: Implementation) => ({\n packageRevisionId,\n exports: {\n` +
exports
.map(
(entry) =>
` ${q(entry.id)}: bindQxHandler(specs[${q(entry.displayName)}], implementation[${q(entry.displayName)}], messages),`,
` ${q(entry.id)}: bindQxHandler(specs[${q(entry.displayName)}], implementation[${q(entry.application ? pkg.genericExports!.find((definition) => definition.id === entry.application!.exportId)!.displayName : entry.displayName)}], messages),`,
)
.join("\n") +
`\n },\n});\n`