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:
Timothy J. Aveni
2026-09-14 10:26:11 -07:00
parent fae4e48f72
commit 01ca965c7f
29 changed files with 1103 additions and 75 deletions
+22 -14
View File
@@ -7,10 +7,12 @@ import {promisify} from "node:util";
import {loadQuixosLock, parseQuixosLockDocument} from "../resource-lock/index.js";
import {contentDigest} from "../capability-model/evolution.js";
import {planStructure, applyStructure, type StructuralRequest} from "./structural-plan.js";
import {snapshotRepository, checkResourceCandidate, checkWorkspaceCandidate} from "./candidate-check.js";
import {snapshotRepository} from "./candidate-check.js";
import {compileWorkspaceRepository, compileCapabilityResourceRepository, type ResolvedCapabilityResource} from "./assembly.js";
import {createGitCapabilityResolver} from "./git-resolver.js";
import {snapshotCommit} from "./checked-build.js";
import {snapshotCommit, buildImmutableCandidate} from "./checked-build.js";
import {planEvolution, type WorkspaceRevision, type EvolutionReview} from "../capability-model/index.js";
import {withFileLock} from "./file-lock.js";
const execFile = promisify(callback);
type Source = {repository: string; commit: string};
export type UpgradeNode = {kind: "workspace" | "package" | "interface"; directory: string; source: Source};
@@ -103,15 +105,21 @@ export type UpgradeEffects = {
const effects: UpgradeEffects = {
async check(node, root, output, spec) {
if (node.kind === "workspace" && !spec.baseline && !spec.bootstrap) throw new Error("Upgrading a workspace requires its checked active baseline for major-review checks (or explicit bootstrap:true for a new workspace)");
// Publication checks consume already-published dependency revisions, never
// workbench dirty overlays masquerading as those immutable identities.
const snapshotMap = `${output}-published-dependencies.json`;
await fs.writeFile(snapshotMap, JSON.stringify({resources: []}), {flag: "wx"});
const result = node.kind === "workspace" ? await checkWorkspaceCandidate({root, output, snapshotMap, baseline: spec.baseline, reviews: spec.reviews})
: await checkResourceCandidate({root, output, kind: node.kind, source: node.source, snapshotMap});
if (result.blockers.length) throw new Error(`Refactor required in ${node.directory}: ${result.blockers.join("; ")}`);
const evolution = (result as {evolution?: {reviews: {accepted: boolean}[]}}).evolution;
if (evolution?.reviews.some((review) => !review.accepted)) throw new Error("Explicit semantic-major review required before publishing the workspace");
// Explicit baseline upgrades use the same immutable Nix checker. Retaining
// an unverified source is safe and must precede a remote flake fetch.
await fs.mkdir(output);
const commit = await snapshotCommit(root);
await effects.publish(root, commit);
const artifact = await buildImmutableCandidate({...node.source, commit}, node.kind, path.join(output, "nix.log"));
const candidate = await fs.readFile(path.join(artifact, "candidate.json"), "utf8");
await fs.writeFile(path.join(output, "candidate.json"), candidate);
if (node.kind === "workspace") {
const baseline = spec.baseline ? JSON.parse(await fs.readFile(spec.baseline, "utf8")) as WorkspaceRevision : null;
const reviews = spec.reviews ? JSON.parse(await fs.readFile(spec.reviews, "utf8")) as EvolutionReview[] : [];
const evolution = planEvolution(baseline, JSON.parse(candidate), {reviews});
await fs.writeFile(path.join(output, "evolution.json"), JSON.stringify(evolution, null, 2));
if (evolution.blockers.length) throw new Error(`Refactor required in ${node.directory}: ${evolution.blockers.join("; ")}`);
}
},
async snapshot(root) {
const commit = await snapshotCommit(root);
@@ -139,10 +147,10 @@ export const applyPinUpgrades = async (plan: UpgradePlan, journalId?: string, im
const directory = path.join(plan.workbench, ".quixos", "upgrades");
await fs.mkdir(directory, {recursive: true, mode: 0o700});
if (await fs.realpath(directory) !== directory) throw new Error("Upgrade journals must not cross symlinks");
const lock = await fs.open(path.join(directory, "writer.lock"), "wx", 0o600);
const id = journalId ?? randomUUID();
if (!/^[a-f0-9-]{36}$/.test(id)) {await lock.close(); await fs.unlink(path.join(directory, "writer.lock")); throw new Error("Invalid upgrade journal ID");}
if (!/^[a-f0-9-]{36}$/.test(id)) throw new Error("Invalid upgrade journal ID");
const filename = path.join(directory, `${id}.json`);
return withFileLock(path.join(directory, "writer.lock"), async () => {
try {
const journal: Journal = journalId ? JSON.parse(await fs.readFile(filename, "utf8")) : {schemaVersion: 1, plan, steps: []};
if (journal.plan.digest !== plan.digest) throw new Error("Upgrade journal belongs to another plan");
@@ -242,5 +250,5 @@ export const applyPinUpgrades = async (plan: UpgradePlan, journalId?: string, im
}
return {id, journal: filename, revisions: journal.steps.map((step) => ({directory: step.directory, commit: step.commit})), activated: false};
} catch (error) {throw new Error(`${error instanceof Error ? error.message : String(error)}; upgrade journal ${filename}`, {cause: error});}
finally {await lock.close(); await fs.unlink(path.join(directory, "writer.lock"));}
});
};