Instrument and improve incremental workspace checks
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.
This commit is contained in:
@@ -5,6 +5,7 @@ import { promisify } from "node:util";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { authoringContext } from "./authoring-context.js";
|
||||
import { snapshotCommit } from "./checked-build.js";
|
||||
import { profile } from "./profile.js";
|
||||
import {
|
||||
loadQuixosLock,
|
||||
parseQuixosLockDocument,
|
||||
@@ -14,6 +15,42 @@ import {
|
||||
} from "../resource-lock/index.js";
|
||||
|
||||
const execFile = promisify(callback);
|
||||
// Bound independent repository operations. One visit owns each repository;
|
||||
// all visits finish before the final concurrent-edit validation pass.
|
||||
const concurrency = 4;
|
||||
async function mapConcurrent<T, R>(values: readonly T[], action: (value: T) => Promise<R>): Promise<R[]> {
|
||||
const results = new Array<R>(values.length);
|
||||
let next = 0;
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
||||
while (next < values.length) {
|
||||
const index = next++;
|
||||
results[index] = await action(values[index]);
|
||||
}
|
||||
}),
|
||||
);
|
||||
return results;
|
||||
}
|
||||
const limiter = () => {
|
||||
let running = 0;
|
||||
const waiting: Array<() => void> = [];
|
||||
return async <T>(action: () => Promise<T>): Promise<T> => {
|
||||
await new Promise<void>((resolve) => {
|
||||
if (running < concurrency) {
|
||||
running++;
|
||||
resolve();
|
||||
} else waiting.push(resolve);
|
||||
});
|
||||
try {
|
||||
return await action();
|
||||
} finally {
|
||||
const next = waiting.shift();
|
||||
if (next) next();
|
||||
else running--;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const command = async (cwd: string, executable: string, args: string[]) => {
|
||||
const start = performance.now();
|
||||
// Do not log arguments: transports may contain credentials. Source identities
|
||||
@@ -44,13 +81,14 @@ export type AuthoringBlocker = {
|
||||
* worklist grants typechecking, semantic review or activation approval.
|
||||
* Caller serializes coordinators; package authors may still be editing. */
|
||||
export async function convergeAuthoring(start: string, target = "root") {
|
||||
const started = performance.now();
|
||||
const context = await authoringContext(start);
|
||||
const graphFile = path.join(context.workbench, ".quixos/resource-graph.json");
|
||||
const graphBefore = await readFile(graphFile, "utf8");
|
||||
const blockers: AuthoringBlocker[] = [];
|
||||
const nodes = new Map<string, { directory: string; kind: string; source: GitSource; dependencies: string[] }>();
|
||||
const selected = new Map<string, string>();
|
||||
for (const entry of context.resources) {
|
||||
const inventory = await mapConcurrent(context.resources, async (entry) => {
|
||||
const root = path.join(context.workbench, entry.directory);
|
||||
let repository = entry.source?.repository;
|
||||
try {
|
||||
@@ -64,6 +102,9 @@ export async function convergeAuthoring(start: string, target = "root") {
|
||||
blockers.push({ directory: entry.directory, phase: "source", message: String(error).slice(0, 2000) });
|
||||
if (!repository) throw error; // The root has no separate registered source.
|
||||
}
|
||||
return { ...entry, repository };
|
||||
});
|
||||
for (const { repository, ...entry } of inventory) {
|
||||
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);
|
||||
@@ -73,7 +114,7 @@ export async function convergeAuthoring(start: string, target = "root") {
|
||||
dependencies: [],
|
||||
});
|
||||
}
|
||||
for (const node of nodes.values()) {
|
||||
await mapConcurrent([...nodes.values()], async (node) => {
|
||||
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"));
|
||||
@@ -88,97 +129,122 @@ export async function convergeAuthoring(start: string, target = "root") {
|
||||
} catch (error) {
|
||||
blockers.push({ directory: node.directory, phase: "resolution", message: String(error) });
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
});
|
||||
// Detect cycles before starting concurrent futures: two roots awaiting each
|
||||
// other must fail clearly rather than deadlocking on their cached promises.
|
||||
const scanned = new Set<string>();
|
||||
const detectCycles = (directory: string, parents: string[] = []) => {
|
||||
if (parents.includes(directory)) {
|
||||
blockers.push({
|
||||
directory,
|
||||
phase: "dependency",
|
||||
message: `Source dependency cycle: ${[...active, directory].join(" -> ")}`,
|
||||
message: `Source dependency cycle: ${[...parents, directory].join(" -> ")}`,
|
||||
});
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
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);
|
||||
if (scanned.has(directory)) return;
|
||||
for (const dependency of nodes.get(directory)!.dependencies) detectCycles(dependency, [...parents, directory]);
|
||||
scanned.add(directory);
|
||||
};
|
||||
for (const directory of nodes.keys()) detectCycles(directory);
|
||||
const complete = new Map<string, GitSource>();
|
||||
const pending = new Map<string, Promise<boolean>>();
|
||||
const capture = limiter();
|
||||
const visited = new Set<string>();
|
||||
if (!nodes.has(target)) throw new Error(`Not a registered repository: ${target}`);
|
||||
const visit = (directory: string): Promise<boolean> => {
|
||||
visited.add(directory);
|
||||
const existing = pending.get(directory);
|
||||
if (existing) return existing;
|
||||
const result = (async () => {
|
||||
if (blockers.some((entry) => entry.directory === directory)) return false;
|
||||
const node = nodes.get(directory)!;
|
||||
const ready = await Promise.all(node.dependencies.map(visit));
|
||||
const unavailable = node.dependencies.find((_, index) => !ready[index]);
|
||||
if (unavailable) {
|
||||
blockers.push({ directory, phase: "dependency", message: `Waiting for ${unavailable}` });
|
||||
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 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;
|
||||
return await capture(async () => {
|
||||
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 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 (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`);
|
||||
await rename(temporary, filename);
|
||||
} finally {
|
||||
await rm(temporary, { force: 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`);
|
||||
await rename(temporary, filename);
|
||||
} 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`,
|
||||
);
|
||||
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");
|
||||
}
|
||||
complete.set(directory, { ...node.source, commit });
|
||||
} catch (error) {
|
||||
blockers.push({ directory, phase, message: String(error).slice(0, 4000) });
|
||||
}
|
||||
}
|
||||
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`,
|
||||
);
|
||||
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");
|
||||
complete.set(directory, { ...node.source, commit });
|
||||
} catch (error) {
|
||||
blockers.push({ directory, phase, message: String(error).slice(0, 4000) });
|
||||
}
|
||||
active.delete(directory);
|
||||
return complete.has(directory);
|
||||
return complete.has(directory);
|
||||
});
|
||||
})();
|
||||
pending.set(directory, result);
|
||||
return result;
|
||||
};
|
||||
// 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") await Promise.all([...nodes.keys()].filter((d) => d !== "root").map(visit));
|
||||
await visit(target);
|
||||
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) {
|
||||
const recheckStarted = performance.now();
|
||||
await mapConcurrent([...complete], async ([directory, source]) => {
|
||||
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) });
|
||||
}
|
||||
}
|
||||
});
|
||||
profile("capture-recheck", recheckStarted, { repositories: complete.size });
|
||||
// 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);
|
||||
@@ -238,11 +304,14 @@ export async function convergeAuthoring(start: string, target = "root") {
|
||||
await rm(temporary, { force: true });
|
||||
}
|
||||
}
|
||||
profile("capture", started, { repositories: nodes.size, snapshots: complete.size * 2, target });
|
||||
return {
|
||||
workbench: context.workbench,
|
||||
converged: blockers.length === 0,
|
||||
candidate: blockers.length ? null : (complete.get(target) ?? null),
|
||||
retained: [...complete].map(([directory, source]) => ({ directory, source })),
|
||||
retained: [...complete]
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([directory, source]) => ({ directory, source })),
|
||||
worklist: blockers,
|
||||
verificationEvidence: false,
|
||||
activated: false,
|
||||
|
||||
Reference in New Issue
Block a user