Files
quixos-protocol/src/capability-language/authoring-worklist.ts
T

120 lines
5.1 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { execFile as callback } from "node:child_process";
import { promisify } from "node:util";
import { authoringContext } from "./authoring-context.js";
import { inspectAuthoringRepository } from "./authoring-inspect.js";
import { checkRecordName } from "./authoring-check.js";
import { loadQuixosLock } from "../resource-lock/index.js";
import { checkerIdentity } from "./checked-build.js";
const execFile = promisify(callback);
export async function authoringWorklist(start: string) {
const context = await authoringContext(start);
const entries: { directory: string; resourceId?: string; phase: string; message: string; next: string }[] = [];
const dependencies = new Map<string, string[]>();
for (const resource of context.resources) {
const root = path.join(context.workbench, resource.directory);
const add = (phase: string, message: string) =>
entries.push({
directory: resource.directory,
resourceId: resource.resourceId,
phase,
message,
next:
phase === "syntax"
? `qx-workspace inspect ${resource.directory}`
: phase === "evolution"
? "Inspect evolution.json in the check output; resolve its named migration/review requirements before cutover"
: `cd ${resource.directory} && qx-workspace check`,
});
try {
if ((await fs.realpath(root)) !== root) throw new Error("Registered checkout crosses a symlink");
const inspected = await inspectAuthoringRepository(root);
for (const file of inspected.files)
if (file.currentErrors.length)
add(
"syntax",
`${file.file}: ${JSON.stringify(file.currentErrors)}${file.status === "historical" ? `; historical contract available at ${file.revision}` : ""}`,
);
const lock = await loadQuixosLock(path.join(root, "quixos.lock"));
if (!lock.ok)
add("resolution", lock.diagnostics.map((entry) => `${entry.fileName}: ${entry.message}`).join("\n"));
else
dependencies.set(
resource.directory,
lock.lock.resources.flatMap((dependency) => {
const selected = context.resources.find(
(entry) => entry.kind === dependency.kind && entry.source?.repository === dependency.source.repository,
);
if (selected?.source && selected.source.commit !== dependency.source.commit)
add("propagation", `Dependency ${dependency.binding} has advanced; check will repin it automatically`);
return selected ? [selected.directory] : [];
}),
);
let record;
try {
record = JSON.parse(
await fs.readFile(
path.join(context.workbench, ".quixos/checks", checkRecordName(resource.directory)),
"utf8",
),
);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
if (!record?.commit) {
if (record?.blockers?.length) add(record.phase, record.blockers.join("\n"));
else add("unchecked", "No immutable candidate check recorded yet");
continue;
}
if (record.checker !== checkerIdentity()) add("unchecked", "The installed checker changed since the last check");
const env = { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1" };
const commit = (
await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], {
cwd: root,
env,
})
).stdout.trim();
const dirty = await execFile("git", ["diff", "--quiet", "--no-ext-diff", record.commit, "--"], {
cwd: root,
}).then(
() => false,
() => true,
);
const untracked = (await execFile("git", ["ls-files", "--others", "--exclude-standard"], { cwd: root })).stdout;
if (commit !== record.commit || dirty || untracked)
add("unchecked", `Edits are newer than the last check (${record.commit.slice(0, 12)})`);
else if (record.blockers?.length) add(record.phase, record.blockers.join("\n"));
} catch (error) {
add("inspection", String(error).slice(0, 3000));
}
}
// Fixed-point propagation, independent of registration order.
const blocked = new Set(entries.map((entry) => entry.directory));
let changed = true;
while (changed) {
changed = false;
for (const [directory, required] of dependencies)
if (!blocked.has(directory)) {
const waiting = required.filter((dependency) => blocked.has(dependency));
if (waiting.length) {
blocked.add(directory);
changed = true;
entries.push({
directory,
phase: "dependency",
message: `Waiting for ${waiting.join(", ")}`,
next: "Resolve the named repositories, then rerun check",
});
}
}
}
return {
workbench: context.workbench,
verificationEvidence: false,
worklist: entries,
note: "Derived authoring guidance, not activation approval. Independent repositories can be delegated separately; join writers before a root check.",
};
}