Add exact runtime conformance lookup and agent-visible language limits
Add authorized exact-contract lookup, revision-fenced views, generated typed package/browser descriptors, and lifecycle-safe React discovery. Preserve closed generic contracts without competing-conformance policies or new authority grants. Document supported workarounds and privacy boundaries for agent feedback. Publish matching protocol/SDK exports and update scaffold/starter pins; leave the deployed template unchanged.
This commit is contained in:
+53
-22
@@ -21,6 +21,23 @@ export type QxHandler<C, O> = (context: C) => O | Promise<O>;
|
||||
export type QxDerived<C, O> = { kind: "derived"; get: QxHandler<C, O> };
|
||||
export type QxSession<C> = { id: string; run<T>(work: (context: C) => Promise<T>): Promise<T>; close(): Promise<void> };
|
||||
export type QxContextLifecycle<C> = { signal?: AbortSignal; openSession?: () => Promise<QxSession<C>> };
|
||||
declare const contractView: unique symbol;
|
||||
/** Generated exact closed contract. A descriptor is type evidence, never authority. */
|
||||
export type QxInterfaceContract<View> = {
|
||||
readonly interfaceRevisionId: string;
|
||||
readonly operations: Record<string, QxOperationSpec>;
|
||||
readonly [contractView]: (value: View) => View;
|
||||
};
|
||||
export const defineQxInterfaceContract = <View>(
|
||||
interfaceRevisionId: string,
|
||||
operations: Record<string, QxOperationSpec>,
|
||||
): QxInterfaceContract<View> => Object.freeze({ interfaceRevisionId, operations }) as QxInterfaceContract<View>;
|
||||
export type QxConformer = {
|
||||
tryConform<View>(
|
||||
object: import("./references.js").QxObjectRef,
|
||||
contract: QxInterfaceContract<View>,
|
||||
): Promise<View | undefined>;
|
||||
};
|
||||
export const qxDerived = <C, O>(get: QxHandler<C, O>): QxDerived<C, O> => ({ kind: "derived", get });
|
||||
|
||||
/** Versioned binding ABI. This mirrors the language-neutral value IR. */
|
||||
@@ -35,7 +52,7 @@ export type QxOperationSpec = { id: string; inputType: QxValueType; outputType:
|
||||
export type QxPortSpec =
|
||||
| { kind: "state"; id: string; valueType: QxValueType; primitives: string[] }
|
||||
| { kind: "edge"; id: string; primitives: string[] }
|
||||
| { kind: "interface"; id: string; operations: Record<string, QxOperationSpec> }
|
||||
| { kind: "interface"; id: string; interfaceRevisionId: string; operations: Record<string, QxOperationSpec> }
|
||||
| { kind: "constructor"; id: string; inputType: QxValueType };
|
||||
export type QxHandlerSpec = {
|
||||
receiver?: "none";
|
||||
@@ -46,6 +63,32 @@ export type QxHandlerSpec = {
|
||||
};
|
||||
export type QxMessages = Record<string, MessageBinding<any>>;
|
||||
|
||||
const bindInterfaceView = (
|
||||
target: import("./index.js").InterfacePort,
|
||||
contract: QxInterfaceContract<unknown>,
|
||||
messages: QxMessages,
|
||||
) => ({
|
||||
objectId: target.objectId,
|
||||
contract,
|
||||
live: Object.fromEntries(
|
||||
Object.entries(contract.operations).map(([name, operation]) => [
|
||||
name,
|
||||
(input: unknown) => target.live(operation.id, inputFields(operation.inputType, input, messages)),
|
||||
]),
|
||||
),
|
||||
...Object.fromEntries(
|
||||
Object.entries(contract.operations).map(([name, operation]) => [
|
||||
name,
|
||||
async (input: unknown) =>
|
||||
decodeQxValue(
|
||||
operation.outputType,
|
||||
(await target.live(operation.id, inputFields(operation.inputType, input, messages))).$quixosValue,
|
||||
messages,
|
||||
),
|
||||
]),
|
||||
),
|
||||
});
|
||||
|
||||
// Conversion belongs at the binding boundary. It does not add orchestrator validation.
|
||||
export const decodeQxValue = (type: QxValueType, value: Value | undefined, messages: QxMessages): any => {
|
||||
if (type.kind === "builtin" && type.name === "unit") return null;
|
||||
@@ -198,27 +241,7 @@ export const bindQxHandler = <C, O>(
|
||||
const target = raw.interface(port.id);
|
||||
return [
|
||||
name,
|
||||
{
|
||||
objectId: target.objectId,
|
||||
live: Object.fromEntries(
|
||||
Object.entries(port.operations).map(([name, operation]) => [
|
||||
name,
|
||||
(input: unknown) => target.live(operation.id, inputFields(operation.inputType, input, messages)),
|
||||
]),
|
||||
),
|
||||
...Object.fromEntries(
|
||||
Object.entries(port.operations).map(([name, operation]) => [
|
||||
name,
|
||||
async (input: unknown) =>
|
||||
decodeQxValue(
|
||||
operation.outputType,
|
||||
(await target.live(operation.id, inputFields(operation.inputType, input, messages)))
|
||||
.$quixosValue,
|
||||
messages,
|
||||
),
|
||||
]),
|
||||
),
|
||||
},
|
||||
bindInterfaceView(target, defineQxInterfaceContract(port.interfaceRevisionId, port.operations), messages),
|
||||
];
|
||||
}
|
||||
case "constructor":
|
||||
@@ -233,6 +256,14 @@ export const bindQxHandler = <C, O>(
|
||||
}),
|
||||
);
|
||||
return {
|
||||
conform: {
|
||||
async tryConform<View>(object: import("./references.js").QxObjectRef, contract: QxInterfaceContract<View>) {
|
||||
const target = await raw.tryConform(object, contract.interfaceRevisionId);
|
||||
return target
|
||||
? (bindInterfaceView(target, contract as QxInterfaceContract<unknown>, messages) as View)
|
||||
: undefined;
|
||||
},
|
||||
} satisfies QxConformer,
|
||||
...(spec.receiver === "none" ? {} : { objectId: raw.objectId }),
|
||||
signal: raw.signal,
|
||||
...(spec.receiver !== "none" && raw.openSession
|
||||
|
||||
Reference in New Issue
Block a user