2026-09-16 00:12:39 -07:00
import type {
InterfaceRevision ,
PackageRevision ,
ValueType ,
DependencyPort ,
WorkspaceRevision ,
} from "../capability-model/types.js" ;
2026-09-08 14:27:24 -07:00
import type { CompiledCapabilityResourceRepository } from "../capability-language/assembly.js" ;
2026-09-16 00:12:39 -07:00
import { genericImplementationType } from "./generics.js" ;
2026-09-17 11:29:56 -07:00
import { capabilityId } from "../capability-model/types.js" ;
2026-09-08 14:27:24 -07:00
/** Portable generator input. New backends consume this instead of the parser or TS runtime. */
export type BindingSchema = {
format : "quixos-bindings" ;
version : 1 ;
interfaces : InterfaceRevision [];
2026-09-16 00:12:39 -07:00
interfaceTemplates? : InterfaceRevision [];
2026-09-08 14:27:24 -07:00
packages : PackageRevision [];
};
2026-09-16 00:12:39 -07:00
export const bindingSchema = ( compiled : Pick < CompiledCapabilityResourceRepository , "resources" >) : BindingSchema => ({
2026-09-15 15:23:24 -07:00
format : "quixos-bindings" ,
version : 1 ,
2026-09-16 00:12:39 -07:00
interfaceTemplates : compiled.resources.flatMap (( node ) =>
node . resource . kind === "interface" && node . resource . revision . template ? [ node . resource . revision ] : [],
2026-09-15 15:23:24 -07:00
),
2026-09-16 00:12:39 -07:00
interfaces : [
... new Map (
compiled . resources
. flatMap (( node ) => [
...( node . resource . kind === "interface" ? [ node . resource . revision ] : []),
...( node . resource . specializations ?? []),
])
. filter (( entry ) => ! entry . template )
. map (( entry ) => [ entry . revisionId , entry ]),
). values (),
],
2026-09-15 15:23:24 -07:00
packages : compiled.resources.flatMap (( node ) => ( node . resource . kind === "package" ? [ node . resource . revision ] : [])),
2026-09-08 14:27:24 -07:00
});
2026-09-16 00:12:39 -07:00
export const specializeBindingSchema = (
base : BindingSchema ,
workspace : WorkspaceRevision ,
revisionId : string ,
) : BindingSchema => {
const pkg = workspace . packageImports . find (( entry ) => entry . revisionId === revisionId );
if ( ! pkg ) throw new Error ( `Missing candidate package ${ revisionId } ` );
const needed = new Set < string >();
const visit = ( value : unknown ) : void => {
if ( ! value || typeof value !== "object" ) return ;
if ( "interfaceRevisionId" in value && typeof value . interfaceRevisionId === "string" )
needed . add ( value . interfaceRevisionId );
Object . values ( value ). forEach ( visit );
};
visit ( pkg . exports );
const interfaces = new Map < string , InterfaceRevision >( base . interfaces . map (( entry ) => [ entry . revisionId , entry ]));
for ( const id of needed ) {
const iface = workspace . interfaceImports . find (( entry ) => entry . revisionId === id );
if ( iface ) {
interfaces . set ( id , iface );
visit ( iface . members );
}
}
return {
... base ,
interfaces : [... interfaces . values ()],
packages : base.packages.map (( entry ) => {
const candidate = workspace . packageImports . find (( value ) => value . revisionId === entry . revisionId );
if ( ! candidate ) throw new Error ( `Missing dependency package ${ entry . revisionId } ` );
return candidate ;
}),
};
};
2026-09-08 14:27:24 -07:00
export type TypeScriptBindingOptions = {
runtimeModule? : string ;
/** Each export must implement MessageBinding<T>, providing both TS type and wire codec. */
messages? : Record < string , { module : string ; export : string }>;
};
2026-09-14 20:35:53 -07:00
export function generatePackageDescriptor ( schema : BindingSchema , revisionId : string ) : string {
2026-09-15 15:23:24 -07:00
const pkg = schema . packages . find (( entry ) => entry . revisionId === revisionId );
2026-09-14 20:35:53 -07:00
if ( ! pkg ) throw new Error ( `Unknown package revision ${ revisionId } ` );
2026-09-15 15:23:24 -07:00
return (
`# Generated from the checked package contract \ npackage_id: ${ JSON . stringify ( pkg . packageId ) } \ npackage_revision_id: ${ JSON . stringify ( pkg . revisionId ) } \ nruntime_protocol_version: "quixos-capabilities-v1" \ n` +
pkg . exports
. map (
( entry ) =>
`exports: { export_id: ${ JSON . stringify ( entry . id ) } runtime_symbol: ${ JSON . stringify ( entry . displayName ) } } \ n` ,
)
. join ( "" )
);
2026-09-14 20:35:53 -07:00
}
2026-09-08 14:27:24 -07:00
const q = JSON . stringify ;
2026-09-15 15:23:24 -07:00
const object = ( entries : [ string , string ][]) =>
`{ ${ entries . map (([ key , value ]) => ` ${ q ( key ) } : ${ value } ` ). join ( "; " ) } }` ;
2026-09-08 14:27:24 -07:00
const unit = ( type : ValueType ) => type . kind === "builtin" && type . name === "unit" ;
export const generateTypeScriptBindings = (
2026-09-15 15:23:24 -07:00
schema : BindingSchema ,
packageRevisionId : string ,
options : TypeScriptBindingOptions = {},
2026-09-08 14:27:24 -07:00
) => {
2026-09-15 15:23:24 -07:00
if ( schema . format !== "quixos-bindings" || schema . version !== 1 )
throw new Error ( "Unsupported binding schema version" );
2026-09-16 00:12:39 -07:00
if ( schema . interfaces . some (( entry ) => entry . template ))
throw new Error ( "Package bindings require closed interface applications, not generic definitions" );
2026-09-08 14:27:24 -07:00
const pkg = schema . packages . find (( entry ) => entry . revisionId === packageRevisionId );
if ( ! pkg ) throw new Error ( `Unknown package revision ${ packageRevisionId } ` );
const messages = new Map < string , string >();
const type = ( value : ValueType ) : string => {
switch ( value . kind ) {
2026-09-15 15:23:24 -07:00
case "builtin" :
return value . name === "unit" ? "null" : "QxWatchHandle" ;
case "scalar" :
return {
bool : "boolean" ,
bytes : "Uint8Array" ,
string : "string" ,
int64 : "bigint" ,
uint64 : "bigint" ,
double : "number" ,
int32 : "number" ,
uint32 : "number" ,
}[ value . name ];
case "object-ref" :
return `QxObjectRef< ${ q ( value . expectation . kind === "atom" ? `atom: ${ value . expectation . atomId } ` : `interface: ${ value . expectation . interfaceRevisionId } ` ) } >` ;
case "optional" :
return `( ${ type ( value . value ) } | null)` ;
case "list" :
return `Array< ${ type ( value . value ) } >` ;
case "record" :
return `{ ${ Object . entries ( value . fields )
. map (([ name , field ]) => ` ${ q ( name ) } : ${ type ( field ) } ` )
. join ( "; " ) } }` ;
2026-09-08 14:27:24 -07:00
case "message" : {
2026-09-15 15:23:24 -07:00
if ( ! options . messages ? .[ value . descriptorId ])
throw new Error ( `Missing TypeScript message binding for ${ value . descriptorId } ` );
2026-09-08 14:27:24 -07:00
if ( ! messages . has ( value . descriptorId )) messages . set ( value . descriptorId , `message ${ messages . size } ` );
return `BindingValue<typeof ${ messages . get ( value . descriptorId ) } >` ;
}
}
};
const ref = ( target : { kind : "atom" ; atomId : string } | { kind : "interface" ; interfaceRevisionId : string }) =>
`QxObjectRef< ${ q ( target . kind === "atom" ? `atom: ${ target . atomId } ` : `interface: ${ target . interfaceRevisionId } ` ) } >` ;
2026-09-15 15:23:24 -07:00
const params = ( input : ValueType ) => ( unit ( input ) ? "" : `input: ${ type ( input ) } ` );
2026-09-08 14:27:24 -07:00
const port = ( entry : DependencyPort ) : { type : string ; spec : unknown } => {
const requirement = entry . requirement ;
switch ( requirement . kind ) {
case "state" : {
const methods = requirement . primitives . map (( primitive ) : [ string , string ] => {
if ( primitive === "read" ) return [ "get" , `() => Promise< ${ type ( requirement . valueType ) } >` ];
if ( primitive === "write" ) return [ "set" , `(value: ${ type ( requirement . valueType ) } ) => Promise<void>` ];
throw new Error ( `State primitive ${ primitive } is not supported by the TypeScript runtime binding yet` );
});
2026-09-13 19:29:33 -07:00
if ( requirement . primitives . includes ( "read" )) methods . push ([ "live" , "() => Promise<QxLiveValue>" ]);
2026-09-08 14:27:24 -07:00
return { type : object ( methods ), spec : { ... requirement , id : entry.id } };
}
case "edge" : {
const methods = requirement . primitives . map (( primitive ) : [ string , string ] => {
if ( primitive === "resolve" ) return [ primitive , `() => Promise<Array< ${ ref ( requirement . target ) } >>` ];
2026-09-15 15:23:24 -07:00
if ( primitive === "connect" || primitive === "disconnect" )
return [ primitive , `(target: ${ ref ( requirement . target ) } ) => Promise<void>` ];
2026-09-08 14:27:24 -07:00
throw new Error ( `Edge primitive ${ primitive } is not supported by the TypeScript runtime binding yet` );
});
2026-09-15 15:23:24 -07:00
if ( requirement . primitives . includes ( "resolve" ))
methods . push ([ "collection" , `() => Promise<RelationshipCollection< ${ ref ( requirement . target ) } >>` ]);
if (
[ "resolve" , "connect" , "disconnect" ]. every (( primitive ) =>
requirement . primitives . includes ( primitive as "resolve" ),
)
)
methods . push ([
"replace" ,
`(entries: RelationshipEntry< ${ ref ( requirement . target ) } >[], expectedRevision: bigint) => Promise<RelationshipCollection< ${ ref ( requirement . target ) } >>` ,
]);
2026-09-08 14:27:24 -07:00
return { type : object ( methods ), spec : { kind : "edge" , id : entry.id , primitives : requirement.primitives } };
}
case "interface" : {
2026-09-15 15:23:24 -07:00
const contract = schema . interfaces . find (
( candidate ) => candidate . revisionId === requirement . interfaceRevisionId ,
);
2026-09-08 14:27:24 -07:00
if ( ! contract ) throw new Error ( `Missing imported interface contract ${ requirement . interfaceRevisionId } ` );
// Streaming ports need a future streaming ABI; ordinary calls are fully typed today.
2026-09-15 15:23:24 -07:00
const operations = contract . members . flatMap (( member ) =>
member . operations
2026-09-16 11:25:20 -07:00
. filter (( operation ) => operation . mode === "call" && operation . scope !== "class" )
2026-09-15 15:23:24 -07:00
. map (( operation ) => ({ ... operation , name : ` ${ member . displayName } . ${ operation . displayName } ` })),
);
2026-09-17 11:29:56 -07:00
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 ) } >` ,
2026-09-15 15:23:24 -07:00
]),
2026-09-17 11:29:56 -07:00
];
return {
type : object ([... methods , [ "contract" , `QxInterfaceContract< ${ object ( methods ) } >` ]]),
2026-09-15 15:23:24 -07:00
spec : {
kind : "interface" ,
id : entry.id ,
2026-09-17 11:29:56 -07:00
interfaceRevisionId : requirement.interfaceRevisionId ,
2026-09-15 15:23:24 -07:00
operations : Object.fromEntries (
operations . map (({ name , id , inputType , outputType }) => [ name , { id , inputType , outputType }]),
),
},
};
2026-09-08 14:27:24 -07:00
}
case "constructor" : {
const input = requirement . inputType ;
2026-09-15 15:23:24 -07:00
if ( ! input )
throw new Error (
`Constructor port ${ entry . id } needs an explicit input contract: add 'input TYPE' after ${ requirement . atomId } in QX` ,
);
return {
type : object ([
[ "construct" , `( ${ params ( input ) } ) => Promise< ${ ref ({ kind : "atom" , atomId : requirement.atomId } )}>` ],
]),
spec : { kind : "constructor" , id : entry.id , inputType : input },
};
2026-09-08 14:27:24 -07:00
}
}
};
2026-09-15 15:23:24 -07:00
const exports = [... pkg . exports ]. sort (( a , b ) => ( a . id < b . id ? - 1 : a.id > b . id ? 1 : 0 ));
2026-09-08 14:27:24 -07:00
const names = new Set < string >();
const specs : Record < string , unknown > = {};
const contexts : [ string , string ][] = [];
const handlers : [ string , string ][] = [];
2026-09-16 00:12:39 -07:00
const results : [ string , string ][] = [];
2026-09-08 14:27:24 -07:00
for ( const entry of exports ) {
if ( names . has ( entry . displayName )) throw new Error ( `Duplicate export name ${ entry . displayName } ` );
names . add ( entry . displayName );
const ports = entry . dependencyPorts . map (( dependency ) => ({ name : dependency.displayName , ... port ( dependency ) }));
2026-09-15 15:23:24 -07:00
if ( new Set ( ports . map (( p ) => p . name )). size !== ports . length )
throw new Error ( `Duplicate dependency name in ${ entry . displayName } ` );
const receiver =
entry . kind === "constructor"
? ref ({ kind : "atom" , atomId : entry.constructsAtom })
: entry . kind === "operation" && entry . receiverRequirement . kind === "exact-atom"
? ref ({ kind : "atom" , atomId : entry.receiverRequirement.atomId })
: entry . kind === "operation" && entry . receiverRequirement . kind === "all-interfaces"
? `QxObjectRef< ${ entry . receiverRequirement . interfaceRevisionIds . map (( id ) => q ( `interface: ${ id } ` )). join ( " | " ) || "never" } >`
: "QxObjectRef<string>" ;
const contextShape = object ([
2026-09-17 11:29:56 -07:00
[ "conform" , "QxConformer" ],
2026-09-16 11:25:20 -07:00
...( entry . kind === "function" ? [] : [[ "objectId" , receiver ] as [ string , string ]]),
2026-09-15 15:23:24 -07:00
[ "input" , type ( entry . inputType )],
[ "ports" , object ( ports . map (( port ) => [ port . name , port . type ]))],
]);
contexts . push ([
entry . displayName ,
2026-09-16 11:25:20 -07:00
entry . kind === "function"
? ` ${ contextShape } & {signal?: AbortSignal}`
: ` ${ contextShape } & QxContextLifecycle< ${ contextShape } & {signal?: AbortSignal}>` ,
2026-09-15 15:23:24 -07:00
]);
2026-09-08 14:27:24 -07:00
const event = entry . kind === "operation" ? entry.eventType : undefined ;
const contextType = `Contexts[ ${ q ( entry . displayName ) } ]` ;
const outputType = type ( event ?? entry . outputType );
2026-09-16 00:12:39 -07:00
results . push ([ entry . displayName , outputType ]);
2026-09-08 14:27:24 -07:00
// Watch-start handlers produce events through the runtime's derived stream protocol.
2026-09-16 00:12:39 -07:00
if ( ! entry . application )
handlers . push ([
entry . displayName ,
event
? `QxDerived< ${ contextType } , ${ outputType } >`
: `QxHandler< ${ contextType } , ${ outputType } > ${ entry . kind === "operation" && entry . mode === "call" ? ` | QxDerived< ${ contextType } , ${ outputType } >` : "" } ` ,
]);
2026-09-15 15:23:24 -07:00
specs [ entry . displayName ] = {
2026-09-16 11:25:20 -07:00
...( entry . kind === "function" ? { receiver : "none" } : {}),
2026-09-15 15:23:24 -07:00
inputType : entry.inputType ,
outputType : entry.outputType ,
...( event ? { eventType : event } : {}),
ports : Object.fromEntries ( ports . map (( port ) => [ port . name , port . spec ])),
};
2026-09-08 14:27:24 -07:00
}
2026-09-16 00:12:39 -07:00
for ( const definition of pkg . genericExports ?? []) {
handlers . push ([
definition . displayName ,
genericImplementationType ( definition , [... schema . interfaces , ...( schema . interfaceTemplates ?? [])], type ),
]);
}
2026-09-17 11:29:56 -07:00
const contracts = schema . interfaces . 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 ) } )` ,
};
});
2026-09-08 14:27:24 -07:00
const imports = [... messages ]. map (([ id , alias ]) => {
const binding = options . messages ! [ id ] ! ;
2026-09-15 15:23:24 -07:00
if ( ! /^[A-Za-z_$][A-Za-z0-9_$]*$/ . test ( binding . export ))
throw new Error ( `Invalid message binding export ${ binding . export } ` );
2026-09-08 14:27:24 -07:00
return `import { ${ binding . export } as ${ alias } } from ${ q ( binding . module ) } ;` ;
});
2026-09-17 11:29:56 -07:00
const signatures = ` ${ object ( contexts ) } ${ object ( handlers ) } ${ object ( results ) } ${ contracts . map (( entry ) => entry . type ). join ( " " ) } ` ;
2026-09-15 15:23:24 -07:00
const typeImports = [
"BindingValue" ,
"QxObjectRef" ,
"QxWatchHandle" ,
"QxHandler" ,
"QxDerived" ,
"QxContextLifecycle" ,
2026-09-17 11:29:56 -07:00
"QxConformer" ,
"QxInterfaceContract" ,
2026-09-15 15:23:24 -07:00
"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` +
2026-09-17 11:29:56 -07:00
`import { ${ contracts . length ? "defineQxInterfaceContract, " : "" }${ exports . length ? "bindQxHandler, " : "" }${ [... typeImports , "QxHandlerSpec" , "QxMessages" ]. map (( name ) => `type ${ name } ` ). join ( ", " ) } } from ${ q ( options . runtimeModule ?? "@quixos/camino-package-runtime" ) } ; \ n` +
2026-09-15 15:23:24 -07:00
imports . join ( "\n" ) +
` \ nexport const packageRevisionId = ${ q ( pkg . revisionId ) } ; \ n` +
2026-09-16 00:12:39 -07:00
`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` +
2026-09-17 11:29:56 -07:00
`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` +
2026-09-08 14:27:24 -07:00
`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` +
2026-09-15 15:23:24 -07:00
exports
. map (
( entry ) =>
2026-09-16 00:12:39 -07:00
` ${ q ( entry . id ) } : bindQxHandler(specs[ ${ q ( entry . displayName ) } ], implementation[ ${ q ( entry . application ? pkg . genericExports ! . find (( definition ) => definition . id === entry . application ! . exportId ) ! . displayName : entry.displayName ) } ], messages),` ,
2026-09-15 15:23:24 -07:00
)
. join ( "\n" ) +
` \ n }, \ n}); \ n`
);
2026-09-08 14:27:24 -07:00
};