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:
@@ -0,0 +1,199 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { createRequire } from "node:module";
|
||||
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
||||
import { generateTypeScriptBindings, generatePackageDescriptor } from "../src/bindings/index.js";
|
||||
import { genericImplementationType } from "../src/bindings/generics.js";
|
||||
import { compileWorkspaceRevision, runtimeContracts } from "../src/capability-model/index.js";
|
||||
import { generateAppliedClientContracts } from "../src/bindings/client.js";
|
||||
const source = { repository: "https://example.test/generic.git", commit: "a".repeat(40) };
|
||||
test("generic port aliases retain their defining scope, including shadowed alias names", () => {
|
||||
const iface = compileCapabilityResourceSource(
|
||||
'type Box<value V> = list<V>; interface Data<value V> id "data" revision "data@1" {value payload id "payload" : Box<V> {get id "payload:get";}}',
|
||||
{ source },
|
||||
);
|
||||
assert.ok(iface.ok, JSON.stringify(iface.diagnostics));
|
||||
if (iface.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const pkg = compileCapabilityResourceSource(
|
||||
'import interface Data; type Box<value V> = optional<V>; package P id "p" revision "p@1" {operation fetch<value V> id "data@1" : unit -> list<Box<V>> mode call receiver any requires {interface data id "data" : Data<Box<V>>;};}',
|
||||
{ source, environment: { interfaces: new Map([["Data", iface.resource.revision]]) } },
|
||||
);
|
||||
assert.ok(pkg.ok, JSON.stringify(pkg.diagnostics));
|
||||
if (pkg.resource.kind !== "package") throw new Error("expected package");
|
||||
const generated = generateTypeScriptBindings(
|
||||
{
|
||||
format: "quixos-bindings",
|
||||
version: 1,
|
||||
interfaces: [],
|
||||
interfaceTemplates: [iface.resource.revision],
|
||||
packages: [pkg.resource.revision],
|
||||
},
|
||||
pkg.resource.revision.revisionId,
|
||||
);
|
||||
assert.match(generated, /"payload.get":\(\)=>Promise<Array<\(T0 \| null\)>>/);
|
||||
});
|
||||
const definition = () => {
|
||||
const result = compileCapabilityResourceSource(
|
||||
`package Generic id "generic" revision "generic@1" {
|
||||
operation echo<value T> id "echo" : T -> T mode call receiver any;
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
if (result.resource.kind !== "package") throw new Error("expected package");
|
||||
return result.resource.revision;
|
||||
};
|
||||
test("generic package exports specialize per binding without changing immutable source identity", () => {
|
||||
const iface = compileCapabilityResourceSource(
|
||||
`interface Echo<value T> id "echo-interface" revision "echo-interface@1" {
|
||||
operation echo id "echo-member" : T -> T {call id "echo-call";}
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(iface.ok);
|
||||
if (iface.resource.kind !== "interface") throw new Error("expected interface");
|
||||
const pkg = definition();
|
||||
const result = compileCapabilitySource(
|
||||
`workspace W id "w" revision "w@1" commit "${source.commit}" {
|
||||
import interface Echo; import package Generic; atom Thing id "thing";
|
||||
conform Thing as Echo<string> id "string-echo" {bind echo.call to package Generic.echo<string>;}
|
||||
conform Thing as Echo<list<int64>> id "list-echo" {bind echo.call to package Generic.echo<list<int64>>;}
|
||||
}`,
|
||||
"workspace.qx",
|
||||
{ interfaces: new Map([["Echo", iface.resource.revision]]), packages: new Map([["Generic", pkg]]) },
|
||||
);
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
assert.equal(pkg.exports.length, 0, "source definition was not mutated");
|
||||
const closed = result.workspace.packageImports[0];
|
||||
assert.equal(closed.revisionId, pkg.revisionId);
|
||||
assert.equal(closed.exports.length, 2);
|
||||
assert.notEqual(closed.exports[0].id, closed.exports[1].id);
|
||||
const oneApplication = structuredClone(result.workspace);
|
||||
oneApplication.packageImports[0].exports.splice(1);
|
||||
oneApplication.conformances.splice(1);
|
||||
assert.notDeepEqual(
|
||||
runtimeContracts(oneApplication),
|
||||
runtimeContracts(result.workspace),
|
||||
"New specializations invalidate the executable contract even with unchanged package source",
|
||||
);
|
||||
const schema = {
|
||||
format: "quixos-bindings" as const,
|
||||
version: 1 as const,
|
||||
interfaces: result.workspace.interfaceImports,
|
||||
packages: [closed],
|
||||
};
|
||||
const generated = generateTypeScriptBindings(schema, pkg.revisionId);
|
||||
assert.match(generated, /"echo":\s*\(<T0>/);
|
||||
for (const entry of closed.exports) {
|
||||
assert.ok(generated.includes(entry.id));
|
||||
assert.ok(generatePackageDescriptor(schema, pkg.revisionId).includes(entry.id));
|
||||
}
|
||||
const forged = structuredClone(result.workspace);
|
||||
forged.packageImports[0].exports[0].outputType = { kind: "scalar", name: "bool" };
|
||||
const rejected = compileWorkspaceRevision(forged);
|
||||
assert.equal(rejected.ok, false);
|
||||
if (!rejected.ok) assert.ok(rejected.issues.some((issue) => issue.message.includes("Specialized export differs")));
|
||||
});
|
||||
test("generated universal implementations compile and cannot assume a concrete value type", async () => {
|
||||
const pkg = definition();
|
||||
const signature = genericImplementationType(pkg.genericExports![0], [], () => {
|
||||
throw new Error("unexpected concrete type");
|
||||
});
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "qx-generic-ts-"));
|
||||
const require = createRequire(import.meta.url);
|
||||
const compiler = path.join(path.dirname(require.resolve("typescript/package.json")), "bin/tsc");
|
||||
const run = promisify(execFile);
|
||||
const preamble = `type QxObjectRef<T extends string>={readonly identity:T}; type QxContextLifecycle<C>={signal?:AbortSignal}; type Handler=${signature};\n`;
|
||||
try {
|
||||
const file = path.join(directory, "generic.ts");
|
||||
await fs.writeFile(file, preamble + `const handler:Handler=({input})=>input;`);
|
||||
await run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]);
|
||||
await fs.writeFile(file, preamble + `const handler:Handler=({input})=>"not universally T";`);
|
||||
await assert.rejects(
|
||||
run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]),
|
||||
(error) => {
|
||||
assert.match(String((error as { stdout: string }).stdout), /not assignable/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("generic Self follows the receiver parameter, never a preceding export", () => {
|
||||
const compile = (declaration: string) =>
|
||||
compileCapabilityResourceSource(`package P id "p" revision "p@1" {${declaration}}`, { source });
|
||||
const valid = compile('operation identity<object T> id "identity" : unit -> ref<Self> mode call receiver object T;');
|
||||
assert.ok(valid.ok, JSON.stringify(valid.diagnostics));
|
||||
if (valid.resource.kind !== "package") throw new Error("expected package");
|
||||
const definition = valid.resource.revision.genericExports![0];
|
||||
assert.deepEqual(definition.outputType, {
|
||||
kind: "object-ref",
|
||||
expectation: { kind: "parameter", parameterId: definition.parameters[0].id },
|
||||
});
|
||||
assert.equal(compile('function identity<value T> id "identity" : T -> ref<Self>;').ok, false);
|
||||
assert.equal(
|
||||
compile('operation events<value T> id "events" : unit -> watch-handle mode watch-start receiver any;').ok,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("typed presentation and factory consumers preserve distinct closed applications and return targets", async () => {
|
||||
const interfaces = new Map();
|
||||
for (const text of [
|
||||
'interface Presentation<value Props> id "presentation" revision "presentation@1" {operation props id "props" : unit -> Props {call id "props:get";}}',
|
||||
'interface Factory<object Result> id "factory" revision "factory@1" {operation create id "create" : unit -> ref<Result> {call id "create:call";}}',
|
||||
]) {
|
||||
const result = compileCapabilityResourceSource(text, { source });
|
||||
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||
interfaces.set(result.resource.revision.displayName, result.resource.revision);
|
||||
}
|
||||
const pkg = compileCapabilityResourceSource(
|
||||
`import interface Presentation; import interface Factory;
|
||||
external atom Note id "note"; external atom Notebook id "notebook";
|
||||
package Views id "views" revision "views@1" {
|
||||
operation note id "note-view" : unit -> unit mode call receiver any requires {interface props id "props" : Presentation<record {title:string; note:atom-ref<Note>;}>;};
|
||||
operation notebook id "notebook-view" : unit -> unit mode call receiver any requires {interface props id "props" : Presentation<record {count:int32;}>;};
|
||||
operation factory id "factory-view" : unit -> unit mode call receiver any requires {interface factory id "factory" : Factory<atom Note>;};
|
||||
}`,
|
||||
{ source, environment: { interfaces } },
|
||||
);
|
||||
assert.ok(pkg.ok, JSON.stringify(pkg.diagnostics));
|
||||
const closed = pkg.resource.specializations!;
|
||||
const presentation = closed.find(
|
||||
(contract) =>
|
||||
contract.application?.definitionId === "presentation@1" && JSON.stringify(contract).includes('"title"'),
|
||||
)!;
|
||||
const factory = closed.find((contract) => contract.application?.definitionId === "factory@1")!;
|
||||
const contracts = generateAppliedClientContracts(closed);
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "qx-presentation-types-"));
|
||||
const compiler = path.join(
|
||||
path.dirname(createRequire(import.meta.url).resolve("typescript/package.json")),
|
||||
"bin/tsc",
|
||||
);
|
||||
const run = promisify(execFile);
|
||||
const usage = `type Props=CapabilityOutput<${JSON.stringify(presentation.revisionId)},"props:get">;
|
||||
type Created=CapabilityOutput<${JSON.stringify(factory.revisionId)},"create:call">;
|
||||
const render=({camino,render}:{camino:Props;render:{onSelect:(note:Created)=>void;compact:boolean}})=>{render.onSelect(camino.note);return camino.title;};\n`;
|
||||
try {
|
||||
const file = path.join(directory, "consumer.ts");
|
||||
await fs.writeFile(file, contracts + usage);
|
||||
await run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]);
|
||||
await fs.writeFile(file, contracts + usage + "const wrong=(props:Props)=>props.count;");
|
||||
await assert.rejects(
|
||||
run(process.execPath, [compiler, "--ignoreConfig", "--noEmit", "--strict", "--target", "es2023", file]),
|
||||
(error) => {
|
||||
assert.match(String((error as { stdout: string }).stdout), /count.*does not exist/);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user