import { execFile as callback } from "node:child_process"; import { promisify } from "node:util"; import { realpath } from "node:fs/promises"; import path from "node:path"; import { parseQx, walkSyntax } from "./source.js"; import { authoringContext } from "./authoring-context.js"; import { readQxSource } from "./source-loader.js"; import { loadQuixosLock } from "../resource-lock/index.js"; const execFile = promisify(callback); const git = async (root: string, args: string[]) => ( await execFile("git", ["-C", root, ...args], { maxBuffer: 8 * 1024 * 1024, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, }) ).stdout; const message = (error: unknown) => String(error instanceof Error ? error.message : error).slice(0, 2000); /** Syntax-only contract inspection is deliberately NOT verification evidence. * Each file can recover independently; current valid files always win. */ export async function inspectAuthoringRepository(root: string, historyLimit = 100) { const names = (await git(root, ["ls-files", "-z", "--cached", "--others", "--exclude-standard"])) .split("\0") .filter((name) => name.endsWith(".qx")); if (names.length > 128) throw new Error("Repository inspection exceeds 128 QX files; split the resource into smaller repositories"); const files = []; for (const name of [...new Set(names)].sort()) { let source = "", errors: unknown[] = [], revision: string | null = null; try { source = await readQxSource(root, name); if (source.length > 262144) throw new Error(`Inspection file exceeds 256 KiB: ${name}`); errors = parseQx(source, name).diagnostics; } catch (error) { errors = [{ message: message(error) }]; } const currentErrors = errors; if (errors.length) { // Git can traverse jj's immutable commit DAG without mutating/snapshotting @. const head = await execFile("jj", ["--ignore-working-copy", "log", "--no-graph", "-r", "@", "-T", "commit_id"], { cwd: root, env: { ...process.env, QUIXOS_JJ_NO_CHECKPOINT: "1" }, }).then( (r) => r.stdout.trim(), () => "HEAD", ); const commits = await git(root, ["rev-list", `--max-count=${historyLimit}`, head, "--", name]).catch(() => ""); for (const commit of commits.trim().split("\n").filter(Boolean)) { const historical = await git(root, ["show", `${commit}:${name}`]).catch(() => null); if (historical === null || historical.length > 262144) continue; if (!parseQx(historical, name).diagnostics.length) { source = historical; revision = commit; errors = []; break; } } } const syntax = errors.length ? null : parseQx(source, name); files.push({ file: name, status: errors.length ? "unavailable" : revision ? "historical" : "current", revision, currentErrors: currentErrors.slice(0, 20), omittedErrors: Math.max(0, currentErrors.length - 20), declarations: syntax ? [...walkSyntax(syntax.root)] .filter((node) => /^(?:interface|package|atom|state|edge|method|function|event|conformance)\w*Decl$/.test(node.kind), ) .map((node) => ({ kind: node.kind, source: source.slice(node.start, node.end) })) : [], }); } return { verificationEvidence: false as const, resolutionChecked: false as const, files }; } export async function inspectWorkbench(start: string, selector?: string) { const context = await authoringContext(start); if (!selector || selector === ".") { const relative = path.relative(context.workbench, await realpath(start)); selector = context.resources.find((entry) => relative === entry.directory || relative.startsWith(entry.directory + path.sep)) ?.directory ?? "root"; } const lock = await loadQuixosLock(path.join(context.workbench, "root/quixos.lock")); const aliases = lock.ok ? lock.lock.resources.filter((entry) => entry.binding === selector) : []; const selected = context.resources.filter( (entry) => !selector || selector === entry.directory || selector === entry.resourceId || selector === path.basename(entry.directory) || aliases.some((alias) => alias.kind === entry.kind && alias.source.repository === entry.source?.repository), ); if (!selected.length) throw new Error(`No registered resource matches ${selector}`); if (selector && selected.length > 1) throw new Error(`Ambiguous resource ${selector}; use its resource ID or directory`); const resources = []; for (const entry of selected) { try { const root = path.join(context.workbench, entry.directory); if ((await realpath(root)) !== root) throw new Error("Managed checkout crosses a symlink"); resources.push({ ...entry, ...(await inspectAuthoringRepository(root)) }); } catch (error) { resources.push({ ...entry, error: message(error) }); } } return { workbench: context.workbench, verificationEvidence: false, resources }; }