2026-09-08 14:27:24 -07:00
import type { InterfaceRevision , PackageRevision , ValueType , DependencyPort } from "../capability-model/types.js" ;
import type { CompiledCapabilityResourceRepository } from "../capability-language/assembly.js" ;
/** Portable generator input. New backends consume this instead of the parser or TS runtime. */
export type BindingSchema = {
format : "quixos-bindings" ;
version : 1 ;
interfaces : InterfaceRevision [];
packages : PackageRevision [];
};
export const bindingSchema = ( compiled : CompiledCapabilityResourceRepository ) : BindingSchema => ({
2026-09-15 15:23:24 -07:00
format : "quixos-bindings" ,
version : 1 ,
interfaces : compiled.resources.flatMap (( node ) =>
node . resource . kind === "interface" ? [ node . resource . revision ] : [],
),
packages : compiled.resources.flatMap (( node ) => ( node . resource . kind === "package" ? [ node . resource . revision ] : [])),
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-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
. filter (( operation ) => operation . mode === "call" )
. 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 ) } >` ,
]),
]),
spec : {
kind : "interface" ,
id : entry.id ,
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 ][] = [];
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 ([
[ "objectId" , receiver ],
[ "input" , type ( entry . inputType )],
[ "ports" , object ( ports . map (( port ) => [ port . name , port . type ]))],
]);
contexts . push ([
entry . displayName ,
` ${ contextShape } & QxContextLifecycle< ${ contextShape } & {signal?: AbortSignal}>` ,
]);
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 );
// Watch-start handlers produce events through the runtime's derived stream protocol.
2026-09-15 15:23:24 -07:00
handlers . push ([
entry . displayName ,
event
? `QxDerived< ${ contextType } , ${ outputType } >`
: `QxHandler< ${ contextType } , ${ outputType } > ${ entry . kind === "operation" && entry . mode === "call" ? ` | QxDerived< ${ contextType } , ${ outputType } >` : "" } ` ,
]);
specs [ entry . displayName ] = {
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
}
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 ) } ;` ;
});
const signatures = ` ${ object ( contexts ) } ${ object ( handlers ) } ` ;
2026-09-15 15:23:24 -07:00
const typeImports = [
"BindingValue" ,
"QxObjectRef" ,
"QxWatchHandle" ,
"QxHandler" ,
"QxDerived" ,
"QxContextLifecycle" ,
"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-08 14:27:24 -07:00
`import { ${ 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-08 14:27:24 -07:00
`export type Contexts = ${ object ( contexts ) } ; \ nexport type Implementation = ${ object ( handlers ) } ; \ 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` +
2026-09-15 15:23:24 -07:00
exports
. map (
( entry ) =>
` ${ q ( entry . id ) } : bindQxHandler(specs[ ${ q ( entry . displayName ) } ], implementation[ ${ q ( entry . displayName ) } ], messages),` ,
)
. join ( "\n" ) +
` \ n }, \ n}); \ n`
);
2026-09-08 14:27:24 -07:00
};