Profile source capture, Nix evaluation/realization, resource/query checking, and implementation builds without putting timings into immutable outputs. Keep modular repositories; bound independent capture work, snapshot jj once per observation, and retain final concurrent-edit and Git completeness checks. Compose exact resource/closure/frontend/toolchain checks as memoized Nix derivations. Preserve whole-workspace validation and the full compiler path when no checked dependency is supplied. Share canonical Nixpkgs by content, index workspace build contexts once, and narrow lock/driver/compiler inputs with conservative complete-source fallback. Batch React witness checks only when distinct source closures cannot share ambient declarations; otherwise run the original independent TypeScript checks. Seed local authoring toolchains with exact canonical fetch identities when they match the selected inputs; retain store-path inputs for overrides. Add generic correctness checks rather than permanent Reader timing/content fixtures. Verify actual Reader edits and rejection/recovery in a private workbench, preserving the original user's workbench and runtime. Query checking currently caches at resource/closure boundaries, not individual query semantic dependency boundaries. Template helper pins and reproducible fixture snapshots follow in a separate publication change.
197 lines
8.8 KiB
TypeScript
197 lines
8.8 KiB
TypeScript
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<string, number> = {};
|
|
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<ReturnType<typeof convergeAuthoring>>;
|
|
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;
|
|
}
|