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(); 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, 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`, }); 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.", }; }