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 => ({
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 ] : []),
});
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 {
const pkg = schema . packages . find ( entry => entry . revisionId === revisionId );
if ( ! pkg ) throw new Error ( `Unknown package revision ${ revisionId } ` );
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-08 14:27:24 -07:00
const q = JSON . stringify ;
const object = ( entries : [ string , string ][]) => `{ ${ entries . map (([ key , value ]) => ` ${ q ( key ) } : ${ value } ` ). join ( "; " ) } }` ;
const unit = ( type : ValueType ) => type . kind === "builtin" && type . name === "unit" ;
export const generateTypeScriptBindings = (
schema : BindingSchema , packageRevisionId : string , options : TypeScriptBindingOptions = {},
) => {
if ( schema . format !== "quixos-bindings" || schema . version !== 1 ) throw new Error ( "Unsupported binding schema version" );
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 ) {
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 ) } >` ;
2026-09-13 16:30:17 -07:00
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" : {
if ( ! options . messages ? .[ value . descriptorId ]) throw new Error ( `Missing TypeScript message binding for ${ value . descriptorId } ` );
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 } ` ) } >` ;
const params = ( input : ValueType ) => unit ( input ) ? "" : `input: ${ type ( input ) } ` ;
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 ) } >>` ];
if ( primitive === "connect" || primitive === "disconnect" ) return [ primitive , `(target: ${ ref ( requirement . target ) } ) => Promise<void>` ];
throw new Error ( `Edge primitive ${ primitive } is not supported by the TypeScript runtime binding yet` );
});
2026-09-10 18:27:41 -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" : {
const contract = schema . interfaces . find (( candidate ) => candidate . revisionId === requirement . interfaceRevisionId );
if ( ! contract ) throw new Error ( `Missing imported interface contract ${ requirement . interfaceRevisionId } ` );
// Streaming ports need a future streaming ABI; ordinary calls are fully typed today.
const operations = contract . members . flatMap (( member ) => member . operations . filter (( operation ) => operation . mode === "call" )
. map (( operation ) => ({ ... operation , name : ` ${ member . displayName } . ${ operation . displayName } ` })));
2026-09-13 19:29:33 -07:00
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 ;
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 } };
}
}
};
const exports = [... pkg . exports ]. sort (( a , b ) => a . id < b . id ? - 1 : a.id > b . id ? 1 : 0 );
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 ) }));
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" ?
2026-09-10 18:27:41 -07:00
`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.
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 ])) };
}
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 ) } ` ;
2026-09-13 19:29:33 -07:00
const typeImports = [ "BindingValue" , "QxObjectRef" , "QxWatchHandle" , "QxHandler" , "QxDerived" , "QxContextLifecycle" , "QxLiveValue" , "RelationshipCollection" , "RelationshipEntry" ]. filter (( name ) => new RegExp ( `\\b ${ name } \\b` ). test ( signatures ));
2026-09-08 14:27:24 -07:00
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` +
imports . join ( "\n" ) + ` \ nexport const packageRevisionId = ${ q ( pkg . revisionId ) } ; \ n` +
`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` +
exports . map (( entry ) => ` ${ q ( entry . id ) } : bindQxHandler(specs[ ${ q ( entry . displayName ) } ], implementation[ ${ q ( entry . displayName ) } ], messages),` ). join ( "\n" ) +
` \ n }, \ n}); \ n` ;
};