2026-09-14 10:26:11 -07:00
import { readFile , writeFile , rename , rm , realpath } from "node:fs/promises" ;
import path from "node:path" ;
import { execFile as callback } from "node:child_process" ;
import { promisify } from "node:util" ;
import { randomUUID } from "node:crypto" ;
import { authoringContext } from "./authoring-context.js" ;
import { snapshotCommit } from "./checked-build.js" ;
import { loadQuixosLock , parseQuixosLockDocument , formatQuixosLockDocument , retentionTagForCommit , type GitSource } from "../resource-lock/index.js" ;
const execFile = promisify ( callback );
2026-09-15 13:45:42 -07:00
const command = async ( cwd : string , executable : string , args : string []) => {
const start = performance . now ();
// Do not log arguments: transports may contain credentials. Source identities
// remain in the normal checked result, not in this timing channel.
const label = ` ${ path . basename ( cwd ) } ${ executable } ${ args [ 0 ] } ` ;
try { return ( await execFile ( executable , args , {
cwd , maxBuffer : 4 * 1024 * 1024 ,
env : { ... process . env , QUIXOS_JJ_NO_CHECKPOINT : "1" , QUIXOS_SUBTREE_PUBLISH : "0" , GIT_TERMINAL_PROMPT : "0" },
})). stdout . trim (); }
finally { if ( process . env . QUIXOS_TRACE_CAPTURE === "1" ) console . error ( `[ ${ new Date (). toISOString () } ] Capture: ${ label } : ${ Math . round ( performance . now () - start ) } ms` ); }
};
2026-09-14 10:26:11 -07:00
const identity = ( kind : string , repository : string ) => ` ${ kind } \ 0 ${ repository } ` ;
export type AuthoringBlocker = { directory : string ; phase : "resolution" | "dependency" | "source" | "publication" | "concurrent-edit" ; message : string };
/** Source retention only. Neither successful convergence nor an empty source
* worklist grants typechecking, semantic review or activation approval.
* Caller serializes coordinators; package authors may still be editing. */
export async function convergeAuthoring ( start : string , target = "root" ) {
const context = await authoringContext ( start );
const graphFile = path . join ( context . workbench , ".quixos/resource-graph.json" );
const graphBefore = await readFile ( graphFile , "utf8" );
const blockers : AuthoringBlocker [] = [];
const nodes = new Map < string , { directory : string ; kind : string ; source : GitSource ; dependencies : string [] }>();
const selected = new Map < string , string >();
for ( const entry of context . resources ) {
const root = path . join ( context . workbench , entry . directory );
let repository = entry . source ? . repository ;
try {
if ( await realpath ( root ) !== root ) throw new Error ( `Managed checkout crosses a symlink: ${ entry . directory } ` );
// Transport rewrites must not become committed source identities.
const origin = await command ( root , "git" , [ "config" , "--get" , "remote.origin.url" ]);
if ( repository && origin !== repository ) throw new Error ( `Origin differs from registered source for ${ entry . directory } ` );
repository ??= origin ;
} catch ( error ) {
blockers . push ({ directory : entry.directory , phase : "source" , message : String ( error ). slice ( 0 , 2000 )});
if ( ! repository ) throw error ; // The root has no separate registered source.
}
const key = identity ( entry . kind , repository );
if ( selected . has ( key )) throw new Error ( `More than one editable checkout for ${ repository } ` );
selected . set ( key , entry . directory );
nodes . set ( entry . directory , { ... entry , source : { resolver : "git" , repository , commit : entry.source?.commit ?? "" }, dependencies : [] });
}
for ( const node of nodes . values ()) {
try {
const lock = await loadQuixosLock ( path . join ( context . workbench , node . directory , "quixos.lock" ));
if ( ! lock . ok ) throw new Error ( lock . diagnostics . map ( d => ` ${ d . fileName } : ${ d . message } ` ). join ( "\n" ));
node . dependencies = [... new Set ( lock . lock . resources . flatMap ( entry => {
const directory = selected . get ( identity ( entry . kind , entry . source . repository ));
return directory ? [ directory ] : [];
}))];
} catch ( error ) { blockers . push ({ directory : node.directory , phase : "resolution" , message : String ( error ) }); }
}
const complete = new Map < string , GitSource >(), active = new Set < string >();
const visited = new Set < string >();
if ( ! nodes . has ( target )) throw new Error ( `Not a registered repository: ${ target } ` );
const visit = async ( directory : string ) : Promise < boolean > => {
visited . add ( directory );
if ( complete . has ( directory )) return true ;
if ( blockers . some ( entry => entry . directory === directory )) return false ;
if ( active . has ( directory )) { blockers . push ({ directory , phase : "dependency" , message : `Source dependency cycle: ${ [... active , directory ]. join ( " -> " ) } ` }); return false ; }
active . add ( directory );
const node = nodes . get ( directory ) ! ;
for ( const dependency of node . dependencies ) if ( ! await visit ( dependency )) {
blockers . push ({ directory , phase : "dependency" , message : `Waiting for ${ dependency } ` }); active . delete ( directory ); return false ;
}
const root = path . join ( context . workbench , directory );
let phase : AuthoringBlocker [ "phase" ] = "source" ;
try {
const lock = await loadQuixosLock ( path . join ( root , "quixos.lock" ));
if ( ! lock . ok ) throw new Error ( "Lock changed during convergence; retry after joining writers" );
for ( const file of lock . lock . sourceFiles ?? [ "quixos.lock" ]) {
const filename = path . join ( root , file ), before = await readFile ( filename , "utf8" );
const parsed = parseQuixosLockDocument ( before , file );
if ( ! parsed . ok ) throw new Error ( `Invalid lock ${ file } ` );
let changed = false ;
for ( const dependency of parsed . document . resources ) {
const target = selected . get ( identity ( dependency . kind , dependency . source . repository ));
const source = target ? complete . get ( target ) : undefined ;
if ( target && ! source ) throw new Error ( `Dependencies changed during convergence ( ${ dependency . binding } ); join writers and retry` );
if ( source && source . commit !== dependency . source . commit ) { dependency . source = source ; changed = true ; }
}
if ( changed ) {
const temporary = ` ${ filename } . ${ randomUUID () } .tmp` ;
try {
await writeFile ( temporary , formatQuixosLockDocument ( parsed . document ), { flag : "wx" });
if ( await readFile ( filename , "utf8" ) !== before ) throw new Error ( `Concurrent edit to ${ file } ; retry after joining writers` );
await rename ( temporary , filename );
} finally { await rm ( temporary , { force : true }); }
}
}
2026-09-15 13:45:42 -07:00
const snapshotStarted = performance . now ();
2026-09-14 10:26:11 -07:00
const commit = await snapshotCommit ( root );
2026-09-15 13:45:42 -07:00
if ( process . env . QUIXOS_TRACE_CAPTURE === "1" ) console . error ( `[ ${ new Date (). toISOString () } ] Capture: ${ directory } snapshot: ${ Math . round ( performance . now () - snapshotStarted ) } ms` );
2026-09-14 10:26:11 -07:00
phase = "publication" ;
const ref = retentionTagForCommit ( commit );
const remote = await command ( root , "git" , [ "ls-remote" , "--refs" , node . source . repository , ref ]);
if ( remote && remote . split ( /\s+/ )[ 0 ] !== commit ) throw new Error ( `Conflicting immutable retention ref ${ ref } ` );
if ( ! remote ) await command ( root , "git" , [ "push" , node . source . repository , ` ${ commit } : ${ ref } ` ]);
if (( await command ( root , "git" , [ "ls-remote" , "--refs" , node . source . repository , ref ])). split ( /\s+/ )[ 0 ] !== commit ) throw new Error ( "Published source retention was not observed" );
complete . set ( directory , { ... node . source , commit });
} catch ( error ) { blockers . push ({ directory , phase , message : String ( error ). slice ( 0 , 4000 ) }); }
active . delete ( directory );
return complete . has ( directory );
};
// Include newly created, not-yet-imported resources, then the root.
if ( target === "root" ) for ( const directory of [... nodes . keys ()]. filter ( d => d !== "root" )) await visit ( directory );
await visit ( target );
for ( let index = blockers . length - 1 ; index >= 0 ; index -- ) if ( ! visited . has ( blockers [ index ]. directory )) blockers . splice ( index , 1 );
for ( const [ directory , source ] of complete ) {
try { if ( await snapshotCommit ( path . join ( context . workbench , directory )) !== source . commit ) throw new Error ( "Source advanced while converging; join writers and retry" ); }
catch ( error ) { blockers . push ({ directory , phase : "concurrent-edit" , message : String ( error ) }); }
}
// Persist successful selections even if another repository is still broken.
// Recovery must not depend on all parents succeeding in the same invocation.
const graph = JSON . parse ( graphBefore );
for ( const resource of graph . resources ) resource . directory = path . relative ( context . workbench , path . resolve ( context . workbench , resource . directory ));
const replacements = new Map < string , string >();
for ( const resource of graph . resources ) {
const source = complete . get ( resource . directory );
if ( ! source ) continue ;
const key = ` ${ resource . kind } \ 0 ${ source . repository } \ 0 ${ source . commit } ` ;
replacements . set ( resource . key , key );
if ( resource . source . commit !== source . commit ) delete resource . revisionId ;
resource . source = source ; resource . key = key ;
}
for ( const resource of graph . resources ) for ( const dependency of resource . dependencies ?? []) {
dependency . resourceKey = replacements . get ( dependency . resourceKey ) ?? dependency . resourceKey ;
}
for ( const direct of graph . directResources ?? []) direct . resourceKey = replacements . get ( direct . resourceKey ) ?? direct . resourceKey ;
// Inventory is a projection of actual locks, including newly added/removed
// imports. Never require a successful parent compilation to repair it.
for ( const [ directory ] of complete ) {
const lock = await loadQuixosLock ( path . join ( context . workbench , directory , "quixos.lock" ));
if ( ! lock . ok ) continue ;
const dependencies = lock . lock . resources . map ( dependency => ({
binding : ` ${ dependency . kind } \ 0 ${ dependency . binding } ` ,
resourceKey : ` ${ dependency . kind } \ 0 ${ dependency . source . repository } \ 0 ${ dependency . source . commit } ` ,
}));
if ( directory === "root" ) {
graph . quixos = lock . lock . quixos ;
graph . directResources = lock . lock . resources . map (( dependency , index ) => ({
kind : dependency.kind , binding : dependency.binding , resourceKey : dependencies [ index ]. resourceKey ,
...( selected . has ( identity ( dependency . kind , dependency . source . repository ))
? { directory : selected.get ( identity ( dependency . kind , dependency . source . repository ))} : {}),
}));
} else {
const resource = graph . resources . find (( entry : { directory : string }) => entry . directory === directory );
if ( resource ) resource . dependencies = dependencies ;
}
}
const graphAfter = JSON . stringify ( graph , null , 2 ) + "\n" ;
if ( graphBefore !== graphAfter ) {
const temporary = ` ${ graphFile } . ${ randomUUID () } .tmp` ;
try {
await writeFile ( temporary , graphAfter , { flag : "wx" , mode : 0o600 });
if ( await readFile ( graphFile , "utf8" ) !== graphBefore ) throw new Error ( "Managed inventory changed during convergence; source is retained, retry after joining writers" );
await rename ( temporary , graphFile );
} finally { await rm ( temporary , { force : true }); }
}
return { workbench : context.workbench , converged : blockers.length === 0 ,
candidate : blockers.length ? null : complete . get ( target ) ?? null ,
retained : [... complete ]. map (([ directory , source ]) => ({ directory , source })),
worklist : blockers , verificationEvidence : false , activated : false };
}