@attest-ai/core 0.6.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,15 +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,
64
+ ProtocolDesyncError: () => ProtocolDesyncError,
65
+ ProtocolDiagnosticBuffer: () => ProtocolDiagnosticBuffer,
60
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,
61
72
  STATUS_HARD_FAIL: () => STATUS_HARD_FAIL,
73
+ STATUS_NOT_EVALUATED: () => STATUS_NOT_EVALUATED,
62
74
  STATUS_PASS: () => STATUS_PASS,
63
75
  STATUS_SOFT_FAIL: () => STATUS_SOFT_FAIL,
64
76
  STEP_AGENT_CALL: () => STEP_AGENT_CALL,
@@ -85,6 +97,7 @@ __export(index_exports, {
85
97
  agentId: () => agentId,
86
98
  assertionId: () => assertionId,
87
99
  attestExpect: () => attestExpect,
100
+ computeAgreement: () => computeAgreement,
88
101
  config: () => config,
89
102
  decodeResponse: () => decodeResponse,
90
103
  delegate: () => delegate,
@@ -94,8 +107,17 @@ __export(index_exports, {
94
107
  extractResult: () => extractResult,
95
108
  getSampleRate: () => getSampleRate,
96
109
  isSimulationMode: () => isSimulationMode,
110
+ layerForType: () => layerForType,
111
+ loadLabelsCSV: () => loadLabelsCSV,
112
+ loadLabelsJSONL: () => loadLabelsJSONL,
113
+ previewLine: () => previewLine,
114
+ promptHash: () => promptHash,
115
+ renderAssertionFailure: () => renderAssertionFailure,
116
+ renderDiagnostics: () => renderDiagnostics,
117
+ renderSummary: () => renderSummary,
118
+ requireSimulationCapability: () => requireSimulationCapability,
97
119
  resetConfig: () => resetConfig,
98
- simulationEvaluateBatch: () => simulationEvaluateBatch,
120
+ simulationEnvEnabled: () => simulationEnvEnabled,
99
121
  tier: () => tier,
100
122
  traceId: () => traceId
101
123
  });
@@ -109,6 +131,15 @@ var STEP_AGENT_CALL = "agent_call";
109
131
  var STATUS_PASS = "pass";
110
132
  var STATUS_SOFT_FAIL = "soft_fail";
111
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";
112
143
  var TYPE_SCHEMA = "schema";
113
144
  var TYPE_CONSTRAINT = "constraint";
114
145
  var TYPE_TRACE = "trace";
@@ -134,6 +165,34 @@ function assertionId(id) {
134
165
  function agentId(id) {
135
166
  return id;
136
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
+ };
175
+ function layerForType(assertionType) {
176
+ switch (assertionType) {
177
+ case "schema":
178
+ return 1;
179
+ case "constraint":
180
+ return 2;
181
+ case "trace":
182
+ case "trace_tree":
183
+ return 3;
184
+ case "content":
185
+ return 4;
186
+ case "embedding":
187
+ return 5;
188
+ case "llm_judge":
189
+ return 6;
190
+ case "plugin":
191
+ return 8;
192
+ default:
193
+ return 0;
194
+ }
195
+ }
137
196
 
138
197
  // src/proto/errors.ts
139
198
  var ProtocolError = class extends Error {
@@ -160,6 +219,58 @@ var EngineTimeoutError = class extends Error {
160
219
  this.timeoutMs = timeoutMs;
161
220
  }
162
221
  };
222
+ var ProtocolDesyncError = class extends Error {
223
+ diagnostics;
224
+ constructor(message, diagnostics) {
225
+ super(message);
226
+ this.name = "ProtocolDesyncError";
227
+ this.diagnostics = diagnostics;
228
+ }
229
+ };
230
+
231
+ // src/proto/diagnostics.ts
232
+ var DEFAULT_BUFFER_SIZE = 32;
233
+ var MAX_LINE_PREVIEW = 512;
234
+ var ProtocolDiagnosticBuffer = class {
235
+ capacity;
236
+ entries = [];
237
+ constructor(capacity = DEFAULT_BUFFER_SIZE) {
238
+ if (capacity <= 0) {
239
+ throw new Error(
240
+ `ProtocolDiagnosticBuffer capacity must be positive, got ${capacity}`
241
+ );
242
+ }
243
+ this.capacity = capacity;
244
+ }
245
+ push(diagnostic) {
246
+ if (this.entries.length >= this.capacity) {
247
+ this.entries.shift();
248
+ }
249
+ this.entries.push(diagnostic);
250
+ }
251
+ snapshot() {
252
+ return this.entries.slice();
253
+ }
254
+ countWithin(windowMs, nowMs) {
255
+ const cutoff = nowMs - windowMs;
256
+ let count = 0;
257
+ for (let i = this.entries.length - 1; i >= 0; i -= 1) {
258
+ if (this.entries[i].timestampMs >= cutoff) {
259
+ count += 1;
260
+ } else {
261
+ break;
262
+ }
263
+ }
264
+ return count;
265
+ }
266
+ clear() {
267
+ this.entries.length = 0;
268
+ }
269
+ };
270
+ function previewLine(line) {
271
+ if (line.length <= MAX_LINE_PREVIEW) return line;
272
+ return line.slice(0, MAX_LINE_PREVIEW) + "\u2026";
273
+ }
163
274
 
164
275
  // src/proto/codec.ts
165
276
  function encodeRequest(id, method, params) {
@@ -222,15 +333,14 @@ function extractId(response) {
222
333
  }
223
334
 
224
335
  // src/version.ts
225
- var VERSION = "0.6.0";
226
- var ENGINE_VERSION = "0.6.0";
336
+ var VERSION = "1.0.0-rc.1";
337
+ var ENGINE_VERSION = "1.0.0-rc.1";
227
338
 
228
339
  // src/engine-manager.ts
229
340
  var import_node_child_process = require("child_process");
230
341
  var import_node_readline = require("readline");
231
342
  var import_node_fs = require("fs");
232
343
  var import_node_path = require("path");
233
- var import_node_url = require("url");
234
344
 
235
345
  // src/engine-downloader.ts
236
346
  var https = __toESM(require("https"), 1);
@@ -242,10 +352,8 @@ var crypto = __toESM(require("crypto"), 1);
242
352
  var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
243
353
  var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
244
354
  var MAX_REDIRECTS = 5;
245
- function attestBinDir() {
246
- const dir = path.join(os.homedir(), ".attest", "bin");
247
- fs.mkdirSync(dir, { recursive: true });
248
- return dir;
355
+ function versionedBinDir() {
356
+ return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
249
357
  }
250
358
  function platformKey() {
251
359
  const platform2 = os.platform();
@@ -273,20 +381,8 @@ function binaryFilename() {
273
381
  return os.platform() === "win32" ? "attest-engine.exe" : "attest-engine";
274
382
  }
275
383
  function cachedEnginePath() {
276
- const dir = path.join(os.homedir(), ".attest", "bin");
277
- const binPath = path.join(dir, binaryFilename());
278
- const versionFile = path.join(dir, ".engine-version");
279
- if (!fs.existsSync(binPath)) {
280
- return null;
281
- }
282
- if (!fs.existsSync(versionFile)) {
283
- return null;
284
- }
285
- const cachedVersion = fs.readFileSync(versionFile, "utf-8").trim();
286
- if (cachedVersion !== ENGINE_VERSION) {
287
- return null;
288
- }
289
- return binPath;
384
+ const binPath = path.join(versionedBinDir(), binaryFilename());
385
+ return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
290
386
  }
291
387
  function parseChecksums(text) {
292
388
  const checksums = /* @__PURE__ */ new Map();
@@ -306,7 +402,7 @@ function httpsGet(url, redirectCount = 0) {
306
402
  new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
307
403
  );
308
404
  }
309
- return new Promise((resolve2, reject) => {
405
+ return new Promise((resolve, reject) => {
310
406
  const client = url.startsWith("https:") ? https : http;
311
407
  client.get(url, (res) => {
312
408
  const status = res.statusCode ?? 0;
@@ -317,7 +413,7 @@ function httpsGet(url, redirectCount = 0) {
317
413
  return;
318
414
  }
319
415
  res.resume();
320
- resolve2(httpsGet(location, redirectCount + 1));
416
+ resolve(httpsGet(location, redirectCount + 1));
321
417
  return;
322
418
  }
323
419
  if (status < 200 || status >= 300) {
@@ -327,14 +423,14 @@ function httpsGet(url, redirectCount = 0) {
327
423
  }
328
424
  const chunks = [];
329
425
  res.on("data", (chunk) => chunks.push(chunk));
330
- res.on("end", () => resolve2(Buffer.concat(chunks)));
426
+ res.on("end", () => resolve(Buffer.concat(chunks)));
331
427
  res.on("error", reject);
332
428
  }).on("error", reject);
333
429
  });
334
430
  }
335
431
  async function downloadEngine() {
336
432
  const key = platformKey();
337
- const filename = `attest-engine-${key}`;
433
+ const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
338
434
  const binaryUrl = `${RELEASE_BASE}/${filename}`;
339
435
  process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
340
436
  `);
@@ -354,66 +450,117 @@ async function downloadEngine() {
354
450
  const actualHash = crypto.createHash("sha256").update(data).digest("hex");
355
451
  if (actualHash !== expectedHash) {
356
452
  throw new Error(
357
- `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.`
358
454
  );
359
455
  }
360
456
  process.stderr.write(`[attest] Checksum verified.
361
457
  `);
362
- const dir = attestBinDir();
363
- const destFilename = binaryFilename();
364
- const destPath = path.join(dir, destFilename);
365
- const tmpPath = path.join(dir, `.${destFilename}.tmp.${process.pid}`);
366
- fs.writeFileSync(tmpPath, data);
367
- fs.chmodSync(tmpPath, 493);
368
- fs.renameSync(tmpPath, destPath);
369
- 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
+ }
370
471
  process.stderr.write(`[attest] Engine installed at ${destPath}
371
472
  `);
372
473
  return destPath;
373
474
  }
374
475
 
375
476
  // src/engine-manager.ts
376
- var import_meta = {};
377
477
  var ENGINE_BINARY_NAME = "attest-engine";
378
- async function findEngineBinary() {
379
- const envPath = process.env["ATTEST_ENGINE_PATH"];
380
- if (envPath) {
381
- if (!(0, import_node_fs.existsSync)(envPath)) {
382
- throw new Error(
383
- `ATTEST_ENGINE_PATH is set to '${envPath}' but the file does not exist.`
384
- );
385
- }
386
- return envPath;
387
- }
388
- try {
389
- const cmd = process.platform === "win32" ? "where" : "which";
390
- const found = (0, import_node_child_process.execSync)(`${cmd} ${ENGINE_BINARY_NAME}`, { encoding: "utf-8" }).trim();
391
- if (found) return found;
392
- } 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;
393
494
  }
394
- const cached = cachedEnginePath();
395
- if (cached) return cached;
396
- const candidates = [
397
- (0, import_node_path.join)((0, import_node_path.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "..", "..", "..", "..", "bin", ENGINE_BINARY_NAME),
398
- (0, import_node_path.join)(process.cwd(), "bin", ENGINE_BINARY_NAME)
399
- ];
400
- for (const candidate of candidates) {
401
- 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());
402
512
  try {
403
- (0, import_node_fs.accessSync)(resolved, import_node_fs.constants.X_OK);
404
- if ((0, import_node_fs.existsSync)(resolved)) return resolved;
513
+ (0, import_node_fs.accessSync)(candidate, import_node_fs.constants.X_OK);
405
514
  } catch {
515
+ continue;
406
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"];
530
+ }
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
+ );
407
544
  }
408
- const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"];
545
+ const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
409
546
  if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
410
547
  throw new Error(
411
- `Cannot find '${ENGINE_BINARY_NAME}' binary and auto-download is disabled (ATTEST_ENGINE_NO_DOWNLOAD is set). Install the engine manually:
412
- 1. Download from https://github.com/attest-framework/attest/releases
413
- 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`
414
553
  );
415
554
  }
416
- 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;
417
564
  }
418
565
  var EngineManager = class {
419
566
  enginePath;
@@ -428,28 +575,41 @@ var EngineManager = class {
428
575
  this.logLevel = options?.logLevel ?? "warn";
429
576
  }
430
577
  async start() {
431
- if (!this.enginePath) {
432
- this.enginePath = await findEngineBinary();
433
- }
578
+ this.enginePath = await resolveEngine(this.enginePath || void 0);
434
579
  this.process = (0, import_node_child_process.spawn)(this.enginePath, [`--log-level=${this.logLevel}`], {
435
580
  stdio: ["pipe", "pipe", "pipe"]
436
581
  });
437
582
  this.reader = (0, import_node_readline.createInterface)({ input: this.process.stdout });
438
- 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", {
439
595
  sdk_name: "attest-typescript",
440
596
  sdk_version: VERSION,
441
597
  protocol_version: 1,
442
598
  required_capabilities: ["layers_1_4"],
443
599
  preferred_encoding: "json"
444
600
  });
445
- this.initResult = result;
446
- if (!this.initResult.compatible) {
447
- throw new Error(
448
- `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
449
607
  );
450
608
  }
451
- this.initialized = true;
452
- return this.initResult;
609
+ if (!init.compatible) {
610
+ throw new Error(`Engine incompatible. Missing capabilities: ${JSON.stringify(init.missing)}`);
611
+ }
612
+ return init;
453
613
  }
454
614
  async stop() {
455
615
  if (this.process === null) return;
@@ -490,11 +650,11 @@ var EngineManager = class {
490
650
  }
491
651
  this.requestId += 1;
492
652
  const requestStr = encodeRequest(this.requestId, method, params);
493
- return new Promise((resolve2, reject) => {
653
+ return new Promise((resolve, reject) => {
494
654
  const onLine = (line) => {
495
655
  try {
496
656
  const response = decodeResponse(line);
497
- resolve2(extractResult(response));
657
+ resolve(extractResult(response));
498
658
  } catch (err) {
499
659
  reject(err);
500
660
  }
@@ -542,7 +702,11 @@ function config(options) {
542
702
  }
543
703
  }
544
704
  function isSimulationMode() {
545
- 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";
546
710
  }
547
711
  function getSampleRate() {
548
712
  if (_sampleRate > 0) return _sampleRate;
@@ -563,25 +727,11 @@ function resetConfig() {
563
727
  _sampleRate = 0;
564
728
  }
565
729
 
566
- // src/simulation.ts
567
- function simulationEvaluateBatch(assertions) {
568
- const results = assertions.map((assertion) => ({
569
- assertion_id: assertion.assertion_id,
570
- status: "pass",
571
- score: 1,
572
- explanation: `[simulation] assertion '${assertion.assertion_id}' passed (type: ${assertion.type})`,
573
- cost: 0,
574
- duration_ms: 0
575
- }));
576
- return {
577
- results,
578
- total_cost: 0,
579
- total_duration_ms: 0
580
- };
581
- }
582
-
583
730
  // src/client.ts
584
731
  var DEFAULT_TIMEOUT_MS = 3e4;
732
+ var DEFAULT_DIAGNOSTIC_BUFFER = 32;
733
+ var DEFAULT_DESYNC_THRESHOLD = 3;
734
+ var DEFAULT_DESYNC_WINDOW_MS = 1e3;
585
735
  function resolveTimeoutMs(override) {
586
736
  if (override !== void 0) return override;
587
737
  const env = process.env["ATTEST_ENGINE_TIMEOUT"];
@@ -603,6 +753,24 @@ function makeTimeoutPromise(method, timeoutMs) {
603
753
  }, timeoutMs);
604
754
  });
605
755
  }
756
+ function debugProtocolEnabled() {
757
+ const flag = process.env["ATTEST_DEBUG_PROTOCOL"];
758
+ return flag === "1" || flag === "true" || flag === "yes";
759
+ }
760
+ function envLoggerDefault() {
761
+ if (debugProtocolEnabled()) {
762
+ return {
763
+ warn: (msg, ...args) => console.warn(msg, ...args),
764
+ error: (msg, ...args) => console.error(msg, ...args)
765
+ };
766
+ }
767
+ return {
768
+ warn: () => {
769
+ },
770
+ error: () => {
771
+ }
772
+ };
773
+ }
606
774
  var AttestClient = class {
607
775
  engine;
608
776
  requestId = 0;
@@ -610,8 +778,34 @@ var AttestClient = class {
610
778
  writeChain = Promise.resolve();
611
779
  readerActive = false;
612
780
  lineHandler = null;
613
- constructor(engine) {
781
+ logger;
782
+ diagnosticBuffer;
783
+ diagnosticListeners = /* @__PURE__ */ new Set();
784
+ desyncThreshold;
785
+ desyncWindowMs;
786
+ desynced = false;
787
+ constructor(engine, options) {
614
788
  this.engine = engine;
789
+ this.logger = options?.logger ?? envLoggerDefault();
790
+ this.diagnosticBuffer = new ProtocolDiagnosticBuffer(
791
+ options?.diagnosticBufferSize ?? DEFAULT_DIAGNOSTIC_BUFFER
792
+ );
793
+ this.desyncThreshold = options?.desyncThreshold ?? DEFAULT_DESYNC_THRESHOLD;
794
+ this.desyncWindowMs = options?.desyncWindowMs ?? DEFAULT_DESYNC_WINDOW_MS;
795
+ if (options?.onDiagnostic !== void 0) {
796
+ this.diagnosticListeners.add(options.onDiagnostic);
797
+ }
798
+ }
799
+ /** Subscribe to protocol diagnostics. Returns an unsubscribe function. */
800
+ onProtocolDiagnostic(listener) {
801
+ this.diagnosticListeners.add(listener);
802
+ return () => {
803
+ this.diagnosticListeners.delete(listener);
804
+ };
805
+ }
806
+ /** Snapshot of the diagnostic ring buffer (most recent last). */
807
+ protocolDiagnostics() {
808
+ return this.diagnosticBuffer.snapshot();
615
809
  }
616
810
  startReader() {
617
811
  if (this.readerActive) return;
@@ -636,35 +830,101 @@ var AttestClient = class {
636
830
  this.lineHandler = null;
637
831
  this.readerActive = false;
638
832
  }
833
+ recordDiagnostic(kind, message, rawLine) {
834
+ const diagnostic = {
835
+ kind,
836
+ message,
837
+ rawLine: previewLine(rawLine),
838
+ timestampMs: Date.now()
839
+ };
840
+ this.diagnosticBuffer.push(diagnostic);
841
+ this.logger.warn(`[attest.protocol] ${kind}: ${message}`);
842
+ for (const listener of this.diagnosticListeners) {
843
+ try {
844
+ listener(diagnostic);
845
+ } catch (err) {
846
+ this.logger.error(
847
+ `[attest.protocol] diagnostic listener threw: ${err instanceof Error ? err.message : String(err)}`
848
+ );
849
+ }
850
+ }
851
+ this.maybeTriggerDesync(diagnostic.timestampMs);
852
+ }
853
+ maybeTriggerDesync(nowMs) {
854
+ if (this.desynced) return;
855
+ const recent = this.diagnosticBuffer.countWithin(this.desyncWindowMs, nowMs);
856
+ if (recent >= this.desyncThreshold) {
857
+ this.desynced = true;
858
+ const snapshot = this.diagnosticBuffer.snapshot();
859
+ const err = new ProtocolDesyncError(
860
+ `Engine protocol desync: ${recent} unroutable response lines within ${this.desyncWindowMs}ms.`,
861
+ snapshot
862
+ );
863
+ this.logger.error(`[attest.protocol] ${err.message}`);
864
+ this.failAll(err);
865
+ }
866
+ }
639
867
  handleLine(line) {
868
+ let response;
869
+ try {
870
+ response = decodeResponse(line);
871
+ } catch (err) {
872
+ this.handleDecodeFailure(line, err);
873
+ return;
874
+ }
640
875
  let reqId;
641
876
  try {
642
- const response = decodeResponse(line);
643
877
  reqId = extractId(response);
644
- const pending = this.pending.get(reqId);
645
- if (pending === void 0) return;
646
- this.pending.delete(reqId);
647
- try {
648
- const result = extractResult(response);
649
- pending.resolve(result);
650
- } catch (err) {
651
- pending.reject(err);
652
- }
653
878
  } catch (err) {
654
- if (err instanceof ProtocolError) {
655
- try {
656
- const raw = JSON.parse(line.trim());
657
- reqId = Number(raw.id ?? -1);
658
- } catch {
659
- reqId = -1;
660
- }
661
- const pending = this.pending.get(reqId);
662
- if (pending !== void 0) {
663
- this.pending.delete(reqId);
664
- pending.reject(err);
879
+ const message = err instanceof Error ? err.message : String(err);
880
+ this.recordDiagnostic("missing_id", message, line);
881
+ return;
882
+ }
883
+ const pending = this.pending.get(reqId);
884
+ if (pending === void 0) {
885
+ this.recordDiagnostic(
886
+ "unknown_id",
887
+ `no pending request for id=${reqId}`,
888
+ line
889
+ );
890
+ return;
891
+ }
892
+ this.pending.delete(reqId);
893
+ try {
894
+ const result = extractResult(response);
895
+ pending.resolve(result);
896
+ } catch (err) {
897
+ pending.reject(err);
898
+ }
899
+ }
900
+ handleDecodeFailure(line, err) {
901
+ if (err instanceof ProtocolError) {
902
+ let reqId = -1;
903
+ try {
904
+ const raw = JSON.parse(line.trim());
905
+ const idCandidate = raw["id"];
906
+ if (typeof idCandidate === "number" && Number.isFinite(idCandidate)) {
907
+ reqId = idCandidate;
665
908
  }
909
+ } catch {
910
+ reqId = -1;
666
911
  }
912
+ const pending = this.pending.get(reqId);
913
+ if (pending !== void 0) {
914
+ this.pending.delete(reqId);
915
+ pending.reject(err);
916
+ return;
917
+ }
918
+ this.recordDiagnostic(
919
+ "non_routable_error",
920
+ `engine error code=${err.code} (${err.errorMessage}) had no matching pending id`,
921
+ line
922
+ );
923
+ return;
667
924
  }
925
+ const message = err instanceof Error ? err.message : String(err);
926
+ const kind = classifyDecodeError(message);
927
+ this.recordDiagnostic(kind, message, line);
668
928
  }
669
929
  failAll(err) {
670
930
  for (const pending of this.pending.values()) {
@@ -673,6 +933,12 @@ var AttestClient = class {
673
933
  this.pending.clear();
674
934
  }
675
935
  async sendRequest(method, params, timeoutMs) {
936
+ if (this.desynced) {
937
+ throw new ProtocolDesyncError(
938
+ "Engine protocol is desynced; AttestClient will not send further requests.",
939
+ this.diagnosticBuffer.snapshot()
940
+ );
941
+ }
676
942
  const resolvedTimeout = resolveTimeoutMs(timeoutMs);
677
943
  if (!this.readerActive) {
678
944
  return Promise.race([
@@ -685,12 +951,12 @@ var AttestClient = class {
685
951
  throw new Error("Engine process not started.");
686
952
  }
687
953
  let capturedReqId;
688
- const requestPromise = new Promise((resolve2, reject) => {
954
+ const requestPromise = new Promise((resolve, reject) => {
689
955
  this.writeChain = this.writeChain.then(() => {
690
956
  this.requestId += 1;
691
957
  const reqId = this.requestId;
692
958
  capturedReqId = reqId;
693
- this.pending.set(reqId, { resolve: resolve2, reject });
959
+ this.pending.set(reqId, { resolve, reject });
694
960
  const requestStr = encodeRequest(reqId, method, params);
695
961
  cp.stdin.write(requestStr, (err) => {
696
962
  if (err) {
@@ -710,7 +976,7 @@ var AttestClient = class {
710
976
  }
711
977
  async evaluateBatch(trace, assertions, options) {
712
978
  if (isSimulationMode()) {
713
- return simulationEvaluateBatch(assertions);
979
+ requireSimulationCapability(this.engine["initResult"]);
714
980
  }
715
981
  const params = {
716
982
  trace,
@@ -749,10 +1015,10 @@ var AttestClient = class {
749
1015
  if (isSimulationMode()) {
750
1016
  return {
751
1017
  assertion_id: assertionId2,
752
- mean: 1,
1018
+ mean: 0,
753
1019
  stddev: 0,
754
1020
  count: 0,
755
- latest_score: 1,
1021
+ latest_score: 0,
756
1022
  deviation: 0,
757
1023
  status: "no_data"
758
1024
  };
@@ -783,6 +1049,23 @@ var AttestClient = class {
783
1049
  return String(raw.message);
784
1050
  }
785
1051
  };
1052
+ function classifyDecodeError(message) {
1053
+ if (message.startsWith("malformed JSON")) return "malformed_json";
1054
+ if (message.startsWith("expected JSON object")) return "non_object_response";
1055
+ if (message.startsWith("invalid jsonrpc version")) return "invalid_jsonrpc_version";
1056
+ if (message.startsWith("empty response line")) return "malformed_json";
1057
+ return "malformed_json";
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
+ }
786
1069
 
787
1070
  // src/trace.ts
788
1071
  var import_node_crypto = require("crypto");
@@ -873,6 +1156,20 @@ var TraceBuilder = class {
873
1156
  };
874
1157
 
875
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
+ }
876
1173
  var AgentResult = class {
877
1174
  trace;
878
1175
  assertionResults;
@@ -880,15 +1177,31 @@ var AgentResult = class {
880
1177
  totalDurationMs;
881
1178
  constructor(trace, assertionResults = [], totalCost = 0, totalDurationMs = 0) {
882
1179
  this.trace = trace;
883
- this.assertionResults = assertionResults;
1180
+ this.assertionResults = assertionResults.map(normalizeAssertionResult);
884
1181
  this.totalCost = totalCost;
885
1182
  this.totalDurationMs = totalDurationMs;
886
1183
  }
1184
+ /** True if every assertion has status "pass". */
887
1185
  get passed() {
888
1186
  return this.assertionResults.every((r) => r.status === STATUS_PASS);
889
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. */
890
1201
  get failedAssertions() {
891
- 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
+ );
892
1205
  }
893
1206
  get hardFailures() {
894
1207
  return this.assertionResults.filter((r) => r.status === STATUS_HARD_FAIL);
@@ -896,14 +1209,226 @@ var AgentResult = class {
896
1209
  get softFailures() {
897
1210
  return this.assertionResults.filter((r) => r.status === STATUS_SOFT_FAIL);
898
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
+ }
899
1233
  get passCount() {
900
1234
  return this.assertionResults.filter((r) => r.status === STATUS_PASS).length;
901
1235
  }
1236
+ /** Number of failed assertions (hard_fail + soft_fail). */
902
1237
  get failCount() {
903
- 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(", ")}`;
904
1246
  }
905
1247
  };
906
1248
 
1249
+ // src/diagnostics.ts
1250
+ var ANSI_RESET = "\x1B[0m";
1251
+ var ANSI_RED = "\x1B[31m";
1252
+ var ANSI_YELLOW = "\x1B[33m";
1253
+ var ANSI_GREEN = "\x1B[32m";
1254
+ var ANSI_DIM = "\x1B[2m";
1255
+ var ANSI_BOLD = "\x1B[1m";
1256
+ var LAYER_NAMES = {
1257
+ 1: "schema",
1258
+ 2: "constraint",
1259
+ 3: "trace",
1260
+ 4: "content",
1261
+ 5: "embedding",
1262
+ 6: "llm_judge",
1263
+ 7: "trace_tree",
1264
+ 8: "plugin"
1265
+ };
1266
+ function layerLabel(layer, assertionType) {
1267
+ if (layer === 0 && assertionType) {
1268
+ return `L? ${assertionType}`;
1269
+ }
1270
+ const name = LAYER_NAMES[layer] ?? assertionType ?? "uncategorized";
1271
+ return `L${layer} ${name}`;
1272
+ }
1273
+ function statusGlyph(status, color) {
1274
+ let glyph;
1275
+ let c;
1276
+ if (status === STATUS_PASS) {
1277
+ glyph = "PASS";
1278
+ c = ANSI_GREEN;
1279
+ } else if (status === STATUS_SOFT_FAIL) {
1280
+ glyph = "SOFT";
1281
+ c = ANSI_YELLOW;
1282
+ } else if (status === STATUS_HARD_FAIL) {
1283
+ glyph = "FAIL";
1284
+ c = ANSI_RED;
1285
+ } else if (status === STATUS_NOT_EVALUATED) {
1286
+ glyph = "N/E";
1287
+ c = ANSI_DIM;
1288
+ } else {
1289
+ glyph = status.toUpperCase() || "????";
1290
+ c = ANSI_DIM;
1291
+ }
1292
+ return color ? `${c}${glyph}${ANSI_RESET}` : glyph;
1293
+ }
1294
+ function bold(s, color) {
1295
+ return color ? `${ANSI_BOLD}${s}${ANSI_RESET}` : s;
1296
+ }
1297
+ function dim(s, color) {
1298
+ return color ? `${ANSI_DIM}${s}${ANSI_RESET}` : s;
1299
+ }
1300
+ function truncate(value, limit = 280) {
1301
+ if (value.length <= limit) {
1302
+ return value;
1303
+ }
1304
+ return value.slice(0, limit - 3) + "...";
1305
+ }
1306
+ function renderAssertionFailure(result, options = {}) {
1307
+ const color = options.color ?? false;
1308
+ const testFile = options.testFile;
1309
+ const layer = result.layer ?? 0;
1310
+ const label = layerLabel(layer, result.type);
1311
+ const lines = [];
1312
+ lines.push(
1313
+ ` ${statusGlyph(result.status, color)} ${bold(result.assertion_id, color)} ${dim(`\u2014 ${label}`, color)}`
1314
+ );
1315
+ if (result.reason) {
1316
+ lines.push(` reason: not evaluated: ${result.reason}`);
1317
+ }
1318
+ if (result.trace_node_path) {
1319
+ lines.push(` trace path: ${result.trace_node_path}`);
1320
+ }
1321
+ if (result.status !== STATUS_NOT_EVALUATED) {
1322
+ lines.push(` score: ${result.score.toFixed(3)}`);
1323
+ }
1324
+ if (result.expected) {
1325
+ lines.push(` expected: ${truncate(result.expected)}`);
1326
+ }
1327
+ if (result.actual) {
1328
+ lines.push(` actual: ${truncate(result.actual)}`);
1329
+ }
1330
+ if (result.explanation) {
1331
+ lines.push(` detail: ${truncate(result.explanation)}`);
1332
+ }
1333
+ if (result.threshold_source && result.threshold_source !== "static") {
1334
+ lines.push(` threshold: ${result.threshold_source}`);
1335
+ }
1336
+ if (result.failure_class) {
1337
+ lines.push(` class: ${result.failure_class}`);
1338
+ }
1339
+ if (result.judge_metadata) {
1340
+ appendJudgeMetadata(lines, result.judge_metadata);
1341
+ }
1342
+ if (result.suggested_action) {
1343
+ lines.push(` hint: ${truncate(result.suggested_action)}`);
1344
+ }
1345
+ if ((result.cost ?? 0) > 0 || (result.duration_ms ?? 0) > 0) {
1346
+ const cost = result.cost ?? 0;
1347
+ const durMs = result.duration_ms ?? 0;
1348
+ lines.push(` cost/lat: $${cost.toFixed(6)} / ${durMs}ms`);
1349
+ }
1350
+ if (testFile) {
1351
+ lines.push(` source: ${testFile}`);
1352
+ }
1353
+ return lines.join("\n") + "\n";
1354
+ }
1355
+ function appendJudgeMetadata(lines, meta) {
1356
+ if (meta.model) {
1357
+ let rubric = meta.rubric_name ?? "default";
1358
+ if (meta.rubric_version) {
1359
+ rubric = `${rubric} @ ${meta.rubric_version}`;
1360
+ }
1361
+ lines.push(` judge: ${meta.model} / ${rubric}`);
1362
+ }
1363
+ if (meta.prompt_hash) {
1364
+ lines.push(` prompt: #${meta.prompt_hash}`);
1365
+ }
1366
+ const samples = meta.sample_scores ?? [];
1367
+ if (samples.length > 1) {
1368
+ const formatted = samples.map((s) => s.toFixed(2)).join(", ");
1369
+ const mean = (meta.score_mean ?? 0).toFixed(2);
1370
+ const stddev = (meta.score_stddev ?? 0).toFixed(2);
1371
+ const flag = meta.high_variance ? " \u26A0 HIGH VARIANCE" : "";
1372
+ lines.push(
1373
+ ` samples: [${formatted}] mean=${mean} stddev=${stddev}${flag}`
1374
+ );
1375
+ }
1376
+ const probes = meta.bias_probes ?? [];
1377
+ if (probes.length > 0) {
1378
+ const formatted = probes.map((p) => `${p.name} \u0394${p.delta >= 0 ? "+" : ""}${p.delta.toFixed(2)}`).join(", ");
1379
+ lines.push(` bias: ${formatted}`);
1380
+ }
1381
+ if (meta.calibration) {
1382
+ const c = meta.calibration;
1383
+ lines.push(
1384
+ ` calibrated: ${c.label_count} labels, agreement=${c.agreement.toFixed(2)}, \u03BA=${c.cohen_kappa.toFixed(2)}`
1385
+ );
1386
+ }
1387
+ }
1388
+ function renderSummary(result, options = {}) {
1389
+ const color = options.color ?? false;
1390
+ const passN = result.passCount;
1391
+ const soft = result.assertionResults.filter(
1392
+ (r) => r.status === STATUS_SOFT_FAIL
1393
+ ).length;
1394
+ const hard = result.assertionResults.filter(
1395
+ (r) => r.status === STATUS_HARD_FAIL
1396
+ ).length;
1397
+ const parts = [
1398
+ `${statusGlyph(STATUS_PASS, color)} ${passN}`,
1399
+ `${statusGlyph(STATUS_SOFT_FAIL, color)} ${soft}`,
1400
+ `${statusGlyph(STATUS_HARD_FAIL, color)} ${hard}`,
1401
+ `${statusGlyph(STATUS_NOT_EVALUATED, color)} ${result.notEvaluated.length}`,
1402
+ `cost $${result.totalCost.toFixed(6)}`,
1403
+ `dur ${result.totalDurationMs}ms`
1404
+ ];
1405
+ return parts.join(" | ");
1406
+ }
1407
+ function renderDiagnostics(result, options = {}) {
1408
+ const color = options.color ?? false;
1409
+ const includePassing = options.includePassing ?? false;
1410
+ const rows = includePassing ? result.assertionResults : result.assertionResults.filter((r) => r.status !== STATUS_PASS);
1411
+ if (rows.length === 0) {
1412
+ return dim(" (no failures)\n", color);
1413
+ }
1414
+ const out = [""];
1415
+ out.push(bold(`Attest diagnostic \u2014 ${headerCounts(result)}:`, color));
1416
+ for (const r of rows) {
1417
+ out.push(renderAssertionFailure(r, options));
1418
+ }
1419
+ out.push(dim(` Summary: ${renderSummary(result, { color })}`, color));
1420
+ return out.join("\n");
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
+ }
1431
+
907
1432
  // src/trace-tree.ts
908
1433
  var TraceTree = class {
909
1434
  root;
@@ -2108,6 +2633,222 @@ var ContinuousEvalRunner = class {
2108
2633
  }
2109
2634
  };
2110
2635
 
2636
+ // src/calibration.ts
2637
+ var import_node_crypto3 = require("crypto");
2638
+ function computeAgreement(pairs, threshold) {
2639
+ if (pairs.length === 0) {
2640
+ throw new Error("no labeled pairs available");
2641
+ }
2642
+ if (threshold <= 0 || threshold >= 1) {
2643
+ throw new Error("threshold must be in (0, 1)");
2644
+ }
2645
+ const n = pairs.length;
2646
+ let humanPos = 0;
2647
+ let judgePos = 0;
2648
+ let bothPos = 0;
2649
+ let bothNeg = 0;
2650
+ for (const p of pairs) {
2651
+ const hPos = p.human >= threshold;
2652
+ const jPos = p.judge >= threshold;
2653
+ if (hPos) humanPos++;
2654
+ if (jPos) judgePos++;
2655
+ if (hPos && jPos) bothPos++;
2656
+ if (!hPos && !jPos) bothNeg++;
2657
+ }
2658
+ const humanNeg = n - humanPos;
2659
+ const judgeNeg = n - judgePos;
2660
+ const agreement = (bothPos + bothNeg) / n;
2661
+ const expected = (humanPos * judgePos + humanNeg * judgeNeg) / (n * n);
2662
+ const cohenKappa = expected < 1 ? (agreement - expected) / (1 - expected) : 0;
2663
+ const rocAuc = rocAUC(pairs, threshold);
2664
+ return { threshold, n, agreement, cohenKappa, rocAuc };
2665
+ }
2666
+ function rocAUC(pairs, threshold) {
2667
+ const rows = pairs.map((p) => ({
2668
+ score: p.judge,
2669
+ pos: p.human >= threshold
2670
+ }));
2671
+ const pos = rows.filter((r) => r.pos).length;
2672
+ const neg = rows.length - pos;
2673
+ if (pos === 0 || neg === 0) return 0;
2674
+ rows.sort((a, b) => a.score - b.score);
2675
+ let rankSumPos = 0;
2676
+ let i = 0;
2677
+ while (i < rows.length) {
2678
+ let j = i;
2679
+ while (j < rows.length && rows[j].score === rows[i].score) {
2680
+ j++;
2681
+ }
2682
+ const avgRank = (i + j + 1) / 2;
2683
+ for (let k = i; k < j; k++) {
2684
+ if (rows[k].pos) rankSumPos += avgRank;
2685
+ }
2686
+ i = j;
2687
+ }
2688
+ const auc = (rankSumPos - pos * (pos + 1) / 2) / (pos * neg);
2689
+ if (Number.isNaN(auc) || !Number.isFinite(auc)) return 0;
2690
+ return auc;
2691
+ }
2692
+ function loadLabelsCSV(text) {
2693
+ const rows = parseCSV(text);
2694
+ if (rows.length === 0) {
2695
+ throw new Error("empty CSV");
2696
+ }
2697
+ let start = 0;
2698
+ if (rows[0].length >= 2) {
2699
+ const firstHuman = parseFloatStrict(rows[0][1]);
2700
+ if (firstHuman === null) start = 1;
2701
+ }
2702
+ const out = [];
2703
+ for (let idx = start; idx < rows.length; idx++) {
2704
+ const row = rows[idx];
2705
+ const lineNum = idx + 1;
2706
+ if (row.length === 0) continue;
2707
+ if (row[0].trimStart().startsWith("#")) continue;
2708
+ if (row.length < 2) {
2709
+ throw new Error(
2710
+ `CSV line ${lineNum}: want at least 2 columns (input, human_label)`
2711
+ );
2712
+ }
2713
+ const human = parseFloatStrict(row[1]);
2714
+ if (human === null) {
2715
+ throw new Error(`CSV line ${lineNum}: human_label not a float: ${row[1]}`);
2716
+ }
2717
+ const rec = {
2718
+ input: row[0],
2719
+ humanLabel: human,
2720
+ judgeScore: 0,
2721
+ judgeKnown: false
2722
+ };
2723
+ if (row.length >= 3 && row[2].trim() !== "") {
2724
+ const judge = parseFloatStrict(row[2]);
2725
+ if (judge === null) {
2726
+ throw new Error(
2727
+ `CSV line ${lineNum}: judge_score not a float: ${row[2]}`
2728
+ );
2729
+ }
2730
+ rec.judgeScore = judge;
2731
+ rec.judgeKnown = true;
2732
+ }
2733
+ out.push(rec);
2734
+ }
2735
+ if (out.length === 0) {
2736
+ throw new Error("CSV contained no labeled rows");
2737
+ }
2738
+ return out;
2739
+ }
2740
+ function loadLabelsJSONL(text) {
2741
+ const out = [];
2742
+ const lines = text.split(/\r?\n/);
2743
+ for (let i = 0; i < lines.length; i++) {
2744
+ const stripped = lines[i].trim();
2745
+ if (!stripped) continue;
2746
+ let obj;
2747
+ try {
2748
+ obj = JSON.parse(stripped);
2749
+ } catch (err) {
2750
+ throw new Error(`JSONL line ${i + 1}: ${err.message}`);
2751
+ }
2752
+ if (Object.keys(obj).length === 0) continue;
2753
+ if (obj["human_label"] === void 0) {
2754
+ throw new Error(`JSONL line ${i + 1}: missing human_label`);
2755
+ }
2756
+ const human = parseJSONNumber(obj["human_label"]);
2757
+ if (human === null) {
2758
+ throw new Error(`JSONL line ${i + 1}: human_label is not a number`);
2759
+ }
2760
+ const rec = {
2761
+ input: typeof obj["input"] === "string" ? obj["input"] : "",
2762
+ humanLabel: human,
2763
+ judgeScore: 0,
2764
+ judgeKnown: false
2765
+ };
2766
+ if (obj["judge_score"] !== void 0 && obj["judge_score"] !== null) {
2767
+ const judge = parseJSONNumber(obj["judge_score"]);
2768
+ if (judge === null) {
2769
+ throw new Error(`JSONL line ${i + 1}: judge_score is not a number`);
2770
+ }
2771
+ rec.judgeScore = judge;
2772
+ rec.judgeKnown = true;
2773
+ }
2774
+ out.push(rec);
2775
+ }
2776
+ if (out.length === 0) {
2777
+ throw new Error("JSONL contained no labeled rows");
2778
+ }
2779
+ return out;
2780
+ }
2781
+ function promptHash(text) {
2782
+ return (0, import_node_crypto3.createHash)("sha256").update(text).digest("hex").slice(0, 16);
2783
+ }
2784
+ function parseFloatStrict(raw) {
2785
+ const trimmed = raw.trim();
2786
+ if (trimmed === "") return null;
2787
+ const n = Number(trimmed);
2788
+ if (Number.isNaN(n)) return null;
2789
+ return n;
2790
+ }
2791
+ function parseJSONNumber(value) {
2792
+ if (typeof value !== "number") return null;
2793
+ if (Number.isNaN(value)) return null;
2794
+ return value;
2795
+ }
2796
+ function parseCSV(text) {
2797
+ const rows = [];
2798
+ let current = [];
2799
+ let field = "";
2800
+ let inQuotes = false;
2801
+ let i = 0;
2802
+ while (i < text.length) {
2803
+ const ch = text[i];
2804
+ if (inQuotes) {
2805
+ if (ch === '"') {
2806
+ if (text[i + 1] === '"') {
2807
+ field += '"';
2808
+ i += 2;
2809
+ continue;
2810
+ }
2811
+ inQuotes = false;
2812
+ i++;
2813
+ continue;
2814
+ }
2815
+ field += ch;
2816
+ i++;
2817
+ continue;
2818
+ }
2819
+ if (ch === '"') {
2820
+ inQuotes = true;
2821
+ i++;
2822
+ continue;
2823
+ }
2824
+ if (ch === ",") {
2825
+ current.push(field);
2826
+ field = "";
2827
+ i++;
2828
+ continue;
2829
+ }
2830
+ if (ch === "\n") {
2831
+ current.push(field);
2832
+ rows.push(current);
2833
+ current = [];
2834
+ field = "";
2835
+ i++;
2836
+ continue;
2837
+ }
2838
+ if (ch === "\r") {
2839
+ i++;
2840
+ continue;
2841
+ }
2842
+ field += ch;
2843
+ i++;
2844
+ }
2845
+ if (field.length > 0 || current.length > 0) {
2846
+ current.push(field);
2847
+ rows.push(current);
2848
+ }
2849
+ return rows;
2850
+ }
2851
+
2111
2852
  // src/personas.ts
2112
2853
  var FRIENDLY_USER = {
2113
2854
  name: "FriendlyUser",
@@ -2543,15 +3284,27 @@ var LangChainAdapter = class {
2543
3284
  EngineTimeoutError,
2544
3285
  ExpectChain,
2545
3286
  FRIENDLY_USER,
3287
+ FailureClass,
2546
3288
  GeminiAdapter,
2547
3289
  LangChainAdapter,
2548
3290
  ManualAdapter,
2549
3291
  OTelAdapter,
3292
+ OUTCOME_FAIL,
3293
+ OUTCOME_PASS,
3294
+ OUTCOME_SKIP,
2550
3295
  OllamaAdapter,
2551
3296
  OpenAIAdapter,
2552
3297
  PluginRegistry,
3298
+ ProtocolDesyncError,
3299
+ ProtocolDiagnosticBuffer,
2553
3300
  ProtocolError,
3301
+ REASON_MISSING_EVIDENCE,
3302
+ REASON_PROVIDER_NOT_CONFIGURED,
3303
+ REASON_PROVIDER_UNAVAILABLE,
3304
+ REASON_SHORT_CIRCUITED,
3305
+ REASON_SIMULATION,
2554
3306
  STATUS_HARD_FAIL,
3307
+ STATUS_NOT_EVALUATED,
2555
3308
  STATUS_PASS,
2556
3309
  STATUS_SOFT_FAIL,
2557
3310
  STEP_AGENT_CALL,
@@ -2578,6 +3331,7 @@ var LangChainAdapter = class {
2578
3331
  agentId,
2579
3332
  assertionId,
2580
3333
  attestExpect,
3334
+ computeAgreement,
2581
3335
  config,
2582
3336
  decodeResponse,
2583
3337
  delegate,
@@ -2587,8 +3341,17 @@ var LangChainAdapter = class {
2587
3341
  extractResult,
2588
3342
  getSampleRate,
2589
3343
  isSimulationMode,
3344
+ layerForType,
3345
+ loadLabelsCSV,
3346
+ loadLabelsJSONL,
3347
+ previewLine,
3348
+ promptHash,
3349
+ renderAssertionFailure,
3350
+ renderDiagnostics,
3351
+ renderSummary,
3352
+ requireSimulationCapability,
2590
3353
  resetConfig,
2591
- simulationEvaluateBatch,
3354
+ simulationEnvEnabled,
2592
3355
  tier,
2593
3356
  traceId
2594
3357
  });