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 12:25:47 -07:00
parent fae4e48f72
commit 01ca965c7f
29 changed files with 1103 additions and 75 deletions
@@ -0,0 +1,49 @@
import { readFile, realpath } from "node:fs/promises";
import path from "node:path";
import { loadQuixosLock, type GitSource } from "../resource-lock/index.js";
export type AuthoringResource = {
kind: "workspace" | "interface" | "package";
directory: string;
resourceId?: string;
source?: GitSource;
};
/** Registration, not directory co-location, defines the editable selection. */
export async function authoringContext(start: string) {
let workbench = await realpath(start);
for (;;) {
let text: string | undefined;
try { text = await readFile(path.join(workbench, ".quixos/resource-graph.json"), "utf8"); }
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
if (text !== undefined) {
const graph = JSON.parse(text) as { resources: AuthoringResource[] };
if (!Array.isArray(graph.resources)) throw new Error("Managed resource inventory is malformed");
const resources: AuthoringResource[] = [{ kind: "workspace", directory: "root" }];
const identities = new Set<string>(), directories = new Set<string>(["root"]);
for (const entry of graph.resources) {
// Compiler graphs carry resolved paths; the authoring API presents
// stable workbench-relative names and validates containment here.
if (typeof entry.directory === "string" && path.isAbsolute(entry.directory)) entry.directory = path.relative(workbench, entry.directory);
if (!["interface", "package"].includes(entry.kind) || !entry.source ||
!/^resources\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(entry.directory)) {
throw new Error("Invalid managed resource registration");
}
const identity = `${entry.kind}\0${entry.resourceId ?? entry.source.repository}`;
if (identities.has(identity) || directories.has(entry.directory)) {
throw new Error(`Multiple editable selections for ${entry.resourceId ?? entry.source.repository}`);
}
identities.add(identity); directories.add(entry.directory);
resources.push({kind: entry.kind, directory: entry.directory, resourceId: entry.resourceId, source: entry.source});
}
return { workbench, resources, async baseline() {
const result = await loadQuixosLock(path.join(workbench, "root/quixos.lock"));
if (!result.ok) throw new Error(`Workspace source baseline is invalid: ${result.diagnostics.map(d => d.message).join("; ")}`);
return result.lock.quixos;
} };
}
const parent = path.dirname(workbench);
if (parent === workbench) throw new Error("Not in a managed workbench; select one with --workbench DIRECTORY");
workbench = parent;
}
}