Format authored monorepo code with pinned language formatters
This commit is contained in:
@@ -5,7 +5,13 @@ import { promisify } from "node:util";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { authoringContext } from "./authoring-context.js";
|
||||
import { snapshotCommit } from "./checked-build.js";
|
||||
import { loadQuixosLock, parseQuixosLockDocument, formatQuixosLockDocument, retentionTagForCommit, type GitSource } from "../resource-lock/index.js";
|
||||
import {
|
||||
loadQuixosLock,
|
||||
parseQuixosLockDocument,
|
||||
formatQuixosLockDocument,
|
||||
retentionTagForCommit,
|
||||
type GitSource,
|
||||
} from "../resource-lock/index.js";
|
||||
|
||||
const execFile = promisify(callback);
|
||||
const command = async (cwd: string, executable: string, args: string[]) => {
|
||||
@@ -13,15 +19,26 @@ const command = async (cwd: string, executable: string, args: string[]) => {
|
||||
// Do not log arguments: transports may contain credentials. Source identities
|
||||
// remain in the normal checked result, not in this timing channel.
|
||||
const label = `${path.basename(cwd)} ${executable} ${args[0]}`;
|
||||
try { return (await execFile(executable, args, {
|
||||
cwd, maxBuffer: 4 * 1024 * 1024,
|
||||
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_SUBTREE_PUBLISH: "0", GIT_TERMINAL_PROMPT: "0" },
|
||||
})).stdout.trim(); }
|
||||
finally { if (process.env.QUIXOS_TRACE_CAPTURE === "1") console.error(`[${new Date().toISOString()}] Capture: ${label}: ${Math.round(performance.now() - start)}ms`); }
|
||||
try {
|
||||
return (
|
||||
await execFile(executable, args, {
|
||||
cwd,
|
||||
maxBuffer: 4 * 1024 * 1024,
|
||||
env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1", QUIXOS_SUBTREE_PUBLISH: "0", GIT_TERMINAL_PROMPT: "0" },
|
||||
})
|
||||
).stdout.trim();
|
||||
} finally {
|
||||
if (process.env.QUIXOS_TRACE_CAPTURE === "1")
|
||||
console.error(`[${new Date().toISOString()}] Capture: ${label}: ${Math.round(performance.now() - start)}ms`);
|
||||
}
|
||||
};
|
||||
const identity = (kind: string, repository: string) => `${kind}\0${repository}`;
|
||||
|
||||
export type AuthoringBlocker = { directory: string; phase: "resolution" | "dependency" | "source" | "publication" | "concurrent-edit"; message: string };
|
||||
export type AuthoringBlocker = {
|
||||
directory: string;
|
||||
phase: "resolution" | "dependency" | "source" | "publication" | "concurrent-edit";
|
||||
message: string;
|
||||
};
|
||||
|
||||
/** Source retention only. Neither successful convergence nor an empty source
|
||||
* worklist grants typechecking, semantic review or activation approval.
|
||||
@@ -37,94 +54,136 @@ export async function convergeAuthoring(start: string, target = "root") {
|
||||
const root = path.join(context.workbench, entry.directory);
|
||||
let repository = entry.source?.repository;
|
||||
try {
|
||||
if (await realpath(root) !== root) throw new Error(`Managed checkout crosses a symlink: ${entry.directory}`);
|
||||
if ((await realpath(root)) !== root) throw new Error(`Managed checkout crosses a symlink: ${entry.directory}`);
|
||||
// Transport rewrites must not become committed source identities.
|
||||
const origin = await command(root, "git", ["config", "--get", "remote.origin.url"]);
|
||||
if (repository && origin !== repository) throw new Error(`Origin differs from registered source for ${entry.directory}`);
|
||||
if (repository && origin !== repository)
|
||||
throw new Error(`Origin differs from registered source for ${entry.directory}`);
|
||||
repository ??= origin;
|
||||
} catch (error) {
|
||||
blockers.push({directory: entry.directory, phase: "source", message: String(error).slice(0, 2000)});
|
||||
blockers.push({ directory: entry.directory, phase: "source", message: String(error).slice(0, 2000) });
|
||||
if (!repository) throw error; // The root has no separate registered source.
|
||||
}
|
||||
const key = identity(entry.kind, repository);
|
||||
if (selected.has(key)) throw new Error(`More than one editable checkout for ${repository}`);
|
||||
selected.set(key, entry.directory);
|
||||
nodes.set(entry.directory, { ...entry, source: { resolver: "git", repository, commit: entry.source?.commit ?? "" }, dependencies: [] });
|
||||
nodes.set(entry.directory, {
|
||||
...entry,
|
||||
source: { resolver: "git", repository, commit: entry.source?.commit ?? "" },
|
||||
dependencies: [],
|
||||
});
|
||||
}
|
||||
for (const node of nodes.values()) {
|
||||
try {
|
||||
const lock = await loadQuixosLock(path.join(context.workbench, node.directory, "quixos.lock"));
|
||||
if (!lock.ok) throw new Error(lock.diagnostics.map(d => `${d.fileName}: ${d.message}`).join("\n"));
|
||||
node.dependencies = [...new Set(lock.lock.resources.flatMap(entry => {
|
||||
const directory = selected.get(identity(entry.kind, entry.source.repository));
|
||||
return directory ? [directory] : [];
|
||||
}))];
|
||||
} catch (error) { blockers.push({ directory: node.directory, phase: "resolution", message: String(error) }); }
|
||||
if (!lock.ok) throw new Error(lock.diagnostics.map((d) => `${d.fileName}: ${d.message}`).join("\n"));
|
||||
node.dependencies = [
|
||||
...new Set(
|
||||
lock.lock.resources.flatMap((entry) => {
|
||||
const directory = selected.get(identity(entry.kind, entry.source.repository));
|
||||
return directory ? [directory] : [];
|
||||
}),
|
||||
),
|
||||
];
|
||||
} catch (error) {
|
||||
blockers.push({ directory: node.directory, phase: "resolution", message: String(error) });
|
||||
}
|
||||
}
|
||||
const complete = new Map<string, GitSource>(), active = new Set<string>();
|
||||
const complete = new Map<string, GitSource>(),
|
||||
active = new Set<string>();
|
||||
const visited = new Set<string>();
|
||||
if (!nodes.has(target)) throw new Error(`Not a registered repository: ${target}`);
|
||||
const visit = async (directory: string): Promise<boolean> => {
|
||||
visited.add(directory);
|
||||
if (complete.has(directory)) return true;
|
||||
if (blockers.some(entry => entry.directory === directory)) return false;
|
||||
if (active.has(directory)) { blockers.push({ directory, phase: "dependency", message: `Source dependency cycle: ${[...active, directory].join(" -> ")}` }); return false; }
|
||||
if (blockers.some((entry) => entry.directory === directory)) return false;
|
||||
if (active.has(directory)) {
|
||||
blockers.push({
|
||||
directory,
|
||||
phase: "dependency",
|
||||
message: `Source dependency cycle: ${[...active, directory].join(" -> ")}`,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
active.add(directory);
|
||||
const node = nodes.get(directory)!;
|
||||
for (const dependency of node.dependencies) if (!await visit(dependency)) {
|
||||
blockers.push({ directory, phase: "dependency", message: `Waiting for ${dependency}` }); active.delete(directory); return false;
|
||||
}
|
||||
for (const dependency of node.dependencies)
|
||||
if (!(await visit(dependency))) {
|
||||
blockers.push({ directory, phase: "dependency", message: `Waiting for ${dependency}` });
|
||||
active.delete(directory);
|
||||
return false;
|
||||
}
|
||||
const root = path.join(context.workbench, directory);
|
||||
let phase: AuthoringBlocker["phase"] = "source";
|
||||
try {
|
||||
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
|
||||
if (!lock.ok) throw new Error("Lock changed during convergence; retry after joining writers");
|
||||
for (const file of lock.lock.sourceFiles ?? ["quixos.lock"]) {
|
||||
const filename = path.join(root, file), before = await readFile(filename, "utf8");
|
||||
const filename = path.join(root, file),
|
||||
before = await readFile(filename, "utf8");
|
||||
const parsed = parseQuixosLockDocument(before, file);
|
||||
if (!parsed.ok) throw new Error(`Invalid lock ${file}`);
|
||||
let changed = false;
|
||||
for (const dependency of parsed.document.resources) {
|
||||
const target = selected.get(identity(dependency.kind, dependency.source.repository));
|
||||
const source = target ? complete.get(target) : undefined;
|
||||
if (target && !source) throw new Error(`Dependencies changed during convergence (${dependency.binding}); join writers and retry`);
|
||||
if (source && source.commit !== dependency.source.commit) { dependency.source = source; changed = true; }
|
||||
if (target && !source)
|
||||
throw new Error(`Dependencies changed during convergence (${dependency.binding}); join writers and retry`);
|
||||
if (source && source.commit !== dependency.source.commit) {
|
||||
dependency.source = source;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
const temporary = `${filename}.${randomUUID()}.tmp`;
|
||||
try {
|
||||
await writeFile(temporary, formatQuixosLockDocument(parsed.document), { flag: "wx" });
|
||||
if (await readFile(filename, "utf8") !== before) throw new Error(`Concurrent edit to ${file}; retry after joining writers`);
|
||||
if ((await readFile(filename, "utf8")) !== before)
|
||||
throw new Error(`Concurrent edit to ${file}; retry after joining writers`);
|
||||
await rename(temporary, filename);
|
||||
} finally { await rm(temporary, { force: true }); }
|
||||
} finally {
|
||||
await rm(temporary, { force: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
const snapshotStarted = performance.now();
|
||||
const commit = await snapshotCommit(root);
|
||||
if (process.env.QUIXOS_TRACE_CAPTURE === "1") console.error(`[${new Date().toISOString()}] Capture: ${directory} snapshot: ${Math.round(performance.now() - snapshotStarted)}ms`);
|
||||
if (process.env.QUIXOS_TRACE_CAPTURE === "1")
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] Capture: ${directory} snapshot: ${Math.round(performance.now() - snapshotStarted)}ms`,
|
||||
);
|
||||
phase = "publication";
|
||||
const ref = retentionTagForCommit(commit);
|
||||
const remote = await command(root, "git", ["ls-remote", "--refs", node.source.repository, ref]);
|
||||
if (remote && remote.split(/\s+/)[0] !== commit) throw new Error(`Conflicting immutable retention ref ${ref}`);
|
||||
if (!remote) await command(root, "git", ["push", node.source.repository, `${commit}:${ref}`]);
|
||||
if ((await command(root, "git", ["ls-remote", "--refs", node.source.repository, ref])).split(/\s+/)[0] !== commit) throw new Error("Published source retention was not observed");
|
||||
if ((await command(root, "git", ["ls-remote", "--refs", node.source.repository, ref])).split(/\s+/)[0] !== commit)
|
||||
throw new Error("Published source retention was not observed");
|
||||
complete.set(directory, { ...node.source, commit });
|
||||
} catch (error) { blockers.push({ directory, phase, message: String(error).slice(0, 4000) }); }
|
||||
} catch (error) {
|
||||
blockers.push({ directory, phase, message: String(error).slice(0, 4000) });
|
||||
}
|
||||
active.delete(directory);
|
||||
return complete.has(directory);
|
||||
};
|
||||
// Include newly created, not-yet-imported resources, then the root.
|
||||
if (target === "root") for (const directory of [...nodes.keys()].filter(d => d !== "root")) await visit(directory);
|
||||
if (target === "root") for (const directory of [...nodes.keys()].filter((d) => d !== "root")) await visit(directory);
|
||||
await visit(target);
|
||||
for (let index = blockers.length - 1; index >= 0; index--) if (!visited.has(blockers[index].directory)) blockers.splice(index, 1);
|
||||
for (let index = blockers.length - 1; index >= 0; index--)
|
||||
if (!visited.has(blockers[index].directory)) blockers.splice(index, 1);
|
||||
for (const [directory, source] of complete) {
|
||||
try { if (await snapshotCommit(path.join(context.workbench, directory)) !== source.commit) throw new Error("Source advanced while converging; join writers and retry"); }
|
||||
catch (error) { blockers.push({ directory, phase: "concurrent-edit", message: String(error) }); }
|
||||
try {
|
||||
if ((await snapshotCommit(path.join(context.workbench, directory))) !== source.commit)
|
||||
throw new Error("Source advanced while converging; join writers and retry");
|
||||
} catch (error) {
|
||||
blockers.push({ directory, phase: "concurrent-edit", message: String(error) });
|
||||
}
|
||||
}
|
||||
// Persist successful selections even if another repository is still broken.
|
||||
// Recovery must not depend on all parents succeeding in the same invocation.
|
||||
const graph = JSON.parse(graphBefore);
|
||||
for (const resource of graph.resources) resource.directory = path.relative(context.workbench, path.resolve(context.workbench, resource.directory));
|
||||
for (const resource of graph.resources)
|
||||
resource.directory = path.relative(context.workbench, path.resolve(context.workbench, resource.directory));
|
||||
const replacements = new Map<string, string>();
|
||||
for (const resource of graph.resources) {
|
||||
const source = complete.get(resource.directory);
|
||||
@@ -132,30 +191,36 @@ export async function convergeAuthoring(start: string, target = "root") {
|
||||
const key = `${resource.kind}\0${source.repository}\0${source.commit}`;
|
||||
replacements.set(resource.key, key);
|
||||
if (resource.source.commit !== source.commit) delete resource.revisionId;
|
||||
resource.source = source; resource.key = key;
|
||||
resource.source = source;
|
||||
resource.key = key;
|
||||
}
|
||||
for (const resource of graph.resources) for (const dependency of resource.dependencies ?? []) {
|
||||
dependency.resourceKey = replacements.get(dependency.resourceKey) ?? dependency.resourceKey;
|
||||
}
|
||||
for (const direct of graph.directResources ?? []) direct.resourceKey = replacements.get(direct.resourceKey) ?? direct.resourceKey;
|
||||
for (const resource of graph.resources)
|
||||
for (const dependency of resource.dependencies ?? []) {
|
||||
dependency.resourceKey = replacements.get(dependency.resourceKey) ?? dependency.resourceKey;
|
||||
}
|
||||
for (const direct of graph.directResources ?? [])
|
||||
direct.resourceKey = replacements.get(direct.resourceKey) ?? direct.resourceKey;
|
||||
// Inventory is a projection of actual locks, including newly added/removed
|
||||
// imports. Never require a successful parent compilation to repair it.
|
||||
for (const [directory] of complete) {
|
||||
const lock = await loadQuixosLock(path.join(context.workbench, directory, "quixos.lock"));
|
||||
if (!lock.ok) continue;
|
||||
const dependencies = lock.lock.resources.map(dependency => ({
|
||||
const dependencies = lock.lock.resources.map((dependency) => ({
|
||||
binding: `${dependency.kind}\0${dependency.binding}`,
|
||||
resourceKey: `${dependency.kind}\0${dependency.source.repository}\0${dependency.source.commit}`,
|
||||
}));
|
||||
if (directory === "root") {
|
||||
graph.quixos = lock.lock.quixos;
|
||||
graph.directResources = lock.lock.resources.map((dependency, index) => ({
|
||||
kind: dependency.kind, binding: dependency.binding, resourceKey: dependencies[index].resourceKey,
|
||||
kind: dependency.kind,
|
||||
binding: dependency.binding,
|
||||
resourceKey: dependencies[index].resourceKey,
|
||||
...(selected.has(identity(dependency.kind, dependency.source.repository))
|
||||
? {directory: selected.get(identity(dependency.kind, dependency.source.repository))} : {}),
|
||||
? { directory: selected.get(identity(dependency.kind, dependency.source.repository)) }
|
||||
: {}),
|
||||
}));
|
||||
} else {
|
||||
const resource = graph.resources.find((entry: {directory: string}) => entry.directory === directory);
|
||||
const resource = graph.resources.find((entry: { directory: string }) => entry.directory === directory);
|
||||
if (resource) resource.dependencies = dependencies;
|
||||
}
|
||||
}
|
||||
@@ -164,12 +229,22 @@ export async function convergeAuthoring(start: string, target = "root") {
|
||||
const temporary = `${graphFile}.${randomUUID()}.tmp`;
|
||||
try {
|
||||
await writeFile(temporary, graphAfter, { flag: "wx", mode: 0o600 });
|
||||
if (await readFile(graphFile, "utf8") !== graphBefore) throw new Error("Managed inventory changed during convergence; source is retained, retry after joining writers");
|
||||
if ((await readFile(graphFile, "utf8")) !== graphBefore)
|
||||
throw new Error(
|
||||
"Managed inventory changed during convergence; source is retained, retry after joining writers",
|
||||
);
|
||||
await rename(temporary, graphFile);
|
||||
} finally { await rm(temporary, { force: true }); }
|
||||
} finally {
|
||||
await rm(temporary, { force: true });
|
||||
}
|
||||
}
|
||||
return { workbench: context.workbench, converged: blockers.length === 0,
|
||||
candidate: blockers.length ? null : complete.get(target) ?? null,
|
||||
return {
|
||||
workbench: context.workbench,
|
||||
converged: blockers.length === 0,
|
||||
candidate: blockers.length ? null : (complete.get(target) ?? null),
|
||||
retained: [...complete].map(([directory, source]) => ({ directory, source })),
|
||||
worklist: blockers, verificationEvidence: false, activated: false };
|
||||
worklist: blockers,
|
||||
verificationEvidence: false,
|
||||
activated: false,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user