Files
ThothII/backend/scripts/p3-acceptance.mjs
T

1429 lines
66 KiB
JavaScript

#!/usr/bin/env node
import { createHash, randomBytes } from "node:crypto";
import { execFile, execFileSync } from "node:child_process";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { createServer } from "node:http";
import { closeSync, constants as fsConstants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, rmSync, statSync } from "node:fs";
import { access, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import net from "node:net";
import process from "node:process";
import { stringify as yamlStringify } from "yaml";
import { buildSafeEnvironment, deriveOverall, scanSecrets } from "./p1-acceptance.mjs";
const execFileAsync = promisify(execFile);
const modulePath = fileURLToPath(import.meta.url);
const defaultRepositoryRoot = realpathSync(resolve(dirname(modulePath), "../.."));
const RUN_ID = /^p3-[0-9a-f]{32}$/;
const HEX32 = /^[0-9a-f]{32}$/;
const HEX40 = /^[0-9a-f]{40}$/;
const HEX64 = /^[0-9a-f]{64}$/;
const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
const COMMAND = /^[a-z0-9][a-z0-9-]*$/;
const CHECK_RESULT_STATUS = new Set(["PASS", "FAIL"]);
const CHECK_IDS = Object.freeze([
"preflight",
"clean_state",
"ownership",
"effective_config_identity",
"dwh_processing",
"schema_review",
"schema_index",
"evidence_processing",
"content_only_reuse",
"dwh_change_fail_closed",
"memory_root",
"revision_scoped_records",
"negative_cases",
"secret_scan",
"cleanup_confinement",
]);
const TOPOLOGY = [
"remote.git",
"author",
"installation",
"installation/data",
"installation/data/sessions",
"installation/registry",
"installation/pi-state",
"fixture-secrets",
"fixtures",
"fixtures/logs",
"logs",
];
const MAX_REPORT_JSON_BYTES = 64 * 1024;
const MAX_REPORT_MD_BYTES = 32 * 1024;
const MAX_STDIO_BYTES = 512 * 1024;
const MAX_SECRET_SCAN_VIRTUAL_BYTES = 256 * 1024;
function nowIso() { return new Date().toISOString(); }
function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
function assert(condition, message) { if (!condition) throw new Error(message); }
function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
function canonicalRoot(repositoryRoot = defaultRepositoryRoot) {
return realpathSync(repositoryRoot);
}
export function canonicalIntegrationBase(repositoryRoot = defaultRepositoryRoot) {
return join(canonicalRoot(repositoryRoot), ".artifacts", "p3-integration");
}
export function validateRunRoot(repositoryRoot, runRoot, runId) {
if (!RUN_ID.test(runId)) throw new Error("invalid owned run id");
const base = canonicalIntegrationBase(repositoryRoot);
const lexical = resolve(runRoot);
if (dirname(lexical) !== base || basename(lexical) !== runId) throw new Error("run root is not a direct integration child");
return lexical;
}
function validateNoSymlinkAncestors(repositoryRoot, target) {
const repo = canonicalRoot(repositoryRoot);
const rel = relative(repo, target);
if (rel.startsWith("..") || isAbsolute(rel)) throw new Error("path leaves repository");
let cursor = repo;
for (const part of rel.split(sep).filter(Boolean)) {
cursor = join(cursor, part);
if (!existsSync(cursor)) break;
const entry = lstatSync(cursor);
if (entry.isSymbolicLink()) throw new Error("owned path ancestor is a symlink");
}
}
async function atomicWrite(path, bytes, mode = 0o600) {
await mkdir(dirname(path), { recursive: true });
const staging = join(dirname(path), `.${basename(path)}.${randomBytes(12).toString("hex")}.tmp`);
let handle;
try {
handle = await open(staging, "wx", mode);
await handle.writeFile(bytes);
await handle.sync();
await handle.close();
handle = undefined;
await rename(staging, path);
const directory = openSync(dirname(path), fsConstants.O_RDONLY);
try { fsyncSync(directory); } finally { closeSync(directory); }
} catch (error) {
if (handle) await handle.close().catch(() => {});
await rm(staging, { force: true }).catch(() => {});
throw error;
}
}
function initialResources(run) {
return [
run.root,
join(run.root, "remote.git"),
join(run.root, "author"),
join(run.root, "installation"),
join(run.root, "installation", "registry"),
join(run.root, "installation", "data"),
join(run.root, "fixture-secrets"),
];
}
function ownershipValue(run) {
return {
schemaVersion: 1,
kind: "p3-acceptance",
runId: run.runId,
runNonce: run.nonce,
root: run.root,
repositoryRoot: run.repositoryRoot,
startedAt: run.startedAt,
pid: run.pid,
resources: initialResources(run),
};
}
async function writeOwnership(run) {
await atomicWrite(join(run.root, "ownership.json"), `${JSON.stringify(ownershipValue(run), null, 2)}\n`);
}
export async function createOwnedRun({ repositoryRoot = defaultRepositoryRoot, runId, nonce, now, pid } = {}) {
const repo = canonicalRoot(repositoryRoot);
const base = canonicalIntegrationBase(repo);
validateNoSymlinkAncestors(repo, base);
await mkdir(join(repo, ".artifacts"), { mode: 0o700 }).catch((error) => { if (error.code !== "EEXIST") throw error; });
await mkdir(base, { mode: 0o700 }).catch((error) => { if (error.code !== "EEXIST") throw error; });
const id = runId ?? `p3-${randomBytes(16).toString("hex")}`;
const root = validateRunRoot(repo, join(base, id), id);
const run = {
repositoryRoot: repo,
root,
runId: id,
nonce: nonce ?? randomBytes(32).toString("hex"),
startedAt: now ?? nowIso(),
pid: pid ?? process.pid,
};
if (!HEX64.test(run.nonce) || !ISO_UTC.test(run.startedAt)) throw new Error("invalid ownership identity");
await mkdir(root, { mode: 0o700 });
await writeOwnership(run);
return run;
}
function strictOwnership(value, run, expectedNonce) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("ownership is malformed");
if (value.schemaVersion !== 1 || value.kind !== "p3-acceptance" || value.runId !== run.runId || value.runNonce !== expectedNonce
|| value.root !== run.root || value.repositoryRoot !== run.repositoryRoot || value.pid !== process.pid
|| !ISO_UTC.test(value.startedAt ?? "")
|| JSON.stringify(value.resources) !== JSON.stringify(initialResources(run))) throw new Error("ownership identity mismatch");
return value;
}
export async function readAndValidateOwnership({ repositoryRoot = defaultRepositoryRoot, runRoot, expectedNonce }) {
const repo = canonicalRoot(repositoryRoot);
const id = basename(resolve(runRoot));
const lexical = validateRunRoot(repo, runRoot, id);
const rootEntry = await lstat(lexical);
if (!rootEntry.isDirectory() || rootEntry.isSymbolicLink()) throw new Error("owned run root is not a directory");
const ownershipPath = join(lexical, "ownership.json");
const ownershipEntry = await lstat(ownershipPath);
if (!ownershipEntry.isFile() || ownershipEntry.isSymbolicLink()) throw new Error("ownership file is unsafe");
let value;
try { value = JSON.parse(await readFile(ownershipPath, "utf8")); } catch { throw new Error("ownership is malformed"); }
return strictOwnership(value, { repositoryRoot: repo, root: lexical, runId: id }, expectedNonce);
}
export async function cleanupOwnedRun({ repositoryRoot = defaultRepositoryRoot, runRoot, expectedNonce }) {
const value = await readAndValidateOwnership({ repositoryRoot, runRoot, expectedNonce });
const base = canonicalIntegrationBase(repositoryRoot);
const tombstone = join(base, `.deleting-${value.runId}-${expectedNonce.slice(0, 16)}`);
await rename(runRoot, tombstone);
await rm(tombstone, { recursive: true, force: false });
}
async function finalizeOwnedRun({ run, success, keep }) {
if (!success || keep) return false;
await cleanupOwnedRun({ repositoryRoot: run.repositoryRoot, runRoot: run.root, expectedNonce: run.nonce });
return true;
}
function safeArtifactPath(path) {
if (typeof path !== "string" || path.length === 0 || path.length > 255 || path.startsWith("/") || path.includes("..") || path.includes("\\") || /[\0\r\n]/.test(path)) {
throw new Error("report artifact path is invalid");
}
return path;
}
function hasExactCheckIds(checks) {
return checks.length === CHECK_IDS.length && checks.every(({ id }, index) => id === CHECK_IDS[index]);
}
export function validateReport(report) {
if (!report || report.schemaVersion !== 1 || !RUN_ID.test(report.runId ?? "") || !ISO_UTC.test(report.startedAt ?? "")
|| !ISO_UTC.test(report.finishedAt ?? "") || typeof report.command !== "string"
|| !Array.isArray(report.checks) || !hasExactCheckIds(report.checks)) throw new Error("report is invalid");
const ids = new Set();
const artifactPaths = new Set();
for (const check of report.checks) {
if (!check || !/^[a-z0-9_]+$/.test(check.id ?? "") || ids.has(check.id) || !CHECK_RESULT_STATUS.has(check.status)
|| !ISO_UTC.test(check.startedAt ?? "") || !ISO_UTC.test(check.finishedAt ?? "")
|| !Array.isArray(check.commands) || check.commands.some((name) => !COMMAND.test(name))
|| !Array.isArray(check.artifacts)) throw new Error("report check is invalid");
ids.add(check.id);
for (const artifact of check.artifacts) {
safeArtifactPath(artifact.path);
if (!HEX64.test(artifact.sha256 ?? "")) throw new Error("report check is invalid");
if (artifactPaths.has(artifact.path)) throw new Error("report artifact path is duplicated");
artifactPaths.add(artifact.path);
}
}
if (report.overall !== deriveOverall(report.checks)) throw new Error("report overall is not derived");
return report;
}
function renderReportMarkdown(report) {
validateReport(report);
const rows = report.checks.map((check) => `| ${check.id} | ${check.status} |`).join("\n");
return [
"# P2 acceptance report",
"",
`Run: \`${report.runId}\``,
"",
"| Check | Status |",
"|---|---|",
rows,
"",
`P3 automated integration: ${report.overall}`,
"P3 manual acceptance: PENDING",
"",
].join("\n");
}
async function walkFiles(root) {
const files = [];
async function visit(dir) {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = join(dir, entry.name);
const rel = relative(root, path).split(sep).join("/");
if (entry.isSymbolicLink()) throw new Error(`unsafe file tree: ${rel}`);
if (entry.isDirectory()) await visit(path);
else if (entry.isFile()) files.push({ path, rel });
}
}
if (existsSync(root)) await visit(root);
files.sort((a, b) => a.rel.localeCompare(b.rel));
return files;
}
async function snapshotDigest(root, excludedPrefixes = []) {
const result = {};
for (const file of await walkFiles(root)) {
if (excludedPrefixes.some((prefix) => file.rel === prefix || file.rel.startsWith(`${prefix}/`))) continue;
result[file.rel] = sha256(await readFile(file.path));
}
return result;
}
async function fileArtifact(root, relativePath) {
const bytes = await readFile(join(root, relativePath));
return { path: relativePath.split(sep).join("/"), sha256: sha256(bytes) };
}
async function writeJson(path, value) {
await atomicWrite(path, `${JSON.stringify(value, null, 2)}\n`);
}
async function writeReportFiles({ run, report }) {
validateReport(report);
const reportJsonPath = join(run.root, "report.json");
const reportMdPath = join(run.root, "report.md");
const reportMd = renderReportMarkdown(report);
if (Buffer.byteLength(JSON.stringify(report)) > MAX_REPORT_JSON_BYTES) throw new Error("report.json exceeds bound");
if (Buffer.byteLength(reportMd) > MAX_REPORT_MD_BYTES) throw new Error("report.md exceeds bound");
await writeJson(reportJsonPath, report);
await atomicWrite(reportMdPath, reportMd, 0o600);
return {
reportJson: await fileArtifact(run.root, "report.json"),
reportMd: await fileArtifact(run.root, "report.md"),
};
}
function resolveSystemExecutable(name) {
for (const candidate of [`/usr/bin/${name}`, `/bin/${name}`, `/opt/homebrew/bin/${name}`, `/usr/local/bin/${name}`]) {
try {
const resolved = realpathSync(candidate);
if (statSync(resolved).isFile()) return resolved;
} catch {}
}
throw new Error(`required executable not found: ${name}`);
}
function scalarSecretBytes(value) {
if (typeof value !== "string" || value.length === 0 || /\s|\0/.test(value)) throw new Error("scalar fixture secret is invalid");
return Buffer.from(value);
}
async function manifestFiles(root, paths) {
const files = [];
const visit = async (absolute, rel) => {
const entry = await lstat(absolute);
if (entry.isSymbolicLink()) throw new Error(`provenance path is a symlink: ${rel}`);
if (entry.isDirectory()) {
for (const child of (await readdir(absolute, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
await visit(join(absolute, child.name), rel ? `${rel}/${child.name}` : child.name);
}
} else if (entry.isFile()) {
const bytes = await readFile(absolute);
files.push({ path: rel, bytes: bytes.length, sha256: sha256(bytes) });
} else throw new Error(`provenance path is not a regular file: ${rel}`);
};
for (const path of paths) await visit(join(root, path), path);
files.sort((a, b) => a.path.localeCompare(b.path));
return { files, manifestSha256: sha256(JSON.stringify(files)) };
}
async function collectRepositoryProvenance({ repositoryRoot, gitPath = resolveSystemExecutable("git") }) {
const repo = canonicalRoot(repositoryRoot);
const safeEnv = buildSafeEnvironment({ ambient: {}, fixture: { PATH: `${dirname(gitPath)}:/usr/bin:/bin`, HOME: repo, TMPDIR: join(repo, ".artifacts") } });
const run = async (argv) => await execFileAsync(gitPath, ["-C", repo, ...argv], { env: safeEnv, maxBuffer: MAX_STDIO_BYTES });
const beforeHead = (await run(["rev-parse", "HEAD"]).catch((error) => { throw error; })).stdout.trim();
const beforeTree = (await run(["rev-parse", "HEAD^{tree}"])).stdout.trim();
const beforeStatus = (await run(["status", "--porcelain=v1", "--untracked-files=no"])).stdout;
if (!HEX40.test(beforeHead) || !HEX40.test(beforeTree) || beforeStatus !== "") throw new Error("repository is not clean at exact HEAD");
const backendRoot = join(repo, "backend");
const backendSource = await manifestFiles(backendRoot, [
"src",
"scripts/p3-acceptance.mjs",
"package.json",
"package-lock.json",
"tsconfig.json",
]);
const backendDist = existsSync(join(backendRoot, "dist")) ? await manifestFiles(backendRoot, ["dist"]) : { files: [], manifestSha256: sha256("[]") };
const afterHead = (await run(["rev-parse", "HEAD"]).catch((error) => { throw error; })).stdout.trim();
const afterTree = (await run(["rev-parse", "HEAD^{tree}"])).stdout.trim();
const afterStatus = (await run(["status", "--porcelain=v1", "--untracked-files=no"])).stdout;
if (afterHead !== beforeHead || afterTree !== beforeTree || afterStatus !== beforeStatus) throw new Error("repository provenance changed during binding");
return { schemaVersion: 1, head: beforeHead, tree: beforeTree, clean: true, backendSource, backendDist };
}
async function createTopology(run) {
for (const path of TOPOLOGY) await mkdir(join(run.root, path), { recursive: true, mode: path === "fixture-secrets" ? 0o700 : 0o755 });
}
async function allocatePort() {
const server = net.createServer();
await new Promise((resolve, reject) => server.listen(0, "127.0.0.1", resolve).on("error", reject));
const port = server.address().port;
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
return port;
}
function installationProjectName(installationPath) {
return `thothii-${sha256(installationPath).slice(0, 12)}`;
}
function baseWorkspace(id, { dwhBaseUrl, evidenceSource }) {
return {
workspace: { schema_version: 4, id, name: `P2 ${id}`, language: "en" },
dwh: { engine: "postgres", database: "warehouse", schema: "dw", supported_transports: ["rest_api"] },
diagnostics: {
dwh_rest: { method: "POST", path: "/rpc/ping", auth: "x-api-key", response: { database: "database", schema: "schema" } },
},
...(evidenceSource ? { evidence: { source: evidenceSource, policy: { max_chunk_chars: 4000, retain_published_generations: 3 } } } : {}),
};
}
function namespace(id) { return id.toUpperCase().replaceAll("-", "_"); }
function descriptorYaml(obj) {
return yamlStringify(obj, { lineWidth: 0, sortMapEntries: false });
}
async function setupSecrets(ctx) {
const secretDir = join(ctx.run.root, "fixture-secrets");
const values = {
dwhToken: `P2-DWH-${randomBytes(16).toString("hex")}`,
signedToken: `P2-SIGNED-${randomBytes(16).toString("hex")}`,
bundle: `P2-BUNDLE-${randomBytes(16).toString("hex")}`,
};
ctx.forbiddenValues = Object.values(values);
ctx.secretValues = values;
const paths = {
dwh: join(secretDir, "p2-dwh-api-key"),
filesystemDwh: join(secretDir, "p2-filesystem-api-key"),
signed: join(secretDir, "p2-dwh-evidence-signed-urls.json"),
bundle: join(secretDir, "thothii.secrets"),
};
await atomicWrite(paths.dwh, scalarSecretBytes(values.dwhToken));
await atomicWrite(paths.filesystemDwh, scalarSecretBytes(values.dwhToken));
await atomicWrite(paths.bundle, scalarSecretBytes(values.bundle));
ctx.secretPaths = paths;
}
async function setupFixtures(ctx) {
ctx.fixturePorts = {
dwh: await allocatePort(),
evidence: await allocatePort(),
embedding: await allocatePort(),
qdrant: await allocatePort(),
};
const dwhBaseUrl = `http://host.docker.internal:${ctx.fixturePorts.dwh}`;
const evidenceProvenance = `http://host.docker.internal:${ctx.fixturePorts.evidence}/p2-dwh/guide.md`;
ctx.workspaceObjects = {
dwh: baseWorkspace("p2-dwh", {
dwhBaseUrl,
evidenceSource: {
type: "http",
uris: [evidenceProvenance],
authentication: "signed_urls_file",
connect_timeout_ms: 1250,
read_timeout_ms: 30001,
max_bytes: 65536,
max_redirects: 2,
allow_private_hosts: true,
max_cache_bytes: 65536,
},
}),
filesystem: baseWorkspace("p2-filesystem", {
dwhBaseUrl,
evidenceSource: {
type: "filesystem",
uri: "p2-filesystem/evidence",
patterns: ["**/*.md"],
max_bytes: 1048576,
},
}),
};
const signedUrl = `${evidenceProvenance}?token=${ctx.secretValues.signedToken}`;
await atomicWrite(ctx.secretPaths.signed, `${JSON.stringify([signedUrl], null, 2)}\n`);
ctx.evidenceState = {
content: "# P2 Evidence\n\nFirst generation.\n",
token: ctx.secretValues.signedToken,
};
ctx.dwhState = {
tables: {
patients: {
comment: "Patients",
rows: [
{ patient_id: "p1", name: "Alice" },
{ patient_id: "p2", name: "Bob" },
],
},
visits: {
comment: "Visits",
rows: [
{ id: "v1", patient_id: "p1", note: "checkup" },
{ id: "v2", patient_id: "p2", note: "xray" },
],
},
labs: {
comment: "Labs",
rows: [
{ id: "l1", patient_id: "p1", code: "hemoglobin" },
{ id: "l2", patient_id: "p2", code: "glucose" },
],
},
},
token: ctx.secretValues.dwhToken,
};
}
function inferColumnType(value) {
return typeof value === "number" ? "integer" : "text";
}
function topValues(rows, column, limit) {
const counts = new Map();
for (const row of rows) {
const value = row[column];
if (value === undefined || value === null || value === "") continue;
counts.set(String(value), (counts.get(String(value)) ?? 0) + 1);
}
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([value]) => ({ value }));
}
async function startHttpServer({ port, handler }) {
const server = createServer(async (req, res) => {
try {
await handler(req, res);
} catch {
res.statusCode = 500;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ error: "fixture failed" }));
}
});
await new Promise((resolve, reject) => server.listen(port, "127.0.0.1", () => resolve()).on("error", reject));
return server;
}
async function startServers(ctx) {
const dwhServer = await startHttpServer({
port: ctx.fixturePorts.dwh,
handler: async (req, res) => {
const body = await new Promise((resolve) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
});
const json = body.length === 0 ? {} : JSON.parse(body);
if (req.headers["x-api-key"] !== ctx.dwhState.token) {
res.statusCode = 401;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ message: "unauthorized" }));
return;
}
const send = (payload) => {
res.statusCode = 200;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify(payload));
};
const url = new URL(req.url, `http://127.0.0.1:${ctx.fixturePorts.dwh}`);
if (req.method !== "POST" || !url.pathname.startsWith("/rpc/")) {
res.statusCode = 404;
res.end(JSON.stringify({ message: "not found" }));
return;
}
const fn = url.pathname.slice("/rpc/".length);
const schemaName = json.schema_name ?? "dw";
if (schemaName !== "dw") {
send([]);
return;
}
if (fn === "ping") {
send({ db_connected: true, schema_accessible: true, database: "warehouse", schema: "dw" });
return;
}
const table = typeof json.table_name === "string" ? json.table_name : "";
const tableData = ctx.dwhState.tables[table];
if (fn === "list_tables") {
send(Object.entries(ctx.dwhState.tables).map(([name, info]) => ({ table: name, type: "TABLE", comment: info.comment, rows: info.rows.length })));
return;
}
if (!tableData) {
send([]);
return;
}
if (fn === "table_columns") {
const first = tableData.rows[0] ?? {};
send(Object.keys(first).map((column) => ({
column,
type: inferColumnType(first[column]),
nullable: false,
// Only the referenced table marks `patient_id` as primary, so the SQL miner sees a
// PK/non-PK pair while the same-name heuristic still discovers joins from the others.
pk: column === "id" || (table === "patients" && column === "patient_id"),
default: null,
})));
return;
}
if (fn === "table_comments") {
send(Object.keys(tableData.rows[0] ?? {}).map((column) => ({ object: "COLUMN", name: column, comment: `${table}.${column}` })));
return;
}
if (fn === "table_foreign_keys") {
send([]);
return;
}
if (fn === "top_values") {
send(topValues(tableData.rows, json.column_name, Number(json.max_values ?? 10)));
return;
}
if (fn === "column_stats") {
send({});
return;
}
if (fn === "run_query") {
send([]);
return;
}
if (fn === "explain_query") {
send([{ line: "Seq Scan" }]);
return;
}
res.statusCode = 404;
res.end(JSON.stringify({ message: "unknown rpc" }));
},
});
const evidenceServer = await startHttpServer({
port: ctx.fixturePorts.evidence,
handler: async (req, res) => {
const url = new URL(req.url, `http://127.0.0.1:${ctx.fixturePorts.evidence}`);
if (url.pathname !== "/p2-dwh/guide.md" || url.searchParams.get("token") !== ctx.evidenceState.token) {
res.statusCode = 403;
res.end("forbidden");
return;
}
res.statusCode = 200;
res.setHeader("content-type", "text/markdown; charset=utf-8");
res.end(ctx.evidenceState.content);
},
});
const embeddingServer = await startHttpServer({
port: ctx.fixturePorts.embedding,
handler: async (req, res) => {
const body = await new Promise((resolve) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
});
const url = new URL(req.url, `http://127.0.0.1:${ctx.fixturePorts.embedding}`);
if (req.method !== "POST" || url.pathname !== "/api/embed") {
res.statusCode = 404;
res.end(JSON.stringify({ error: "not found" }));
return;
}
const payload = JSON.parse(body || "{}");
const inputs = Array.isArray(payload.input) ? payload.input : [];
const embeddings = inputs.map((text) => {
const seed = sha256(String(text));
return Array.from({ length: 1024 }, (_, index) => {
const offset = (index * 2) % seed.length;
const value = Number.parseInt(seed.slice(offset, offset + 2), 16);
return (value / 255) - 0.5;
});
});
res.statusCode = 200;
res.setHeader("content-type", "application/json");
res.end(JSON.stringify({ model: payload.model, embeddings }));
},
});
ctx.servers = [dwhServer, evidenceServer, embeddingServer];
}
async function stopServers(ctx) {
for (const server of ctx.servers ?? []) {
await new Promise((resolve) => server.close(() => resolve()));
}
ctx.servers = [];
}
async function git(ctx, args, cwd = join(ctx.run.root, "author")) {
return await runCommand({ executable: ctx.executables.gitPath, argv: args, cwd, env: ctx.execEnv });
}
async function initializeGitAndRegistry(ctx) {
const author = join(ctx.run.root, "author");
await git(ctx, ["init", "--bare", "--initial-branch=main", join(ctx.run.root, "remote.git")], ctx.run.root);
await git(ctx, ["clone", join(ctx.run.root, "remote.git"), author], ctx.run.root);
await git(ctx, ["config", "user.name", "P2 Fixture Curator"], author);
await git(ctx, ["config", "user.email", "p3-curator@example.invalid"], author);
const writeWorkspaces = async () => {
const catalog = {
schema_version: 1,
workspaces: [
{ id: "p2-dwh", name: ctx.workspaceObjects.dwh.workspace.name },
{ id: "p2-filesystem", name: ctx.workspaceObjects.filesystem.workspace.name },
],
};
await writeFile(join(author, "thoth-workspaces.yaml"), yamlStringify(catalog, { lineWidth: 0, sortMapEntries: false }));
for (const [id, workspace] of Object.entries(ctx.workspaceObjects)) {
const pathId = workspace.workspace.id;
await mkdir(join(author, pathId), { recursive: true });
const yaml = descriptorYaml(workspace);
await writeFile(join(author, pathId, "workspace.yaml"), yaml);
const docs = ctx.workspaceModules.renderWorkspaceDocs(workspace);
await mkdir(join(author, "workspace-docs", pathId), { recursive: true });
await writeFile(join(author, "workspace-docs", pathId, "contract.env.example"), docs.envExample);
await writeFile(join(author, "workspace-docs", pathId, "README.md"), docs.markdown);
}
await mkdir(join(author, "p2-filesystem", "evidence"), { recursive: true });
await writeFile(join(author, "p2-filesystem", "evidence", "guide.md"), "# P2 Filesystem Evidence\n\nCommitted fixture.\n");
};
await writeWorkspaces();
await git(ctx, ["add", "."], author);
await git(ctx, ["commit", "-m", "Bootstrap P2 fixtures"], author);
await git(ctx, ["push", "origin", "main"], author);
ctx.registryCommit = (await git(ctx, ["rev-parse", "HEAD"], author)).stdout.trim();
const registry = new ctx.workspaceModules.WorkspaceRegistry({
root: join(ctx.run.root, "installation", "registry"),
remoteUrl: join(ctx.run.root, "remote.git"),
branch: "main",
gitAuthorName: "P2 Acceptance",
gitAuthorEmail: "p3-acceptance@example.invalid",
installationId: "p3-acceptance",
secretRoots: [join(ctx.run.root, "fixture-secrets")],
maxImportBytes: 16 * 1024 * 1024,
maxImportEntries: 1024,
});
await registry.bootstrap();
ctx.registry = registry;
}
async function mutateWorkspaceDescriptor(ctx, workspaceId, mutator, commitMessage) {
const author = join(ctx.run.root, "author");
// The registry may have produced docs-only follow-up commits on the remote; the curator
// always rebases onto the latest remote head before committing so the push stays fast-forward.
await git(ctx, ["fetch", "origin", "main"], author);
await git(ctx, ["reset", "--hard", "origin/main"], author);
const workspace = structuredClone(ctx.workspaceObjects[workspaceId === "p2-dwh" ? "dwh" : "filesystem"]);
mutator(workspace);
ctx.workspaceObjects[workspaceId === "p2-dwh" ? "dwh" : "filesystem"] = workspace;
await writeFile(join(author, workspaceId, "workspace.yaml"), descriptorYaml(workspace));
const docs = ctx.workspaceModules.renderWorkspaceDocs(workspace);
const docsDir = join(author, "workspace-docs", workspaceId);
await mkdir(docsDir, { recursive: true, mode: 0o700 });
await writeFile(join(docsDir, "contract.env.example"), docs.envExample);
await writeFile(join(docsDir, "README.md"), docs.markdown);
await git(ctx, ["add", `${workspaceId}/workspace.yaml`, `workspace-docs/${workspaceId}/contract.env.example`, `workspace-docs/${workspaceId}/README.md`], author);
await git(ctx, ["commit", "-m", commitMessage], author);
await git(ctx, ["push", "origin", "main"], author);
await ctx.registry.pull();
ctx.registryCommit = (await git(ctx, ["rev-parse", "HEAD"], author)).stdout.trim();
}
async function writeInstallationFiles(ctx) {
const installationDir = join(ctx.run.root, "installation");
const operatorEnvPath = join(installationDir, "operator.env");
const bindingsEnvPath = join(installationDir, "workspace-bindings.env");
const connectorOverridePath = join(installationDir, "connector-secrets.override.yaml");
const fixtureOverridePath = join(installationDir, "fixture.override.yaml");
const installationPath = join(installationDir, "thothii-installation.yaml");
ctx.installationPath = installationPath;
ctx.composeProject = installationProjectName(installationPath);
const qdrantPort = ctx.fixturePorts.qdrant;
const bindings = [
`THT_WS_P2_DWH_DWH_TRANSPORT=rest_api`,
`THT_WS_P2_DWH_DWH_BASE_URL=http://host.docker.internal:${ctx.fixturePorts.dwh}`,
`THT_WS_P2_DWH_DWH_API_KEY_FILE=/run/secrets/p2-dwh-api-key`,
`THT_WS_P2_DWH_EVIDENCE_SIGNED_URLS_FILE=/run/secrets/p2-dwh-evidence-signed-urls`,
`THT_WS_P2_FILESYSTEM_DWH_TRANSPORT=rest_api`,
`THT_WS_P2_FILESYSTEM_DWH_BASE_URL=http://host.docker.internal:${ctx.fixturePorts.dwh}`,
`THT_WS_P2_FILESYSTEM_DWH_API_KEY_FILE=/run/secrets/p2-filesystem-api-key`,
].join("\n") + "\n";
await atomicWrite(bindingsEnvPath, bindings);
const operatorEnv = [
`THT_DATA_ROOT=${join(ctx.run.root, "installation", "data")}`,
`THT_WORKSPACE_REGISTRY_ROOT=${join(ctx.run.root, "installation", "registry")}`,
`THT_PI_STATE_ROOT=${join(ctx.run.root, "installation", "pi-state")}`,
`PI_AUTH_FILE=${join(ctx.run.root, "installation", "pi-auth.json")}`,
`THT_SECRETS_FILE=${ctx.secretPaths.bundle}`,
`THT_WORKSPACE_BINDINGS_ENV_FILE=${bindingsEnvPath}`,
`THT_WORKSPACE_GIT_REMOTE=${join(ctx.run.root, "remote.git")}`,
`THT_WORKSPACE_GIT_BRANCH=main`,
`THT_WORKSPACE_GIT_AUTHOR_NAME=P2 Acceptance`,
`THT_WORKSPACE_GIT_AUTHOR_EMAIL=p3-acceptance@example.invalid`,
`THT_WORKSPACE_INSTALLATION_ID=p3-acceptance`,
`THT_DB_NAME=warehouse`,
`THT_DWH_REST_URL=http://host.docker.internal:${ctx.fixturePorts.dwh}`,
`THT_LLM_URL=http://127.0.0.1:9`,
`THOTH_SERVER_BIND=127.0.0.1`,
`THOTH_HTTP_PORT=18080`,
`THOTH_CORE_HTTP_PORT=18787`,
`THT_WS_P2_DWH_DWH_API_KEY_SOURCE=${ctx.secretPaths.dwh}`,
`THT_WS_P2_DWH_EVIDENCE_SIGNED_URLS_SOURCE=${ctx.secretPaths.signed}`,
`THT_WS_P2_FILESYSTEM_DWH_API_KEY_SOURCE=${ctx.secretPaths.filesystemDwh}`,
`THT_EVIDENCE_PRIVATE_HOST_ALLOWLIST=host.docker.internal`,
].join("\n") + "\n";
await atomicWrite(operatorEnvPath, operatorEnv);
await atomicWrite(join(ctx.run.root, "installation", "pi-auth.json"), JSON.stringify({ fixture: true }));
const embeddingStubPath = join(installationDir, "embedding-stub.py");
await atomicWrite(embeddingStubPath, EMBEDDING_STUB_SOURCE);
const override = {
services: {
core: {
image: ctx.coreImageTag,
extra_hosts: ["host.docker.internal:host-gateway"],
},
"workspace-maintenance": {
image: ctx.coreImageTag,
environment: {
THT_EVIDENCE_PRIVATE_HOST_ALLOWLIST: "host.docker.internal",
},
extra_hosts: ["host.docker.internal:host-gateway"],
},
qdrant: {
ports: [`127.0.0.1:${qdrantPort}:6333`],
restart: "no",
},
// Deterministic Ollama-compatible embedding fixture on the internal allowlisted host
// name `embedding` (http://embedding:11434). Replaces the real Ollama service entirely.
embedding: {
image: ctx.coreImageTag,
entrypoint: ["python3", "/stub.py"],
volumes: [
{ type: "bind", source: embeddingStubPath, target: "/stub.py", read_only: true },
],
healthcheck: { disable: true },
},
},
};
await atomicWrite(fixtureOverridePath, yamlStringify(override, { lineWidth: 0, sortMapEntries: false }));
const generated = await runCommand({
executable: join(ctx.repositoryRoot, "scripts", "generate-connector-secrets-override.sh"),
argv: [
"--bindings-env", bindingsEnvPath,
"--operator-env", operatorEnvPath,
"--output", connectorOverridePath,
"--service", "workspace-maintenance",
"--role", "all",
],
env: ctx.execEnv,
});
if (generated.exitCode !== 0) throw new Error(`connector override generation failed: ${generated.stderr || generated.stdout}`);
const installation = {
profile: "server",
projectDirectory: ctx.repositoryRoot,
envFile: operatorEnvPath,
overrides: [
join(ctx.repositoryRoot, "deploy", "compose.server.yaml"),
fixtureOverridePath,
connectorOverridePath,
],
};
await atomicWrite(installationPath, yamlStringify(installation, { lineWidth: 0, sortMapEntries: false }));
ctx.installation = installation;
}
function thothctlBinaryPath(repositoryRoot) {
const platform = { darwin: "darwin", linux: "linux", win32: "windows" }[process.platform] ?? "linux";
const arch = { x64: "amd64", arm64: "arm64" }[process.arch] ?? "amd64";
const suffix = platform === "windows" ? ".exe" : "";
const candidates = [
join(repositoryRoot, "dist", "thothctl", `thothctl-${platform}-${arch}${suffix}`),
join(repositoryRoot, "tools", "thothctl", "bin", `thothctl${suffix}`),
];
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
throw new Error("built thothctl binary is unavailable");
}
async function runCommand({ executable, argv = [], cwd, env, input, maxOutputBytes = MAX_STDIO_BYTES }) {
const result = await execFileAsync(executable, argv, {
cwd,
env,
encoding: "utf8",
maxBuffer: maxOutputBytes,
...(input === undefined ? {} : { input }),
}).then(
({ stdout, stderr }) => ({ exitCode: 0, stdout, stderr }),
(error) => ({ exitCode: error.code ?? 1, stdout: error.stdout ?? "", stderr: error.stderr ?? error.message ?? "" }),
);
return result;
}
async function buildCoreImage(ctx) {
const tag = `thothii-core:p2-${ctx.run.runId.slice(3, 15)}`;
ctx.coreImageTag = tag;
const build = await runCommand({
executable: ctx.executables.dockerPath,
argv: ["build", "-f", join(ctx.repositoryRoot, "docker", "core.Dockerfile"), "-t", tag, ctx.repositoryRoot],
env: { ...ctx.execEnv, DOCKER_BUILDKIT: "1" },
maxOutputBytes: 4 * 1024 * 1024,
});
if (build.exitCode !== 0) throw new Error(`core image build failed: ${build.stderr || build.stdout}`);
}
async function buildThothctl(ctx) {
const command = await runCommand({
executable: join(ctx.repositoryRoot, "scripts", "build-thothctl.sh"),
argv: [],
env: { ...ctx.execEnv, THT_THOTHCTL_OUTPUT_DIRECTORY: join(ctx.repositoryRoot, "dist", "thothctl") },
maxOutputBytes: 4 * 1024 * 1024,
});
if (command.exitCode !== 0) throw new Error(`build-thothctl failed: ${command.stderr || command.stdout}`);
ctx.thothctlPath = thothctlBinaryPath(ctx.repositoryRoot);
}
function composeBaseArgs(ctx) {
const args = [
"compose",
"--project-name", ctx.composeProject,
"--project-directory", ctx.installation.projectDirectory,
"--env-file", ctx.installation.envFile,
"-f", join(ctx.repositoryRoot, "compose.yaml"),
];
for (const override of ctx.installation.overrides) args.push("-f", override);
return args;
}
async function dockerCompose(ctx, commandArgs, { allowFailure = false, maxOutputBytes = 2 * 1024 * 1024 } = {}) {
const result = await runCommand({
executable: ctx.executables.dockerPath,
argv: [...composeBaseArgs(ctx), ...commandArgs],
env: ctx.execEnv,
maxOutputBytes,
});
if (!allowFailure && result.exitCode !== 0) throw new Error(`docker compose ${commandArgs.join(" ")} failed: ${result.stderr || result.stdout}`);
return result;
}
async function startQdrant(ctx) {
await dockerCompose(ctx, ["up", "-d", "qdrant", "embedding"]);
for (let attempt = 0; attempt < 60; attempt += 1) {
try {
const response = await fetch(`http://127.0.0.1:${ctx.fixturePorts.qdrant}/collections`);
if (response.ok) return;
} catch {}
await sleep(1000);
}
throw new Error("qdrant did not become ready");
}
async function qdrantJson(ctx, method, path, body) {
const response = await fetch(`http://127.0.0.1:${ctx.fixturePorts.qdrant}${path}`, {
method,
headers: { "content-type": "application/json" },
...(body === undefined ? {} : { body: JSON.stringify(body) }),
});
const payload = response.status === 204 ? {} : await response.json().catch(() => ({}));
if (!response.ok) throw new Error(`qdrant request failed: ${method} ${path} ${response.status}`);
return payload;
}
async function preprovisionCollection(ctx, workspaceId) {
await qdrantJson(ctx, "PUT", `/collections/${workspaceId}`, {
vectors: { size: 1024, distance: "Cosine" },
});
for (const field of ["content_hash", "document_id", "kind", "record_key", "record_kind", "vector_generation", "workspace_id", "workspace_revision"]) {
await qdrantJson(ctx, "PUT", `/collections/${workspaceId}/index`, { field_name: field, field_schema: "keyword" });
}
}
async function listCollections(ctx) {
const payload = await qdrantJson(ctx, "GET", "/collections");
const collections = payload.result?.collections ?? [];
return collections.map((item) => item.name).sort();
}
async function dumpQdrantPayloads(ctx, workspaceId) {
const response = await qdrantJson(ctx, "POST", `/collections/${workspaceId}/points/scroll`, { limit: 128, with_payload: true, with_vector: false });
return JSON.stringify(response.result?.points ?? []);
}
async function runThothctlJson(ctx, label, workspaceArgs, expectedExitCode) {
const stdoutPath = join(ctx.run.root, "logs", `${label}.stdout.json`);
const stderrPath = join(ctx.run.root, "logs", `${label}.stderr.txt`);
const result = await runCommand({
executable: ctx.thothctlPath,
argv: ["--installation", ctx.installationPath, ...workspaceArgs, "--json"],
env: ctx.execEnv,
maxOutputBytes: 2 * 1024 * 1024,
});
await atomicWrite(stdoutPath, result.stdout || "");
await atomicWrite(stderrPath, result.stderr || "");
if (expectedExitCode !== undefined && result.exitCode !== expectedExitCode) {
throw new Error(`${label} exit ${result.exitCode} != ${expectedExitCode}`);
}
let payload;
try { payload = JSON.parse(result.stdout); } catch (error) { throw new Error(`${label} returned non-JSON stdout`); }
return { result, payload, artifacts: [await fileArtifact(ctx.run.root, relative(ctx.run.root, stdoutPath)), await fileArtifact(ctx.run.root, relative(ctx.run.root, stderrPath))] };
}
async function loadWorkspaceSnapshot(ctx, workspaceId) {
const active = JSON.parse(await readFile(join(ctx.run.root, "installation", "registry", "state", "active.json"), "utf8"));
const revision = active.revisions.find((entry) => entry.id === workspaceId);
const snapshotPath = revision.snapshotPath;
const contents = await readFile(snapshotPath, "utf8");
return { active, revision, contents };
}
async function assertNoCoreFrontendRunning(ctx) {
const ps = await dockerCompose(ctx, ["ps", "--status", "running", "--format", "json"], { allowFailure: true });
if (ps.exitCode !== 0) return [];
const lines = ps.stdout.trim() === "" ? [] : ps.stdout.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
const services = lines.map((item) => item.Service);
if (services.includes("core") || services.includes("frontend") || services.includes("workspace-maintenance")) {
throw new Error("core/frontend/maintenance is unexpectedly running");
}
return services;
}
function sameSet(left, right) {
return JSON.stringify([...left].sort()) === JSON.stringify([...right].sort());
}
const EMBEDDING_STUB_SOURCE = String.raw`import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class _Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", "0"))
payload = json.loads(self.rfile.read(length))
inputs = payload.get("input", [])
if isinstance(inputs, str):
inputs = [inputs]
embeddings = [[0.01] * 1024 for _ in inputs]
body = json.dumps({"model": payload.get("model", "qwen3-embedding:0.6b"), "embeddings": embeddings}).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
HTTPServer(("0.0.0.0", 11434), _Handler).serve_forever()
`;
function realUserHome() {
try {
const output = execFileSync("bash", ["-lc", 'printf "%s" ~'], { encoding: "utf8" }).trim();
return output.length > 0 ? output : undefined;
} catch {
return undefined;
}
}
async function setupRealContext({ repositoryRoot = defaultRepositoryRoot, env = process.env }) {
const run = await createOwnedRun({ repositoryRoot });
const provenance = await collectRepositoryProvenance({ repositoryRoot });
const execs = {
gitPath: resolveSystemExecutable("git"),
dockerPath: resolveSystemExecutable("docker"),
bashPath: resolveSystemExecutable("bash"),
};
const pathValue = [...new Set([dirname(execs.gitPath), dirname(execs.dockerPath), "/usr/bin", "/bin", "/opt/homebrew/bin", "/usr/local/bin"])].join(":");
// Docker CLI plugins (buildx) live under the real user's ~/.docker; the wrapper runs with a
// scrubbed environment, so derive the real home from the passwd entry and expose DOCKER_CONFIG.
const realHome = env.P3_REAL_HOME ?? realUserHome();
const execEnv = buildSafeEnvironment({ ambient: env, fixture: {
PATH: pathValue,
HOME: run.root,
TMPDIR: join(run.root, "tmp"),
...(realHome ? { DOCKER_CONFIG: join(realHome, ".docker") } : {}),
} });
const workspaceModules = await import("../dist/workspaces/registry.js").then(async (registryModule) => ({
WorkspaceRegistry: registryModule.WorkspaceRegistry,
...(await import("../dist/workspaces/schema.js")),
}));
const ctx = {
run,
repositoryRoot: canonicalRoot(repositoryRoot),
provenance,
executables: execs,
execEnv,
workspaceModules,
forbiddenValues: [],
deviations: [],
servers: [],
};
await createTopology(run);
await mkdir(join(run.root, "tmp"), { recursive: true, mode: 0o700 });
await setupSecrets(ctx);
await setupFixtures(ctx);
return ctx;
}
async function executeChecksLocal({ checks, failAt } = {}) {
if (!Array.isArray(checks) || !hasExactCheckIds(checks)) throw new Error("scenarios must match the exact ordered check set");
if (failAt !== undefined && !CHECK_IDS.includes(failAt)) throw new Error("failure hook must name an exact check");
const results = [];
let stopped = false;
for (const scenario of checks) {
const startedAt = nowIso();
let result;
if (stopped) {
result = { id: scenario.id, status: "FAIL", startedAt, finishedAt: nowIso(), commands: [], artifacts: [], error: "Not executed after earlier failure." };
} else {
try {
const output = await scenario.run();
if (scenario.id === failAt) throw new Error("injected acceptance failure");
result = { id: scenario.id, status: "PASS", startedAt, finishedAt: nowIso(), commands: output.commands ?? [], artifacts: output.artifacts ?? [] };
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
result = { id: scenario.id, status: "FAIL", startedAt, finishedAt: nowIso(), commands: [], artifacts: [], error: `Acceptance scenario failed safely: ${detail}` };
stopped = true;
}
}
results.push(result);
}
return results;
}
async function syntheticChecks(ctx) {
const artifact = async (name, value) => {
const path = join(ctx.run.root, "logs", `${name}.json`);
await writeJson(path, value);
return await fileArtifact(ctx.run.root, relative(ctx.run.root, path));
};
return CHECK_IDS.map((id, index) => ({
id,
async run() {
return {
commands: [index === 0 ? "node" : "git"],
artifacts: [await artifact(id, { id, synthetic: true })],
};
},
}));
}
async function realChecks(ctx) {
const state = {};
return [
{
id: "preflight",
async run() {
await buildThothctl(ctx);
await buildCoreImage(ctx);
await writeInstallationFiles(ctx);
return {
commands: ["docker", "node", "git"],
artifacts: [
{ path: "logs/provenance.json", sha256: sha256(JSON.stringify(ctx.provenance)) },
],
};
},
},
{
id: "clean_state",
async run() {
await startServers(ctx);
await initializeGitAndRegistry(ctx);
await startQdrant(ctx);
await preprovisionCollection(ctx, "p2-dwh");
await preprovisionCollection(ctx, "p2-filesystem");
state.collectionsBefore = await listCollections(ctx);
state.runningServices = await assertNoCoreFrontendRunning(ctx);
await writeJson(join(ctx.run.root, "logs", "collections-before.json"), state.collectionsBefore);
return { commands: ["git", "docker"], artifacts: [await fileArtifact(ctx.run.root, "logs/collections-before.json")] };
},
},
{
id: "ownership",
async run() {
await readAndValidateOwnership({ repositoryRoot: ctx.repositoryRoot, runRoot: ctx.run.root, expectedNonce: ctx.run.nonce });
const installStat = await stat(ctx.installationPath);
assert(installStat.isFile(), "installation descriptor missing");
return { commands: ["git"], artifacts: [await fileArtifact(ctx.run.root, "ownership.json")] };
},
},
{
id: "effective_config_identity",
async run() {
const response = await runThothctlJson(ctx, "inspect-p2-dwh", ["workspace", "inspect", "--workspace", "p2-dwh"], 0);
assert(typeof response.payload.effectiveConfigIdentity === "string" && response.payload.effectiveConfigIdentity.startsWith("workspace://p2-dwh@v1:"), "effective config identity missing");
assert(/^sha256:[0-9a-f]{64}$/.test(response.payload.configFingerprint ?? ""), "config fingerprint missing");
assert(/^sha256:[0-9a-f]{64}$/.test(response.payload.inputFingerprint ?? ""), "input fingerprint missing");
const snapshot = await loadWorkspaceSnapshot(ctx, "p2-dwh");
assert(response.payload.workspaceId === "p2-dwh", "inspect workspace id mismatch");
assert(response.payload.workspaceRevision === snapshot.active.head, "inspect revision mismatch");
assert(`sha256:${sha256(snapshot.contents)}` === response.payload.descriptorBlob, "inspect descriptor mismatch");
state.inspect = response.payload;
return { commands: ["thothctl"], artifacts: response.artifacts };
},
},
{
id: "dwh_processing",
async run() {
const first = await runThothctlJson(ctx, "preprocess-dwh-first", ["workspace", "preprocess", "dwh", "--workspace", "p2-dwh"], 0);
assert(first.payload.status === "succeeded" && first.payload.code === "ok", "dwh first run failed");
const rerun = await runThothctlJson(ctx, "preprocess-dwh-rerun", ["workspace", "preprocess", "dwh", "--workspace", "p2-dwh"], 0);
const resume = await runThothctlJson(ctx, "preprocess-dwh-resume", ["workspace", "preprocess", "dwh", "--workspace", "p2-dwh", "--resume", first.payload.runId], 0);
assert(["unchanged", "succeeded"].includes(rerun.payload.status), "dwh rerun not idempotent");
assert(["unchanged", "succeeded"].includes(resume.payload.status), "dwh resume failed");
state.dwhRunId = first.payload.runId;
return { commands: ["thothctl"], artifacts: [...first.artifacts, ...rerun.artifacts, ...resume.artifacts] };
},
},
{
id: "schema_review",
async run() {
// FK suggestion consumes the workspace's own introspected physical schema and mines
// approved SQL joins for candidates.
await runThothctlJson(ctx, "preprocess-dwh-filesystem", ["workspace", "preprocess", "dwh", "--workspace", "p2-filesystem"], 0);
const sqlPath = join(ctx.run.root, "fixtures", "p2-filesystem.sql");
await atomicWrite(sqlPath, "SELECT v.id FROM dw.visits v JOIN dw.patients p ON v.patient_id = p.patient_id\n");
const suggest = await runThothctlJson(ctx, "schema-suggest-filesystem", ["workspace", "schema", "suggest-fks", "--workspace", "p2-filesystem", "--from-sql", sqlPath], 3);
assert(suggest.payload.code === "manual_review_required", "suggest did not block");
assert(typeof suggest.payload.suggestedFksYaml === "string" && suggest.payload.suggestedFksYaml.length > 0, "suggested FK YAML missing");
const digest = suggest.payload.artifactIdentities?.[0]?.digest;
assert(/^sha256:[0-9a-f]{64}$/.test(digest ?? ""), "candidate digest missing");
const candidatePath = join(ctx.run.root, "fixtures", "p2-filesystem.candidates.yaml");
await atomicWrite(candidatePath, suggest.payload.suggestedFksYaml);
assert(`sha256:${sha256(suggest.payload.suggestedFksYaml)}` === digest, "candidate digest mismatch");
const annotationsPath = join(ctx.run.root, "fixtures", "p2-filesystem.annotations.yaml");
await atomicWrite(annotationsPath, "tables: {}\n");
const checked = await runThothctlJson(ctx, "schema-check-filesystem", [
"workspace", "schema", "check", "--workspace", "p2-filesystem",
"--annotations", annotationsPath,
"--reviewed-candidates", digest,
], 0);
assert(checked.payload.status === "succeeded", "schema check failed");
state.filesystemCandidateDigest = digest;
return { commands: ["thothctl"], artifacts: [...suggest.artifacts, ...checked.artifacts, await fileArtifact(ctx.run.root, "fixtures/p2-filesystem.candidates.yaml"), await fileArtifact(ctx.run.root, "fixtures/p2-filesystem.annotations.yaml"), await fileArtifact(ctx.run.root, "fixtures/p2-filesystem.sql")] };
},
},
{
id: "schema_index",
async run() {
const first = await runThothctlJson(ctx, "index-schema-filesystem", ["workspace", "index-schema", "--workspace", "p2-filesystem"], 0);
const second = await runThothctlJson(ctx, "index-schema-filesystem-rerun", ["workspace", "index-schema", "--workspace", "p2-filesystem"], 0);
assert(["succeeded", "unchanged"].includes(first.payload.status), "index schema first failed");
assert(["unchanged", "succeeded"].includes(second.payload.status), "index schema rerun failed");
return { commands: ["thothctl"], artifacts: [...first.artifacts, ...second.artifacts] };
},
},
{
id: "evidence_processing",
async run() {
// Full-run FK checkpoint: the filesystem workspace already has mined FK candidates,
// so a full run must stop for human review before schema/Evidence writes.
const full = await runThothctlJson(ctx, "preprocess-run-fs-blocked", ["workspace", "preprocess", "run", "--workspace", "p2-filesystem"], 3);
assert(full.payload.code === "manual_review_required", "full run did not block for review");
const digest = full.payload.artifactIdentities?.[0]?.digest;
assert(/^sha256:[0-9a-f]{64}$/.test(digest ?? ""), "full run digest missing");
const reviewPath = join(ctx.run.root, "fixtures", "p2-filesystem.full-annotations.yaml");
await atomicWrite(reviewPath, "tables: {}\n");
const reviewed = await runThothctlJson(ctx, "schema-check-fs-full", [
"workspace", "schema", "check", "--workspace", "p2-filesystem",
"--annotations", reviewPath,
"--reviewed-candidates", digest,
], 0);
assert(reviewed.payload.status === "succeeded", "full-run review failed");
// Resume continues through index-schema and stops at filesystem Evidence materialization.
const resumed = await runThothctlJson(ctx, "preprocess-run-fs-resume", ["workspace", "preprocess", "run", "--workspace", "p2-filesystem", "--resume", full.payload.runId], 3);
assert(resumed.payload.code === "evidence_materialization_required", "filesystem evidence did not block after review");
// HTTP Evidence on the p2-dwh workspace: dry-run, publish, unchanged rerun, mutation.
const dryRun = await runThothctlJson(ctx, "preprocess-evidence-dry-run", ["workspace", "preprocess", "evidence", "--workspace", "p2-dwh", "--dry-run"], 0);
const publish = await runThothctlJson(ctx, "preprocess-evidence-publish", ["workspace", "preprocess", "evidence", "--workspace", "p2-dwh"], 0);
const rerun = await runThothctlJson(ctx, "preprocess-evidence-rerun", ["workspace", "preprocess", "evidence", "--workspace", "p2-dwh"], 0);
ctx.evidenceState.content = "# P2 Evidence\n\nSecond generation.\n";
const mutated = await runThothctlJson(ctx, "preprocess-evidence-mutated", ["workspace", "preprocess", "evidence", "--workspace", "p2-dwh"], 0);
state.fullRunId = full.payload.runId;
return { commands: ["thothctl"], artifacts: [
...full.artifacts, ...reviewed.artifacts, ...resumed.artifacts, ...dryRun.artifacts,
...publish.artifacts, ...rerun.artifacts, ...mutated.artifacts,
] };
},
},
{
id: "content_only_reuse",
async run() {
// A content-only Evidence change must NOT alter the effective configuration identity:
// the prepared DWH generation remains owned by the same canonical binding (no forced
// reconfiguration, no mixed artifacts). The engine re-runs the explicit introspection
// stage with a fresh timestamped physical.yaml, which is why the run reports succeeded.
const before = await runThothctlJson(ctx, "p3-content-only-before", ["workspace", "inspect", "--workspace", "p2-dwh"], 0);
await mutateWorkspaceDescriptor(ctx, "p2-dwh", () => { ctx.evidenceState.content = "# P2 Evidence\n\nThird generation (content-only).\n"; }, "Content-only Evidence change");
const after = await runThothctlJson(ctx, "p3-content-only-after", ["workspace", "inspect", "--workspace", "p2-dwh"], 0);
assert(before.payload.effectiveConfigIdentity === after.payload.effectiveConfigIdentity, "content-only change altered the effective config identity");
const rerun = await runThothctlJson(ctx, "p3-content-only-dwh-rerun", ["workspace", "preprocess", "dwh", "--workspace", "p2-dwh"], 0);
assert(rerun.payload.status !== "failed", "content-only change broke DWH preprocessing");
assert(rerun.payload.configFingerprint === after.payload.configFingerprint, "content-only change altered the config fingerprint");
return { commands: ["thothctl"], artifacts: [...before.artifacts, ...after.artifacts, ...rerun.artifacts] };
},
},
{
id: "dwh_change_fail_closed",
async run() {
const before = await runThothctlJson(ctx, "p2-dwh-before-change", ["workspace", "inspect", "--workspace", "p2-dwh"], 0);
await mutateWorkspaceDescriptor(ctx, "p2-dwh", (workspace) => { workspace.dwh.database = "warehouse2"; }, "Change DWH database");
const after = await runThothctlJson(ctx, "p2-dwh-after-change", ["workspace", "inspect", "--workspace", "p2-dwh"], 0);
assert(before.payload.configFingerprint !== after.payload.configFingerprint, "DWH-affecting change kept the same config fingerprint");
const rerun = await runThothctlJson(ctx, "p2-dwh-change-rerun", ["workspace", "preprocess", "dwh", "--workspace", "p2-dwh"], 1);
// Fail-closed: the harness must never silently reuse the old generation. It either
// regenerates for the new binding or refuses with a stable error.
assert(rerun.payload.status !== "unchanged", "DWH-affecting change silently reused the old generation");
return { commands: ["thothctl"], artifacts: [...before.artifacts, ...after.artifacts, ...rerun.artifacts] };
},
},
{
id: "memory_root",
async run() {
const manifests = join(ctx.run.root, "installation", "data", "sessions", "p2-dwh", "preprocessing", "runtime-config-manifests");
const files = (await readdir(manifests)).filter((name) => name.endsWith(".json"));
assert(files.length > 0, "no runtime config manifest");
const manifest = JSON.parse(await readFile(join(manifests, files[0]), "utf8"));
assert(typeof manifest.effectiveConfigIdentity === "string", "manifest lacks effectiveConfigIdentity");
assert(/^sha256:[0-9a-f]{64}$/.test(manifest.configFingerprint ?? ""), "manifest lacks configFingerprint");
assert(/^sha256:[0-9a-f]{64}$/.test(manifest.inputFingerprint ?? ""), "manifest lacks inputFingerprint");
return { commands: [], artifacts: [await fileArtifact(ctx.run.root, relative(ctx.run.root, join(manifests, files[0])))] };
},
},
{
id: "revision_scoped_records",
async run() {
// Schema records live in the filesystem workspace collection (index-schema ran there);
// Evidence records live in the p2-dwh collection (evidence preprocessing ran there).
const base = `http://127.0.0.1:${ctx.fixturePorts.qdrant}`;
const scroll = async (collection) => {
const res = await fetch(`${base}/collections/${collection}/points/scroll?limit=500`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ with_payload: true, with_vector: false }) });
const body = await res.json();
return body.result?.points ?? [];
};
const schema = (await scroll("p2-filesystem")).filter((p) => (p.payload?.record_kind ?? p.payload?.kind ?? "") === "schema_table");
const evidence = (await scroll("p2-dwh")).filter((p) => (p.payload?.record_kind ?? p.payload?.kind ?? "") === "evidence");
const memory = (await scroll("p2-filesystem")).filter((p) => (p.payload?.record_kind ?? p.payload?.kind ?? "") === "memory");
assert(schema.length > 0, "no revision-scoped schema records found");
assert(evidence.length > 0, "no revision-scoped evidence records found");
assert(schema.every((p) => /^[0-9a-f]{40}$/.test(p.payload?.workspace_revision ?? "")), "schema records lack workspace_revision");
assert(evidence.every((p) => /^[0-9a-f]{40}$/.test(p.payload?.workspace_revision ?? "")), "evidence records lack workspace_revision");
if (memory.length > 0) {
assert(memory.every((p) => p.payload?.workspace_revision === undefined), "memory records must stay workspace-wide");
}
return { commands: [], artifacts: [] };
},
},
{
id: "negative_cases",
async run() {
const missing = await runThothctlJson(ctx, "negative-missing-workspace", ["workspace", "inspect", "--workspace", "missing-workspace"], 1);
const resumeMismatch = await runThothctlJson(ctx, "negative-resume-mismatch", ["workspace", "preprocess", "dwh", "--workspace", "p2-dwh", "--resume", "0".repeat(32)], 1);
const annotationInvalid = await runThothctlJson(ctx, "negative-annotation-invalid", [
"workspace", "schema", "check", "--workspace", "p2-filesystem",
"--annotations", join(ctx.run.root, "fixtures", "p2-filesystem.annotations.yaml"),
"--reviewed-candidates", `sha256:${"0".repeat(64)}`,
], 1);
await mutateWorkspaceDescriptor(ctx, "p2-dwh", (workspace) => { delete workspace.evidence; }, "Remove P2 Evidence");
const noEvidence = await runThothctlJson(ctx, "negative-no-evidence-run", ["workspace", "preprocess", "evidence", "--workspace", "p2-dwh"], 0);
assert(["succeeded", "unchanged"].includes(noEvidence.payload.status), "no-evidence evidence did not skip");
assert(Array.isArray(noEvidence.payload.warnings) && noEvidence.payload.warnings.length > 0, "no-evidence warning missing");
const conflict = await runThothctlJson(ctx, "negative-revision-conflict", ["workspace", "preprocess", "run", "--workspace", "p2-dwh", "--resume", state.fullRunId], 1);
const after = await listCollections(ctx);
assert(sameSet(after, state.collectionsBefore), "product path created or removed a collection");
assert(missing.payload.code === "workspace_not_activatable" || missing.payload.code === "workspace_not_found", "missing workspace code mismatch");
assert(annotationInvalid.payload.code === "annotation_invalid", "annotation invalid code mismatch");
assert(noEvidence.payload.warnings?.includes("workspace has no Evidence source"), "no-Evidence warning missing");
// Resuming a foreign run is refused (resume mismatch). The different-revision
// resumable-session conflict is exercised at the unit level by the session-inventory guard.
assert(["preprocessing_conflict", "preprocessing_resume_mismatch"].includes(conflict.payload.code), "revision conflict code mismatch");
const inspectServices = await assertNoCoreFrontendRunning(ctx);
await writeJson(join(ctx.run.root, "logs", "services-after.json"), inspectServices);
return { commands: ["thothctl", "docker"], artifacts: [
...missing.artifacts, ...resumeMismatch.artifacts, ...annotationInvalid.artifacts,
...noEvidence.artifacts, ...conflict.artifacts, await fileArtifact(ctx.run.root, "logs/services-after.json"),
] };
},
},
{
id: "secret_scan",
async run() {
const virtualFiles = [];
const qdrantDump = await dumpQdrantPayloads(ctx, "p2-dwh");
if (Buffer.byteLength(qdrantDump) <= MAX_SECRET_SCAN_VIRTUAL_BYTES) virtualFiles.push({ path: "virtual/qdrant-p2-dwh.json", bytes: qdrantDump });
const findings = await scanSecrets({
runRoot: ctx.run.root,
forbiddenValues: ctx.forbiddenValues,
virtualFiles,
expectedGitRepositories: ["remote.git", "author"],
});
await writeJson(join(ctx.run.root, "logs", "secret-scan.json"), findings);
if (findings.length > 0) throw new Error(`secret scan found ${findings.length} leak(s)`);
return { commands: ["git"], artifacts: [await fileArtifact(ctx.run.root, "logs/secret-scan.json")] };
},
},
{
id: "cleanup_confinement",
async run() {
const foreignRoot = join(canonicalIntegrationBase(ctx.repositoryRoot), `p3-${"f".repeat(32)}`);
await mkdir(foreignRoot, { recursive: true });
await atomicWrite(join(foreignRoot, "foreign.txt"), "foreign");
assert(readFileSync(join(foreignRoot, "foreign.txt"), "utf8") === "foreign", "foreign sentinel changed unexpectedly");
return { commands: ["git"], artifacts: [] };
},
},
];
}
async function cleanupRuntime(ctx) {
await stopServers(ctx).catch(() => {});
if (ctx.installation) await dockerCompose(ctx, ["down", "--remove-orphans", "--timeout", "5"], { allowFailure: true }).catch(() => {});
if (ctx.coreImageTag) await runCommand({ executable: ctx.executables.dockerPath, argv: ["image", "rm", "-f", ctx.coreImageTag], env: ctx.execEnv, maxOutputBytes: MAX_STDIO_BYTES }).catch(() => {});
}
export async function runIntegration({ repositoryRoot = defaultRepositoryRoot, keep = false, env = process.env, announce } = {}) {
const synthetic = env.P3_ACCEPTANCE_SYNTHETIC === "1";
const failAt = env.P3_ACCEPTANCE_FAIL_AT;
const ctx = synthetic
? { run: await createOwnedRun({ repositoryRoot }), repositoryRoot: canonicalRoot(repositoryRoot) }
: await setupRealContext({ repositoryRoot, env });
let success = false;
try {
const checks = synthetic ? await syntheticChecks(ctx) : await realChecks(ctx);
const results = await executeChecksLocal({ checks, failAt });
const report = {
schemaVersion: 1,
runId: ctx.run.runId,
startedAt: ctx.run.startedAt,
finishedAt: nowIso(),
command: "p3-acceptance integration --keep",
overall: deriveOverall(results),
checks: results,
};
await writeReportFiles({ run: ctx.run, report });
success = report.overall === "PASS";
if (announce) await announce({ report, runRoot: ctx.run.root });
return { exitCode: success ? 0 : 1, runRoot: ctx.run.root, retained: !(await finalizeOwnedRun({ run: ctx.run, success, keep })) };
} finally {
if (!synthetic) await cleanupRuntime(ctx).catch(() => {});
}
}
export async function main(argv = process.argv.slice(2), env = process.env) {
if (argv.length < 1 || argv[0] !== "integration" || argv.length > 2 || (argv[1] && argv[1] !== "--keep")) {
throw new Error("usage: p3-acceptance.mjs integration [--keep]");
}
const result = await runIntegration({ keep: argv.includes("--keep"), env });
return result.exitCode;
}
if (process.argv[1] && realpathSync(process.argv[1]) === modulePath) {
try {
const code = await main();
process.exitCode = code;
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
export { CHECK_IDS };