@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.
@@ -50,17 +50,27 @@ __export(index_exports, {
50
50
  EngineTimeoutError: () => EngineTimeoutError,
51
51
  ExpectChain: () => ExpectChain,
52
52
  FRIENDLY_USER: () => FRIENDLY_USER,
53
+ FailureClass: () => FailureClass,
53
54
  GeminiAdapter: () => GeminiAdapter,
54
55
  LangChainAdapter: () => LangChainAdapter,
55
56
  ManualAdapter: () => ManualAdapter,
56
57
  OTelAdapter: () => OTelAdapter,
58
+ OUTCOME_FAIL: () => OUTCOME_FAIL,
59
+ OUTCOME_PASS: () => OUTCOME_PASS,
60
+ OUTCOME_SKIP: () => OUTCOME_SKIP,
57
61
  OllamaAdapter: () => OllamaAdapter,
58
62
  OpenAIAdapter: () => OpenAIAdapter,
59
63
  PluginRegistry: () => PluginRegistry,
60
64
  ProtocolDesyncError: () => ProtocolDesyncError,
61
65
  ProtocolDiagnosticBuffer: () => ProtocolDiagnosticBuffer,
62
66
  ProtocolError: () => ProtocolError,
67
+ REASON_MISSING_EVIDENCE: () => REASON_MISSING_EVIDENCE,
68
+ REASON_PROVIDER_NOT_CONFIGURED: () => REASON_PROVIDER_NOT_CONFIGURED,
69
+ REASON_PROVIDER_UNAVAILABLE: () => REASON_PROVIDER_UNAVAILABLE,
70
+ REASON_SHORT_CIRCUITED: () => REASON_SHORT_CIRCUITED,
71
+ REASON_SIMULATION: () => REASON_SIMULATION,
63
72
  STATUS_HARD_FAIL: () => STATUS_HARD_FAIL,
73
+ STATUS_NOT_EVALUATED: () => STATUS_NOT_EVALUATED,
64
74
  STATUS_PASS: () => STATUS_PASS,
65
75
  STATUS_SOFT_FAIL: () => STATUS_SOFT_FAIL,
66
76
  STEP_AGENT_CALL: () => STEP_AGENT_CALL,
@@ -105,8 +115,9 @@ __export(index_exports, {
105
115
  renderAssertionFailure: () => renderAssertionFailure,
106
116
  renderDiagnostics: () => renderDiagnostics,
107
117
  renderSummary: () => renderSummary,
118
+ requireSimulationCapability: () => requireSimulationCapability,
108
119
  resetConfig: () => resetConfig,
109
- simulationEvaluateBatch: () => simulationEvaluateBatch,
120
+ simulationEnvEnabled: () => simulationEnvEnabled,
110
121
  tier: () => tier,
111
122
  traceId: () => traceId
112
123
  });
@@ -120,6 +131,15 @@ var STEP_AGENT_CALL = "agent_call";
120
131
  var STATUS_PASS = "pass";
121
132
  var STATUS_SOFT_FAIL = "soft_fail";
122
133
  var STATUS_HARD_FAIL = "hard_fail";
134
+ var STATUS_NOT_EVALUATED = "not_evaluated";
135
+ var REASON_PROVIDER_UNAVAILABLE = "provider_unavailable";
136
+ var REASON_PROVIDER_NOT_CONFIGURED = "provider_not_configured";
137
+ var REASON_SIMULATION = "simulation";
138
+ var REASON_MISSING_EVIDENCE = "missing_evidence";
139
+ var REASON_SHORT_CIRCUITED = "short_circuited";
140
+ var OUTCOME_PASS = "pass";
141
+ var OUTCOME_FAIL = "fail";
142
+ var OUTCOME_SKIP = "skip";
123
143
  var TYPE_SCHEMA = "schema";
124
144
  var TYPE_CONSTRAINT = "constraint";
125
145
  var TYPE_TRACE = "trace";
@@ -145,6 +165,13 @@ function assertionId(id) {
145
165
  function agentId(id) {
146
166
  return id;
147
167
  }
168
+ var FailureClass = {
169
+ BrokenCode: "broken_code",
170
+ FlakyJudge: "flaky_judge",
171
+ BadRubric: "bad_rubric",
172
+ MissingTraceData: "missing_trace_data",
173
+ StochasticVariance: "stochastic_variance"
174
+ };
148
175
  function layerForType(assertionType) {
149
176
  switch (assertionType) {
150
177
  case "schema":
@@ -306,15 +333,14 @@ function extractId(response) {
306
333
  }
307
334
 
308
335
  // src/version.ts
309
- var VERSION = "0.9.0";
310
- var ENGINE_VERSION = "0.9.0";
336
+ var VERSION = "1.0.0-rc.1";
337
+ var ENGINE_VERSION = "1.0.0-rc.1";
311
338
 
312
339
  // src/engine-manager.ts
313
340
  var import_node_child_process = require("child_process");
314
341
  var import_node_readline = require("readline");
315
342
  var import_node_fs = require("fs");
316
343
  var import_node_path = require("path");
317
- var import_node_url = require("url");
318
344
 
319
345
  // src/engine-downloader.ts
320
346
  var https = __toESM(require("https"), 1);
@@ -326,10 +352,8 @@ var crypto = __toESM(require("crypto"), 1);
326
352
  var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
327
353
  var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
328
354
  var MAX_REDIRECTS = 5;
329
- function attestBinDir() {
330
- const dir = path.join(os.homedir(), ".attest", "bin");
331
- fs.mkdirSync(dir, { recursive: true });
332
- return dir;
355
+ function versionedBinDir() {
356
+ return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
333
357
  }
334
358
  function platformKey() {
335
359
  const platform2 = os.platform();
@@ -357,20 +381,8 @@ function binaryFilename() {
357
381
  return os.platform() === "win32" ? "attest-engine.exe" : "attest-engine";
358
382
  }
359
383
  function cachedEnginePath() {
360
- const dir = path.join(os.homedir(), ".attest", "bin");
361
- const binPath = path.join(dir, binaryFilename());
362
- const versionFile = path.join(dir, ".engine-version");
363
- if (!fs.existsSync(binPath)) {
364
- return null;
365
- }
366
- if (!fs.existsSync(versionFile)) {
367
- return null;
368
- }
369
- const cachedVersion = fs.readFileSync(versionFile, "utf-8").trim();
370
- if (cachedVersion !== ENGINE_VERSION) {
371
- return null;
372
- }
373
- return binPath;
384
+ const binPath = path.join(versionedBinDir(), binaryFilename());
385
+ return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
374
386
  }
375
387
  function parseChecksums(text) {
376
388
  const checksums = /* @__PURE__ */ new Map();
@@ -390,7 +402,7 @@ function httpsGet(url, redirectCount = 0) {
390
402
  new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
391
403
  );
392
404
  }
393
- return new Promise((resolve2, reject) => {
405
+ return new Promise((resolve, reject) => {
394
406
  const client = url.startsWith("https:") ? https : http;
395
407
  client.get(url, (res) => {
396
408
  const status = res.statusCode ?? 0;
@@ -401,7 +413,7 @@ function httpsGet(url, redirectCount = 0) {
401
413
  return;
402
414
  }
403
415
  res.resume();
404
- resolve2(httpsGet(location, redirectCount + 1));
416
+ resolve(httpsGet(location, redirectCount + 1));
405
417
  return;
406
418
  }
407
419
  if (status < 200 || status >= 300) {
@@ -411,14 +423,14 @@ function httpsGet(url, redirectCount = 0) {
411
423
  }
412
424
  const chunks = [];
413
425
  res.on("data", (chunk) => chunks.push(chunk));
414
- res.on("end", () => resolve2(Buffer.concat(chunks)));
426
+ res.on("end", () => resolve(Buffer.concat(chunks)));
415
427
  res.on("error", reject);
416
428
  }).on("error", reject);
417
429
  });
418
430
  }
419
431
  async function downloadEngine() {
420
432
  const key = platformKey();
421
- const filename = `attest-engine-${key}`;
433
+ const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
422
434
  const binaryUrl = `${RELEASE_BASE}/${filename}`;
423
435
  process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
424
436
  `);
@@ -438,66 +450,117 @@ async function downloadEngine() {
438
450
  const actualHash = crypto.createHash("sha256").update(data).digest("hex");
439
451
  if (actualHash !== expectedHash) {
440
452
  throw new Error(
441
- `SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Delete ~/.attest/bin/ and retry.`
453
+ `SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Retry or download manually.`
442
454
  );
443
455
  }
444
456
  process.stderr.write(`[attest] Checksum verified.
445
457
  `);
446
- const dir = attestBinDir();
447
- const destFilename = binaryFilename();
448
- const destPath = path.join(dir, destFilename);
449
- const tmpPath = path.join(dir, `.${destFilename}.tmp.${process.pid}`);
450
- fs.writeFileSync(tmpPath, data);
451
- fs.chmodSync(tmpPath, 493);
452
- fs.renameSync(tmpPath, destPath);
453
- fs.writeFileSync(path.join(dir, ".engine-version"), ENGINE_VERSION);
458
+ const dir = versionedBinDir();
459
+ fs.mkdirSync(dir, { recursive: true });
460
+ const destPath = path.join(dir, binaryFilename());
461
+ const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
462
+ const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
463
+ try {
464
+ fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
465
+ fs.chmodSync(tmpPath, 493);
466
+ fs.renameSync(tmpPath, destPath);
467
+ } catch (err) {
468
+ if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
469
+ throw err;
470
+ }
454
471
  process.stderr.write(`[attest] Engine installed at ${destPath}
455
472
  `);
456
473
  return destPath;
457
474
  }
458
475
 
459
476
  // src/engine-manager.ts
460
- var import_meta = {};
461
477
  var ENGINE_BINARY_NAME = "attest-engine";
462
- async function findEngineBinary() {
463
- const envPath = process.env["ATTEST_ENGINE_PATH"];
464
- if (envPath) {
465
- if (!(0, import_node_fs.existsSync)(envPath)) {
466
- throw new Error(
467
- `ATTEST_ENGINE_PATH is set to '${envPath}' but the file does not exist.`
468
- );
469
- }
470
- return envPath;
471
- }
472
- try {
473
- const cmd = process.platform === "win32" ? "where" : "which";
474
- const found = (0, import_node_child_process.execSync)(`${cmd} ${ENGINE_BINARY_NAME}`, { encoding: "utf-8" }).trim();
475
- if (found) return found;
476
- } catch {
478
+ var VERSION_PROBE_TIMEOUT_MS = 1e4;
479
+ var EngineVersionMismatchError = class extends Error {
480
+ enginePath;
481
+ source;
482
+ found;
483
+ required;
484
+ constructor(enginePath, source, found, required) {
485
+ const reported = found === null ? "reports no version" : `reports version ${found}`;
486
+ super(
487
+ `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}/.`
488
+ );
489
+ this.name = "EngineVersionMismatchError";
490
+ this.enginePath = enginePath;
491
+ this.source = source;
492
+ this.found = found;
493
+ this.required = required;
477
494
  }
478
- const cached = cachedEnginePath();
479
- if (cached) return cached;
480
- const candidates = [
481
- (0, import_node_path.join)((0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "..", "..", "..", "..", "bin", ENGINE_BINARY_NAME),
482
- (0, import_node_path.join)(process.cwd(), "bin", ENGINE_BINARY_NAME)
483
- ];
484
- for (const candidate of candidates) {
485
- const resolved = (0, import_node_path.resolve)(candidate);
495
+ };
496
+ function engineVersion(enginePath) {
497
+ const proc = (0, import_node_child_process.spawnSync)(enginePath, ["version"], {
498
+ stdio: ["ignore", "pipe", "pipe"],
499
+ encoding: "utf-8",
500
+ timeout: VERSION_PROBE_TIMEOUT_MS
501
+ });
502
+ if (proc.error) throw proc.error;
503
+ const out = proc.stdout.trim();
504
+ const prefix = `${ENGINE_BINARY_NAME} `;
505
+ if (proc.status !== 0 || !out.startsWith(prefix)) return null;
506
+ return out.slice(prefix.length).trim();
507
+ }
508
+ function onPath() {
509
+ for (const dir of (process.env["PATH"] ?? "").split(import_node_path.delimiter)) {
510
+ if (!dir) continue;
511
+ const candidate = (0, import_node_path.join)(dir, binaryFilename());
486
512
  try {
487
- (0, import_node_fs.accessSync)(resolved, import_node_fs.constants.X_OK);
488
- if ((0, import_node_fs.existsSync)(resolved)) return resolved;
513
+ (0, import_node_fs.accessSync)(candidate, import_node_fs.constants.X_OK);
489
514
  } catch {
515
+ continue;
490
516
  }
517
+ if ((0, import_node_fs.statSync)(candidate).isFile()) return candidate;
518
+ }
519
+ return null;
520
+ }
521
+ function requireFile(enginePath, label) {
522
+ if (!(0, import_node_fs.existsSync)(enginePath) || !(0, import_node_fs.statSync)(enginePath).isFile()) {
523
+ throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
524
+ }
525
+ }
526
+ async function locateEngine(explicitPath, allowDownload) {
527
+ if (explicitPath) {
528
+ requireFile(explicitPath, "Engine path");
529
+ return [explicitPath, "explicit engine path"];
491
530
  }
492
- const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"];
531
+ const envPath = process.env["ATTEST_ENGINE_PATH"];
532
+ if (envPath) {
533
+ requireFile(envPath, "ATTEST_ENGINE_PATH");
534
+ return [envPath, "ATTEST_ENGINE_PATH"];
535
+ }
536
+ const found = onPath();
537
+ if (found) return [found, "PATH"];
538
+ const cached = cachedEnginePath();
539
+ if (cached) return [cached, "engine cache"];
540
+ if (!allowDownload) {
541
+ throw new Error(
542
+ `No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
543
+ );
544
+ }
545
+ const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
493
546
  if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
494
547
  throw new Error(
495
- `Cannot find '${ENGINE_BINARY_NAME}' binary and auto-download is disabled (ATTEST_ENGINE_NO_DOWNLOAD is set). Install the engine manually:
496
- 1. Download from https://github.com/attest-framework/attest/releases
497
- 2. Place the binary on your PATH or set ATTEST_ENGINE_PATH`
548
+ `Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
549
+ Install options:
550
+ 1. Build from source: make engine, then set ATTEST_ENGINE_PATH
551
+ 2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
552
+ 3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
498
553
  );
499
554
  }
500
- return downloadEngine();
555
+ return [await downloadEngine(), "download"];
556
+ }
557
+ async function resolveEngine(explicitPath, options = {}) {
558
+ const [enginePath, source] = await locateEngine(explicitPath, options.allowDownload ?? true);
559
+ const found = engineVersion(enginePath);
560
+ if (found !== ENGINE_VERSION) {
561
+ throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
562
+ }
563
+ return enginePath;
501
564
  }
502
565
  var EngineManager = class {
503
566
  enginePath;
@@ -512,28 +575,41 @@ var EngineManager = class {
512
575
  this.logLevel = options?.logLevel ?? "warn";
513
576
  }
514
577
  async start() {
515
- if (!this.enginePath) {
516
- this.enginePath = await findEngineBinary();
517
- }
578
+ this.enginePath = await resolveEngine(this.enginePath || void 0);
518
579
  this.process = (0, import_node_child_process.spawn)(this.enginePath, [`--log-level=${this.logLevel}`], {
519
580
  stdio: ["pipe", "pipe", "pipe"]
520
581
  });
521
582
  this.reader = (0, import_node_readline.createInterface)({ input: this.process.stdout });
522
- const result = await this.sendRequestInternal("initialize", {
583
+ try {
584
+ this.initResult = await this.initialize();
585
+ } catch (err) {
586
+ await this.stop();
587
+ throw err;
588
+ }
589
+ this.initialized = true;
590
+ return this.initResult;
591
+ }
592
+ /** Send initialize and check the engine's version and capabilities. */
593
+ async initialize() {
594
+ const init = await this.sendRequestInternal("initialize", {
523
595
  sdk_name: "attest-typescript",
524
596
  sdk_version: VERSION,
525
597
  protocol_version: 1,
526
598
  required_capabilities: ["layers_1_4"],
527
599
  preferred_encoding: "json"
528
600
  });
529
- this.initResult = result;
530
- if (!this.initResult.compatible) {
531
- throw new Error(
532
- `Engine incompatible. Missing capabilities: ${JSON.stringify(this.initResult.missing)}`
601
+ if (init.engine_version !== ENGINE_VERSION) {
602
+ throw new EngineVersionMismatchError(
603
+ this.enginePath,
604
+ "initialize",
605
+ init.engine_version,
606
+ ENGINE_VERSION
533
607
  );
534
608
  }
535
- this.initialized = true;
536
- return this.initResult;
609
+ if (!init.compatible) {
610
+ throw new Error(`Engine incompatible. Missing capabilities: ${JSON.stringify(init.missing)}`);
611
+ }
612
+ return init;
537
613
  }
538
614
  async stop() {
539
615
  if (this.process === null) return;
@@ -574,11 +650,11 @@ var EngineManager = class {
574
650
  }
575
651
  this.requestId += 1;
576
652
  const requestStr = encodeRequest(this.requestId, method, params);
577
- return new Promise((resolve2, reject) => {
653
+ return new Promise((resolve, reject) => {
578
654
  const onLine = (line) => {
579
655
  try {
580
656
  const response = decodeResponse(line);
581
- resolve2(extractResult(response));
657
+ resolve(extractResult(response));
582
658
  } catch (err) {
583
659
  reject(err);
584
660
  }
@@ -626,7 +702,11 @@ function config(options) {
626
702
  }
627
703
  }
628
704
  function isSimulationMode() {
629
- return _simulationMode || process.env["ATTEST_SIMULATION"] === "1" || process.env["ATTEST_SIMULATION"] === "true" || process.env["ATTEST_SIMULATION"] === "yes";
705
+ return _simulationMode || simulationEnvEnabled();
706
+ }
707
+ function simulationEnvEnabled() {
708
+ const env = (process.env["ATTEST_SIMULATION"] ?? "").trim().toLowerCase();
709
+ return env === "1" || env === "true" || env === "yes";
630
710
  }
631
711
  function getSampleRate() {
632
712
  if (_sampleRate > 0) return _sampleRate;
@@ -647,24 +727,6 @@ function resetConfig() {
647
727
  _sampleRate = 0;
648
728
  }
649
729
 
650
- // src/simulation.ts
651
- function simulationEvaluateBatch(assertions) {
652
- const results = assertions.map((assertion) => ({
653
- assertion_id: assertion.assertion_id,
654
- status: "pass",
655
- score: 1,
656
- explanation: `[simulation] assertion '${assertion.assertion_id}' passed (type: ${assertion.type})`,
657
- cost: 0,
658
- duration_ms: 0
659
- }));
660
- return {
661
- results,
662
- total_cost: 0,
663
- total_duration_ms: 0,
664
- simulated: true
665
- };
666
- }
667
-
668
730
  // src/client.ts
669
731
  var DEFAULT_TIMEOUT_MS = 3e4;
670
732
  var DEFAULT_DIAGNOSTIC_BUFFER = 32;
@@ -889,12 +951,12 @@ var AttestClient = class {
889
951
  throw new Error("Engine process not started.");
890
952
  }
891
953
  let capturedReqId;
892
- const requestPromise = new Promise((resolve2, reject) => {
954
+ const requestPromise = new Promise((resolve, reject) => {
893
955
  this.writeChain = this.writeChain.then(() => {
894
956
  this.requestId += 1;
895
957
  const reqId = this.requestId;
896
958
  capturedReqId = reqId;
897
- this.pending.set(reqId, { resolve: resolve2, reject });
959
+ this.pending.set(reqId, { resolve, reject });
898
960
  const requestStr = encodeRequest(reqId, method, params);
899
961
  cp.stdin.write(requestStr, (err) => {
900
962
  if (err) {
@@ -914,7 +976,7 @@ var AttestClient = class {
914
976
  }
915
977
  async evaluateBatch(trace, assertions, options) {
916
978
  if (isSimulationMode()) {
917
- return simulationEvaluateBatch(assertions);
979
+ requireSimulationCapability(this.engine["initResult"]);
918
980
  }
919
981
  const params = {
920
982
  trace,
@@ -953,10 +1015,10 @@ var AttestClient = class {
953
1015
  if (isSimulationMode()) {
954
1016
  return {
955
1017
  assertion_id: assertionId2,
956
- mean: 1,
1018
+ mean: 0,
957
1019
  stddev: 0,
958
1020
  count: 0,
959
- latest_score: 1,
1021
+ latest_score: 0,
960
1022
  deviation: 0,
961
1023
  status: "no_data"
962
1024
  };
@@ -994,6 +1056,16 @@ function classifyDecodeError(message) {
994
1056
  if (message.startsWith("empty response line")) return "malformed_json";
995
1057
  return "malformed_json";
996
1058
  }
1059
+ function requireSimulationCapability(init) {
1060
+ if (init === null || init === void 0) {
1061
+ throw new Error("simulation mode: the engine has not been initialized");
1062
+ }
1063
+ if (!(init.capabilities ?? []).includes("simulation_mode")) {
1064
+ throw new Error(
1065
+ `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.`
1066
+ );
1067
+ }
1068
+ }
997
1069
 
998
1070
  // src/trace.ts
999
1071
  var import_node_crypto = require("crypto");
@@ -1084,6 +1156,20 @@ var TraceBuilder = class {
1084
1156
  };
1085
1157
 
1086
1158
  // src/result.ts
1159
+ var KNOWN_STATUSES = /* @__PURE__ */ new Set([
1160
+ STATUS_PASS,
1161
+ STATUS_SOFT_FAIL,
1162
+ STATUS_HARD_FAIL,
1163
+ STATUS_NOT_EVALUATED
1164
+ ]);
1165
+ function normalizeAssertionResult(r) {
1166
+ if (KNOWN_STATUSES.has(r.status)) return r;
1167
+ return {
1168
+ ...r,
1169
+ status: STATUS_NOT_EVALUATED,
1170
+ explanation: `[unknown status '${r.status}'] ${r.explanation}`
1171
+ };
1172
+ }
1087
1173
  var AgentResult = class {
1088
1174
  trace;
1089
1175
  assertionResults;
@@ -1091,15 +1177,31 @@ var AgentResult = class {
1091
1177
  totalDurationMs;
1092
1178
  constructor(trace, assertionResults = [], totalCost = 0, totalDurationMs = 0) {
1093
1179
  this.trace = trace;
1094
- this.assertionResults = assertionResults;
1180
+ this.assertionResults = assertionResults.map(normalizeAssertionResult);
1095
1181
  this.totalCost = totalCost;
1096
1182
  this.totalDurationMs = totalDurationMs;
1097
1183
  }
1184
+ /** True if every assertion has status "pass". */
1098
1185
  get passed() {
1099
1186
  return this.assertionResults.every((r) => r.status === STATUS_PASS);
1100
1187
  }
1188
+ /**
1189
+ * Test outcome after the whole batch. Any hard_fail fails; otherwise any
1190
+ * not_evaluated skips (fails when `strict`); otherwise pass, including
1191
+ * when only soft_fail rows are present (see `softFailures`).
1192
+ */
1193
+ outcome(options) {
1194
+ if (this.hardFailures.length > 0) return OUTCOME_FAIL;
1195
+ if (this.notEvaluated.length > 0) {
1196
+ return options?.strict === true ? OUTCOME_FAIL : OUTCOME_SKIP;
1197
+ }
1198
+ return OUTCOME_PASS;
1199
+ }
1200
+ /** Failed assertions: hard_fail and soft_fail only. */
1101
1201
  get failedAssertions() {
1102
- return this.assertionResults.filter((r) => r.status !== STATUS_PASS);
1202
+ return this.assertionResults.filter(
1203
+ (r) => r.status === STATUS_HARD_FAIL || r.status === STATUS_SOFT_FAIL
1204
+ );
1103
1205
  }
1104
1206
  get hardFailures() {
1105
1207
  return this.assertionResults.filter((r) => r.status === STATUS_HARD_FAIL);
@@ -1107,11 +1209,40 @@ var AgentResult = class {
1107
1209
  get softFailures() {
1108
1210
  return this.assertionResults.filter((r) => r.status === STATUS_SOFT_FAIL);
1109
1211
  }
1212
+ /** Assertions the engine did not evaluate; each carries a `reason`. */
1213
+ get notEvaluated() {
1214
+ return this.assertionResults.filter((r) => r.status === STATUS_NOT_EVALUATED);
1215
+ }
1216
+ /** Count of not_evaluated assertions per reason, in first-seen order. */
1217
+ get notEvaluatedReasons() {
1218
+ const counts = {};
1219
+ for (const r of this.notEvaluated) {
1220
+ const key = r.reason || "unknown";
1221
+ counts[key] = (counts[key] ?? 0) + 1;
1222
+ }
1223
+ return counts;
1224
+ }
1225
+ /**
1226
+ * Passed / evaluated assertions; null when nothing was evaluated.
1227
+ * not_evaluated rows are excluded from the denominator.
1228
+ */
1229
+ get passRate() {
1230
+ const evaluated = this.assertionResults.length - this.notEvaluated.length;
1231
+ return evaluated === 0 ? null : this.passCount / evaluated;
1232
+ }
1110
1233
  get passCount() {
1111
1234
  return this.assertionResults.filter((r) => r.status === STATUS_PASS).length;
1112
1235
  }
1236
+ /** Number of failed assertions (hard_fail + soft_fail). */
1113
1237
  get failCount() {
1114
- return this.assertionResults.length - this.passCount;
1238
+ return this.failedAssertions.length;
1239
+ }
1240
+ /** "not evaluated: <reason>[, <reason> ×n ...]" for skipped tests. */
1241
+ skipReason() {
1242
+ const parts = Object.entries(this.notEvaluatedReasons).map(
1243
+ ([reason, n]) => n === 1 ? reason : `${reason} \xD7${n}`
1244
+ );
1245
+ return `not evaluated: ${parts.join(", ")}`;
1115
1246
  }
1116
1247
  };
1117
1248
 
@@ -1151,6 +1282,9 @@ function statusGlyph(status, color) {
1151
1282
  } else if (status === STATUS_HARD_FAIL) {
1152
1283
  glyph = "FAIL";
1153
1284
  c = ANSI_RED;
1285
+ } else if (status === STATUS_NOT_EVALUATED) {
1286
+ glyph = "N/E";
1287
+ c = ANSI_DIM;
1154
1288
  } else {
1155
1289
  glyph = status.toUpperCase() || "????";
1156
1290
  c = ANSI_DIM;
@@ -1178,10 +1312,15 @@ function renderAssertionFailure(result, options = {}) {
1178
1312
  lines.push(
1179
1313
  ` ${statusGlyph(result.status, color)} ${bold(result.assertion_id, color)} ${dim(`\u2014 ${label}`, color)}`
1180
1314
  );
1315
+ if (result.reason) {
1316
+ lines.push(` reason: not evaluated: ${result.reason}`);
1317
+ }
1181
1318
  if (result.trace_node_path) {
1182
1319
  lines.push(` trace path: ${result.trace_node_path}`);
1183
1320
  }
1184
- lines.push(` score: ${result.score.toFixed(3)}`);
1321
+ if (result.status !== STATUS_NOT_EVALUATED) {
1322
+ lines.push(` score: ${result.score.toFixed(3)}`);
1323
+ }
1185
1324
  if (result.expected) {
1186
1325
  lines.push(` expected: ${truncate(result.expected)}`);
1187
1326
  }
@@ -1194,6 +1333,9 @@ function renderAssertionFailure(result, options = {}) {
1194
1333
  if (result.threshold_source && result.threshold_source !== "static") {
1195
1334
  lines.push(` threshold: ${result.threshold_source}`);
1196
1335
  }
1336
+ if (result.failure_class) {
1337
+ lines.push(` class: ${result.failure_class}`);
1338
+ }
1197
1339
  if (result.judge_metadata) {
1198
1340
  appendJudgeMetadata(lines, result.judge_metadata);
1199
1341
  }
@@ -1256,6 +1398,7 @@ function renderSummary(result, options = {}) {
1256
1398
  `${statusGlyph(STATUS_PASS, color)} ${passN}`,
1257
1399
  `${statusGlyph(STATUS_SOFT_FAIL, color)} ${soft}`,
1258
1400
  `${statusGlyph(STATUS_HARD_FAIL, color)} ${hard}`,
1401
+ `${statusGlyph(STATUS_NOT_EVALUATED, color)} ${result.notEvaluated.length}`,
1259
1402
  `cost $${result.totalCost.toFixed(6)}`,
1260
1403
  `dur ${result.totalDurationMs}ms`
1261
1404
  ];
@@ -1264,23 +1407,27 @@ function renderSummary(result, options = {}) {
1264
1407
  function renderDiagnostics(result, options = {}) {
1265
1408
  const color = options.color ?? false;
1266
1409
  const includePassing = options.includePassing ?? false;
1267
- const rows = includePassing ? result.assertionResults : result.failedAssertions;
1410
+ const rows = includePassing ? result.assertionResults : result.assertionResults.filter((r) => r.status !== STATUS_PASS);
1268
1411
  if (rows.length === 0) {
1269
1412
  return dim(" (no failures)\n", color);
1270
1413
  }
1271
1414
  const out = [""];
1272
- out.push(
1273
- bold(
1274
- `Attest diagnostic \u2014 ${rows.length} of ${result.assertionResults.length} assertions failed:`,
1275
- color
1276
- )
1277
- );
1415
+ out.push(bold(`Attest diagnostic \u2014 ${headerCounts(result)}:`, color));
1278
1416
  for (const r of rows) {
1279
1417
  out.push(renderAssertionFailure(r, options));
1280
1418
  }
1281
1419
  out.push(dim(` Summary: ${renderSummary(result, { color })}`, color));
1282
1420
  return out.join("\n");
1283
1421
  }
1422
+ function headerCounts(result) {
1423
+ const total = result.assertionResults.length;
1424
+ const notEvaluated = result.notEvaluated.length;
1425
+ if (result.failCount === 0 && notEvaluated > 0) {
1426
+ return `${notEvaluated} of ${total} assertions not evaluated`;
1427
+ }
1428
+ const text = `${result.failCount} of ${total} assertions failed`;
1429
+ return notEvaluated > 0 ? `${text}, ${notEvaluated} not evaluated` : text;
1430
+ }
1284
1431
 
1285
1432
  // src/trace-tree.ts
1286
1433
  var TraceTree = class {
@@ -3137,17 +3284,27 @@ var LangChainAdapter = class {
3137
3284
  EngineTimeoutError,
3138
3285
  ExpectChain,
3139
3286
  FRIENDLY_USER,
3287
+ FailureClass,
3140
3288
  GeminiAdapter,
3141
3289
  LangChainAdapter,
3142
3290
  ManualAdapter,
3143
3291
  OTelAdapter,
3292
+ OUTCOME_FAIL,
3293
+ OUTCOME_PASS,
3294
+ OUTCOME_SKIP,
3144
3295
  OllamaAdapter,
3145
3296
  OpenAIAdapter,
3146
3297
  PluginRegistry,
3147
3298
  ProtocolDesyncError,
3148
3299
  ProtocolDiagnosticBuffer,
3149
3300
  ProtocolError,
3301
+ REASON_MISSING_EVIDENCE,
3302
+ REASON_PROVIDER_NOT_CONFIGURED,
3303
+ REASON_PROVIDER_UNAVAILABLE,
3304
+ REASON_SHORT_CIRCUITED,
3305
+ REASON_SIMULATION,
3150
3306
  STATUS_HARD_FAIL,
3307
+ STATUS_NOT_EVALUATED,
3151
3308
  STATUS_PASS,
3152
3309
  STATUS_SOFT_FAIL,
3153
3310
  STEP_AGENT_CALL,
@@ -3192,8 +3349,9 @@ var LangChainAdapter = class {
3192
3349
  renderAssertionFailure,
3193
3350
  renderDiagnostics,
3194
3351
  renderSummary,
3352
+ requireSimulationCapability,
3195
3353
  resetConfig,
3196
- simulationEvaluateBatch,
3354
+ simulationEnvEnabled,
3197
3355
  tier,
3198
3356
  traceId
3199
3357
  });