assertledger 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.fr.md +5 -2
- package/README.md +5 -2
- package/conformance/schema-extensions.json +15 -0
- package/dist/build-info.json +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +213 -8
- package/dist/cli.js.map +1 -1
- package/dist/contracts/index.d.ts +467 -4
- package/dist/contracts/index.d.ts.map +1 -1
- package/dist/contracts/index.js +256 -3
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/qualification.d.ts +528 -0
- package/dist/contracts/qualification.d.ts.map +1 -0
- package/dist/contracts/qualification.js +357 -0
- package/dist/contracts/qualification.js.map +1 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +46 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/qualification.d.ts +17 -0
- package/dist/core/qualification.d.ts.map +1 -0
- package/dist/core/qualification.js +571 -0
- package/dist/core/qualification.js.map +1 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +81 -4
- package/dist/diagnostics.js.map +1 -1
- package/dist/engine/adapters/bun-native.d.ts +35 -0
- package/dist/engine/adapters/bun-native.d.ts.map +1 -0
- package/dist/engine/adapters/bun-native.js +179 -0
- package/dist/engine/adapters/bun-native.js.map +1 -0
- package/dist/engine/adapters/bun-test-designated-profile.d.ts +23 -0
- package/dist/engine/adapters/bun-test-designated-profile.d.ts.map +1 -0
- package/dist/engine/adapters/bun-test-designated-profile.js +24 -0
- package/dist/engine/adapters/bun-test-designated-profile.js.map +1 -0
- package/dist/engine/container.d.ts +7 -0
- package/dist/engine/container.d.ts.map +1 -1
- package/dist/engine/container.js +22 -7
- package/dist/engine/container.js.map +1 -1
- package/dist/engine/git-regression.d.ts.map +1 -1
- package/dist/engine/git-regression.js +9 -2
- package/dist/engine/git-regression.js.map +1 -1
- package/dist/engine/index.d.ts +14 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +828 -97
- package/dist/engine/index.js.map +1 -1
- package/dist/engine/qualification.d.ts +154 -0
- package/dist/engine/qualification.d.ts.map +1 -0
- package/dist/engine/qualification.js +725 -0
- package/dist/engine/qualification.js.map +1 -0
- package/dist/engine/test-closure.d.ts +39 -0
- package/dist/engine/test-closure.d.ts.map +1 -0
- package/dist/engine/test-closure.js +513 -0
- package/dist/engine/test-closure.js.map +1 -0
- package/dist/engine/witness-import.d.ts +31 -0
- package/dist/engine/witness-import.d.ts.map +1 -0
- package/dist/engine/witness-import.js +224 -0
- package/dist/engine/witness-import.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +65 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/sdk/index.d.ts +292 -3
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +39 -1
- package/dist/sdk/index.js.map +1 -1
- package/docs/container-isolation.md +6 -0
- package/docs/migration-orchestration-qualification-v1.md +109 -0
- package/docs/migration-verification-v4.md +94 -0
- package/docs/orchestration-qualification.md +167 -0
- package/docs/reference.md +7 -0
- package/docs/witness-import.md +67 -0
- package/examples/orchestration/README.md +68 -0
- package/examples/orchestration/consumer-obligations.json +101 -0
- package/examples/orchestration/workspace/bitbucket-pipelines.yml +10 -0
- package/examples/orchestration/workspace/command-shape.test.mjs +25 -0
- package/examples/orchestration/workspace/commands.mjs +14 -0
- package/examples/orchestration/workspace/completion.mjs +39 -0
- package/examples/orchestration/workspace/faults.json +1 -0
- package/examples/orchestration/workspace/native.test.ts +4 -0
- package/examples/orchestration/workspace/package.json +6 -0
- package/examples/orchestration/workspace/packages/app/package.json +12 -0
- package/examples/orchestration/workspace/packages/app/verdict.txt +1 -0
- package/examples/orchestration/workspace/packages/leaf/package.json +9 -0
- package/examples/orchestration/workspace/packages/leaf/verdict.txt +1 -0
- package/examples/orchestration/workspace/pnpm-lock.yaml +170 -0
- package/examples/orchestration/workspace/pnpm-workspace.yaml +2 -0
- package/examples/orchestration/workspace/task.mjs +23 -0
- package/examples/orchestration/workspace/turbo.json +38 -0
- package/examples/orchestration/workspace/wrapper.mjs +45 -0
- package/integrations/bun/driver.d.mts +30 -1
- package/integrations/bun/driver.mjs +215 -6
- package/integrations/bun/preload.mjs +186 -5
- package/integrations/bun-native/README.md +86 -0
- package/integrations/bun-native/driver.mjs +396 -0
- package/integrations/bun-native/preload.mjs +375 -0
- package/integrations/skill/SKILL.md +25 -5
- package/package.json +1 -1
- package/schemas/evidence-manifest.v4.json +995 -0
- package/schemas/qualification-execution-request.v1.json +571 -0
- package/schemas/qualification-plan.v1.json +528 -0
- package/schemas/qualification-receipt.v1.json +821 -0
- package/schemas/qualification-replay-result.v1.json +37 -0
- package/schemas/verification-request.v4.json +590 -0
- package/schemas/witness-import-request.v1.json +231 -0
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile, } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { parseQualificationPlan, QUALIFICATION_ADAPTER_VERSIONS, QualificationExecutionRequestSchema, QUALIFICATION_SNAPSHOT_EXCLUDES, } from "../contracts/qualification.js";
|
|
8
|
+
import { canonicalize, sha256Canonical } from "../core/index.js";
|
|
9
|
+
import { createQualificationReceipt, qualificationBinding } from "../core/qualification.js";
|
|
10
|
+
import { ASSERTLEDGER_VERSION } from "../version.js";
|
|
11
|
+
import { collectBunNative } from "./adapters/bun-native.js";
|
|
12
|
+
import { runProcess } from "./index.js";
|
|
13
|
+
import { NODE_TEST_REPORTER_SOURCE } from "./node-test-reporter.js";
|
|
14
|
+
export const ORCHESTRATION_ADAPTER_VERSION = QUALIFICATION_ADAPTER_VERSIONS.command;
|
|
15
|
+
const EXCLUDED_ROOTS = new Set(QUALIFICATION_SNAPSHOT_EXCLUDES);
|
|
16
|
+
const MAXIMUM_INPUT_BYTES = 32 * 1024 * 1024;
|
|
17
|
+
const MAXIMUM_FILES = 10_000;
|
|
18
|
+
const COMMAND_TERMINAL_OBSERVER = `import { writeFileSync } from 'node:fs';
|
|
19
|
+
const output = process.env.ASSERTLEDGER_QUALIFICATION_TERMINAL_FILE;
|
|
20
|
+
const nonce = process.env.ASSERTLEDGER_QUALIFICATION_NONCE;
|
|
21
|
+
const runtime = process.versions.bun ? 'bun' : 'node';
|
|
22
|
+
const runtimeVersion = process.versions.bun || process.versions.node;
|
|
23
|
+
let exceptional = false, natural = false, explicit = false, code = null;
|
|
24
|
+
const persist = () => writeFileSync(output, JSON.stringify({exceptional,exitCode:code,explicit,natural,nonce,protocolVersion:'1.0.0',ready:true,runtime,runtimeVersion})+'\\n');
|
|
25
|
+
process.on('uncaughtExceptionMonitor', () => { exceptional = true; persist(); });
|
|
26
|
+
process.on('beforeExit', () => { natural = true; });
|
|
27
|
+
process.on('exit', value => { code = value; persist(); });
|
|
28
|
+
const originalExit = process.exit.bind(process);
|
|
29
|
+
process.exit = value => { explicit = true; return originalExit(value); };
|
|
30
|
+
persist();
|
|
31
|
+
`;
|
|
32
|
+
export async function qualificationFileDigest(file) {
|
|
33
|
+
return `sha256:${createHash("sha256")
|
|
34
|
+
.update(await readFile(file))
|
|
35
|
+
.digest("hex")}`;
|
|
36
|
+
}
|
|
37
|
+
/** Pin a standalone executable, or the complete transitive distribution of a JavaScript tool. */
|
|
38
|
+
export async function qualificationToolDigest(identityPath) {
|
|
39
|
+
const resolved = await realpath(identityPath);
|
|
40
|
+
const metadata = await lstat(resolved);
|
|
41
|
+
return metadata.isDirectory()
|
|
42
|
+
? qualificationRepositoryDigest(resolved)
|
|
43
|
+
: qualificationFileDigest(resolved);
|
|
44
|
+
}
|
|
45
|
+
/** Identity of the loaded decision/collection implementation, independent of its package version. */
|
|
46
|
+
export async function qualificationMechanismDigest() {
|
|
47
|
+
const sourceExtension = import.meta.url.endsWith(".ts") ? ".ts" : ".js";
|
|
48
|
+
const relative = [
|
|
49
|
+
"./qualification.js",
|
|
50
|
+
"./index.js",
|
|
51
|
+
"./node-test-reporter.js",
|
|
52
|
+
"./adapters/bun-native.js",
|
|
53
|
+
"../core/qualification.js",
|
|
54
|
+
"../core/index.js",
|
|
55
|
+
"../contracts/qualification.js",
|
|
56
|
+
"../cli.js",
|
|
57
|
+
"../sdk/index.js",
|
|
58
|
+
"../mcp/index.js",
|
|
59
|
+
"../../integrations/bun-native/driver.mjs",
|
|
60
|
+
"../../integrations/bun-native/preload.mjs",
|
|
61
|
+
];
|
|
62
|
+
const identities = [];
|
|
63
|
+
for (const name of relative) {
|
|
64
|
+
const extension = name.endsWith(".mjs") ? name : name.replace(/\.js$/u, sourceExtension);
|
|
65
|
+
identities.push({
|
|
66
|
+
path: name,
|
|
67
|
+
digest: await qualificationFileDigest(fileURLToPath(new URL(extension, import.meta.url))),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
identities.push({
|
|
71
|
+
path: "engine-node-binary",
|
|
72
|
+
digest: await qualificationFileDigest(await realpath(process.execPath)),
|
|
73
|
+
});
|
|
74
|
+
const require = createRequire(import.meta.url);
|
|
75
|
+
const zodRoot = path.dirname(require.resolve("zod/package.json"));
|
|
76
|
+
identities.push({ path: "zod-distribution", digest: await qualificationToolDigest(zodRoot) });
|
|
77
|
+
return sha256Canonical(identities);
|
|
78
|
+
}
|
|
79
|
+
function confinedPath(root, relative) {
|
|
80
|
+
if (!relative || relative.includes("\\") || relative.startsWith("/") || relative.includes(":")) {
|
|
81
|
+
throw new Error("QUALIFICATION_PATH_INVALID");
|
|
82
|
+
}
|
|
83
|
+
const components = relative.split("/");
|
|
84
|
+
if (components.some((part) => part === "" || part === "." || part === "..")) {
|
|
85
|
+
throw new Error("QUALIFICATION_PATH_INVALID");
|
|
86
|
+
}
|
|
87
|
+
return path.join(root, ...components);
|
|
88
|
+
}
|
|
89
|
+
async function snapshotInputs(root) {
|
|
90
|
+
const files = [];
|
|
91
|
+
let bytes = 0;
|
|
92
|
+
async function visit(relative) {
|
|
93
|
+
const directory = relative === "" ? root : confinedPath(root, relative);
|
|
94
|
+
for (const entry of (await readdir(directory)).sort()) {
|
|
95
|
+
if (relative === "" && EXCLUDED_ROOTS.has(entry))
|
|
96
|
+
continue;
|
|
97
|
+
const name = relative === "" ? entry : `${relative}/${entry}`;
|
|
98
|
+
const target = confinedPath(root, name);
|
|
99
|
+
const metadata = await lstat(target);
|
|
100
|
+
if (metadata.isSymbolicLink())
|
|
101
|
+
throw new Error("QUALIFICATION_INPUT_SYMLINK");
|
|
102
|
+
if (metadata.isDirectory())
|
|
103
|
+
await visit(name);
|
|
104
|
+
else if (metadata.isFile()) {
|
|
105
|
+
const content = await readFile(target);
|
|
106
|
+
bytes += content.byteLength;
|
|
107
|
+
if (bytes > MAXIMUM_INPUT_BYTES || files.length >= MAXIMUM_FILES) {
|
|
108
|
+
throw new Error("QUALIFICATION_INPUT_BUDGET");
|
|
109
|
+
}
|
|
110
|
+
files.push({
|
|
111
|
+
path: name,
|
|
112
|
+
content,
|
|
113
|
+
digest: `sha256:${createHash("sha256").update(content).digest("hex")}`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else
|
|
117
|
+
throw new Error("QUALIFICATION_INPUT_NOT_REGULAR");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
await visit("");
|
|
121
|
+
return files;
|
|
122
|
+
}
|
|
123
|
+
export async function qualificationRepositoryDigest(root) {
|
|
124
|
+
const files = await snapshotInputs(await realpath(root));
|
|
125
|
+
return sha256Canonical(files.map(({ path: file, digest }) => ({ path: file, digest })));
|
|
126
|
+
}
|
|
127
|
+
async function overlay(root, files) {
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
const target = confinedPath(root, file.path);
|
|
130
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
131
|
+
await writeFile(target, file.content, { flag: "w" });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async function boundedFile(file, limit) {
|
|
135
|
+
const metadata = await lstat(file);
|
|
136
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > limit) {
|
|
137
|
+
throw new Error("QUALIFICATION_REPORT_INVALID");
|
|
138
|
+
}
|
|
139
|
+
return readFile(file, "utf8");
|
|
140
|
+
}
|
|
141
|
+
function completedState(result) {
|
|
142
|
+
if (result.timedOut)
|
|
143
|
+
return "TIMEOUT";
|
|
144
|
+
if (result.error !== undefined)
|
|
145
|
+
return "INFRA_ERROR";
|
|
146
|
+
if (result.signal !== null || result.exitCode === null)
|
|
147
|
+
return "CRASH";
|
|
148
|
+
// Numeric termination alone is only a process fact. Nonzero command completion is
|
|
149
|
+
// admitted below only with a fresh structured semantic attestation.
|
|
150
|
+
return "COMPLETED";
|
|
151
|
+
}
|
|
152
|
+
async function readTrace(file, nonce, maximumBytes) {
|
|
153
|
+
const text = await boundedFile(file, maximumBytes);
|
|
154
|
+
const rows = [];
|
|
155
|
+
if (text !== "") {
|
|
156
|
+
if (!text.endsWith("\n"))
|
|
157
|
+
throw new Error("QUALIFICATION_TRACE_INCOMPLETE");
|
|
158
|
+
for (const line of text.trimEnd().split("\n")) {
|
|
159
|
+
const value = JSON.parse(line);
|
|
160
|
+
if (value.nonce !== nonce ||
|
|
161
|
+
typeof value.task !== "string" ||
|
|
162
|
+
typeof value.event !== "string" ||
|
|
163
|
+
Object.keys(value).sort().join(",") !== "event,nonce,task") {
|
|
164
|
+
throw new Error("QUALIFICATION_TRACE_INVALID");
|
|
165
|
+
}
|
|
166
|
+
rows.push(value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
executedTasks: rows
|
|
171
|
+
.filter((row) => row.event === "start")
|
|
172
|
+
.map((row) => row.task)
|
|
173
|
+
.sort(),
|
|
174
|
+
events: rows.map((row) => `${row.task}:${row.event}`),
|
|
175
|
+
traceDigest: `sha256:${createHash("sha256").update(text).digest("hex")}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
async function turboFacts(root, maximumBytes) {
|
|
179
|
+
const directory = path.join(root, ".turbo", "runs");
|
|
180
|
+
const files = (await readdir(directory)).filter((file) => file.endsWith(".json"));
|
|
181
|
+
if (files.length !== 1)
|
|
182
|
+
throw new Error("QUALIFICATION_TURBO_SUMMARY_MISSING_OR_AMBIGUOUS");
|
|
183
|
+
const text = await boundedFile(path.join(directory, files[0] ?? ""), maximumBytes);
|
|
184
|
+
const summary = JSON.parse(text);
|
|
185
|
+
if (!Array.isArray(summary.tasks) || summary.tasks.length === 0)
|
|
186
|
+
throw new Error("QUALIFICATION_TURBO_NO_TASKS");
|
|
187
|
+
const selectedTasks = [];
|
|
188
|
+
const taskHashes = {};
|
|
189
|
+
const cache = {};
|
|
190
|
+
for (const entry of summary.tasks) {
|
|
191
|
+
if (typeof entry !== "object" ||
|
|
192
|
+
entry === null ||
|
|
193
|
+
typeof entry.taskId !== "string" ||
|
|
194
|
+
typeof entry.hash !== "string" ||
|
|
195
|
+
typeof entry.cache !== "object" ||
|
|
196
|
+
entry.cache === null ||
|
|
197
|
+
typeof entry.cache.status !== "string" ||
|
|
198
|
+
selectedTasks.includes(entry.taskId)) {
|
|
199
|
+
throw new Error("QUALIFICATION_TURBO_SUMMARY_INVALID");
|
|
200
|
+
}
|
|
201
|
+
selectedTasks.push(entry.taskId);
|
|
202
|
+
taskHashes[entry.taskId] = entry.hash;
|
|
203
|
+
cache[entry.taskId] = entry.cache.status;
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
selectedTasks: selectedTasks.sort(),
|
|
207
|
+
taskHashes,
|
|
208
|
+
cache,
|
|
209
|
+
summaryDigest: `sha256:${createHash("sha256").update(text).digest("hex")}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// The parser is an adapter asset outside every candidate/world. YAML structure, never substring
|
|
213
|
+
// presence, supplies routes. This observation still says nothing about hosted Bitbucket execution.
|
|
214
|
+
const CI_PARSER = `
|
|
215
|
+
import { readFile } from "node:fs/promises";
|
|
216
|
+
const value = Bun.YAML.parse(await readFile(process.argv[2], "utf8"));
|
|
217
|
+
if (!value || typeof value !== "object" || !value.pipelines) throw new Error("CI_PIPELINES_MISSING");
|
|
218
|
+
const routes = {};
|
|
219
|
+
function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
|
|
220
|
+
function keys(value, allowed) {
|
|
221
|
+
if (!object(value) || Object.keys(value).some(key => !allowed.includes(key))) throw new Error("CI_EXECUTION_FIELD_UNSUPPORTED");
|
|
222
|
+
}
|
|
223
|
+
keys(value, ["definitions", "pipelines"]);
|
|
224
|
+
function configuration(value, allowed) {
|
|
225
|
+
keys(value, allowed);
|
|
226
|
+
if (value.trigger !== undefined && !["automatic", "manual"].includes(value.trigger)) throw new Error("CI_TRIGGER_UNSUPPORTED");
|
|
227
|
+
if (value.condition !== undefined && !object(value.condition)) throw new Error("CI_CONDITION_INVALID");
|
|
228
|
+
const { script, steps, ...rest } = value;
|
|
229
|
+
return { ...rest, condition: value.condition ?? null, trigger: value.trigger ?? "automatic" };
|
|
230
|
+
}
|
|
231
|
+
function steps(items, ancestry) {
|
|
232
|
+
if (!Array.isArray(items)) throw new Error("CI_ROUTE_INVALID");
|
|
233
|
+
const result = [];
|
|
234
|
+
for (const [index, item] of items.entries()) {
|
|
235
|
+
keys(item, ["step", "parallel", "stage"]);
|
|
236
|
+
if (Object.keys(item).length !== 1) throw new Error("CI_STEP_AMBIGUOUS");
|
|
237
|
+
if (item.step) {
|
|
238
|
+
if (!Array.isArray(item.step.script) || !item.step.script.every(x => typeof x === "string")) throw new Error("CI_STEP_INVALID");
|
|
239
|
+
const config = configuration(item.step, ["name", "script", "condition", "trigger"]);
|
|
240
|
+
result.push({ name: item.step.name ?? "", commands: item.step.script, ancestry: [...ancestry, {kind: "step", index, configuration: config}] });
|
|
241
|
+
} else if (item.parallel) {
|
|
242
|
+
const group = Array.isArray(item.parallel) ? { steps: item.parallel } : item.parallel;
|
|
243
|
+
const config = configuration(group, ["steps", "fail-fast", "condition", "trigger"]);
|
|
244
|
+
result.push(...steps(group.steps, [...ancestry, {kind: "parallel", index, configuration: config}]));
|
|
245
|
+
} else if (item.stage) {
|
|
246
|
+
const config = configuration(item.stage, ["name", "steps", "condition", "trigger"]);
|
|
247
|
+
result.push(...steps(item.stage.steps, [...ancestry, {kind: "stage", index, configuration: config}]));
|
|
248
|
+
}
|
|
249
|
+
else throw new Error("CI_STEP_UNSUPPORTED");
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
for (const [kind, entries] of Object.entries(value.pipelines)) {
|
|
254
|
+
if (!["default", "branches", "pull-requests", "tags", "custom"].includes(kind)) throw new Error("CI_PIPELINE_KIND_UNSUPPORTED");
|
|
255
|
+
if (Array.isArray(entries)) {
|
|
256
|
+
if (kind !== "default") throw new Error("CI_ROUTE_KIND_INVALID");
|
|
257
|
+
routes[kind] = steps(entries, [{kind: "route", route: kind}]);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
if (kind === "default") throw new Error("CI_ROUTE_KIND_INVALID");
|
|
261
|
+
if (!object(entries)) throw new Error("CI_ROUTE_INVALID");
|
|
262
|
+
for (const [selector, items] of Object.entries(entries)) {
|
|
263
|
+
const route = kind + ":" + selector;
|
|
264
|
+
routes[route] = steps(items, [{kind: "route", route}]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
console.log(JSON.stringify({ ciRoutes: routes }));
|
|
269
|
+
`;
|
|
270
|
+
const NODE_SUITE_REPORTER = String.raw `
|
|
271
|
+
import path from "node:path";
|
|
272
|
+
import baseReporter from "./node-base.mjs";
|
|
273
|
+
export default async function* reporter(source) {
|
|
274
|
+
const files = new Set();
|
|
275
|
+
const assertionFailureFiles = new Set();
|
|
276
|
+
async function* observed() {
|
|
277
|
+
for await (const event of source) {
|
|
278
|
+
const data = event?.data;
|
|
279
|
+
if (event?.type === "test:dequeue" && data?.type === "test" && typeof data.file === "string" &&
|
|
280
|
+
(typeof data.name !== "string" || path.resolve(data.name) !== path.resolve(data.file))) {
|
|
281
|
+
files.add(path.relative(process.env.ASSERTLEDGER_QUALIFICATION_WORKSPACE, data.file).split(path.sep).join("/"));
|
|
282
|
+
}
|
|
283
|
+
if (event?.type === "test:fail" && typeof data?.file === "string" &&
|
|
284
|
+
data.details?.error?.failureType === "testCodeFailure" &&
|
|
285
|
+
data.details.error.cause?.code === "ERR_ASSERTION") {
|
|
286
|
+
assertionFailureFiles.add(path.relative(process.env.ASSERTLEDGER_QUALIFICATION_WORKSPACE, data.file).split(path.sep).join("/"));
|
|
287
|
+
}
|
|
288
|
+
yield event;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
for await (const chunk of baseReporter(observed())) {
|
|
292
|
+
yield JSON.stringify({ ...JSON.parse(chunk), testFiles: [...files].sort(), assertionFailureFiles: [...assertionFailureFiles].sort() }) + "\n";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
`;
|
|
296
|
+
async function executeAction(action, root, control, cache, plan) {
|
|
297
|
+
await overlay(root, action.prepareFiles);
|
|
298
|
+
for (const relative of action.removePaths)
|
|
299
|
+
await rm(confinedPath(root, relative), { recursive: true, force: true });
|
|
300
|
+
const nonce = randomUUID();
|
|
301
|
+
const trace = path.join(control, `${nonce}.jsonl`);
|
|
302
|
+
await writeFile(trace, "");
|
|
303
|
+
const replace = (value) => value
|
|
304
|
+
.replaceAll("{cache}", cache)
|
|
305
|
+
.replaceAll("{workspace}", root)
|
|
306
|
+
.replaceAll("{trace}", trace)
|
|
307
|
+
.replaceAll("{nonce}", nonce);
|
|
308
|
+
const environment = Object.fromEntries(Object.entries(action.environment).map(([key, value]) => [key, replace(value)]));
|
|
309
|
+
Object.assign(environment, {
|
|
310
|
+
ASSERTLEDGER_QUALIFICATION_WORKSPACE: root,
|
|
311
|
+
ASSERTLEDGER_QUALIFICATION_TRACE: trace,
|
|
312
|
+
ASSERTLEDGER_QUALIFICATION_NONCE: nonce,
|
|
313
|
+
ASSERTLEDGER_QUALIFICATION_CACHE: cache,
|
|
314
|
+
});
|
|
315
|
+
const structuredResult = path.join(control, `${nonce}.result.json`);
|
|
316
|
+
environment.ASSERTLEDGER_QUALIFICATION_RESULT_FILE = structuredResult;
|
|
317
|
+
const terminalResult = path.join(control, `${nonce}.terminal.json`);
|
|
318
|
+
let facts = {};
|
|
319
|
+
let state;
|
|
320
|
+
let tapReport;
|
|
321
|
+
if (action.adapter === "bun-native") {
|
|
322
|
+
const result = await collectBunNative({
|
|
323
|
+
executable: action.executable,
|
|
324
|
+
files: action.arguments,
|
|
325
|
+
cwd: root,
|
|
326
|
+
environment,
|
|
327
|
+
timeoutMs: plan.timeoutMs,
|
|
328
|
+
maximumOutputBytes: plan.maximumOutputBytes,
|
|
329
|
+
});
|
|
330
|
+
state = result.state;
|
|
331
|
+
facts = { ...result.facts, adapterRuntime: result.runtime };
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
let args = action.arguments.map(replace);
|
|
335
|
+
let report;
|
|
336
|
+
let opaqueCommand = false;
|
|
337
|
+
if (action.adapter === "command") {
|
|
338
|
+
const probe = await runProcess({
|
|
339
|
+
executable: action.executable,
|
|
340
|
+
args: ["-e", "console.log(JSON.stringify({runtime:process.versions.bun?'bun':'node'}))"],
|
|
341
|
+
cwd: root,
|
|
342
|
+
environment: {},
|
|
343
|
+
timeoutMs: plan.timeoutMs,
|
|
344
|
+
maximumOutputBytes: plan.maximumOutputBytes,
|
|
345
|
+
});
|
|
346
|
+
let runtime;
|
|
347
|
+
try {
|
|
348
|
+
runtime = JSON.parse(probe.stdout.text).runtime;
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
/* Unsupported runtime. */
|
|
352
|
+
}
|
|
353
|
+
if (completedState(probe) !== "COMPLETED" ||
|
|
354
|
+
probe.exitCode !== 0 ||
|
|
355
|
+
probe.stdout.truncated ||
|
|
356
|
+
probe.stderr.truncated ||
|
|
357
|
+
(runtime !== "node" && runtime !== "bun")) {
|
|
358
|
+
opaqueCommand = true;
|
|
359
|
+
if (action.observe.trace ||
|
|
360
|
+
action.observe.outputs.length !== 0 ||
|
|
361
|
+
action.observe.report !== null)
|
|
362
|
+
return {
|
|
363
|
+
state: "COLLECTION_ERROR",
|
|
364
|
+
facts: { terminalOutcome: "UNSUPPORTED_OBSERVATION" },
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
const observer = path.join(control, "command-terminal.mjs");
|
|
369
|
+
await writeFile(observer, COMMAND_TERMINAL_OBSERVER);
|
|
370
|
+
environment.ASSERTLEDGER_QUALIFICATION_TERMINAL_FILE = terminalResult;
|
|
371
|
+
args = [
|
|
372
|
+
runtime === "node" ? "--import" : "--preload",
|
|
373
|
+
runtime === "node" ? pathToFileURL(observer).href : observer,
|
|
374
|
+
...args,
|
|
375
|
+
];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else if (action.adapter === "node-test") {
|
|
379
|
+
report = path.join(control, `${nonce}.node.json`);
|
|
380
|
+
tapReport = path.join(control, `${nonce}.tap`);
|
|
381
|
+
const reporter = path.join(control, "node-reporter.mjs");
|
|
382
|
+
await writeFile(path.join(control, "node-base.mjs"), NODE_TEST_REPORTER_SOURCE);
|
|
383
|
+
await writeFile(reporter, NODE_SUITE_REPORTER);
|
|
384
|
+
environment.TESTFORGE_NODE_CANDIDATE_FILES = JSON.stringify(action.arguments.map((file) => confinedPath(root, file)));
|
|
385
|
+
args = [
|
|
386
|
+
"--test",
|
|
387
|
+
`--test-reporter=${pathToFileURL(reporter).href}`,
|
|
388
|
+
`--test-reporter-destination=${report}`,
|
|
389
|
+
"--test-reporter=tap",
|
|
390
|
+
`--test-reporter-destination=${tapReport}`,
|
|
391
|
+
"--",
|
|
392
|
+
...action.arguments,
|
|
393
|
+
];
|
|
394
|
+
}
|
|
395
|
+
else if (action.adapter === "ci-config") {
|
|
396
|
+
const parser = path.join(control, "ci-parser.mjs");
|
|
397
|
+
await writeFile(parser, CI_PARSER);
|
|
398
|
+
args = [parser, ...args];
|
|
399
|
+
}
|
|
400
|
+
else if (action.adapter === "turbo") {
|
|
401
|
+
await rm(path.join(root, ".turbo", "runs"), { recursive: true, force: true });
|
|
402
|
+
const separator = action.arguments.indexOf("--");
|
|
403
|
+
const declared = separator < 0 ? action.arguments : action.arguments.slice(0, separator);
|
|
404
|
+
const directoryFlags = declared.filter((value) => value === "--cache-dir" || value.startsWith("--cache-dir="));
|
|
405
|
+
const cacheFlags = declared.filter((value) => value === "--cache" || value.startsWith("--cache="));
|
|
406
|
+
const summaryFlags = declared.filter((value) => value === "--summarize" || value.startsWith("--summarize="));
|
|
407
|
+
const directoryIndex = declared.indexOf("--cache-dir");
|
|
408
|
+
const controlledDirectory = declared.includes("--cache-dir={cache}") ||
|
|
409
|
+
(directoryIndex >= 0 && declared[directoryIndex + 1] === "{cache}");
|
|
410
|
+
if (summaryFlags.length !== 1 ||
|
|
411
|
+
summaryFlags[0] !== "--summarize" ||
|
|
412
|
+
directoryFlags.length !== 1 ||
|
|
413
|
+
cacheFlags.length !== 1 ||
|
|
414
|
+
!controlledDirectory ||
|
|
415
|
+
cacheFlags[0] !== "--cache=local:rw")
|
|
416
|
+
throw new Error("QUALIFICATION_TURBO_CACHE_NOT_CONTROLLED");
|
|
417
|
+
}
|
|
418
|
+
const processResult = await runProcess({
|
|
419
|
+
executable: action.executable,
|
|
420
|
+
args,
|
|
421
|
+
cwd: root,
|
|
422
|
+
environment,
|
|
423
|
+
timeoutMs: plan.timeoutMs,
|
|
424
|
+
maximumOutputBytes: plan.maximumOutputBytes,
|
|
425
|
+
});
|
|
426
|
+
state = completedState(processResult);
|
|
427
|
+
facts = {
|
|
428
|
+
exitCode: processResult.exitCode,
|
|
429
|
+
stdoutDigest: processResult.stdout.digest,
|
|
430
|
+
stderrDigest: processResult.stderr.digest,
|
|
431
|
+
};
|
|
432
|
+
if (action.adapter === "command" && state === "COMPLETED" && processResult.exitCode === 0)
|
|
433
|
+
facts.commandOutcome = "PASS";
|
|
434
|
+
if (processResult.stdout.truncated || processResult.stderr.truncated)
|
|
435
|
+
state = "COLLECTION_ERROR";
|
|
436
|
+
if (opaqueCommand) {
|
|
437
|
+
if (state === "COMPLETED" && processResult.exitCode === 0)
|
|
438
|
+
return { state, facts: { ...facts, terminalProvenance: "PROCESS_EXIT_ZERO" } };
|
|
439
|
+
return { state: state === "COMPLETED" ? "COLLECTION_ERROR" : state, facts };
|
|
440
|
+
}
|
|
441
|
+
if (state === "COMPLETED") {
|
|
442
|
+
try {
|
|
443
|
+
if (report !== undefined) {
|
|
444
|
+
const document = JSON.parse(await boundedFile(report, plan.maximumOutputBytes));
|
|
445
|
+
if (document.protocolVersion !== "1.0.0" ||
|
|
446
|
+
!Number.isSafeInteger(document.testsDiscovered) ||
|
|
447
|
+
!Number.isSafeInteger(document.candidateTestsDiscovered) ||
|
|
448
|
+
!Number.isSafeInteger(document.candidateFailureCount) ||
|
|
449
|
+
!Number.isSafeInteger(document.nonCandidateFailureCount) ||
|
|
450
|
+
typeof document.candidateFailuresAllAssertions !== "boolean") {
|
|
451
|
+
throw new Error("QUALIFICATION_NODE_REPORT_INVALID");
|
|
452
|
+
}
|
|
453
|
+
if (!Array.isArray(document.testFiles) ||
|
|
454
|
+
!document.testFiles.every((file) => typeof file === "string"))
|
|
455
|
+
throw new Error("QUALIFICATION_NODE_FILE_REPORT_INVALID");
|
|
456
|
+
facts.testFiles = document.testFiles;
|
|
457
|
+
if (!Array.isArray(document.assertionFailureFiles) ||
|
|
458
|
+
!document.assertionFailureFiles.every((file) => typeof file === "string" && document.testFiles.includes(file)) ||
|
|
459
|
+
canonicalize(document.assertionFailureFiles) !==
|
|
460
|
+
canonicalize([...new Set(document.assertionFailureFiles)].sort()) ||
|
|
461
|
+
(document.candidateFailureCount > 0 &&
|
|
462
|
+
document.candidateFailuresAllAssertions &&
|
|
463
|
+
document.assertionFailureFiles.length === 0) ||
|
|
464
|
+
(document.candidateFailureCount === 0 && document.assertionFailureFiles.length !== 0))
|
|
465
|
+
throw new Error("QUALIFICATION_NODE_ASSERTION_FILE_REPORT_INVALID");
|
|
466
|
+
facts.assertionFailureFiles = document.assertionFailureFiles;
|
|
467
|
+
facts.testsDiscovered = document.candidateTestsDiscovered;
|
|
468
|
+
if (action.arguments.some((file) => !document.testFiles.includes(file)))
|
|
469
|
+
state = "NO_TESTS";
|
|
470
|
+
facts.attributed =
|
|
471
|
+
document.candidateFailureCount > 0 &&
|
|
472
|
+
document.nonCandidateFailureCount === 0 &&
|
|
473
|
+
document.candidateFailuresAllAssertions;
|
|
474
|
+
const tap = await boundedFile(tapReport ?? "", plan.maximumOutputBytes);
|
|
475
|
+
for (const name of ["skipped", "cancelled", "todo"]) {
|
|
476
|
+
const matches = [...tap.matchAll(new RegExp(`^# ${name} (\\d+)$`, "gmu"))];
|
|
477
|
+
if (matches.length !== 1)
|
|
478
|
+
throw new Error("QUALIFICATION_NODE_SUMMARY_INVALID");
|
|
479
|
+
const count = Number(matches[0]?.[1]);
|
|
480
|
+
facts[`${name}Tests`] = count;
|
|
481
|
+
if (count !== 0)
|
|
482
|
+
state = "COLLECTION_ERROR";
|
|
483
|
+
}
|
|
484
|
+
facts.testOutcome =
|
|
485
|
+
document.nonCandidateFailureCount > 0 || !document.candidateFailuresAllAssertions
|
|
486
|
+
? "PROCESS_CRASH"
|
|
487
|
+
: document.candidateFailureCount > 0
|
|
488
|
+
? "ASSERTION_FAILURE"
|
|
489
|
+
: "PASS";
|
|
490
|
+
if (document.candidateTestsDiscovered === 0)
|
|
491
|
+
state = "NO_TESTS";
|
|
492
|
+
else if (facts.testOutcome === "PROCESS_CRASH")
|
|
493
|
+
state = "COLLECTION_ERROR";
|
|
494
|
+
else if ((facts.testOutcome === "PASS") !== (processResult.exitCode === 0))
|
|
495
|
+
state = "COLLECTION_ERROR";
|
|
496
|
+
}
|
|
497
|
+
else if (action.adapter === "ci-config") {
|
|
498
|
+
if (processResult.exitCode !== 0)
|
|
499
|
+
state = "COLLECTION_ERROR";
|
|
500
|
+
else
|
|
501
|
+
facts = { ...facts, ...JSON.parse(processResult.stdout.text) };
|
|
502
|
+
}
|
|
503
|
+
else if (action.adapter === "turbo") {
|
|
504
|
+
facts = {
|
|
505
|
+
...facts,
|
|
506
|
+
...(await turboFacts(root, Math.max(plan.maximumOutputBytes, 1024 * 1024))),
|
|
507
|
+
};
|
|
508
|
+
// A task summary is not a qualified semantic failure attestation.
|
|
509
|
+
// This adapter currently supports successful Turbo completion only.
|
|
510
|
+
if (processResult.exitCode !== 0)
|
|
511
|
+
state = "COLLECTION_ERROR";
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
state = "COLLECTION_ERROR";
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (state === "COMPLETED") {
|
|
520
|
+
try {
|
|
521
|
+
if (action.adapter === "command") {
|
|
522
|
+
const terminalRaw = await boundedFile(terminalResult, plan.maximumOutputBytes);
|
|
523
|
+
const terminal = JSON.parse(terminalRaw);
|
|
524
|
+
facts.terminal = terminal;
|
|
525
|
+
facts.terminalDigest = `sha256:${createHash("sha256").update(terminalRaw).digest("hex")}`;
|
|
526
|
+
facts.terminalProvenance = "ENGINE_LIFECYCLE_OBSERVED";
|
|
527
|
+
if (terminalRaw !== `${canonicalize(terminal)}\n` ||
|
|
528
|
+
terminal.nonce !== nonce ||
|
|
529
|
+
terminal.ready !== true ||
|
|
530
|
+
terminal.exceptional !== false ||
|
|
531
|
+
(terminal.natural !== true && terminal.explicit !== true) ||
|
|
532
|
+
terminal.exitCode !== facts.exitCode)
|
|
533
|
+
throw new Error("QUALIFICATION_COMMAND_TERMINAL_INVALID");
|
|
534
|
+
}
|
|
535
|
+
if (action.observe.report !== null ||
|
|
536
|
+
(action.adapter === "command" && facts.exitCode !== 0)) {
|
|
537
|
+
const raw = await boundedFile(structuredResult, plan.maximumOutputBytes);
|
|
538
|
+
const document = JSON.parse(raw);
|
|
539
|
+
if (raw !== `${canonicalize(document)}\n` ||
|
|
540
|
+
Object.keys(document).sort().join(",") !== "facts,nonce,protocolVersion" ||
|
|
541
|
+
document.protocolVersion !== "1.0.0" ||
|
|
542
|
+
document.nonce !== nonce ||
|
|
543
|
+
typeof document.facts !== "object" ||
|
|
544
|
+
document.facts === null ||
|
|
545
|
+
Array.isArray(document.facts)) {
|
|
546
|
+
throw new Error("QUALIFICATION_STRUCTURED_REPORT_INVALID");
|
|
547
|
+
}
|
|
548
|
+
facts.report = document.facts;
|
|
549
|
+
facts.reportNonce = nonce;
|
|
550
|
+
facts.reportDigest = `sha256:${createHash("sha256").update(raw).digest("hex")}`;
|
|
551
|
+
facts.reportProvenance = "STRUCTURED_ADAPTER_REPORTED";
|
|
552
|
+
if (action.adapter === "command") {
|
|
553
|
+
const completion = document.facts;
|
|
554
|
+
if ((facts.exitCode !== 0 ||
|
|
555
|
+
Object.hasOwn(completion, "exitCode") ||
|
|
556
|
+
Object.hasOwn(completion, "commandOutcome")) &&
|
|
557
|
+
(completion.commandOutcome !== (facts.exitCode === 0 ? "PASS" : "EXPECTED_FAILURE") ||
|
|
558
|
+
completion.exitCode !== facts.exitCode))
|
|
559
|
+
throw new Error("QUALIFICATION_COMMAND_COMPLETION_INVALID");
|
|
560
|
+
facts.commandOutcome = facts.exitCode === 0 ? "PASS" : "EXPECTED_FAILURE";
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (action.observe.trace)
|
|
564
|
+
facts = { ...facts, ...(await readTrace(trace, nonce, plan.maximumOutputBytes)) };
|
|
565
|
+
const outputs = {};
|
|
566
|
+
for (const file of action.observe.outputs) {
|
|
567
|
+
try {
|
|
568
|
+
const target = confinedPath(root, file);
|
|
569
|
+
const metadata = await lstat(target);
|
|
570
|
+
if (!metadata.isFile() || metadata.isSymbolicLink())
|
|
571
|
+
throw new Error("INVALID_OUTPUT");
|
|
572
|
+
outputs[file] = await qualificationFileDigest(target);
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
if (error.code === "ENOENT")
|
|
576
|
+
outputs[file] = null;
|
|
577
|
+
else
|
|
578
|
+
throw error;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
facts.outputs = outputs;
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
state = "COLLECTION_ERROR";
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return { state, facts };
|
|
588
|
+
}
|
|
589
|
+
/** The digest of an operator-owned plan must be anchored outside candidate-controlled inputs. */
|
|
590
|
+
export async function qualifyOrchestration(value, options = {}) {
|
|
591
|
+
if (options.allowUnsafeExecution !== true)
|
|
592
|
+
throw new Error("QUALIFICATION_UNSAFE_EXECUTION_NOT_AUTHORIZED");
|
|
593
|
+
value = QualificationExecutionRequestSchema.parse(value);
|
|
594
|
+
const plan = parseQualificationPlan(value.plan);
|
|
595
|
+
if (sha256Canonical(plan) !== value.planDigest)
|
|
596
|
+
throw new Error("QUALIFICATION_PLAN_DIGEST_MISMATCH");
|
|
597
|
+
const candidateDigest = sha256Canonical(value.candidate.files);
|
|
598
|
+
if (candidateDigest !== plan.subject.candidateDigest)
|
|
599
|
+
throw new Error("QUALIFICATION_CANDIDATE_DIGEST_MISMATCH");
|
|
600
|
+
if (value.candidate.files.some((file) => !plan.allowedCandidatePaths.includes(file.path)))
|
|
601
|
+
throw new Error("QUALIFICATION_CANDIDATE_SCOPE");
|
|
602
|
+
const root = await realpath(value.root);
|
|
603
|
+
const inputs = await snapshotInputs(root);
|
|
604
|
+
const inputDigest = sha256Canonical(inputs.map(({ path: file, digest }) => ({ path: file, digest })));
|
|
605
|
+
if (inputDigest !== plan.subject.inputDigest)
|
|
606
|
+
throw new Error("QUALIFICATION_INPUT_DIGEST_MISMATCH");
|
|
607
|
+
const executableFacts = {};
|
|
608
|
+
const actionExecutables = new Map();
|
|
609
|
+
for (const executable of new Set(plan.actions.map((action) => action.executable))) {
|
|
610
|
+
if (!path.isAbsolute(executable))
|
|
611
|
+
throw new Error("QUALIFICATION_EXECUTABLE_NOT_ABSOLUTE");
|
|
612
|
+
const digest = await qualificationFileDigest(await realpath(executable));
|
|
613
|
+
actionExecutables.set(executable, digest);
|
|
614
|
+
if (!plan.tools.some((entry) => entry.digest === digest))
|
|
615
|
+
throw new Error("QUALIFICATION_TOOL_DIGEST_MISMATCH");
|
|
616
|
+
}
|
|
617
|
+
for (const tool of plan.tools) {
|
|
618
|
+
const inferred = [...actionExecutables].find(([, digest]) => digest === tool.digest)?.[0];
|
|
619
|
+
const identityPath = tool.identityPath ?? inferred;
|
|
620
|
+
if (identityPath === undefined || !path.isAbsolute(identityPath))
|
|
621
|
+
throw new Error("QUALIFICATION_TOOL_IDENTITY_MISSING");
|
|
622
|
+
const digest = await qualificationToolDigest(identityPath);
|
|
623
|
+
if (digest !== tool.digest)
|
|
624
|
+
throw new Error("QUALIFICATION_TOOL_DIGEST_MISMATCH");
|
|
625
|
+
const executable = tool.versionCommand?.executable ?? inferred;
|
|
626
|
+
if (executable === undefined ||
|
|
627
|
+
!path.isAbsolute(executable) ||
|
|
628
|
+
!actionExecutables.has(executable))
|
|
629
|
+
throw new Error("QUALIFICATION_TOOL_PROBE_UNBOUND");
|
|
630
|
+
const args = tool.versionCommand?.arguments ?? ["--version"];
|
|
631
|
+
const probe = await runProcess({
|
|
632
|
+
executable,
|
|
633
|
+
args,
|
|
634
|
+
cwd: root,
|
|
635
|
+
environment: plan.actions.find((action) => action.executable === executable)?.environment ?? {},
|
|
636
|
+
timeoutMs: 5000,
|
|
637
|
+
maximumOutputBytes: 65536,
|
|
638
|
+
});
|
|
639
|
+
if (probe.exitCode !== 0 ||
|
|
640
|
+
probe.timedOut ||
|
|
641
|
+
probe.stdout.truncated ||
|
|
642
|
+
probe.stdout.text.trim().replace(/^v/u, "") !== tool.version)
|
|
643
|
+
throw new Error("QUALIFICATION_TOOL_VERSION_MISMATCH");
|
|
644
|
+
executableFacts[tool.id] = {
|
|
645
|
+
executable,
|
|
646
|
+
identityPath,
|
|
647
|
+
digest,
|
|
648
|
+
version: tool.version,
|
|
649
|
+
versionArguments: args,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
// runProcess canonicalizes cwd. Use the same identity for adapter-owned paths and
|
|
653
|
+
// candidate attribution when the host temporary directory is a symlink or junction.
|
|
654
|
+
const temporary = await realpath(await mkdtemp(path.join(os.tmpdir(), "assertledger-orchestration-")));
|
|
655
|
+
const mechanismDigest = await qualificationMechanismDigest();
|
|
656
|
+
const observations = [];
|
|
657
|
+
try {
|
|
658
|
+
for (const world of plan.worlds) {
|
|
659
|
+
for (let attempt = 1; attempt <= plan.requiredAttempts; attempt += 1) {
|
|
660
|
+
const domain = path.join(temporary, `${world.id}-${attempt}`);
|
|
661
|
+
const workspace = path.join(domain, "workspace");
|
|
662
|
+
const control = path.join(domain, "control");
|
|
663
|
+
const cache = path.join(domain, "cache");
|
|
664
|
+
await Promise.all([
|
|
665
|
+
mkdir(workspace, { recursive: true }),
|
|
666
|
+
mkdir(control, { recursive: true }),
|
|
667
|
+
mkdir(cache, { recursive: true }),
|
|
668
|
+
]);
|
|
669
|
+
await overlay(workspace, inputs);
|
|
670
|
+
await overlay(workspace, value.candidate.files);
|
|
671
|
+
await overlay(workspace, world.files);
|
|
672
|
+
for (const action of plan.actions) {
|
|
673
|
+
const observation = await executeAction(action, workspace, control, cache, plan);
|
|
674
|
+
observations.push({
|
|
675
|
+
worldId: world.id,
|
|
676
|
+
attempt,
|
|
677
|
+
actionId: action.id,
|
|
678
|
+
...observation,
|
|
679
|
+
bindingDigest: qualificationBinding(value.planDigest, candidateDigest, world.id, attempt, action.id),
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
// Protect domain reuse against source/tool mutation while observations were collected.
|
|
685
|
+
if ((await qualificationRepositoryDigest(root)) !== inputDigest)
|
|
686
|
+
throw new Error("QUALIFICATION_INPUT_DIGEST_CHANGED");
|
|
687
|
+
if ((await qualificationMechanismDigest()) !== mechanismDigest)
|
|
688
|
+
throw new Error("QUALIFICATION_MECHANISM_CHANGED");
|
|
689
|
+
for (const fact of Object.values(executableFacts)) {
|
|
690
|
+
const tool = fact;
|
|
691
|
+
if ((await qualificationToolDigest(tool.identityPath)) !== tool.digest)
|
|
692
|
+
throw new Error("QUALIFICATION_TOOL_DIGEST_CHANGED");
|
|
693
|
+
}
|
|
694
|
+
return createQualificationReceipt({
|
|
695
|
+
plan,
|
|
696
|
+
planDigest: value.planDigest,
|
|
697
|
+
candidateDigest,
|
|
698
|
+
observations,
|
|
699
|
+
provenance: {
|
|
700
|
+
engineVersion: ASSERTLEDGER_VERSION,
|
|
701
|
+
adapterVersions: { ...QUALIFICATION_ADAPTER_VERSIONS },
|
|
702
|
+
runtime: {
|
|
703
|
+
mechanismDigest,
|
|
704
|
+
platform: process.platform,
|
|
705
|
+
architecture: process.arch,
|
|
706
|
+
node: process.versions.node,
|
|
707
|
+
tools: executableFacts,
|
|
708
|
+
collectorDigest: sha256Canonical({
|
|
709
|
+
ciParser: CI_PARSER,
|
|
710
|
+
nodeReporter: NODE_TEST_REPORTER_SOURCE,
|
|
711
|
+
commandTerminalObserver: COMMAND_TERMINAL_OBSERVER,
|
|
712
|
+
}),
|
|
713
|
+
snapshotExcludes: [...QUALIFICATION_SNAPSHOT_EXCLUDES],
|
|
714
|
+
dependencySnapshot: "EXCLUDED_NODE_MODULES",
|
|
715
|
+
},
|
|
716
|
+
executionTrust: "TRUSTED_LOCAL_UNSANDBOXED",
|
|
717
|
+
},
|
|
718
|
+
externalCi: null,
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
finally {
|
|
722
|
+
await rm(temporary, { recursive: true, force: true, maxRetries: 3 });
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
//# sourceMappingURL=qualification.js.map
|