Make workspace authoring converge through immutable Nix candidates
Coordinate registered resource edits bottom-up into retained exact remote sources. Use one Nix-owned source graph for provisional checking, template publication, explicit baseline upgrades and host activation; retain independent runtime pins. Add scoped contract inspection, historical recovery, derived worklists, crash-safe locks, named dependency adoption and plain-QX structural editing. Repair TODO ownership and template instantiation, and document the supported agent workflow. Validated with protocol and command suites, real jj/Nix convergence and cache checks, TS/React installed-command acceptance, and fresh TODO first-edit acceptance. No live deployment or public publication performed. Props projection generation and a one-command rich feature generator remain explicitly outside this delivery.
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import fs from "node:fs/promises";
|
||||
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, 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 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 } = {
|
||||
directory, checker: checkerIdentity(), candidateOnly: true, activationEvidence: false, blockers: [], phase: "convergence", output,
|
||||
};
|
||||
try {
|
||||
// Serialize only source capture, not the potentially slow Nix build.
|
||||
// Repository-scoped agents can check separate immutable candidates in parallel.
|
||||
const captured = await promisify(callback)("quixos-qx", ["converge", context.workbench, directory], {
|
||||
maxBuffer: 4 * 1024 * 1024, env: {...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1"},
|
||||
}).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<ReturnType<typeof convergeAuthoring>>;
|
||||
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";
|
||||
report.artifactPath = await buildImmutableCandidate(converged.candidate, resource.kind, path.join(output, "nix.log"), options.contractOnly);
|
||||
const candidateText = await fs.readFile(path.join(report.artifactPath, "candidate.json"), "utf8");
|
||||
await fs.writeFile(path.join(output, "candidate.json"), candidateText);
|
||||
if (resource.kind === "workspace" && !options.contractOnly) {
|
||||
report.phase = "evolution";
|
||||
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);
|
||||
}
|
||||
if (!report.blockers.length) report.phase = options.contractOnly ? "contract-only" : "checked";
|
||||
} catch (error) { report.blockers.push(String(error instanceof Error ? error.message : error)); }
|
||||
await fs.writeFile(path.join(output, "report.json"), JSON.stringify(report, null, 2));
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user