Add exact runtime conformance lookup and agent-visible language limits

This commit is contained in:
Timothy J. Aveni
2026-09-17 11:29:56 -07:00
parent 59735c5e38
commit ea51ca89e7
10 changed files with 378 additions and 55 deletions
+4 -2
View File
@@ -127,7 +127,7 @@ export const genericImplementationType = (
.filter((op) => op.mode === "call" && op.scope !== "class")
.map((op) => ({ ...op, name: `${member.displayName}.${op.displayName}` })),
);
return object([
const methods: [string, string][] = [
["objectId", `QxObjectRef<${target({ kind: "application", application: requirement.application }, scope)}>`],
["live", object(operations.map((op) => [op.name, `(${params(op.inputType, local)})=>Promise<QxLiveValue>`]))],
...operations.map(
@@ -137,7 +137,8 @@ export const genericImplementationType = (
string,
],
),
]);
];
return object([...methods, ["contract", `QxInterfaceContract<${object(methods)}>`]]);
}
}
};
@@ -146,6 +147,7 @@ export const genericImplementationType = (
.join(",");
const receiver = definition.receiverRequirement;
const context = object([
["conform", "QxConformer"],
...(definition.kind === "function"
? []
: [
+58 -18
View File
@@ -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` +
+5
View File
@@ -15,6 +15,11 @@ declare module "@quixos/web-studio-react-runtime" {
export type WritableField<T> = ReadableField<T> & {readonly $writeType?: (value: T) => T; readonly writable: true; readonly capability: FieldCapability & {setOperationId: string}};
export type LiveFieldProp<T> = ReadableField<T>;
export type InterfaceReference<I extends string, Fields> = {readonly $quixosRef: string; readonly interfaceRevisionId: I; readonly fields: Fields};
export type ReactInterfaceContract<View> = {readonly interfaceRevisionId: string; readonly $viewType?: (view: View) => View};
export function defineReactInterfaceContract<View>(interfaceRevisionId: string, interfaces: unknown): ReactInterfaceContract<View>;
export function tryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>, options?: {signal?: AbortSignal}): Promise<View | undefined>;
export type ConformanceResult<View> = {status: "loading"} | {status: "absent"} | {status: "available"; view: View} | {status: "error"; error: Error};
export function useTryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>): ConformanceResult<View>;
export type ReactComponentHostProps<Action> = {
onAction?: (action: Action) => void;
fallback?: React.ReactNode;
+45 -1
View File
@@ -78,8 +78,52 @@ export function generateReactBindings(
throw new Error("React component check must name a local module relative to generated bindings");
return `type Component${i} = CheckedComponent<${q(component.propsExport)}, typeof import(${q(component.module)})["default"]>;`;
});
// Emit optional discovery descriptors only when the browser has all codecs.
// Unrelated opaque-message imports must not break an otherwise checked UI.
const browserValue = (value: ValueType, seen: Set<string>): boolean => {
if (value.kind === "message") return false;
if (value.kind === "builtin") return value.name === "unit";
if (value.kind === "record") return Object.values(value.fields).every((field) => browserValue(field, seen));
if (value.kind === "list" || value.kind === "optional") return browserValue(value.value, seen);
if (value.kind === "object-ref" && value.expectation.kind === "interface") {
const id = value.expectation.interfaceRevisionId;
if (seen.has(id)) return true;
const contract = schema.interfaces.find((entry) => entry.revisionId === id);
return !!contract && browserInterface(contract, new Set([...seen, id]));
}
return true;
};
const browserInterface = (iface: (typeof schema.interfaces)[number], seen: Set<string>): boolean =>
iface.members.every((member) =>
member.operations
.filter((op) => op.mode === "call" && op.scope !== "class")
.every((op) => browserValue(op.inputType, seen) && browserValue(op.outputType, seen)),
);
const contracts = schema.interfaces
.filter((iface) => browserInterface(iface, new Set([iface.revisionId])))
.map((iface) => {
const reference = type({
kind: "object-ref",
expectation: { kind: "interface", interfaceRevisionId: iface.revisionId },
});
const calls = iface.members.flatMap((member) =>
member.operations
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
.map(
(operation) =>
`${q(`${member.displayName}.${operation.displayName}`)}: (${operation.inputType.kind === "builtin" && operation.inputType.name === "unit" ? "" : `input: ${type(operation.inputType)}`}) => Promise<${type(operation.outputType)}>`,
),
);
return { iface, view: `${reference} & {call: {${calls.join(";")}}}` };
});
return (
`// Generated from checked QX contracts. Do not edit.\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
`// Generated from checked QX contracts. Do not edit.\nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime";\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
`const contractsData = ${JSON.stringify(schema.interfaces)};\n` +
`export const reactContracts = {${contracts.map(({ iface, view }) => `${q(iface.revisionId)}: defineReactInterfaceContract<${view}>(${q(iface.revisionId)}, contractsData)`).join(",\n")}};\n` +
`export const reactInterfaces = {${contracts
.filter(({ iface }) => contracts.filter((other) => other.iface.displayName === iface.displayName).length === 1)
.map(({ iface }) => `${q(iface.displayName)}: reactContracts[${q(iface.revisionId)}]`)
.join(",\n")}};\n` +
(checks.length
? `type CheckedComponent<K extends keyof ReactResults, C extends (props: {camino: ReactResults[K]; render: any; dispatch: (action: any) => void}) => unknown> = C;\n${checks.join("\n")}\n`
: "")
File diff suppressed because one or more lines are too long
+48 -4
View File
@@ -10,7 +10,7 @@ import type { Message } from "@bufbuild/protobuf";
* Describes the file quixos/refs.proto.
*/
export const file_quixos_refs: GenFile = /*@__PURE__*/
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zIkQKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCSJCChBQYWNrYWdlRXhwb3J0UmVmEhsKE3BhY2thZ2VfcmV2aXNpb25faWQYASABKAkSEQoJZXhwb3J0X2lkGAIgASgJIsQBChJJbmplY3RlZERlcGVuZGVuY3kSDwoHcG9ydF9pZBgBIAEoCRIXCg1zdGF0ZV9zbG90X2lkGAIgASgJSAASJgoEZWRnZRgDIAEoCzIWLnF1aXhvcy5FZGdlRGVwZW5kZW5jeUgAEh8KFWludGVyZmFjZV9yZXZpc2lvbl9pZBgEIAEoCUgAEh0KE2NvbnN0cnVjdG9yX2F0b21faWQYBSABKAlIABIRCglvYmplY3RfaWQYBiABKAlCCQoHYmluZGluZyI9Cg5FZGdlRGVwZW5kZW5jeRIUCgxlZGdlX3R5cGVfaWQYASABKAkSFQoNcHJvamVjdGlvbl9pZBgCIAEoCWIGcHJvdG8z");
fileDesc("ChFxdWl4b3MvcmVmcy5wcm90bxIGcXVpeG9zInUKDUNhcGFiaWxpdHlSZWYSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAEgASgJEhQKDG9wZXJhdGlvbl9pZBgCIAEoCRIvCgtjb25mb3JtYW5jZRgDIAEoCzIaLnF1aXhvcy5Db25mb3JtYW5jZVdpdG5lc3MilgEKEkNvbmZvcm1hbmNlV2l0bmVzcxIRCglvYmplY3RfaWQYASABKAkSHQoVaW50ZXJmYWNlX3JldmlzaW9uX2lkGAIgASgJEhYKDmNvbmZvcm1hbmNlX2lkGAMgASgJEh0KFXdvcmtzcGFjZV9yZXZpc2lvbl9pZBgEIAEoCRIXCg93b3Jrc3BhY2VfZXBvY2gYBSABKAkiQgoQUGFja2FnZUV4cG9ydFJlZhIbChNwYWNrYWdlX3JldmlzaW9uX2lkGAEgASgJEhEKCWV4cG9ydF9pZBgCIAEoCSLEAQoSSW5qZWN0ZWREZXBlbmRlbmN5Eg8KB3BvcnRfaWQYASABKAkSFwoNc3RhdGVfc2xvdF9pZBgCIAEoCUgAEiYKBGVkZ2UYAyABKAsyFi5xdWl4b3MuRWRnZURlcGVuZGVuY3lIABIfChVpbnRlcmZhY2VfcmV2aXNpb25faWQYBCABKAlIABIdChNjb25zdHJ1Y3Rvcl9hdG9tX2lkGAUgASgJSAASEQoJb2JqZWN0X2lkGAYgASgJQgkKB2JpbmRpbmciPQoORWRnZURlcGVuZGVuY3kSFAoMZWRnZV90eXBlX2lkGAEgASgJEhUKDXByb2plY3Rpb25faWQYAiABKAliBnByb3RvMw");
/**
* @generated from message quixos.CapabilityRef
@@ -25,6 +25,13 @@ export type CapabilityRef = Message<"quixos.CapabilityRef"> & {
* @generated from field: string operation_id = 2;
*/
operationId: string;
/**
* Optional fence for a view acquired through TryConform. Not an authority grant.
*
* @generated from field: quixos.ConformanceWitness conformance = 3;
*/
conformance?: ConformanceWitness | undefined;
};
/**
@@ -34,6 +41,43 @@ export type CapabilityRef = Message<"quixos.CapabilityRef"> & {
export const CapabilityRefSchema: GenMessage<CapabilityRef> = /*@__PURE__*/
messageDesc(file_quixos_refs, 0);
/**
* @generated from message quixos.ConformanceWitness
*/
export type ConformanceWitness = Message<"quixos.ConformanceWitness"> & {
/**
* @generated from field: string object_id = 1;
*/
objectId: string;
/**
* @generated from field: string interface_revision_id = 2;
*/
interfaceRevisionId: string;
/**
* @generated from field: string conformance_id = 3;
*/
conformanceId: string;
/**
* @generated from field: string workspace_revision_id = 4;
*/
workspaceRevisionId: string;
/**
* @generated from field: string workspace_epoch = 5;
*/
workspaceEpoch: string;
};
/**
* Describes the message quixos.ConformanceWitness.
* Use `create(ConformanceWitnessSchema)` to create a new message.
*/
export const ConformanceWitnessSchema: GenMessage<ConformanceWitness> = /*@__PURE__*/
messageDesc(file_quixos_refs, 1);
/**
* @generated from message quixos.PackageExportRef
*/
@@ -54,7 +98,7 @@ export type PackageExportRef = Message<"quixos.PackageExportRef"> & {
* Use `create(PackageExportRefSchema)` to create a new message.
*/
export const PackageExportRefSchema: GenMessage<PackageExportRef> = /*@__PURE__*/
messageDesc(file_quixos_refs, 1);
messageDesc(file_quixos_refs, 2);
/**
* @generated from message quixos.InjectedDependency
@@ -108,7 +152,7 @@ export type InjectedDependency = Message<"quixos.InjectedDependency"> & {
* Use `create(InjectedDependencySchema)` to create a new message.
*/
export const InjectedDependencySchema: GenMessage<InjectedDependency> = /*@__PURE__*/
messageDesc(file_quixos_refs, 2);
messageDesc(file_quixos_refs, 3);
/**
* @generated from message quixos.EdgeDependency
@@ -130,5 +174,5 @@ export type EdgeDependency = Message<"quixos.EdgeDependency"> & {
* Use `create(EdgeDependencySchema)` to create a new message.
*/
export const EdgeDependencySchema: GenMessage<EdgeDependency> = /*@__PURE__*/
messageDesc(file_quixos_refs, 3);
messageDesc(file_quixos_refs, 4);