@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/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.9.0";
196
- var ENGINE_VERSION = "0.9.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, execSync } from "child_process";
215
+ import { spawn, spawnSync } from "child_process";
200
216
  import { createInterface } from "readline";
201
- import { existsSync as existsSync2, accessSync, constants as fsConstants } from "fs";
202
- import { resolve, join as join2, dirname } from "path";
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 attestBinDir() {
216
- const dir = path.join(os.homedir(), ".attest", "bin");
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 dir = path.join(os.homedir(), ".attest", "bin");
247
- const binPath = path.join(dir, binaryFilename());
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((resolve2, reject) => {
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
- resolve2(httpsGet(location, redirectCount + 1));
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", () => resolve2(Buffer.concat(chunks)));
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. Delete ~/.attest/bin/ and retry.`
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 = attestBinDir();
333
- const destFilename = binaryFilename();
334
- const destPath = path.join(dir, destFilename);
335
- const tmpPath = path.join(dir, `.${destFilename}.tmp.${process.pid}`);
336
- fs.writeFileSync(tmpPath, data);
337
- fs.chmodSync(tmpPath, 493);
338
- fs.renameSync(tmpPath, destPath);
339
- fs.writeFileSync(path.join(dir, ".engine-version"), ENGINE_VERSION);
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
- async function findEngineBinary() {
348
- const envPath = process.env["ATTEST_ENGINE_PATH"];
349
- if (envPath) {
350
- if (!existsSync2(envPath)) {
351
- throw new Error(
352
- `ATTEST_ENGINE_PATH is set to '${envPath}' but the file does not exist.`
353
- );
354
- }
355
- return envPath;
356
- }
357
- try {
358
- const cmd = process.platform === "win32" ? "where" : "which";
359
- const found = execSync(`${cmd} ${ENGINE_BINARY_NAME}`, { encoding: "utf-8" }).trim();
360
- if (found) return found;
361
- } catch {
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
- const cached = cachedEnginePath();
364
- if (cached) return cached;
365
- const candidates = [
366
- join2(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..", "..", "bin", ENGINE_BINARY_NAME),
367
- join2(process.cwd(), "bin", ENGINE_BINARY_NAME)
368
- ];
369
- for (const candidate of candidates) {
370
- const resolved = resolve(candidate);
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(resolved, fsConstants.X_OK);
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 noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"];
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}' binary and auto-download is disabled (ATTEST_ENGINE_NO_DOWNLOAD is set). Install the engine manually:
381
- 1. Download from https://github.com/attest-framework/attest/releases
382
- 2. Place the binary on your PATH or set ATTEST_ENGINE_PATH`
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
- if (!this.enginePath) {
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
- const result = await this.sendRequestInternal("initialize", {
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
- this.initResult = result;
415
- if (!this.initResult.compatible) {
416
- throw new Error(
417
- `Engine incompatible. Missing capabilities: ${JSON.stringify(this.initResult.missing)}`
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
- this.initialized = true;
421
- return this.initResult;
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((resolve2, reject) => {
528
+ return new Promise((resolve, reject) => {
463
529
  const onLine = (line) => {
464
530
  try {
465
531
  const response = decodeResponse(line);
466
- resolve2(extractResult(response));
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 || process.env["ATTEST_SIMULATION"] === "1" || process.env["ATTEST_SIMULATION"] === "true" || process.env["ATTEST_SIMULATION"] === "yes";
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((resolve2, reject) => {
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: resolve2, reject });
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
- return simulationEvaluateBatch(assertions);
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: 1,
893
+ mean: 0,
842
894
  stddev: 0,
843
895
  count: 0,
844
- latest_score: 1,
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((r) => r.status !== STATUS_PASS);
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.assertionResults.length - this.passCount;
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
- lines.push(` score: ${result.score.toFixed(3)}`);
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.failedAssertions;
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
- simulationEvaluateBatch,
3228
+ simulationEnvEnabled,
3081
3229
  tier,
3082
3230
  traceId
3083
3231
  };