Add exact runtime conformance lookup and agent-visible language limits
This commit is contained in:
+58
-18
@@ -7,6 +7,7 @@ import type {
|
||||
} from "../capability-model/types.js";
|
||||
import type { CompiledCapabilityResourceRepository } from "../capability-language/assembly.js";
|
||||
import { genericImplementationType } from "./generics.js";
|
||||
import { capabilityId } from "../capability-model/types.js";
|
||||
|
||||
/** Portable generator input. New backends consume this instead of the parser or TS runtime. */
|
||||
export type BindingSchema = {
|
||||
@@ -183,26 +184,28 @@ export const generateTypeScriptBindings = (
|
||||
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
|
||||
.map((operation) => ({ ...operation, name: `${member.displayName}.${operation.displayName}` })),
|
||||
);
|
||||
return {
|
||||
type: object([
|
||||
["objectId", ref({ kind: "interface", interfaceRevisionId: requirement.interfaceRevisionId })],
|
||||
[
|
||||
"live",
|
||||
object(
|
||||
operations.map((operation) => [
|
||||
operation.name,
|
||||
`(${params(operation.inputType)}) => Promise<QxLiveValue>`,
|
||||
]),
|
||||
),
|
||||
],
|
||||
...operations.map((operation): [string, string] => [
|
||||
operation.name,
|
||||
`(${params(operation.inputType)}) => Promise<${type(operation.outputType)}>`,
|
||||
]),
|
||||
const methods: [string, string][] = [
|
||||
["objectId", ref({ kind: "interface", interfaceRevisionId: requirement.interfaceRevisionId })],
|
||||
[
|
||||
"live",
|
||||
object(
|
||||
operations.map((operation) => [
|
||||
operation.name,
|
||||
`(${params(operation.inputType)}) => Promise<QxLiveValue>`,
|
||||
]),
|
||||
),
|
||||
],
|
||||
...operations.map((operation): [string, string] => [
|
||||
operation.name,
|
||||
`(${params(operation.inputType)}) => Promise<${type(operation.outputType)}>`,
|
||||
]),
|
||||
];
|
||||
return {
|
||||
type: object([...methods, ["contract", `QxInterfaceContract<${object(methods)}>`]]),
|
||||
spec: {
|
||||
kind: "interface",
|
||||
id: entry.id,
|
||||
interfaceRevisionId: requirement.interfaceRevisionId,
|
||||
operations: Object.fromEntries(
|
||||
operations.map(({ name, id, inputType, outputType }) => [name, { id, inputType, outputType }]),
|
||||
),
|
||||
@@ -245,6 +248,7 @@ export const generateTypeScriptBindings = (
|
||||
? `QxObjectRef<${entry.receiverRequirement.interfaceRevisionIds.map((id) => q(`interface:${id}`)).join(" | ") || "never"}>`
|
||||
: "QxObjectRef<string>";
|
||||
const contextShape = object([
|
||||
["conform", "QxConformer"],
|
||||
...(entry.kind === "function" ? [] : [["objectId", receiver] as [string, string]]),
|
||||
["input", type(entry.inputType)],
|
||||
["ports", object(ports.map((port) => [port.name, port.type]))],
|
||||
@@ -281,13 +285,42 @@ export const generateTypeScriptBindings = (
|
||||
genericImplementationType(definition, [...schema.interfaces, ...(schema.interfaceTemplates ?? [])], type),
|
||||
]);
|
||||
}
|
||||
// Unused imported interfaces must not introduce new message-codec obligations.
|
||||
// A used port still fails above if its own required codec is missing.
|
||||
const hasCodec = (value: ValueType): boolean => {
|
||||
if (value.kind === "message") return !!options.messages?.[value.descriptorId];
|
||||
if (value.kind === "record") return Object.values(value.fields).every(hasCodec);
|
||||
if (value.kind === "list" || value.kind === "optional") return hasCodec(value.value);
|
||||
return true;
|
||||
};
|
||||
const contracts = schema.interfaces
|
||||
.filter((iface) =>
|
||||
iface.members.every((member) =>
|
||||
member.operations
|
||||
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
|
||||
.every((operation) => hasCodec(operation.inputType) && hasCodec(operation.outputType)),
|
||||
),
|
||||
)
|
||||
.map((iface) => {
|
||||
const generated = port({
|
||||
id: capabilityId.dependencyPort("contract"),
|
||||
displayName: iface.displayName,
|
||||
requirement: { kind: "interface", interfaceRevisionId: iface.revisionId },
|
||||
});
|
||||
const spec = generated.spec as { operations: unknown };
|
||||
return {
|
||||
iface,
|
||||
type: generated.type,
|
||||
code: `defineQxInterfaceContract<${generated.type}>(${q(iface.revisionId)}, ${JSON.stringify(spec.operations)})`,
|
||||
};
|
||||
});
|
||||
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)} ${object(results)}`;
|
||||
const signatures = `${object(contexts)} ${object(handlers)} ${object(results)} ${contracts.map((entry) => entry.type).join(" ")}`;
|
||||
const typeImports = [
|
||||
"BindingValue",
|
||||
"QxObjectRef",
|
||||
@@ -295,17 +328,24 @@ export const generateTypeScriptBindings = (
|
||||
"QxHandler",
|
||||
"QxDerived",
|
||||
"QxContextLifecycle",
|
||||
"QxConformer",
|
||||
"QxInterfaceContract",
|
||||
"QxLiveValue",
|
||||
"RelationshipCollection",
|
||||
"RelationshipEntry",
|
||||
].filter((name) => new RegExp(`\\b${name}\\b`).test(signatures));
|
||||
return (
|
||||
`// Generated by quixos-codegen-ts. Do not edit. Binding ABI version 1.\n` +
|
||||
`import { ${exports.length ? "bindQxHandler, " : ""}${[...typeImports, "QxHandlerSpec", "QxMessages"].map((name) => `type ${name}`).join(", ")} } from ${q(options.runtimeModule ?? "@quixos/camino-package-runtime")};\n` +
|
||||
`import { ${contracts.length ? "defineQxInterfaceContract, " : ""}${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` +
|
||||
`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` +
|
||||
`export const contracts = {${contracts.map((entry) => `${q(entry.iface.revisionId)}: ${entry.code}`).join(",\n")}};\n` +
|
||||
`export const interfaces = {${contracts
|
||||
.filter((entry) => contracts.filter((other) => other.iface.displayName === entry.iface.displayName).length === 1)
|
||||
.map((entry) => `${q(entry.iface.displayName)}: contracts[${q(entry.iface.revisionId)}]`)
|
||||
.join(",\n")}};\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` +
|
||||
|
||||
Reference in New Issue
Block a user