Compare commits

...

4 Commits

Author SHA1 Message Date
Quixos Subtree Publisher 7863ce3e12 Publish quixos-protocol from db6d7990a4ffec273258ac313b7d2aeac57b261a 2026-09-17 18:54:46 +00:00
Timothy J. Aveni ea51ca89e7 Add exact runtime conformance lookup and agent-visible language limits 2026-09-17 11:54:46 -07:00
Quixos Subtree Publisher c3fe7c86b8 Publish quixos-protocol from a45fd9a85b541ce6b449b28e8b47954bf04de96b 2026-09-17 18:49:38 +00:00
Timothy J. Aveni 74f00fe28f Add exact runtime conformance lookup and agent-visible language limits 2026-09-17 11:49:38 -07:00
5 changed files with 78 additions and 28 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"version": 1, "version": 1,
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos", "sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
"sourceCommit": "0355d845d7ad4b797343b681e9b5e1a77c63c720", "sourceCommit": "db6d7990a4ffec273258ac313b7d2aeac57b261a",
"sourcePath": "quixos-protocol", "sourcePath": "quixos-protocol",
"exportName": "quixos-protocol", "exportName": "quixos-protocol",
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git" "mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
+17 -1
View File
@@ -285,7 +285,23 @@ export const generateTypeScriptBindings = (
genericImplementationType(definition, [...schema.interfaces, ...(schema.interfaceTemplates ?? [])], type), genericImplementationType(definition, [...schema.interfaces, ...(schema.interfaceTemplates ?? [])], type),
]); ]);
} }
const contracts = schema.interfaces.map((iface) => { // 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({ const generated = port({
id: capabilityId.dependencyPort("contract"), id: capabilityId.dependencyPort("contract"),
displayName: iface.displayName, displayName: iface.displayName,
+1 -2
View File
@@ -15,8 +15,7 @@ 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 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 LiveFieldProp<T> = ReadableField<T>;
export type InterfaceReference<I extends string, Fields> = {readonly $quixosRef: string; readonly interfaceRevisionId: I; readonly fields: Fields}; export type InterfaceReference<I extends string, Fields> = {readonly $quixosRef: string; readonly interfaceRevisionId: I; readonly fields: Fields};
const conformanceView: unique symbol; export type ReactInterfaceContract<View> = {readonly interfaceRevisionId: string; readonly $viewType?: (view: View) => View};
export type ReactInterfaceContract<View> = {readonly interfaceRevisionId: string; readonly [conformanceView]: (view: View) => View};
export function defineReactInterfaceContract<View>(interfaceRevisionId: string, interfaces: unknown): ReactInterfaceContract<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 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 type ConformanceResult<View> = {status: "loading"} | {status: "absent"} | {status: "available"; view: View} | {status: "error"; error: Error};
+24 -1
View File
@@ -78,7 +78,30 @@ export function generateReactBindings(
throw new Error("React component check must name a local module relative to generated bindings"); 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"]>;`; return `type Component${i} = CheckedComponent<${q(component.propsExport)}, typeof import(${q(component.module)})["default"]>;`;
}); });
const contracts = schema.interfaces.map((iface) => { // 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({ const reference = type({
kind: "object-ref", kind: "object-ref",
expectation: { kind: "interface", interfaceRevisionId: iface.revisionId }, expectation: { kind: "interface", interfaceRevisionId: iface.revisionId },
+12
View File
@@ -33,6 +33,18 @@ test("React bindings preserve read-only, writable and nested reference contracts
interfaces: [iface.resource.revision], interfaces: [iface.resource.revision],
packages: [pkg.resource.revision], packages: [pkg.resource.revision],
} as const; } 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( const generated = generateReactBindings(
{ ...schema, interfaces: [...schema.interfaces], packages: [...schema.packages] }, { ...schema, interfaces: [...schema.interfaces], packages: [...schema.packages] },
"p@1", "p@1",