2026-09-21 00:44:47 -07:00
import fs from "node:fs/promises" ;
import path from "node:path" ;
import type { ScaffoldRecipe } from "./scaffold-recipes.js" ;
import type { StructuralRequest } from "./structural-plan.js" ;
import { parseQx , walkSyntax } from "./source.js" ;
import { addImplementation } from "./implementation-edit.js" ;
import { formatQuixosLock } from "../resource-lock/index.js" ;
const json = ( value : unknown ) => JSON . stringify ( value , null , 2 ) + "\n" ;
const q = ( value : string ) => JSON . stringify ( value );
const nixString = ( value : string ) => q ( value ). replaceAll ( "${" , "\\${" );
export async function isExecutionScaffold ( root : string , prefix : string ) : Promise < boolean > {
try {
return JSON . parse ( await fs . readFile ( path . join ( root , prefix , "quixos.check.json" ), "utf8" )). backend === "execution" ;
} catch ( error ) {
if (( error as NodeJS . ErrnoException ). code === "ENOENT" ) return false ;
throw error ;
}
}
/** Insert ordinary authored code; declarations remain the only export inventory. */
export async function executionScaffold (
root : string ,
command : string ,
spec : ScaffoldRecipe ,
prefix : string ,
nixSource : ( source : NonNullable < ScaffoldRecipe [ "tools" ] >[ "helpers" ]) => string ,
) : Promise < StructuralRequest > {
if ( ! spec . name || ! /^[A-Za-z_][A-Za-z0-9_]*$/ . test ( spec . name ) || ! spec . id )
throw new Error ( "Execution scaffold requires a simple name and an ID" );
2026-09-21 16:59:35 -07:00
if ( spec . initialFiles && command !== "package" ) throw new Error ( "Initial files are only for new packages" );
2026-09-21 00:44:47 -07:00
const files : StructuralRequest [ "files" ] = [];
const create = ( file : string , content : string ) => files . push ({ file : prefix + file , create : content });
if ( command === "package" ) {
if ( ! spec . tools || ! spec . revision ) throw new Error ( "Execution package requires exact tool sources and a revision" );
create ( "package.qx" , `package ${ spec . name } id ${ q ( spec . id ) } revision ${ q ( spec . revision ) } { \ n} \ n` );
create (
"quixos.lock" ,
formatQuixosLock ({ formatVersion : 1 , quixos : { resolver : "git" , ... spec . tools . quixos }, resources : [] }),
);
create ( "quixos.check.json" , json ({ backend : "execution" }));
2026-09-21 16:59:35 -07:00
create ( "quixos.components.json" , "{}\n" );
2026-09-21 00:44:47 -07:00
create ( "package.json" , json ({ name : `@quixos/ ${ spec . name . toLowerCase () } ` , private : true , type : "module" }));
create ( ".gitignore" , "node_modules/\ndist/\nsrc/gen/\n.quixos/\nresult\n" );
for ( const lane of [ "browser" , "server" , "remote" ])
create (
`src/ ${ lane } .ts` ,
`import { createRegistry } from "./gen/ ${ lane === "remote" ? "remote" : "qx" } .js"; \ nexport const registry = createRegistry({}); \ n` ,
);
create (
"flake.nix" ,
`{
inputs.protocol.url = ${ nixString ( nixSource ( spec . tools . protocol )) } ;
inputs.nixpkgs.follows = "protocol/nixpkgs";
inputs.flake-utils.follows = "protocol/flake-utils";
inputs.helpers = { url = ${ nixString ( nixSource ( spec . tools . helpers )) } ; flake = false; };
outputs = { nixpkgs, flake-utils, helpers, ... }:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs { inherit system; };
in {
2026-09-21 16:59:35 -07:00
quixosPackages.checkedArtifacts = { plan, generator, core, sharedRuntime, typescript, packageRevisionId, react ? null }:
2026-09-21 15:24:05 -07:00
(import (helpers + "/artifacts.nix")) {
2026-09-21 00:44:47 -07:00
inherit pkgs plan core sharedRuntime typescript packageRevisionId;
protocol = generator;
src = ./.;
2026-09-21 16:59:35 -07:00
components = let entries = builtins.fromJSON (builtins.readFile ./quixos.components.json); in
if entries == {} then null else
if react == null then throw "Component entries require the workspace-selected React runtime" else
{ inherit (react) sdk runtime; inherit entries; };
2026-09-21 00:44:47 -07:00
portable = {
entry = "src/server.ts";
entries.browser = "src/browser.ts";
bindingOutput = "src/gen/qx.ts";
};
remote = { entry = "src/remote.ts"; bindingOutput = "src/gen/remote.ts"; };
};
});
}
` ,
);
create (
"README.md" ,
2026-09-21 16:59:35 -07:00
`# ${ spec . name } \ n \ nThis execution package exports checked browser/server modules and a process service. \ nUse scaffold function with an explicit execution contract to add a typed handler. \ nSnapshot/transaction targets choose browser.ts and/or server.ts; remote handlers \ nuse remote.ts. Shared helpers are ordinary imports. Generated bindings are build \ noutputs, not authored source. The coordinator supplies the checked workspace plan \ nand runtime dependencies to checkedArtifacts. \ n \ nRun qx-workspace check from the workspace root to compile exact source and build \ nchecked artifacts. Activate the resulting report separately through the workspace \ nhost. A source-only structural check is not runtime qualification. \ n` ,
2026-09-21 00:44:47 -07:00
);
} else if ( command === "function" ) {
if ( ! spec . declaration )
throw new Error ( "Execution functions require a declaration with explicit execution effect and targets" );
const wrapped = `package Scaffold id "scaffold" revision "scaffold@1" { ${ spec . declaration } }` ;
const parsed = parseQx ( wrapped );
const pkg = parsed . root . children . find (( node ) => node . kind === "packageResourceDecl" );
const exports = [... walkSyntax ( parsed . root )]. filter (( node ) =>
[ "packageFunctionExport" , "packageOperationExport" ]. includes ( node . kind ),
);
const declaration = exports [ 0 ];
if (
parsed . diagnostics . length ||
exports . length !== 1 ||
! declaration ||
parsed . root . children . length !== 1 ||
pkg ? . children . filter (( node ) => node . kind === "packageExport" ). length !== 1 ||
pkg ? . children . some (( node ) => ! [ "identifier" , "stringLiteral" , "packageExport" ]. includes ( node . kind ))
)
throw new Error ( "Expected one function or operation export with an execution contract" );
const text = ( node : { start : number ; end : number }) => wrapped . slice ( node . start , node . end );
const name = declaration . children . find (( node ) => node . kind === "identifier" );
const id = declaration . children . find (( node ) => node . kind === "stringLiteral" );
const execution = declaration . children . find (( node ) => node . kind === "executionClause" );
if ( ! name || text ( name ) !== spec . name || ! id || JSON . parse ( text ( id )) !== spec . id || ! execution )
throw new Error ( "Declaration name/ID must match; an explicit execution contract is required" );
const tokens = parsed . tokens
. filter (( token ) => ! token . trivia && token . start >= execution . start && token . end <= execution . end )
. map (( token ) => token . text );
const remote = tokens [ 1 ] === "remote" ;
const targets = remote
? [ "remote" ]
: [... walkSyntax ( execution )]. filter (( node ) => node . kind === "identifier" ). map ( text );
if (
! targets . length ||
new Set ( targets ). size !== targets . length ||
targets . some (( target ) => ! ( remote ? [ "remote" ] : [ "browser" , "server" ]). includes ( target ))
)
throw new Error ( "Execution scaffold supports browser/server portable targets and remote service handlers" );
const authored = await fs . readFile ( path . join ( root , prefix , "package.qx" ), "utf8" );
const syntax = parseQx ( authored );
const packageNode = syntax . root . children . find (( node ) => node . kind === "packageResourceDecl" );
if ( syntax . diagnostics . length || ! packageNode ) throw new Error ( "Fix package.qx before adding a handler" );
for ( const node of [... walkSyntax ( packageNode )]. filter (( node ) =>
[ "packageFunctionExport" , "packageOperationExport" , "packageConstructorExport" ]. includes ( node . kind ),
)) {
const identifier = node . children . find (( child ) => child . kind === "identifier" );
const literal = node . children . find (( child ) => child . kind === "stringLiteral" );
if (
( identifier && authored . slice ( identifier . start , identifier . end ) === spec . name ) ||
( literal && JSON . parse ( authored . slice ( literal . start , literal . end )) === spec . id )
)
throw new Error ( "New export requires a unique name and ID" );
}
const packageID = packageNode . children . find (( node ) => node . kind === "stringLiteral" ) ! ;
files . push ({
file : prefix + "package.qx" ,
edits : [
{
operation : "append" ,
parent : { kind : "packageResourceDecl" , id : JSON.parse ( authored . slice ( packageID . start , packageID . end )) },
source : spec.declaration ,
},
],
});
create (
`src/impl/ ${ spec . name } .ts` ,
`import type { ${ remote ? "Remote" : "Portable" } Implementation } from "../gen/ ${ remote ? "remote" : "qx" } .js"; \ nexport const handler: ${ remote ? "Remote" : "Portable" } Implementation[ ${ q ( spec . name ) } ] = async (_context) => { throw new Error( ${ q ( "Implement " + spec . name ) } ); }; \ n` ,
);
for ( const target of targets ) {
const file = prefix + `src/ ${ target } .ts` ;
const before = await fs . readFile ( path . join ( root , file ), "utf8" );
files . push ({
file ,
expected : before ,
replace : addImplementation ( before , "createRegistry" , spec . name , `./impl/ ${ spec . name } .js` ),
});
}
} else
throw new Error ( "Execution packages use ordinary factory transactions; migration scaffolding is not supported" );
2026-09-21 16:59:35 -07:00
if ( spec . initialFiles ) {
for ( const [ file , content ] of Object . entries ( spec . initialFiles )) {
if (
typeof content !== "string" ||
! /^(?:package\.qx|quixos\.components\.json|quixos\.controllers\.json|README\.md|(?:src|queries)\/(?:[A-Za-z0-9_-]+\/)*[A-Za-z0-9_.-]+)$/ . test (
file ,
) ||
file . split ( "/" ). some (( part ) => part === ".." || part === "." || part === "gen" )
)
throw new Error ( "Not an initial authored execution file: " + file );
const previous = files . findIndex (( entry ) => entry . file === prefix + file );
if ( previous >= 0 ) files . splice ( previous , 1 );
create ( file , content );
}
}
2026-09-21 00:44:47 -07:00
return { kind : "package" , source : spec.source , resourceRoot : spec.directory , validation : "syntax" , files };
}