2026-09-14 10:26:11 -07:00
import fs from "node:fs/promises" ;
import path from "node:path" ;
import { execFile as callback } from "node:child_process" ;
import { promisify } from "node:util" ;
import { authoringContext } from "./authoring-context.js" ;
import { inspectAuthoringRepository } from "./authoring-inspect.js" ;
import { checkRecordName } from "./authoring-check.js" ;
import { loadQuixosLock } from "../resource-lock/index.js" ;
import { checkerIdentity } from "./checked-build.js" ;
const execFile = promisify ( callback );
export async function authoringWorklist ( start : string ) {
const context = await authoringContext ( start );
const entries : { directory : string ; resourceId? : string ; phase : string ; message : string ; next : string }[] = [];
const dependencies = new Map < string , string [] >();
for ( const resource of context . resources ) {
const root = path . join ( context . workbench , resource . directory );
const add = ( phase : string , message : string ) => entries . push ({ directory : resource.directory , resourceId : resource.resourceId , phase , message ,
2026-09-14 15:24:37 -07:00
next : phase === "syntax" ? `qx-workspace inspect ${ resource . directory } ` : phase === "evolution" ? "Inspect evolution.json in the check output; resolve its named migration/review requirements before cutover" : `cd ${ resource . directory } && qx-workspace check` });
2026-09-14 10:26:11 -07:00
try {
if ( await fs . realpath ( root ) !== root ) throw new Error ( "Registered checkout crosses a symlink" );
const inspected = await inspectAuthoringRepository ( root );
for ( const file of inspected . files ) if ( file . currentErrors . length ) add ( "syntax" , ` ${ file . file } : ${ JSON . stringify ( file . currentErrors ) }${ file . status === "historical" ? `; historical contract available at ${ file . revision } ` : "" } ` );
const lock = await loadQuixosLock ( path . join ( root , "quixos.lock" ));
if ( ! lock . ok ) add ( "resolution" , lock . diagnostics . map ( entry => ` ${ entry . fileName } : ${ entry . message } ` ). join ( "\n" ));
else dependencies . set ( resource . directory , lock . lock . resources . flatMap ( dependency => {
const selected = context . resources . find ( entry => entry . kind === dependency . kind && entry . source ? . repository === dependency . source . repository );
if ( selected ? . source && selected . source . commit !== dependency . source . commit ) add ( "propagation" , `Dependency ${ dependency . binding } has advanced; check will repin it automatically` );
return selected ? [ selected . directory ] : [];
}));
let record ;
try { record = JSON . parse ( await fs . readFile ( path . join ( context . workbench , ".quixos/checks" , checkRecordName ( resource . directory )), "utf8" )); }
catch ( error ) { if (( error as NodeJS . ErrnoException ). code !== "ENOENT" ) throw error ; }
if ( ! record ? . commit ) {
if ( record ? . blockers ? . length ) add ( record . phase , record . blockers . join ( "\n" ));
else add ( "unchecked" , "No immutable candidate check recorded yet" );
continue ;
}
if ( record . checker !== checkerIdentity ()) add ( "unchecked" , "The installed checker changed since the last check" );
const env = {... process . env , QUIXOS_JJ_NO_CHECKPOINT : "1" };
const commit = ( await execFile ( "jj" , [ "--ignore-working-copy" , "log" , "--no-graph" , "-r" , "@" , "-T" , "commit_id" ], { cwd : root , env })). stdout . trim ();
const dirty = await execFile ( "git" , [ "diff" , "--quiet" , "--no-ext-diff" , record . commit , "--" ], { cwd : root }). then (() => false , () => true );
const untracked = ( await execFile ( "git" , [ "ls-files" , "--others" , "--exclude-standard" ], { cwd : root })). stdout ;
if ( commit !== record . commit || dirty || untracked ) add ( "unchecked" , `Edits are newer than the last check ( ${ record . commit . slice ( 0 , 12 ) } )` );
else if ( record . blockers ? . length ) add ( record . phase , record . blockers . join ( "\n" ));
} catch ( error ) { add ( "inspection" , String ( error ). slice ( 0 , 3000 )); }
}
// Fixed-point propagation, independent of registration order.
const blocked = new Set ( entries . map ( entry => entry . directory ));
let changed = true ;
while ( changed ) {
changed = false ;
for ( const [ directory , required ] of dependencies ) if ( ! blocked . has ( directory )) {
const waiting = required . filter ( dependency => blocked . has ( dependency ));
if ( waiting . length ) { blocked . add ( directory ); changed = true ; entries . push ({ directory , phase : "dependency" , message : `Waiting for ${ waiting . join ( ", " ) } ` , next : "Resolve the named repositories, then rerun check" }); }
}
}
return { workbench : context.workbench , verificationEvidence : false , worklist : entries ,
note : "Derived authoring guidance, not activation approval. Independent repositories can be delegated separately; join writers before a root check." };
}