import fs from "node:fs/promises"; import { appendFileSync } from "node:fs"; import path from "node:path"; import { createHash, randomUUID } from "node:crypto"; import { authoringContext } from "./authoring-context.js"; import type { convergeAuthoring } from "./authoring-converge.js"; import { execFile as callback } from "node:child_process"; import { promisify } from "node:util"; import { buildImmutableCandidate, checkerIdentity } from "./checked-build.js"; import { planEvolution, reviewSemanticMajors, type WorkspaceRevision, type EvolutionReview, } from "../capability-model/index.js"; export const checkRecordName = (directory: string) => createHash("sha256").update(directory).digest("hex") + ".json"; export async function checkAuthoring( start: string, output: string, options: { baseline?: string; reviews?: string; contractOnly?: boolean } = {}, ) { const started = performance.now(); const timings: Record = {}; const context = await authoringContext(start); const location = await fs.realpath(start); const directory = location === context.workbench ? "root" : path.relative(context.workbench, location); const resource = context.resources.find((entry) => entry.directory === directory); if (!resource) throw new Error("Run check from a registered repository root or the workbench"); await fs.mkdir(output, { mode: 0o700 }); const report: { directory: string; checker: string; candidateOnly: true; activationEvidence: false; commit?: string; artifactPath?: string; blockers: string[]; phase: string; output: string; compilation?: "passed"; runtime?: "execution"; activationReadiness?: "preserve" | "migration-required" | "blocked"; migrationRequired?: string[]; } = { directory, checker: checkerIdentity(), candidateOnly: true, activationEvidence: false, blockers: [], phase: "convergence", output, }; const progress = async (running = true) => { const file = path.join(output, "report.json"), temp = `${file}.tmp`; await fs.writeFile(temp, JSON.stringify({ ...report, timings, running }, null, 2)); await fs.rename(temp, file); }; await progress(); try { console.error(`[${new Date().toISOString()}] Check: capture source and converge dependencies`); // Serialize only source capture, not the potentially slow Nix build. // Repository-scoped agents can check separate immutable candidates in parallel. const capture = promisify(callback)("quixos-qx", ["converge", context.workbench, directory], { maxBuffer: 4 * 1024 * 1024, env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_TRACE_CAPTURE: "1", QUIXOS_TRACE_PROFILE: "1" }, }); console.error(`Capture details: ${path.join(output, "capture.log")}`); capture.child.stderr?.on("data", (chunk) => appendFileSync(path.join(output, "capture.log"), chunk)); const captured = await capture.catch((error) => { if (typeof error.stdout === "string" && error.stdout.trim().startsWith("{")) return { stdout: error.stdout }; throw error; }); const converged = JSON.parse(captured.stdout) as Awaited>; timings.captureMs = Math.round(performance.now() - started); console.error(`[${new Date().toISOString()}] Check: source captured in ${(timings.captureMs / 1000).toFixed(1)}s`); if (!converged.candidate) { report.phase = converged.worklist.find((entry) => entry.phase !== "dependency")?.phase ?? "convergence"; throw new Error( converged.worklist.map((entry) => `${entry.directory} [${entry.phase}]: ${entry.message}`).join("\n"), ); } report.commit = converged.candidate.commit; report.phase = "verification"; await progress(); const buildStarted = performance.now(); console.error( `[${new Date().toISOString()}] Check: immutable Nix ${options.contractOnly ? "contract" : "verification"}; build output: ${path.join(output, "nix.log")}`, ); try { report.artifactPath = await buildImmutableCandidate( converged.candidate, resource.kind, path.join(output, "nix.log"), options.contractOnly, (phase, ms) => { timings[phase] = ms; }, ); } finally { timings.immutableCheckMs = Math.round(performance.now() - buildStarted); console.error( `[${new Date().toISOString()}] Check: immutable phase ended after ${(timings.immutableCheckMs / 1000).toFixed(1)}s`, ); } const candidateText = await fs.readFile(path.join(report.artifactPath, "candidate.json"), "utf8"); await fs.writeFile(path.join(output, "candidate.json"), candidateText); report.compilation = "passed"; try { await fs.access(path.join(report.artifactPath, "execution-plan.json")); report.runtime = "execution"; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } if (resource.kind === "workspace" && !options.contractOnly && report.runtime === "execution") { report.phase = "semantic-review"; if (options.reviews && !options.baseline) throw new Error("Execution semantic reviews require an explicit --baseline candidate"); const baseline = options.baseline ? (JSON.parse(await fs.readFile(options.baseline, "utf8")) as WorkspaceRevision) : null; const receipts = options.reviews ? (JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[]) : []; const review = reviewSemanticMajors(baseline, JSON.parse(candidateText), receipts); await fs.writeFile(path.join(output, "semantic-review.json"), JSON.stringify(review, null, 2)); report.blockers.push(...review.blockers); } else if (resource.kind === "workspace" && !options.contractOnly) { report.phase = "evolution"; await progress(); let baseline = options.baseline; if (!baseline) { try { const host = JSON.parse(await fs.readFile("/etc/quixos/workspace-source.json", "utf8")); if ((await fs.realpath(host.workbenchRoot)) === context.workbench) baseline = JSON.parse( await fs.readFile(path.join(host.runtimeClosureRoot, "manifest.json"), "utf8"), ).workspacePlanPath; } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } } const before = baseline ? (JSON.parse(await fs.readFile(baseline, "utf8")) as WorkspaceRevision) : null; const reviews = options.reviews ? (JSON.parse(await fs.readFile(options.reviews, "utf8")) as EvolutionReview[]) : []; const evolution = planEvolution(before, JSON.parse(candidateText), { reviews }); await fs.writeFile(path.join(output, "evolution.json"), JSON.stringify(evolution, null, 2)); report.blockers.push(...evolution.blockers); report.migrationRequired = evolution.migrationRequired; report.activationReadiness = evolution.blockers.length ? "blocked" : evolution.migrationRequired.length ? "migration-required" : "preserve"; if (evolution.migrationRequired.length) report.blockers.push( `Explicit migration required for: ${evolution.migrationRequired.join(", ")}. Compilation passed; supply a migration path before cutover.`, ); } if (!report.blockers.length) report.phase = options.contractOnly ? "contract-only" : "checked"; } catch (error) { report.blockers.push(String(error instanceof Error ? error.message : error)); } timings.totalMs = Math.round(performance.now() - started); Object.assign(report, { timings }); await progress(false); if (options.contractOnly) return report; const records = path.join(context.workbench, ".quixos/checks"); await fs.mkdir(records, { recursive: true }); const remember = async (value: typeof report) => { const filename = path.join(records, checkRecordName(value.directory)), temporary = `${filename}.${randomUUID()}.tmp`; await fs.writeFile(temporary, JSON.stringify(value, null, 2), { flag: "wx", mode: 0o600 }); await fs.rename(temporary, filename); }; if (report.artifactPath && report.compilation === "passed" && !report.blockers.length) { const graph = JSON.parse(await fs.readFile(path.join(report.artifactPath, "graph.json"), "utf8")); for (const checked of graph.resources) { const managed = context.resources.find( (entry) => entry.kind === checked.kind && entry.source?.repository === checked.source.repository, ); if (managed && managed.directory !== directory) await remember({ ...report, directory: managed.directory, commit: checked.source.commit, blockers: [], phase: "checked", }); } } await remember(report); return report; }