@attest-ai/core 0.9.0 → 1.0.0-rc.1
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/dist/cjs/index.cjs +272 -114
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +47 -16
- package/dist/cjs/proto/index.cjs +36 -0
- package/dist/cjs/proto/index.cjs.map +1 -1
- package/dist/cjs/proto/index.d.cts +34 -1
- package/dist/esm/cli.d.ts +19 -4
- package/dist/esm/cli.js +450 -407
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index.d.ts +47 -16
- package/dist/esm/index.js +263 -115
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/proto/index.d.ts +34 -1
- package/dist/esm/proto/index.js +26 -0
- package/dist/esm/proto/index.js.map +1 -1
- package/package.json +4 -3
package/dist/esm/index.js
CHANGED
|
@@ -6,6 +6,15 @@ var STEP_AGENT_CALL = "agent_call";
|
|
|
6
6
|
var STATUS_PASS = "pass";
|
|
7
7
|
var STATUS_SOFT_FAIL = "soft_fail";
|
|
8
8
|
var STATUS_HARD_FAIL = "hard_fail";
|
|
9
|
+
var STATUS_NOT_EVALUATED = "not_evaluated";
|
|
10
|
+
var REASON_PROVIDER_UNAVAILABLE = "provider_unavailable";
|
|
11
|
+
var REASON_PROVIDER_NOT_CONFIGURED = "provider_not_configured";
|
|
12
|
+
var REASON_SIMULATION = "simulation";
|
|
13
|
+
var REASON_MISSING_EVIDENCE = "missing_evidence";
|
|
14
|
+
var REASON_SHORT_CIRCUITED = "short_circuited";
|
|
15
|
+
var OUTCOME_PASS = "pass";
|
|
16
|
+
var OUTCOME_FAIL = "fail";
|
|
17
|
+
var OUTCOME_SKIP = "skip";
|
|
9
18
|
var TYPE_SCHEMA = "schema";
|
|
10
19
|
var TYPE_CONSTRAINT = "constraint";
|
|
11
20
|
var TYPE_TRACE = "trace";
|
|
@@ -31,6 +40,13 @@ function assertionId(id) {
|
|
|
31
40
|
function agentId(id) {
|
|
32
41
|
return id;
|
|
33
42
|
}
|
|
43
|
+
var FailureClass = {
|
|
44
|
+
BrokenCode: "broken_code",
|
|
45
|
+
FlakyJudge: "flaky_judge",
|
|
46
|
+
BadRubric: "bad_rubric",
|
|
47
|
+
MissingTraceData: "missing_trace_data",
|
|
48
|
+
StochasticVariance: "stochastic_variance"
|
|
49
|
+
};
|
|
34
50
|
function layerForType(assertionType) {
|
|
35
51
|
switch (assertionType) {
|
|
36
52
|
case "schema":
|
|
@@ -192,15 +208,14 @@ function extractId(response) {
|
|
|
192
208
|
}
|
|
193
209
|
|
|
194
210
|
// src/version.ts
|
|
195
|
-
var VERSION = "0.
|
|
196
|
-
var ENGINE_VERSION = "0.
|
|
211
|
+
var VERSION = "1.0.0-rc.1";
|
|
212
|
+
var ENGINE_VERSION = "1.0.0-rc.1";
|
|
197
213
|
|
|
198
214
|
// src/engine-manager.ts
|
|
199
|
-
import { spawn,
|
|
215
|
+
import { spawn, spawnSync } from "child_process";
|
|
200
216
|
import { createInterface } from "readline";
|
|
201
|
-
import { existsSync as
|
|
202
|
-
import {
|
|
203
|
-
import { fileURLToPath } from "url";
|
|
217
|
+
import { existsSync, accessSync, statSync as statSync2, constants as fsConstants } from "fs";
|
|
218
|
+
import { delimiter, join as join2 } from "path";
|
|
204
219
|
|
|
205
220
|
// src/engine-downloader.ts
|
|
206
221
|
import * as https from "https";
|
|
@@ -212,10 +227,8 @@ import * as crypto from "crypto";
|
|
|
212
227
|
var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
|
|
213
228
|
var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
|
|
214
229
|
var MAX_REDIRECTS = 5;
|
|
215
|
-
function
|
|
216
|
-
|
|
217
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
218
|
-
return dir;
|
|
230
|
+
function versionedBinDir() {
|
|
231
|
+
return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
|
|
219
232
|
}
|
|
220
233
|
function platformKey() {
|
|
221
234
|
const platform2 = os.platform();
|
|
@@ -243,20 +256,8 @@ function binaryFilename() {
|
|
|
243
256
|
return os.platform() === "win32" ? "attest-engine.exe" : "attest-engine";
|
|
244
257
|
}
|
|
245
258
|
function cachedEnginePath() {
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
const versionFile = path.join(dir, ".engine-version");
|
|
249
|
-
if (!fs.existsSync(binPath)) {
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
if (!fs.existsSync(versionFile)) {
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
const cachedVersion = fs.readFileSync(versionFile, "utf-8").trim();
|
|
256
|
-
if (cachedVersion !== ENGINE_VERSION) {
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
return binPath;
|
|
259
|
+
const binPath = path.join(versionedBinDir(), binaryFilename());
|
|
260
|
+
return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
|
|
260
261
|
}
|
|
261
262
|
function parseChecksums(text) {
|
|
262
263
|
const checksums = /* @__PURE__ */ new Map();
|
|
@@ -276,7 +277,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
276
277
|
new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
|
|
277
278
|
);
|
|
278
279
|
}
|
|
279
|
-
return new Promise((
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
280
281
|
const client = url.startsWith("https:") ? https : http;
|
|
281
282
|
client.get(url, (res) => {
|
|
282
283
|
const status = res.statusCode ?? 0;
|
|
@@ -287,7 +288,7 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
287
288
|
return;
|
|
288
289
|
}
|
|
289
290
|
res.resume();
|
|
290
|
-
|
|
291
|
+
resolve(httpsGet(location, redirectCount + 1));
|
|
291
292
|
return;
|
|
292
293
|
}
|
|
293
294
|
if (status < 200 || status >= 300) {
|
|
@@ -297,14 +298,14 @@ function httpsGet(url, redirectCount = 0) {
|
|
|
297
298
|
}
|
|
298
299
|
const chunks = [];
|
|
299
300
|
res.on("data", (chunk) => chunks.push(chunk));
|
|
300
|
-
res.on("end", () =>
|
|
301
|
+
res.on("end", () => resolve(Buffer.concat(chunks)));
|
|
301
302
|
res.on("error", reject);
|
|
302
303
|
}).on("error", reject);
|
|
303
304
|
});
|
|
304
305
|
}
|
|
305
306
|
async function downloadEngine() {
|
|
306
307
|
const key = platformKey();
|
|
307
|
-
const filename = `attest-engine-${key}`;
|
|
308
|
+
const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
|
|
308
309
|
const binaryUrl = `${RELEASE_BASE}/${filename}`;
|
|
309
310
|
process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
|
|
310
311
|
`);
|
|
@@ -324,19 +325,24 @@ async function downloadEngine() {
|
|
|
324
325
|
const actualHash = crypto.createHash("sha256").update(data).digest("hex");
|
|
325
326
|
if (actualHash !== expectedHash) {
|
|
326
327
|
throw new Error(
|
|
327
|
-
`SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted.
|
|
328
|
+
`SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Retry or download manually.`
|
|
328
329
|
);
|
|
329
330
|
}
|
|
330
331
|
process.stderr.write(`[attest] Checksum verified.
|
|
331
332
|
`);
|
|
332
|
-
const dir =
|
|
333
|
-
|
|
334
|
-
const destPath = path.join(dir,
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
333
|
+
const dir = versionedBinDir();
|
|
334
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
335
|
+
const destPath = path.join(dir, binaryFilename());
|
|
336
|
+
const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
|
|
337
|
+
const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
|
|
338
|
+
try {
|
|
339
|
+
fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
|
|
340
|
+
fs.chmodSync(tmpPath, 493);
|
|
341
|
+
fs.renameSync(tmpPath, destPath);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
340
346
|
process.stderr.write(`[attest] Engine installed at ${destPath}
|
|
341
347
|
`);
|
|
342
348
|
return destPath;
|
|
@@ -344,45 +350,92 @@ async function downloadEngine() {
|
|
|
344
350
|
|
|
345
351
|
// src/engine-manager.ts
|
|
346
352
|
var ENGINE_BINARY_NAME = "attest-engine";
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
353
|
+
var VERSION_PROBE_TIMEOUT_MS = 1e4;
|
|
354
|
+
var EngineVersionMismatchError = class extends Error {
|
|
355
|
+
enginePath;
|
|
356
|
+
source;
|
|
357
|
+
found;
|
|
358
|
+
required;
|
|
359
|
+
constructor(enginePath, source, found, required) {
|
|
360
|
+
const reported = found === null ? "reports no version" : `reports version ${found}`;
|
|
361
|
+
super(
|
|
362
|
+
`attest-engine at ${enginePath} (from ${source}) ${reported}; this SDK requires attest-engine ${required}. Point ATTEST_ENGINE_PATH at a matching engine, or remove the mismatched one from PATH so the SDK can download ${required} into ~/.attest/bin/${required}/.`
|
|
363
|
+
);
|
|
364
|
+
this.name = "EngineVersionMismatchError";
|
|
365
|
+
this.enginePath = enginePath;
|
|
366
|
+
this.source = source;
|
|
367
|
+
this.found = found;
|
|
368
|
+
this.required = required;
|
|
362
369
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
370
|
+
};
|
|
371
|
+
function engineVersion(enginePath) {
|
|
372
|
+
const proc = spawnSync(enginePath, ["version"], {
|
|
373
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
374
|
+
encoding: "utf-8",
|
|
375
|
+
timeout: VERSION_PROBE_TIMEOUT_MS
|
|
376
|
+
});
|
|
377
|
+
if (proc.error) throw proc.error;
|
|
378
|
+
const out = proc.stdout.trim();
|
|
379
|
+
const prefix = `${ENGINE_BINARY_NAME} `;
|
|
380
|
+
if (proc.status !== 0 || !out.startsWith(prefix)) return null;
|
|
381
|
+
return out.slice(prefix.length).trim();
|
|
382
|
+
}
|
|
383
|
+
function onPath() {
|
|
384
|
+
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
385
|
+
if (!dir) continue;
|
|
386
|
+
const candidate = join2(dir, binaryFilename());
|
|
371
387
|
try {
|
|
372
|
-
accessSync(
|
|
373
|
-
if (existsSync2(resolved)) return resolved;
|
|
388
|
+
accessSync(candidate, fsConstants.X_OK);
|
|
374
389
|
} catch {
|
|
390
|
+
continue;
|
|
375
391
|
}
|
|
392
|
+
if (statSync2(candidate).isFile()) return candidate;
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
function requireFile(enginePath, label) {
|
|
397
|
+
if (!existsSync(enginePath) || !statSync2(enginePath).isFile()) {
|
|
398
|
+
throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function locateEngine(explicitPath, allowDownload) {
|
|
402
|
+
if (explicitPath) {
|
|
403
|
+
requireFile(explicitPath, "Engine path");
|
|
404
|
+
return [explicitPath, "explicit engine path"];
|
|
376
405
|
}
|
|
377
|
-
const
|
|
406
|
+
const envPath = process.env["ATTEST_ENGINE_PATH"];
|
|
407
|
+
if (envPath) {
|
|
408
|
+
requireFile(envPath, "ATTEST_ENGINE_PATH");
|
|
409
|
+
return [envPath, "ATTEST_ENGINE_PATH"];
|
|
410
|
+
}
|
|
411
|
+
const found = onPath();
|
|
412
|
+
if (found) return [found, "PATH"];
|
|
413
|
+
const cached = cachedEnginePath();
|
|
414
|
+
if (cached) return [cached, "engine cache"];
|
|
415
|
+
if (!allowDownload) {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
|
|
378
421
|
if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
|
|
379
422
|
throw new Error(
|
|
380
|
-
`Cannot find '${ENGINE_BINARY_NAME}'
|
|
381
|
-
|
|
382
|
-
|
|
423
|
+
`Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
|
|
424
|
+
Install options:
|
|
425
|
+
1. Build from source: make engine, then set ATTEST_ENGINE_PATH
|
|
426
|
+
2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
|
|
427
|
+
3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
|
|
383
428
|
);
|
|
384
429
|
}
|
|
385
|
-
return downloadEngine();
|
|
430
|
+
return [await downloadEngine(), "download"];
|
|
431
|
+
}
|
|
432
|
+
async function resolveEngine(explicitPath, options = {}) {
|
|
433
|
+
const [enginePath, source] = await locateEngine(explicitPath, options.allowDownload ?? true);
|
|
434
|
+
const found = engineVersion(enginePath);
|
|
435
|
+
if (found !== ENGINE_VERSION) {
|
|
436
|
+
throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
|
|
437
|
+
}
|
|
438
|
+
return enginePath;
|
|
386
439
|
}
|
|
387
440
|
var EngineManager = class {
|
|
388
441
|
enginePath;
|
|
@@ -397,28 +450,41 @@ var EngineManager = class {
|
|
|
397
450
|
this.logLevel = options?.logLevel ?? "warn";
|
|
398
451
|
}
|
|
399
452
|
async start() {
|
|
400
|
-
|
|
401
|
-
this.enginePath = await findEngineBinary();
|
|
402
|
-
}
|
|
453
|
+
this.enginePath = await resolveEngine(this.enginePath || void 0);
|
|
403
454
|
this.process = spawn(this.enginePath, [`--log-level=${this.logLevel}`], {
|
|
404
455
|
stdio: ["pipe", "pipe", "pipe"]
|
|
405
456
|
});
|
|
406
457
|
this.reader = createInterface({ input: this.process.stdout });
|
|
407
|
-
|
|
458
|
+
try {
|
|
459
|
+
this.initResult = await this.initialize();
|
|
460
|
+
} catch (err) {
|
|
461
|
+
await this.stop();
|
|
462
|
+
throw err;
|
|
463
|
+
}
|
|
464
|
+
this.initialized = true;
|
|
465
|
+
return this.initResult;
|
|
466
|
+
}
|
|
467
|
+
/** Send initialize and check the engine's version and capabilities. */
|
|
468
|
+
async initialize() {
|
|
469
|
+
const init = await this.sendRequestInternal("initialize", {
|
|
408
470
|
sdk_name: "attest-typescript",
|
|
409
471
|
sdk_version: VERSION,
|
|
410
472
|
protocol_version: 1,
|
|
411
473
|
required_capabilities: ["layers_1_4"],
|
|
412
474
|
preferred_encoding: "json"
|
|
413
475
|
});
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
476
|
+
if (init.engine_version !== ENGINE_VERSION) {
|
|
477
|
+
throw new EngineVersionMismatchError(
|
|
478
|
+
this.enginePath,
|
|
479
|
+
"initialize",
|
|
480
|
+
init.engine_version,
|
|
481
|
+
ENGINE_VERSION
|
|
418
482
|
);
|
|
419
483
|
}
|
|
420
|
-
|
|
421
|
-
|
|
484
|
+
if (!init.compatible) {
|
|
485
|
+
throw new Error(`Engine incompatible. Missing capabilities: ${JSON.stringify(init.missing)}`);
|
|
486
|
+
}
|
|
487
|
+
return init;
|
|
422
488
|
}
|
|
423
489
|
async stop() {
|
|
424
490
|
if (this.process === null) return;
|
|
@@ -459,11 +525,11 @@ var EngineManager = class {
|
|
|
459
525
|
}
|
|
460
526
|
this.requestId += 1;
|
|
461
527
|
const requestStr = encodeRequest(this.requestId, method, params);
|
|
462
|
-
return new Promise((
|
|
528
|
+
return new Promise((resolve, reject) => {
|
|
463
529
|
const onLine = (line) => {
|
|
464
530
|
try {
|
|
465
531
|
const response = decodeResponse(line);
|
|
466
|
-
|
|
532
|
+
resolve(extractResult(response));
|
|
467
533
|
} catch (err) {
|
|
468
534
|
reject(err);
|
|
469
535
|
}
|
|
@@ -511,7 +577,11 @@ function config(options) {
|
|
|
511
577
|
}
|
|
512
578
|
}
|
|
513
579
|
function isSimulationMode() {
|
|
514
|
-
return _simulationMode ||
|
|
580
|
+
return _simulationMode || simulationEnvEnabled();
|
|
581
|
+
}
|
|
582
|
+
function simulationEnvEnabled() {
|
|
583
|
+
const env = (process.env["ATTEST_SIMULATION"] ?? "").trim().toLowerCase();
|
|
584
|
+
return env === "1" || env === "true" || env === "yes";
|
|
515
585
|
}
|
|
516
586
|
function getSampleRate() {
|
|
517
587
|
if (_sampleRate > 0) return _sampleRate;
|
|
@@ -532,24 +602,6 @@ function resetConfig() {
|
|
|
532
602
|
_sampleRate = 0;
|
|
533
603
|
}
|
|
534
604
|
|
|
535
|
-
// src/simulation.ts
|
|
536
|
-
function simulationEvaluateBatch(assertions) {
|
|
537
|
-
const results = assertions.map((assertion) => ({
|
|
538
|
-
assertion_id: assertion.assertion_id,
|
|
539
|
-
status: "pass",
|
|
540
|
-
score: 1,
|
|
541
|
-
explanation: `[simulation] assertion '${assertion.assertion_id}' passed (type: ${assertion.type})`,
|
|
542
|
-
cost: 0,
|
|
543
|
-
duration_ms: 0
|
|
544
|
-
}));
|
|
545
|
-
return {
|
|
546
|
-
results,
|
|
547
|
-
total_cost: 0,
|
|
548
|
-
total_duration_ms: 0,
|
|
549
|
-
simulated: true
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
|
|
553
605
|
// src/client.ts
|
|
554
606
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
555
607
|
var DEFAULT_DIAGNOSTIC_BUFFER = 32;
|
|
@@ -774,12 +826,12 @@ var AttestClient = class {
|
|
|
774
826
|
throw new Error("Engine process not started.");
|
|
775
827
|
}
|
|
776
828
|
let capturedReqId;
|
|
777
|
-
const requestPromise = new Promise((
|
|
829
|
+
const requestPromise = new Promise((resolve, reject) => {
|
|
778
830
|
this.writeChain = this.writeChain.then(() => {
|
|
779
831
|
this.requestId += 1;
|
|
780
832
|
const reqId = this.requestId;
|
|
781
833
|
capturedReqId = reqId;
|
|
782
|
-
this.pending.set(reqId, { resolve
|
|
834
|
+
this.pending.set(reqId, { resolve, reject });
|
|
783
835
|
const requestStr = encodeRequest(reqId, method, params);
|
|
784
836
|
cp.stdin.write(requestStr, (err) => {
|
|
785
837
|
if (err) {
|
|
@@ -799,7 +851,7 @@ var AttestClient = class {
|
|
|
799
851
|
}
|
|
800
852
|
async evaluateBatch(trace, assertions, options) {
|
|
801
853
|
if (isSimulationMode()) {
|
|
802
|
-
|
|
854
|
+
requireSimulationCapability(this.engine["initResult"]);
|
|
803
855
|
}
|
|
804
856
|
const params = {
|
|
805
857
|
trace,
|
|
@@ -838,10 +890,10 @@ var AttestClient = class {
|
|
|
838
890
|
if (isSimulationMode()) {
|
|
839
891
|
return {
|
|
840
892
|
assertion_id: assertionId2,
|
|
841
|
-
mean:
|
|
893
|
+
mean: 0,
|
|
842
894
|
stddev: 0,
|
|
843
895
|
count: 0,
|
|
844
|
-
latest_score:
|
|
896
|
+
latest_score: 0,
|
|
845
897
|
deviation: 0,
|
|
846
898
|
status: "no_data"
|
|
847
899
|
};
|
|
@@ -879,6 +931,16 @@ function classifyDecodeError(message) {
|
|
|
879
931
|
if (message.startsWith("empty response line")) return "malformed_json";
|
|
880
932
|
return "malformed_json";
|
|
881
933
|
}
|
|
934
|
+
function requireSimulationCapability(init) {
|
|
935
|
+
if (init === null || init === void 0) {
|
|
936
|
+
throw new Error("simulation mode: the engine has not been initialized");
|
|
937
|
+
}
|
|
938
|
+
if (!(init.capabilities ?? []).includes("simulation_mode")) {
|
|
939
|
+
throw new Error(
|
|
940
|
+
`simulation mode is on but attest-engine ${init.engine_version} was not started in simulation mode (capability 'simulation_mode' missing), so it would call real providers. Set ATTEST_SIMULATION=1 before the engine starts (the Vitest fixtures do this for you) and use attest-engine 1.0 or later.`
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
882
944
|
|
|
883
945
|
// src/trace.ts
|
|
884
946
|
import { randomUUID } from "crypto";
|
|
@@ -969,6 +1031,20 @@ var TraceBuilder = class {
|
|
|
969
1031
|
};
|
|
970
1032
|
|
|
971
1033
|
// src/result.ts
|
|
1034
|
+
var KNOWN_STATUSES = /* @__PURE__ */ new Set([
|
|
1035
|
+
STATUS_PASS,
|
|
1036
|
+
STATUS_SOFT_FAIL,
|
|
1037
|
+
STATUS_HARD_FAIL,
|
|
1038
|
+
STATUS_NOT_EVALUATED
|
|
1039
|
+
]);
|
|
1040
|
+
function normalizeAssertionResult(r) {
|
|
1041
|
+
if (KNOWN_STATUSES.has(r.status)) return r;
|
|
1042
|
+
return {
|
|
1043
|
+
...r,
|
|
1044
|
+
status: STATUS_NOT_EVALUATED,
|
|
1045
|
+
explanation: `[unknown status '${r.status}'] ${r.explanation}`
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
972
1048
|
var AgentResult = class {
|
|
973
1049
|
trace;
|
|
974
1050
|
assertionResults;
|
|
@@ -976,15 +1052,31 @@ var AgentResult = class {
|
|
|
976
1052
|
totalDurationMs;
|
|
977
1053
|
constructor(trace, assertionResults = [], totalCost = 0, totalDurationMs = 0) {
|
|
978
1054
|
this.trace = trace;
|
|
979
|
-
this.assertionResults = assertionResults;
|
|
1055
|
+
this.assertionResults = assertionResults.map(normalizeAssertionResult);
|
|
980
1056
|
this.totalCost = totalCost;
|
|
981
1057
|
this.totalDurationMs = totalDurationMs;
|
|
982
1058
|
}
|
|
1059
|
+
/** True if every assertion has status "pass". */
|
|
983
1060
|
get passed() {
|
|
984
1061
|
return this.assertionResults.every((r) => r.status === STATUS_PASS);
|
|
985
1062
|
}
|
|
1063
|
+
/**
|
|
1064
|
+
* Test outcome after the whole batch. Any hard_fail fails; otherwise any
|
|
1065
|
+
* not_evaluated skips (fails when `strict`); otherwise pass, including
|
|
1066
|
+
* when only soft_fail rows are present (see `softFailures`).
|
|
1067
|
+
*/
|
|
1068
|
+
outcome(options) {
|
|
1069
|
+
if (this.hardFailures.length > 0) return OUTCOME_FAIL;
|
|
1070
|
+
if (this.notEvaluated.length > 0) {
|
|
1071
|
+
return options?.strict === true ? OUTCOME_FAIL : OUTCOME_SKIP;
|
|
1072
|
+
}
|
|
1073
|
+
return OUTCOME_PASS;
|
|
1074
|
+
}
|
|
1075
|
+
/** Failed assertions: hard_fail and soft_fail only. */
|
|
986
1076
|
get failedAssertions() {
|
|
987
|
-
return this.assertionResults.filter(
|
|
1077
|
+
return this.assertionResults.filter(
|
|
1078
|
+
(r) => r.status === STATUS_HARD_FAIL || r.status === STATUS_SOFT_FAIL
|
|
1079
|
+
);
|
|
988
1080
|
}
|
|
989
1081
|
get hardFailures() {
|
|
990
1082
|
return this.assertionResults.filter((r) => r.status === STATUS_HARD_FAIL);
|
|
@@ -992,11 +1084,40 @@ var AgentResult = class {
|
|
|
992
1084
|
get softFailures() {
|
|
993
1085
|
return this.assertionResults.filter((r) => r.status === STATUS_SOFT_FAIL);
|
|
994
1086
|
}
|
|
1087
|
+
/** Assertions the engine did not evaluate; each carries a `reason`. */
|
|
1088
|
+
get notEvaluated() {
|
|
1089
|
+
return this.assertionResults.filter((r) => r.status === STATUS_NOT_EVALUATED);
|
|
1090
|
+
}
|
|
1091
|
+
/** Count of not_evaluated assertions per reason, in first-seen order. */
|
|
1092
|
+
get notEvaluatedReasons() {
|
|
1093
|
+
const counts = {};
|
|
1094
|
+
for (const r of this.notEvaluated) {
|
|
1095
|
+
const key = r.reason || "unknown";
|
|
1096
|
+
counts[key] = (counts[key] ?? 0) + 1;
|
|
1097
|
+
}
|
|
1098
|
+
return counts;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Passed / evaluated assertions; null when nothing was evaluated.
|
|
1102
|
+
* not_evaluated rows are excluded from the denominator.
|
|
1103
|
+
*/
|
|
1104
|
+
get passRate() {
|
|
1105
|
+
const evaluated = this.assertionResults.length - this.notEvaluated.length;
|
|
1106
|
+
return evaluated === 0 ? null : this.passCount / evaluated;
|
|
1107
|
+
}
|
|
995
1108
|
get passCount() {
|
|
996
1109
|
return this.assertionResults.filter((r) => r.status === STATUS_PASS).length;
|
|
997
1110
|
}
|
|
1111
|
+
/** Number of failed assertions (hard_fail + soft_fail). */
|
|
998
1112
|
get failCount() {
|
|
999
|
-
return this.
|
|
1113
|
+
return this.failedAssertions.length;
|
|
1114
|
+
}
|
|
1115
|
+
/** "not evaluated: <reason>[, <reason> ×n ...]" for skipped tests. */
|
|
1116
|
+
skipReason() {
|
|
1117
|
+
const parts = Object.entries(this.notEvaluatedReasons).map(
|
|
1118
|
+
([reason, n]) => n === 1 ? reason : `${reason} \xD7${n}`
|
|
1119
|
+
);
|
|
1120
|
+
return `not evaluated: ${parts.join(", ")}`;
|
|
1000
1121
|
}
|
|
1001
1122
|
};
|
|
1002
1123
|
|
|
@@ -1036,6 +1157,9 @@ function statusGlyph(status, color) {
|
|
|
1036
1157
|
} else if (status === STATUS_HARD_FAIL) {
|
|
1037
1158
|
glyph = "FAIL";
|
|
1038
1159
|
c = ANSI_RED;
|
|
1160
|
+
} else if (status === STATUS_NOT_EVALUATED) {
|
|
1161
|
+
glyph = "N/E";
|
|
1162
|
+
c = ANSI_DIM;
|
|
1039
1163
|
} else {
|
|
1040
1164
|
glyph = status.toUpperCase() || "????";
|
|
1041
1165
|
c = ANSI_DIM;
|
|
@@ -1063,10 +1187,15 @@ function renderAssertionFailure(result, options = {}) {
|
|
|
1063
1187
|
lines.push(
|
|
1064
1188
|
` ${statusGlyph(result.status, color)} ${bold(result.assertion_id, color)} ${dim(`\u2014 ${label}`, color)}`
|
|
1065
1189
|
);
|
|
1190
|
+
if (result.reason) {
|
|
1191
|
+
lines.push(` reason: not evaluated: ${result.reason}`);
|
|
1192
|
+
}
|
|
1066
1193
|
if (result.trace_node_path) {
|
|
1067
1194
|
lines.push(` trace path: ${result.trace_node_path}`);
|
|
1068
1195
|
}
|
|
1069
|
-
|
|
1196
|
+
if (result.status !== STATUS_NOT_EVALUATED) {
|
|
1197
|
+
lines.push(` score: ${result.score.toFixed(3)}`);
|
|
1198
|
+
}
|
|
1070
1199
|
if (result.expected) {
|
|
1071
1200
|
lines.push(` expected: ${truncate(result.expected)}`);
|
|
1072
1201
|
}
|
|
@@ -1079,6 +1208,9 @@ function renderAssertionFailure(result, options = {}) {
|
|
|
1079
1208
|
if (result.threshold_source && result.threshold_source !== "static") {
|
|
1080
1209
|
lines.push(` threshold: ${result.threshold_source}`);
|
|
1081
1210
|
}
|
|
1211
|
+
if (result.failure_class) {
|
|
1212
|
+
lines.push(` class: ${result.failure_class}`);
|
|
1213
|
+
}
|
|
1082
1214
|
if (result.judge_metadata) {
|
|
1083
1215
|
appendJudgeMetadata(lines, result.judge_metadata);
|
|
1084
1216
|
}
|
|
@@ -1141,6 +1273,7 @@ function renderSummary(result, options = {}) {
|
|
|
1141
1273
|
`${statusGlyph(STATUS_PASS, color)} ${passN}`,
|
|
1142
1274
|
`${statusGlyph(STATUS_SOFT_FAIL, color)} ${soft}`,
|
|
1143
1275
|
`${statusGlyph(STATUS_HARD_FAIL, color)} ${hard}`,
|
|
1276
|
+
`${statusGlyph(STATUS_NOT_EVALUATED, color)} ${result.notEvaluated.length}`,
|
|
1144
1277
|
`cost $${result.totalCost.toFixed(6)}`,
|
|
1145
1278
|
`dur ${result.totalDurationMs}ms`
|
|
1146
1279
|
];
|
|
@@ -1149,23 +1282,27 @@ function renderSummary(result, options = {}) {
|
|
|
1149
1282
|
function renderDiagnostics(result, options = {}) {
|
|
1150
1283
|
const color = options.color ?? false;
|
|
1151
1284
|
const includePassing = options.includePassing ?? false;
|
|
1152
|
-
const rows = includePassing ? result.assertionResults : result.
|
|
1285
|
+
const rows = includePassing ? result.assertionResults : result.assertionResults.filter((r) => r.status !== STATUS_PASS);
|
|
1153
1286
|
if (rows.length === 0) {
|
|
1154
1287
|
return dim(" (no failures)\n", color);
|
|
1155
1288
|
}
|
|
1156
1289
|
const out = [""];
|
|
1157
|
-
out.push(
|
|
1158
|
-
bold(
|
|
1159
|
-
`Attest diagnostic \u2014 ${rows.length} of ${result.assertionResults.length} assertions failed:`,
|
|
1160
|
-
color
|
|
1161
|
-
)
|
|
1162
|
-
);
|
|
1290
|
+
out.push(bold(`Attest diagnostic \u2014 ${headerCounts(result)}:`, color));
|
|
1163
1291
|
for (const r of rows) {
|
|
1164
1292
|
out.push(renderAssertionFailure(r, options));
|
|
1165
1293
|
}
|
|
1166
1294
|
out.push(dim(` Summary: ${renderSummary(result, { color })}`, color));
|
|
1167
1295
|
return out.join("\n");
|
|
1168
1296
|
}
|
|
1297
|
+
function headerCounts(result) {
|
|
1298
|
+
const total = result.assertionResults.length;
|
|
1299
|
+
const notEvaluated = result.notEvaluated.length;
|
|
1300
|
+
if (result.failCount === 0 && notEvaluated > 0) {
|
|
1301
|
+
return `${notEvaluated} of ${total} assertions not evaluated`;
|
|
1302
|
+
}
|
|
1303
|
+
const text = `${result.failCount} of ${total} assertions failed`;
|
|
1304
|
+
return notEvaluated > 0 ? `${text}, ${notEvaluated} not evaluated` : text;
|
|
1305
|
+
}
|
|
1169
1306
|
|
|
1170
1307
|
// src/trace-tree.ts
|
|
1171
1308
|
var TraceTree = class {
|
|
@@ -3021,17 +3158,27 @@ export {
|
|
|
3021
3158
|
EngineTimeoutError,
|
|
3022
3159
|
ExpectChain,
|
|
3023
3160
|
FRIENDLY_USER,
|
|
3161
|
+
FailureClass,
|
|
3024
3162
|
GeminiAdapter,
|
|
3025
3163
|
LangChainAdapter,
|
|
3026
3164
|
ManualAdapter,
|
|
3027
3165
|
OTelAdapter,
|
|
3166
|
+
OUTCOME_FAIL,
|
|
3167
|
+
OUTCOME_PASS,
|
|
3168
|
+
OUTCOME_SKIP,
|
|
3028
3169
|
OllamaAdapter,
|
|
3029
3170
|
OpenAIAdapter,
|
|
3030
3171
|
PluginRegistry,
|
|
3031
3172
|
ProtocolDesyncError,
|
|
3032
3173
|
ProtocolDiagnosticBuffer,
|
|
3033
3174
|
ProtocolError,
|
|
3175
|
+
REASON_MISSING_EVIDENCE,
|
|
3176
|
+
REASON_PROVIDER_NOT_CONFIGURED,
|
|
3177
|
+
REASON_PROVIDER_UNAVAILABLE,
|
|
3178
|
+
REASON_SHORT_CIRCUITED,
|
|
3179
|
+
REASON_SIMULATION,
|
|
3034
3180
|
STATUS_HARD_FAIL,
|
|
3181
|
+
STATUS_NOT_EVALUATED,
|
|
3035
3182
|
STATUS_PASS,
|
|
3036
3183
|
STATUS_SOFT_FAIL,
|
|
3037
3184
|
STEP_AGENT_CALL,
|
|
@@ -3076,8 +3223,9 @@ export {
|
|
|
3076
3223
|
renderAssertionFailure,
|
|
3077
3224
|
renderDiagnostics,
|
|
3078
3225
|
renderSummary,
|
|
3226
|
+
requireSimulationCapability,
|
|
3079
3227
|
resetConfig,
|
|
3080
|
-
|
|
3228
|
+
simulationEnvEnabled,
|
|
3081
3229
|
tier,
|
|
3082
3230
|
traceId
|
|
3083
3231
|
};
|