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:
@@ -8,6 +8,7 @@ import "quixos/refs.proto";
|
||||
import "quixos/runtime.proto";
|
||||
|
||||
service OrchestratorRuntime {
|
||||
rpc TryConform(TryConformRequest) returns (TryConformResponse);
|
||||
rpc InvokeCapability(InvokeCapabilityRequest) returns (InvokeCapabilityResponse);
|
||||
rpc EditCapabilityField(EditCapabilityFieldRequest) returns (InvokeCapabilityResponse);
|
||||
rpc InvokeClassCapability(InvokeClassCapabilityRequest) returns (InvokeCapabilityResponse);
|
||||
@@ -22,6 +23,16 @@ service OrchestratorRuntime {
|
||||
rpc CloseActivation(CloseActivationRequest) returns (CloseActivationResponse);
|
||||
}
|
||||
|
||||
// Exact closed interface lookup; no policy selection or competing conformances.
|
||||
message TryConformRequest {
|
||||
string object_id = 1;
|
||||
string interface_revision_id = 2;
|
||||
}
|
||||
message TryConformResponse {
|
||||
// Absent only when this object lacks a known contract. Other failures are errors.
|
||||
quixos.ConformanceWitness conformance = 1;
|
||||
}
|
||||
|
||||
message ConstructObjectRequest {
|
||||
string atom_id = 1;
|
||||
map<string, camino.Value> input = 2;
|
||||
|
||||
@@ -5,6 +5,16 @@ package quixos;
|
||||
message CapabilityRef {
|
||||
string interface_revision_id = 1;
|
||||
string operation_id = 2;
|
||||
// Optional fence for a view acquired through TryConform. Not an authority grant.
|
||||
ConformanceWitness conformance = 3;
|
||||
}
|
||||
|
||||
message ConformanceWitness {
|
||||
string object_id = 1;
|
||||
string interface_revision_id = 2;
|
||||
string conformance_id = 3;
|
||||
string workspace_revision_id = 4;
|
||||
string workspace_epoch = 5;
|
||||
}
|
||||
|
||||
message PackageExportRef {
|
||||
|
||||
@@ -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
@@ -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` +
|
||||
|
||||
@@ -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
@@ -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`
|
||||
: "")
|
||||
|
||||
+79
-28
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
|
||||
|
||||
@@ -12,6 +12,100 @@ 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("runtime conformance descriptors preserve closed and universal collection item types", async (t) => {
|
||||
const sdk = path.resolve("../quixos-instance/packages/camino-package-runtime/dist/index.js");
|
||||
// The protocol is also published and tested as a standalone subtree. This
|
||||
// integration uses the real SDK only when both projects are checked out.
|
||||
if (
|
||||
!(await fs.access(sdk).then(
|
||||
() => true,
|
||||
() => false,
|
||||
))
|
||||
) {
|
||||
t.skip("Real SDK integration requires the monorepo checkout");
|
||||
return;
|
||||
}
|
||||
const collection = compileCapabilityResourceSource(
|
||||
`interface Collection<object Item> id "collection" revision "collection@1" {
|
||||
relation items id "items" : many-unique object Item {resolve id "get"; connect id "add";}
|
||||
}`,
|
||||
{ source },
|
||||
);
|
||||
assert.ok(collection.ok && collection.resource.kind === "interface");
|
||||
if (!collection.ok || collection.resource.kind !== "interface") throw Error("interface failed");
|
||||
const pkg = compileCapabilityResourceSource(
|
||||
`import interface Collection;
|
||||
external atom Book id "book"; external atom Tool id "tool";
|
||||
package P id "p" revision "p@1" {
|
||||
operation both id "both" : record {book:atom-ref<Book>; tool:atom-ref<Tool>;} -> unit mode call receiver any requires {
|
||||
interface books id "books" : Collection<atom Book>;
|
||||
interface tools id "tools" : Collection<atom Tool>;
|
||||
};
|
||||
operation inspect<object Item> id "inspect" : ref<Item> -> unit mode call receiver any requires {
|
||||
interface items id "items" : Collection<Item>;
|
||||
};
|
||||
}`,
|
||||
{ source, environment: { interfaces: new Map([["Collection", collection.resource.revision]]) } },
|
||||
);
|
||||
assert.ok(pkg.ok && pkg.resource.kind === "package", JSON.stringify(pkg.diagnostics));
|
||||
if (!pkg.ok || pkg.resource.kind !== "package") throw Error("package failed");
|
||||
const directory = await fs.mkdtemp(path.join(os.tmpdir(), "qx-conformance-types-"));
|
||||
try {
|
||||
const generated = generateTypeScriptBindings(
|
||||
{
|
||||
format: "quixos-bindings",
|
||||
version: 1,
|
||||
interfaces: pkg.resource.specializations ?? [],
|
||||
interfaceTemplates: [collection.resource.revision],
|
||||
packages: [pkg.resource.revision],
|
||||
},
|
||||
"p@1",
|
||||
{ runtimeModule: sdk },
|
||||
);
|
||||
await fs.writeFile(path.join(directory, "bindings.mts"), generated);
|
||||
await fs.writeFile(
|
||||
path.join(directory, "consumer.mts"),
|
||||
`import type {Implementation} from "./bindings.mjs";
|
||||
const implementation: Implementation = {
|
||||
both: async (context) => {
|
||||
const view = await context.conform.tryConform(context.objectId, context.ports.books.contract);
|
||||
if (view) {
|
||||
await view["items.connect"](context.input.book);
|
||||
// @ts-expect-error Tool is not Book
|
||||
await view["items.connect"](context.input.tool);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
inspect: async (context) => {
|
||||
const view = await context.conform.tryConform(context.objectId, context.ports.items.contract);
|
||||
if (view) await view["items.connect"](context.input);
|
||||
return null;
|
||||
},
|
||||
};`,
|
||||
);
|
||||
const compiler = path.join(
|
||||
path.dirname(createRequire(import.meta.url).resolve("typescript/package.json")),
|
||||
"bin/tsc",
|
||||
);
|
||||
const result = await promisify(execFile)(process.execPath, [
|
||||
compiler,
|
||||
"--ignoreConfig",
|
||||
"--noEmit",
|
||||
"--strict",
|
||||
"--skipLibCheck",
|
||||
"--target",
|
||||
"es2023",
|
||||
"--module",
|
||||
"nodenext",
|
||||
path.join(directory, "consumer.mts"),
|
||||
]).catch((error) => {
|
||||
throw Error(error.stdout + error.stderr);
|
||||
});
|
||||
assert.equal(result.stderr, "");
|
||||
} finally {
|
||||
await fs.rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
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";}}',
|
||||
@@ -108,7 +202,7 @@ test("generated universal implementations compile and cannot assume a concrete v
|
||||
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`;
|
||||
const preamble = `type QxObjectRef<T extends string>={readonly identity:T}; type QxConformer={}; 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;`);
|
||||
|
||||
@@ -33,6 +33,18 @@ test("React bindings preserve read-only, writable and nested reference contracts
|
||||
interfaces: [iface.resource.revision],
|
||||
packages: [pkg.resource.revision],
|
||||
} as const;
|
||||
const opaque = structuredClone(iface.resource.revision);
|
||||
opaque.revisionId = "opaque@1" as typeof opaque.revisionId;
|
||||
opaque.displayName = "Opaque";
|
||||
for (const member of opaque.members)
|
||||
for (const operation of member.operations)
|
||||
operation.outputType = { kind: "message", descriptorId: "UnsupportedBrowserMessage" };
|
||||
const withUnusedOpaque = generateReactBindings(
|
||||
{ ...schema, interfaces: [...schema.interfaces, opaque], packages: [...schema.packages] },
|
||||
"p@1",
|
||||
["props"],
|
||||
);
|
||||
assert.doesNotMatch(withUnusedOpaque, /"opaque@1": defineReactInterfaceContract/);
|
||||
const generated = generateReactBindings(
|
||||
{ ...schema, interfaces: [...schema.interfaces], packages: [...schema.packages] },
|
||||
"p@1",
|
||||
@@ -54,8 +66,18 @@ test("React bindings preserve read-only, writable and nested reference contracts
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(root, "consumer.ts"),
|
||||
`import {useLiveField, type ReadableField, type WritableField} from "@quixos/web-studio-react-runtime";
|
||||
`import {useLiveField, tryConform, type ReadableField, type WritableField} from "@quixos/web-studio-react-runtime";
|
||||
import {reactInterfaces} from "./react-props.gen.js";
|
||||
import type {ReactResults} from "./react-props.gen.js";
|
||||
async function lookup() {
|
||||
const view = await tryConform("object", reactInterfaces.Fields);
|
||||
if (!view) return;
|
||||
await view.call["title.set"]("works");
|
||||
// @ts-expect-error exact checked setter input
|
||||
await view.call["title.set"](123);
|
||||
// @ts-expect-error no invented operation
|
||||
await view.call["summary.set"]("no setter");
|
||||
}
|
||||
declare const props: ReactResults["props"];
|
||||
const [title, setTitle] = useLiveField(props.fields.fields.title);
|
||||
setTitle("new");
|
||||
|
||||
Reference in New Issue
Block a user