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:
@@ -1,29 +1,120 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFile, writeFile, mkdtemp, rm } from "node:fs/promises";
|
||||
import { readFile, writeFile, mkdtemp, rm, realpath } from "node:fs/promises";
|
||||
import { parseQx, formatQx, lintQx } from "./source.js";
|
||||
import { scaffoldAtom } from "./scaffold.js";
|
||||
import { createGitCapabilityResolver } from "./git-resolver.js";
|
||||
import { planEvolution } from "../capability-model/index.js";
|
||||
import { checkWorkspaceCandidate, checkResourceCandidate, snapshotRepository } from "./candidate-check.js";
|
||||
import { snapshotRepository } from "./candidate-check.js";
|
||||
import {planPinUpgrades, applyPinUpgrades, discoverUpgradeSpec, type UpgradeSpec} from "./pin-upgrades.js";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import {spawnSync} from "node:child_process";
|
||||
import { planStructure, applyStructure, resumeStructure, type StructuralRequest } from "./structural-plan.js";
|
||||
import {scaffoldRecipe, type ScaffoldRecipe} from "./scaffold-recipes.js";
|
||||
import {buildCheckedPackage, snapshotCommit} from "./checked-build.js";
|
||||
import {buildCheckedPackage, buildImmutableCandidate, snapshotCommit} from "./checked-build.js";
|
||||
import {formatQuixosLock, loadQuixosLock, parseQuixosLockDocument} from "../resource-lock/index.js";
|
||||
import { walkSyntax } from "./source.js";
|
||||
import { inspectWorkbench } from "./authoring-inspect.js";
|
||||
import { authoringContext } from "./authoring-context.js";
|
||||
import { convergeAuthoring } from "./authoring-converge.js";
|
||||
import { checkAuthoring } from "./authoring-check.js";
|
||||
import { authoringWorklist } from "./authoring-worklist.js";
|
||||
|
||||
const authorSource = async (root: string) => {
|
||||
const result = spawnSync("git", ["remote", "get-url", "origin"], {cwd: root, encoding: "utf8"});
|
||||
const result = spawnSync("git", ["config", "--get", "remote.origin.url"], {cwd: root, encoding: "utf8"});
|
||||
if (result.error || result.status !== 0) throw new Error("Managed resource has no origin");
|
||||
return {repository: result.stdout.trim(), commit: await snapshotCommit(root)};
|
||||
};
|
||||
|
||||
const readSpec = async (value: string) => {
|
||||
if (value === "-") { let input = ""; for await (const chunk of process.stdin) { input += chunk; if (input.length > 1024 * 1024) throw new Error("Scaffold specification exceeds 1 MiB"); } return JSON.parse(input); }
|
||||
return JSON.parse(value.trimStart().startsWith("{") ? value : await readFile(value, "utf8"));
|
||||
};
|
||||
const planSummary = (plan: Awaited<ReturnType<typeof planStructure>>) => ({
|
||||
root: plan.root, validation: plan.validation,
|
||||
changes: plan.changes.map(change => ({file: change.file, beforeBytes: change.before?.length ?? 0, afterBytes: change.after?.length ?? 0})),
|
||||
note: "Structural plan only, not implementation verification. Run qx-workspace check while iterating.",
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
const [command, ...args] = process.argv.slice(2);
|
||||
if (command === "worklist" && !args.includes("--help")) {
|
||||
if (args.length !== 1) throw new Error("usage: quixos-qx worklist WORKBENCH");
|
||||
process.stdout.write(`${JSON.stringify(await authoringWorklist(args[0]), null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (["author-check", "author-contract"].includes(command) && !args.includes("--help")) {
|
||||
const [root, output, ...flags] = args;
|
||||
if (!root || !output) throw new Error("usage: quixos-qx author-check ROOT OUTPUT [--baseline FILE] [--reviews FILE]");
|
||||
const options: {baseline?: string; reviews?: string} = {};
|
||||
for (let index = 0; index < flags.length; index += 2) {
|
||||
if (!flags[index + 1]) throw new Error("Missing check option value");
|
||||
if (flags[index] === "--baseline") options.baseline = flags[index + 1];
|
||||
else if (flags[index] === "--reviews") options.reviews = flags[index + 1];
|
||||
else throw new Error(`Unknown check option ${flags[index]}`);
|
||||
}
|
||||
const result = await checkAuthoring(root, output, {...options, contractOnly: command === "author-contract"});
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
if (result.blockers.length) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (["converge", "_converge"].includes(command) && !args.includes("--help")) {
|
||||
if (!args.length || args.length > 2) throw new Error("usage: quixos-qx converge WORKBENCH [REGISTERED_DIRECTORY] (join package writers first)");
|
||||
const context = await authoringContext(args[0]);
|
||||
if (command === "converge") {
|
||||
const result = spawnSync("flock", ["--exclusive", "--nonblock", "--conflict-exit-code", "75", path.join(context.workbench, ".quixos/converge.lock"),
|
||||
process.execPath, process.argv[1], "_converge", context.workbench, ...(args[1] ? [args[1]] : [])], {stdio: "inherit"});
|
||||
if (result.error) throw result.error;
|
||||
if (result.status === 75) process.stderr.write("Another source coordinator is running; retry when it finishes.\n");
|
||||
process.exitCode = result.status ?? 1; return;
|
||||
}
|
||||
const result = await convergeAuthoring(context.workbench, args[1]);
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
if (!result.converged) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (command === "check-committed" && !args.includes("--help")) {
|
||||
const [kind, repository, commit, log, ...extra] = args;
|
||||
if (!["workspace", "interface", "package"].includes(kind) || !log || extra.length) throw new Error("usage: quixos-qx check-committed workspace|interface|package REPOSITORY COMMIT LOG_FILE");
|
||||
process.stdout.write(`${await buildImmutableCandidate({repository, commit}, kind as "workspace" | "interface" | "package", log)}\n`);
|
||||
return;
|
||||
}
|
||||
if (!command || command === "--help" || args.includes("--help")) {
|
||||
process.stdout.write("quixos-qx: author-check, author-contract, converge, worklist, inspect, resources, check-committed, source-baseline, scaffold-package, scaffold-interface, scaffold-function, scaffold-dependency, scaffold-structure, scaffold-resume, pin-upgrade, parse, lint, format\n" +
|
||||
"inspect WORKBENCH [RESOURCE] shows provisional contracts, with explicit historical fallback; never verification evidence.\n" +
|
||||
"resources WORKBENCH lists registered editable repositories. Use qx-workspace for the workspace authoring workflow.\n");
|
||||
return;
|
||||
}
|
||||
if (command === "inspect" || command === "resources") {
|
||||
if (!args[0] || args.length > (command === "inspect" ? 2 : 1)) throw new Error(`usage: quixos-qx ${command} WORKBENCH${command === "inspect" ? " [RESOURCE]" : ""}`);
|
||||
const result = command === "inspect" ? await inspectWorkbench(args[0], args[1]) : await authoringContext(args[0]);
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "source-baseline") {
|
||||
if (args.length !== 1) throw new Error("usage: quixos-qx source-baseline WORKBENCH");
|
||||
process.stdout.write(`${JSON.stringify(await (await authoringContext(args[0])).baseline())}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-dependency") {
|
||||
const [root, kind, name, repository, commit, ...flags] = args;
|
||||
const [root, kind, name, ...remaining] = args;
|
||||
let repository: string | undefined, commit: string | undefined;
|
||||
const flags = [...remaining];
|
||||
if (flags.length && !flags[0].startsWith("--")) { repository = flags.shift(); commit = flags.shift(); }
|
||||
else if (root && ["interface", "package"].includes(kind) && name) {
|
||||
const context = await authoringContext(root);
|
||||
const alias = await realpath(path.join(context.workbench, `${kind}s`, name)).catch(() => null);
|
||||
const matches = context.resources.filter(entry => entry.kind === kind && (entry.resourceId === name || path.basename(entry.directory) === name || path.join(context.workbench, entry.directory) === alias));
|
||||
if (matches.length !== 1 || !matches[0].source) throw new Error(`Select exactly one registered ${kind} with qx-workspace resources; no match for ${name}`);
|
||||
const selected = matches[0];
|
||||
let source = selected.source!;
|
||||
if (flags.includes("--write")) {
|
||||
const retained = spawnSync("quixos-qx", ["converge", context.workbench, selected.directory], {encoding: "utf8", env: {...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1"}});
|
||||
if (retained.error || retained.status !== 0) throw new Error(`Dependency source needs attention: ${retained.error?.message ?? retained.stdout ?? retained.stderr}`);
|
||||
source = JSON.parse(retained.stdout).candidate;
|
||||
}
|
||||
repository = source.repository; commit = source.commit;
|
||||
}
|
||||
if (!root || !["interface", "package"].includes(kind) || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(name ?? "") || !repository || !commit || flags.some(flag => flag !== "--write")) throw new Error("usage: quixos-qx scaffold-dependency ROOT interface|package NAME REPOSITORY COMMIT [--write]");
|
||||
const resourceKind = kind as "package" | "interface";
|
||||
let entrypoint: "workspace" | "package" | "interface" | undefined;
|
||||
@@ -44,13 +135,13 @@ const main = async () => {
|
||||
{file: target, edits: [{operation: "dependency", kind: resourceKind, name, source: {repository, commit}}]},
|
||||
]};
|
||||
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
|
||||
process.stdout.write(`${JSON.stringify({...plan, applied: flags.includes("--write") ? await applyStructure(plan) : undefined}, null, 2)}\n`);
|
||||
process.stdout.write(`${JSON.stringify({...planSummary(plan), applied: flags.includes("--write") ? await applyStructure(plan) : undefined}, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-interface") {
|
||||
const [root, specFile, ...flags] = args;
|
||||
if (!root || !specFile || flags.some(flag => flag !== "--write")) throw new Error("usage: quixos-qx scaffold-interface ROOT SPEC_JSON [--write]");
|
||||
const spec = JSON.parse(await readFile(specFile, "utf8")) as ScaffoldRecipe;
|
||||
const spec = await readSpec(specFile) as ScaffoldRecipe;
|
||||
if (!spec.name || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.name) || !spec.id || !spec.revision || !spec.tools?.quixos) throw new Error("Interface scaffold requires name, id, revision and Quixos toolchain source");
|
||||
const request: StructuralRequest = {kind: "interface", source: spec.source, files: [
|
||||
{file: "interface.qx", create: `interface ${spec.name} id ${JSON.stringify(spec.id)} revision ${JSON.stringify(spec.revision)} {\n}\n`},
|
||||
@@ -58,7 +149,7 @@ const main = async () => {
|
||||
{file: ".gitignore", create: ".quixos/\n"},
|
||||
]};
|
||||
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
|
||||
process.stdout.write(`${JSON.stringify({...plan, applied: flags.includes("--write") ? await applyStructure(plan) : undefined}, null, 2)}\n`);
|
||||
process.stdout.write(`${JSON.stringify({...planSummary(plan), applied: flags.includes("--write") ? await applyStructure(plan) : undefined}, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "build-package") {
|
||||
@@ -89,14 +180,6 @@ const main = async () => {
|
||||
process.stdout.write(`${JSON.stringify(publish ? await applyPinUpgrades(plan, resume, undefined, {acceptEdits}) : plan, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "check-resource") {
|
||||
const [root, kind, repository, commit, output, ...extra] = args;
|
||||
if (!root || !output || !["package", "interface"].includes(kind) || extra.length) throw new Error("usage: quixos-qx check-resource ROOT package|interface REPOSITORY COMMIT OUTPUT");
|
||||
const result = await checkResourceCandidate({root, kind: kind as "package" | "interface", source: {repository, commit}, output, publishedOnly: true});
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
if (result.blockers.length) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (["scaffold-package", "scaffold-function", "scaffold-migration", "scaffold-refresh"].includes(command)) {
|
||||
const [root, specFile, ...flags] = args;
|
||||
let spec: ScaffoldRecipe;
|
||||
@@ -107,9 +190,16 @@ const main = async () => {
|
||||
if (declaration >= 0) {
|
||||
if (!flags[declaration + 1]) throw new Error("--declaration requires a QX declaration file");
|
||||
spec.declaration = await readFile(flags[declaration + 1], "utf8");
|
||||
const parsed = parseQx(`package Draft id "package:draft" revision "package:draft@1" { ${spec.declaration} }`);
|
||||
if (parsed.diagnostics.length) throw new Error(parsed.diagnostics.map(d => d.message).join("\n"));
|
||||
const exported = [...walkSyntax(parsed.root)].filter(node => ["packageOperationExport", "packageFunctionExport", "packageConstructorExport"].includes(node.kind));
|
||||
if (exported.length !== 1) throw new Error("--declaration must contain exactly one function, operation or constructor export");
|
||||
const literal = exported[0].children.find(node => node.kind === "stringLiteral");
|
||||
if (!literal) throw new Error("Declaration requires an authored export ID");
|
||||
spec.id = JSON.parse(parsed.source.slice(literal.start, literal.end));
|
||||
flags.splice(declaration, 2);
|
||||
}
|
||||
} else spec = JSON.parse(await readFile(specFile, "utf8")) as ScaffoldRecipe;
|
||||
} else spec = await readSpec(specFile) as ScaffoldRecipe;
|
||||
if (!root || !specFile || flags.some((flag) => !["--write", "--install"].includes(flag)) || (flags.includes("--install") && !flags.includes("--write"))) throw new Error("usage: quixos-qx scaffold-package|function|migration|refresh ROOT SPEC_JSON [--write [--install]]");
|
||||
const request = await scaffoldRecipe(root, command.slice(9) as "package" | "function" | "migration" | "refresh", spec);
|
||||
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
|
||||
@@ -128,16 +218,17 @@ const main = async () => {
|
||||
const locked = spawnSync("nix", ["flake", "lock"], {cwd, stdio: ["inherit", 2, 2]});
|
||||
if (locked.error || locked.status !== 0) throw new Error("Scaffold files retained; nix flake lock failed");
|
||||
}
|
||||
process.stdout.write(`${JSON.stringify({...plan, applied}, null, 2)}\n`);
|
||||
process.stdout.write(`${JSON.stringify({...planSummary(plan), applied}, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-structure") {
|
||||
const [root, spec, ...flags] = args;
|
||||
if (!root || !spec || flags.some((flag) => flag !== "--write")) throw new Error("usage: quixos-qx scaffold-structure ROOT SPEC_JSON [--write]");
|
||||
const request = JSON.parse(await readFile(spec, "utf8")) as StructuralRequest;
|
||||
const request = await readSpec(spec) as StructuralRequest;
|
||||
if (request.kind !== "workspace" && !request.source) request.source = await authorSource(root);
|
||||
const plan = await planStructure(root, request, process.env.QUIXOS_SNAPSHOT_MAP);
|
||||
const applied = flags.includes("--write") ? await applyStructure(plan) : undefined;
|
||||
process.stdout.write(`${JSON.stringify({...plan, applied}, null, 2)}\n`);
|
||||
process.stdout.write(`${JSON.stringify({...planSummary(plan), applied}, null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "scaffold-resume") {
|
||||
@@ -146,19 +237,7 @@ const main = async () => {
|
||||
process.stdout.write(`${JSON.stringify(await resumeStructure(root, id), null, 2)}\n`);
|
||||
return;
|
||||
}
|
||||
if (command === "check") {
|
||||
const [root, output, ...flags] = args;
|
||||
if (!root || !output || flags.length % 2) throw new Error("usage: quixos-qx check ROOT OUTPUT [--snapshot-map FILE] [--baseline FILE] [--reviews FILE]");
|
||||
const values = new Map<string, string>();
|
||||
for (let index = 0; index < flags.length; index += 2) {
|
||||
if (!["--snapshot-map", "--baseline", "--reviews"].includes(flags[index])) throw new Error(`Unknown check option ${flags[index]}`);
|
||||
values.set(flags[index], flags[index + 1]);
|
||||
}
|
||||
const result = await checkWorkspaceCandidate({ root, output, snapshotMap: values.get("--snapshot-map"), baseline: values.get("--baseline"), reviews: values.get("--reviews") });
|
||||
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
||||
if (result.blockers.length) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
if (["check", "check-resource"].includes(command)) throw new Error("Use qx-workspace check in the registered repository; handwritten source/snapshot-map candidates are no longer an authoring check path");
|
||||
if (command === "evolution") {
|
||||
const [baseline, candidate, reviews, ...extra] = args;
|
||||
if (!baseline || !candidate || extra.length) throw new Error("usage: quixos-qx evolution BASELINE_JSON CANDIDATE_JSON [REVIEWS_JSON]");
|
||||
|
||||
Reference in New Issue
Block a user