@tangle-network/agent-eval 0.133.2 → 0.133.3

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.
Files changed (103) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/dist/{analyze-runs-BmX-h_yn.d.ts → analyze-runs-BClW9OSe.d.ts} +3 -3
  3. package/dist/{analyze-runs-BmX-h_yn.d.ts.map → analyze-runs-BClW9OSe.d.ts.map} +1 -1
  4. package/dist/{analyze-runs-B-afTpCv.js → analyze-runs-qk8op0tN.js} +63 -42
  5. package/dist/analyze-runs-qk8op0tN.js.map +1 -0
  6. package/dist/baseline-BaPxoROc.js +149 -0
  7. package/dist/baseline-BaPxoROc.js.map +1 -0
  8. package/dist/{baseline-hG3K85h4.d.ts → baseline-D_fT6277.d.ts} +43 -11
  9. package/dist/baseline-D_fT6277.d.ts.map +1 -0
  10. package/dist/benchmarks/index.d.ts +1 -1
  11. package/dist/benchmarks/index.js +1 -1
  12. package/dist/{benchmarks-CJr1H1_a.js → benchmarks-BP9sgMia.js} +3 -3
  13. package/dist/{benchmarks-CJr1H1_a.js.map → benchmarks-BP9sgMia.js.map} +1 -1
  14. package/dist/builder-eval/index.js +1 -1
  15. package/dist/campaign/index.d.ts +2 -2
  16. package/dist/campaign/index.js +2 -2
  17. package/dist/{campaign-BJjn1rhw.js → campaign--V4ffEKR.js} +12 -6
  18. package/dist/{campaign-BJjn1rhw.js.map → campaign--V4ffEKR.js.map} +1 -1
  19. package/dist/{client-COvaLoQG.d.ts → client-Du7B81wW.d.ts} +28 -14
  20. package/dist/client-Du7B81wW.d.ts.map +1 -0
  21. package/dist/{client-CYzbdJOZ.js → client-LIuo-KPv.js} +19 -7
  22. package/dist/client-LIuo-KPv.js.map +1 -0
  23. package/dist/contract/index.d.ts +3 -3
  24. package/dist/contract/index.d.ts.map +1 -1
  25. package/dist/contract/index.js +9 -8
  26. package/dist/contract/index.js.map +1 -1
  27. package/dist/{eval-campaign-DXhpZghy.js → eval-campaign-CvPcvqXC.js} +2 -2
  28. package/dist/{eval-campaign-DXhpZghy.js.map → eval-campaign-CvPcvqXC.js.map} +1 -1
  29. package/dist/hosted/index.d.ts +1 -1
  30. package/dist/hosted/index.d.ts.map +1 -1
  31. package/dist/hosted/index.js +1 -1
  32. package/dist/{index-BREtv3ZZ.d.ts → index-B5MNN1f1.d.ts} +3 -3
  33. package/dist/{index-BREtv3ZZ.d.ts.map → index-B5MNN1f1.d.ts.map} +1 -1
  34. package/dist/{index-C7Wue8R6.d.ts → index-DOqvIJ8I.d.ts} +27 -10
  35. package/dist/index-DOqvIJ8I.d.ts.map +1 -0
  36. package/dist/{index-DSC51roc.d.ts → index-DSC51roc2.d.ts} +1 -1
  37. package/dist/index-DSC51roc2.d.ts.map +1 -0
  38. package/dist/{index-nhIYz9hn.d.ts → index-DuhJaaiH.d.ts} +68 -7
  39. package/dist/index-DuhJaaiH.d.ts.map +1 -0
  40. package/dist/index.d.ts +56 -10
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +29 -20
  43. package/dist/index.js.map +1 -1
  44. package/dist/ledger-core/index.d.ts +2 -2
  45. package/dist/ledger-core/index.js +2 -2
  46. package/dist/{ledger-core-CPZfcrC2.js → ledger-core-DAKFKRzi.js} +136 -18
  47. package/dist/ledger-core-DAKFKRzi.js.map +1 -0
  48. package/dist/matrix/index.d.ts +1 -1
  49. package/dist/meta-eval/index.d.ts +1 -1
  50. package/dist/meta-eval/index.js +2 -2
  51. package/dist/multishot/index.d.ts +1 -1
  52. package/dist/openapi.json +1 -1
  53. package/dist/{paired-arms-6XItKzd1.js → paired-arms-CA_8pN01.js} +2 -2
  54. package/dist/{paired-arms-6XItKzd1.js.map → paired-arms-CA_8pN01.js.map} +1 -1
  55. package/dist/pipelines/index.d.ts +1 -1
  56. package/dist/pipelines/index.js +3 -2
  57. package/dist/pipelines/index.js.map +1 -1
  58. package/dist/{release-report-wuilQkvK.js → release-report-BVZBmRZp.js} +2 -2
  59. package/dist/{release-report-wuilQkvK.js.map → release-report-BVZBmRZp.js.map} +1 -1
  60. package/dist/{release-report-CjHWa8Ia.d.ts → release-report-DKBtegGt.d.ts} +2 -2
  61. package/dist/{release-report-CjHWa8Ia.d.ts.map → release-report-DKBtegGt.d.ts.map} +1 -1
  62. package/dist/reporting.d.ts +3 -3
  63. package/dist/reporting.js +4 -4
  64. package/dist/{researcher-CbSKhK8z.d.ts → researcher-BtD5U1Up.d.ts} +2 -2
  65. package/dist/{researcher-CbSKhK8z.d.ts.map → researcher-BtD5U1Up.d.ts.map} +1 -1
  66. package/dist/{reward-hacking-Dl2UBzej.js → reward-hacking-DCdRK9TY.js} +2 -2
  67. package/dist/{reward-hacking-Dl2UBzej.js.map → reward-hacking-DCdRK9TY.js.map} +1 -1
  68. package/dist/rl.d.ts +1 -1
  69. package/dist/rl.js +4 -4
  70. package/dist/{rubric-predictive-validity-QG7ydk0s.js → rubric-predictive-validity-D6Q6n9oq.js} +2 -2
  71. package/dist/{rubric-predictive-validity-QG7ydk0s.js.map → rubric-predictive-validity-D6Q6n9oq.js.map} +1 -1
  72. package/dist/{skillopt-optimization-method-wHF5xsUv.d.ts → skillopt-optimization-method-Dxr8pdZd.d.ts} +12 -7
  73. package/dist/{skillopt-optimization-method-wHF5xsUv.d.ts.map → skillopt-optimization-method-Dxr8pdZd.d.ts.map} +1 -1
  74. package/dist/{skillopt-optimization-method-CF6a327Q.js → skillopt-optimization-method-vvJ4bMNI.js} +119 -24
  75. package/dist/skillopt-optimization-method-vvJ4bMNI.js.map +1 -0
  76. package/dist/{statistics-DbvkkDPa.d.ts → statistics-D_4Snl-5.d.ts} +158 -30
  77. package/dist/statistics-D_4Snl-5.d.ts.map +1 -0
  78. package/dist/{statistics-DWM_AyLe.js → statistics-RwRNu2__.js} +546 -98
  79. package/dist/statistics-RwRNu2__.js.map +1 -0
  80. package/dist/{summary-report-Ci17nIdU.js → summary-report-BxtossFi.js} +3 -3
  81. package/dist/{summary-report-Ci17nIdU.js.map → summary-report-BxtossFi.js.map} +1 -1
  82. package/dist/{summary-report-CFnQgNfg.d.ts → summary-report-DyOhItws.d.ts} +3 -2
  83. package/dist/summary-report-DyOhItws.d.ts.map +1 -0
  84. package/dist/{baseline-DcX5hQDv.js → tool-use-metrics-DEGMKycK.js} +2 -114
  85. package/dist/tool-use-metrics-DEGMKycK.js.map +1 -0
  86. package/docs/design/statistics-decisions.md +271 -0
  87. package/docs/design.md +1 -0
  88. package/docs/insight-report.md +1 -1
  89. package/docs/research-report-methodology.md +4 -1
  90. package/package.json +2 -1
  91. package/dist/analyze-runs-B-afTpCv.js.map +0 -1
  92. package/dist/baseline-DcX5hQDv.js.map +0 -1
  93. package/dist/baseline-hG3K85h4.d.ts.map +0 -1
  94. package/dist/client-COvaLoQG.d.ts.map +0 -1
  95. package/dist/client-CYzbdJOZ.js.map +0 -1
  96. package/dist/index-C7Wue8R6.d.ts.map +0 -1
  97. package/dist/index-DSC51roc.d.ts.map +0 -1
  98. package/dist/index-nhIYz9hn.d.ts.map +0 -1
  99. package/dist/ledger-core-CPZfcrC2.js.map +0 -1
  100. package/dist/skillopt-optimization-method-CF6a327Q.js.map +0 -1
  101. package/dist/statistics-DWM_AyLe.js.map +0 -1
  102. package/dist/statistics-DbvkkDPa.d.ts.map +0 -1
  103. package/dist/summary-report-CFnQgNfg.d.ts.map +0 -1
@@ -1,2 +1,2 @@
1
- import { A as AtomicFileLockOptions, C as LedgerCanonicalizationError, D as AtomicFileLock, E as hashCanonical, M as AtomicFileLockUnavailable, N as probeAtomicFileLock, O as AtomicFileLockAcquisition, P as tryAcquireAtomicFileLock, S as LEDGER_HASH_PATTERN, T as canonicalString, _ as LedgerFileContext, a as LedgerChainFields, b as withLedgerFileLock, c as LedgerJournalCodec, d as LedgerReplayResult, f as LedgerAnchoredEntry, g as FileLockResult, h as verifyEntriesAgainstTrustedHead, i as LedgerAppendResult, j as AtomicFileLockOwner, k as AtomicFileLockError, l as LedgerLineContext, m as trustedHeadPathFor, n as FileLedgerJournalOptions, o as LedgerEntryOf, p as LedgerTrustedHead, r as LedgerAppendOptions, s as LedgerEventBase, t as FileLedgerJournal, u as LedgerProjector, v as appendLedgerLine, w as LedgerHash, x as writeLedgerFileAtomically, y as tryWithLedgerFileLock } from "../index-nhIYz9hn.js";
2
- export { type AtomicFileLock, type AtomicFileLockAcquisition, AtomicFileLockError, type AtomicFileLockOptions, type AtomicFileLockOwner, type AtomicFileLockUnavailable, FileLedgerJournal, type FileLedgerJournalOptions, type FileLockResult, LEDGER_HASH_PATTERN, type LedgerAnchoredEntry, type LedgerAppendOptions, type LedgerAppendResult, LedgerCanonicalizationError, type LedgerChainFields, type LedgerEntryOf, type LedgerEventBase, type LedgerFileContext, type LedgerHash, type LedgerJournalCodec, type LedgerLineContext, type LedgerProjector, type LedgerReplayResult, type LedgerTrustedHead, appendLedgerLine, canonicalString, hashCanonical, probeAtomicFileLock, trustedHeadPathFor, tryAcquireAtomicFileLock, tryWithLedgerFileLock, verifyEntriesAgainstTrustedHead, withLedgerFileLock, writeLedgerFileAtomically };
1
+ import { A as AtomicFileLock, C as withLedgerFileLock, D as LedgerHash, E as LedgerCanonicalizationError, F as AtomicFileLockUnavailable, I as probeAtomicFileLock, L as tryAcquireAtomicFileLock, M as AtomicFileLockError, N as AtomicFileLockOptions, O as canonicalString, P as AtomicFileLockOwner, S as tryWithLedgerFileLock, T as LEDGER_HASH_PATTERN, _ as trustedHeadPathFor, a as LedgerChainFields, b as LedgerFileContext, c as LedgerJournalCodec, d as LedgerReplayResult, f as LedgerAnchoredEntry, g as readTrustedHeadFile, h as LedgerTrustedHeadSubject, i as LedgerAppendResult, j as AtomicFileLockAcquisition, k as hashCanonical, l as LedgerLineContext, m as LedgerTrustedHeadRemoval, n as FileLedgerJournalOptions, o as LedgerEntryOf, p as LedgerTrustedHead, r as LedgerAppendOptions, s as LedgerEventBase, t as FileLedgerJournal, u as LedgerProjector, v as verifyEntriesAgainstTrustedHead, w as writeLedgerFileAtomically, x as appendLedgerLine, y as FileLockResult } from "../index-DuhJaaiH.js";
2
+ export { type AtomicFileLock, type AtomicFileLockAcquisition, AtomicFileLockError, type AtomicFileLockOptions, type AtomicFileLockOwner, type AtomicFileLockUnavailable, FileLedgerJournal, type FileLedgerJournalOptions, type FileLockResult, LEDGER_HASH_PATTERN, type LedgerAnchoredEntry, type LedgerAppendOptions, type LedgerAppendResult, LedgerCanonicalizationError, type LedgerChainFields, type LedgerEntryOf, type LedgerEventBase, type LedgerFileContext, type LedgerHash, type LedgerJournalCodec, type LedgerLineContext, type LedgerProjector, type LedgerReplayResult, type LedgerTrustedHead, type LedgerTrustedHeadRemoval, type LedgerTrustedHeadSubject, appendLedgerLine, canonicalString, hashCanonical, probeAtomicFileLock, readTrustedHeadFile, trustedHeadPathFor, tryAcquireAtomicFileLock, tryWithLedgerFileLock, verifyEntriesAgainstTrustedHead, withLedgerFileLock, writeLedgerFileAtomically };
@@ -1,2 +1,2 @@
1
- import { a as tryWithLedgerFileLock, c as LEDGER_HASH_PATTERN, d as hashCanonical, f as AtomicFileLockError, i as appendLedgerLine, l as LedgerCanonicalizationError, m as tryAcquireAtomicFileLock, n as trustedHeadPathFor, o as withLedgerFileLock, p as probeAtomicFileLock, r as verifyEntriesAgainstTrustedHead, s as writeLedgerFileAtomically, t as FileLedgerJournal, u as canonicalString } from "../ledger-core-CPZfcrC2.js";
2
- export { AtomicFileLockError, FileLedgerJournal, LEDGER_HASH_PATTERN, LedgerCanonicalizationError, appendLedgerLine, canonicalString, hashCanonical, probeAtomicFileLock, trustedHeadPathFor, tryAcquireAtomicFileLock, tryWithLedgerFileLock, verifyEntriesAgainstTrustedHead, withLedgerFileLock, writeLedgerFileAtomically };
1
+ import { a as appendLedgerLine, c as writeLedgerFileAtomically, d as canonicalString, f as hashCanonical, h as tryAcquireAtomicFileLock, i as verifyEntriesAgainstTrustedHead, l as LEDGER_HASH_PATTERN, m as probeAtomicFileLock, n as readTrustedHeadFile, o as tryWithLedgerFileLock, p as AtomicFileLockError, r as trustedHeadPathFor, s as withLedgerFileLock, t as FileLedgerJournal, u as LedgerCanonicalizationError } from "../ledger-core-DAKFKRzi.js";
2
+ export { AtomicFileLockError, FileLedgerJournal, LEDGER_HASH_PATTERN, LedgerCanonicalizationError, appendLedgerLine, canonicalString, hashCanonical, probeAtomicFileLock, readTrustedHeadFile, trustedHeadPathFor, tryAcquireAtomicFileLock, tryWithLedgerFileLock, verifyEntriesAgainstTrustedHead, withLedgerFileLock, writeLedgerFileAtomically };
@@ -1,6 +1,6 @@
1
1
  import { s as ValidationError } from "./errors-8YnH8WlF.js";
2
2
  import { t as Mutex } from "./concurrency-MUjT7VjM.js";
3
- import { closeSync, constants, existsSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
3
+ import { closeSync, constants, existsSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
4
4
  import { dirname, resolve } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { hostname } from "node:os";
@@ -311,6 +311,20 @@ function writeLedgerFileAtomically(path, contents, context) {
311
311
  renameSync(temporaryPath, path);
312
312
  fsyncDirectory(directory);
313
313
  }
314
+ /** Remove whatever occupies a sidecar path and fsync its directory, so the
315
+ * removal is as durable as the write that created it. False when the path was
316
+ * already empty. Recursive because a sidecar path holding a directory is one of
317
+ * the states this has to be able to clear, not one it can refuse. */
318
+ function removeLedgerFile(path, context) {
319
+ try {
320
+ rmSync(path, { recursive: true });
321
+ } catch (error) {
322
+ if (error.code === "ENOENT") return false;
323
+ throw context.integrityError(`${context.subject} ${path} could not be removed`, { cause: error });
324
+ }
325
+ fsyncDirectory(dirname(path));
326
+ return true;
327
+ }
314
328
  function withLedgerFileLock(path, context, run) {
315
329
  const result = tryWithLedgerFileLock(path, context, run);
316
330
  if (!result.acquired) throw context.integrityError(`${context.subject} lock is held (${path})`);
@@ -392,32 +406,50 @@ function fsyncDirectory(path) {
392
406
  * cannot be distinguished from a journal written before pinning existed. That
393
407
  * is what `requireTrustedHead` is for: a journal opened under it refuses to
394
408
  * read at all once a non-empty journal has no pin.
409
+ *
410
+ * ## Abandoning a pin
411
+ *
412
+ * A pin outlives the journal it attests: deleting or rebuilding the journal
413
+ * leaves a pin naming history the file no longer carries, and every read is
414
+ * refused because that is precisely the deletion the pin exists to catch. The
415
+ * refusal names the pin file, and `clearTrustedHeadFile` is the explicit way to
416
+ * discard it — the same downgrade a writer with filesystem access could already
417
+ * perform with `rm`, made reachable through the API so it is a decision in the
418
+ * code rather than an undocumented manual step. It returns the guarantee it
419
+ * gave up and writes no record of its own; a caller that needs the discard on
420
+ * the record has to write one.
395
421
  */
396
422
  /** Sibling pin file for a journal — deliberately NOT the journal, so rewriting
397
423
  * the log is not enough to rewrite the trust recorded about it. */
398
424
  function trustedHeadPathFor(journalPath) {
399
425
  return `${journalPath}.head`;
400
426
  }
427
+ const TRUSTED_HEAD_RECOVERY = " Restore the file, or discard the pin it held with clearTrustedHead().";
401
428
  /** The pin, or null when this journal has never been pinned. A pin that exists
402
429
  * but does not parse is corruption or tamper of the trust record itself and
403
430
  * fails loudly: reporting it as "no pin" would silently downgrade what the
404
431
  * journal can prove. */
405
432
  function readTrustedHeadFile(path, context) {
406
- if (!existsSync(path)) return null;
407
- const text = readFileSync(path, "utf8");
433
+ let text;
434
+ try {
435
+ text = readFileSync(path, "utf8");
436
+ } catch (error) {
437
+ if (error.code === "ENOENT") return null;
438
+ throw context.integrityError(`${context.subject} trusted head ${path} could not be read.${TRUSTED_HEAD_RECOVERY}`, { cause: error });
439
+ }
408
440
  let raw;
409
441
  try {
410
442
  raw = JSON.parse(text);
411
443
  } catch (error) {
412
- throw context.integrityError(`${context.subject} trusted head ${path} is not valid JSON`, { cause: error });
444
+ throw context.integrityError(`${context.subject} trusted head ${path} is not valid JSON.${TRUSTED_HEAD_RECOVERY}`, { cause: error });
413
445
  }
414
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw context.integrityError(`${context.subject} trusted head ${path} is not an object`);
446
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw context.integrityError(`${context.subject} trusted head ${path} is not an object.${TRUSTED_HEAD_RECOVERY}`);
415
447
  const record = raw;
416
448
  const keys = Object.keys(record).sort();
417
- if (keys.length !== 2 || keys[0] !== "entryHash" || keys[1] !== "sequence") throw context.integrityError(`${context.subject} trusted head ${path} has keys [${keys.join(", ")}], expected [entryHash, sequence]`);
449
+ if (keys.length !== 2 || keys[0] !== "entryHash" || keys[1] !== "sequence") throw context.integrityError(`${context.subject} trusted head ${path} has keys [${keys.join(", ")}], expected [entryHash, sequence].${TRUSTED_HEAD_RECOVERY}`);
418
450
  const { sequence, entryHash } = record;
419
- if (typeof sequence !== "number" || !Number.isSafeInteger(sequence) || sequence < 0) throw context.integrityError(`${context.subject} trusted head ${path} sequence is not a non-negative integer`);
420
- if (typeof entryHash !== "string" || !LEDGER_HASH_PATTERN.test(entryHash)) throw context.integrityError(`${context.subject} trusted head ${path} entryHash is not a sha256 digest`);
451
+ if (typeof sequence !== "number" || !Number.isSafeInteger(sequence) || sequence < 0) throw context.integrityError(`${context.subject} trusted head ${path} sequence is not a non-negative integer.${TRUSTED_HEAD_RECOVERY}`);
452
+ if (typeof entryHash !== "string" || !LEDGER_HASH_PATTERN.test(entryHash)) throw context.integrityError(`${context.subject} trusted head ${path} entryHash is not a sha256 digest.${TRUSTED_HEAD_RECOVERY}`);
421
453
  return {
422
454
  sequence,
423
455
  entryHash
@@ -425,18 +457,68 @@ function readTrustedHeadFile(path, context) {
425
457
  }
426
458
  /** Publish a pin by write-then-rename, so a crash leaves either the previous
427
459
  * pin or the new one and never a torn file that would lock a journal out of
428
- * its own trust record. */
460
+ * its own trust record.
461
+ *
462
+ * Not re-exported from the package's public surface. It takes no lock, checks no
463
+ * chain, and enforces no monotonicity, so a public form would be the one call
464
+ * able to move a pin BACKWARD onto a journal it never read — which is the
465
+ * truncation the pin exists to refuse. `FileLedgerJournal.pinTrustedHead` is
466
+ * the public writer: locked, chain-verified, forward-only. */
429
467
  function writeTrustedHeadFile(path, head, context) {
430
- writeLedgerFileAtomically(path, `${canonicalString(head)}\n`, context);
468
+ try {
469
+ writeLedgerFileAtomically(path, `${canonicalString(head)}\n`, context);
470
+ } catch (error) {
471
+ throw context.integrityError(`${context.subject} trusted head ${path} could not be written`, { cause: error });
472
+ }
473
+ }
474
+ /** Discard a journal's pin, reporting what was discarded. The journal file is
475
+ * untouched and reads as unpinned afterwards, so the next pinning append opens
476
+ * a new pin at the entry it writes. This gives up the deletion guarantee for
477
+ * every entry the pin covered; it is the recovery for a pin whose journal was
478
+ * deliberately deleted or rebuilt, and the only way past a pin file that can no
479
+ * longer be read at all. */
480
+ function clearTrustedHeadFile(path, context) {
481
+ let head = null;
482
+ let unreadable = "";
483
+ try {
484
+ head = readTrustedHeadFile(path, context);
485
+ } catch (error) {
486
+ unreadable = error instanceof Error ? error.message : String(error);
487
+ }
488
+ const removed = removeLedgerFile(path, context);
489
+ let cleanupError;
490
+ try {
491
+ removeLedgerFile(`${path}.tmp`, context);
492
+ } catch (error) {
493
+ cleanupError = error instanceof Error ? error.message : String(error);
494
+ }
495
+ const cleanup = cleanupError === void 0 ? {} : { cleanupError };
496
+ if (!removed) return {
497
+ removed: false,
498
+ ...cleanup
499
+ };
500
+ if (head === null) return {
501
+ removed: true,
502
+ head: null,
503
+ unreadable,
504
+ ...cleanup
505
+ };
506
+ return {
507
+ removed: true,
508
+ head,
509
+ ...cleanup
510
+ };
431
511
  }
432
512
  /** The entry at exactly `head.sequence` must still carry `head.entryHash`.
433
513
  * Entries must already have passed chain verification, which is what makes
434
514
  * index and sequence interchangeable here. */
435
- function verifyEntriesAgainstTrustedHead(entries, head, context, subject) {
515
+ function verifyEntriesAgainstTrustedHead(entries, head, context, naming) {
516
+ if (typeof naming !== "object" || naming === null) throw new TypeError("verifyEntriesAgainstTrustedHead expects { subject, trustedHeadPath } as its fourth argument");
517
+ const { subject, trustedHeadPath } = naming;
436
518
  const pinned = entries[head.sequence];
437
- if (pinned === void 0) throw context.integrityError(`${subject} trusted head pins sequence ${head.sequence} (${head.entryHash}) but the journal has ${entries.length} entries — pinned history is missing (truncation, rollback, or rewrite)`);
438
- if (pinned.sequence !== head.sequence) throw context.integrityError(`${subject} entry at index ${head.sequence} carries sequence ${pinned.sequence}; the chain was not verified before the trusted head`);
439
- if (pinned.entryHash !== head.entryHash) throw context.integrityError(`${subject} entry at pinned sequence ${head.sequence} carries ${pinned.entryHash} but the trusted head recorded ${head.entryHash} — the journal was rewritten`);
519
+ if (pinned === void 0) throw context.integrityError(`${subject} trusted head ${trustedHeadPath} pins sequence ${head.sequence} (${head.entryHash}) but the journal has ${entries.length} entries — pinned history is missing (truncation, rollback, or rewrite). Restore the journal, or abandon the pinned history on purpose with clearTrustedHead().`);
520
+ if (pinned.sequence !== head.sequence) throw context.integrityError(`${subject} entry at index ${head.sequence} carries sequence ${pinned.sequence}; the chain was not verified before the trusted head ${trustedHeadPath}`);
521
+ if (pinned.entryHash !== head.entryHash) throw context.integrityError(`${subject} entry at pinned sequence ${head.sequence} carries ${pinned.entryHash} but the trusted head ${trustedHeadPath} recorded ${head.entryHash} — the journal was rewritten. Restore the journal, or abandon the pinned history on purpose with clearTrustedHead().`);
440
522
  }
441
523
  //#endregion
442
524
  //#region src/ledger-core/journal.ts
@@ -495,6 +577,7 @@ var FileLedgerJournal = class {
495
577
  const existing = before.entries.find((entry) => entry.event.eventId === event.eventId);
496
578
  if (existing) {
497
579
  if (canonicalString(existing.event) !== canonicalString(event)) throw this.codec.conflictError(`eventId ${event.eventId} already exists with different content`);
580
+ if (options.pinHead === true) this.pinAcknowledged(existing, before.entries);
498
581
  return {
499
582
  entry: existing,
500
583
  appended: false,
@@ -546,6 +629,38 @@ var FileLedgerJournal = class {
546
629
  return head;
547
630
  }));
548
631
  }
632
+ /** Discard this journal's pin, reporting what was discarded. The journal file
633
+ * is untouched and reads as unpinned afterwards, so the next pinning append
634
+ * opens a new pin at the entry it writes. It is the way out of a pin whose
635
+ * journal was deliberately deleted or rebuilt — every read of such a path is
636
+ * refused, correctly, because a pin naming history the file no longer carries
637
+ * is indistinguishable from a deletion. Clearing removes the deletion
638
+ * guarantee for every entry the pin covered. */
639
+ async clearTrustedHead() {
640
+ return this.mutex.runExclusive(() => withLedgerFileLock(this.path, this.codec, () => clearTrustedHeadFile(this.trustedHeadPath, this.codec)));
641
+ }
642
+ /** Move the pin up to an entry this caller just acknowledged on the
643
+ * idempotent path, when a pin write that failed after its row was already
644
+ * fsynced left the pin behind. The move is safe because it is forward-only
645
+ * along a chain `replayLocked` has just verified: every entry up to this one
646
+ * is bound to it by hash, so the pin can only ever come to name more history
647
+ * than it did, never different history.
648
+ *
649
+ * With no pin at all this only pins the journal's current head. An entry in
650
+ * the middle would publish a pin that reads as protection while leaving every
651
+ * later entry silently truncatable, which is a worse state to hand an auditor
652
+ * than the honest absence of a pin; `pinTrustedHead()` is the way to pin an
653
+ * unpinned journal on purpose. */
654
+ pinAcknowledged(entry, entries) {
655
+ const pinned = readTrustedHeadFile(this.trustedHeadPath, this.codec);
656
+ if (pinned === null) {
657
+ if (entry.sequence !== entries.length - 1) return;
658
+ } else if (pinned.sequence >= entry.sequence) return;
659
+ writeTrustedHeadFile(this.trustedHeadPath, {
660
+ sequence: entry.sequence,
661
+ entryHash: entry.entryHash
662
+ }, this.codec);
663
+ }
549
664
  replayLocked() {
550
665
  const entries = this.readEntries();
551
666
  const projection = this.project(entries);
@@ -558,10 +673,13 @@ var FileLedgerJournal = class {
558
673
  verifyTrustedHead(entries, requirePin) {
559
674
  const pinned = readTrustedHeadFile(this.trustedHeadPath, this.codec);
560
675
  if (pinned === null) {
561
- if (requirePin && entries.length > 0) throw this.codec.integrityError(`${this.codec.subject} ${this.path} has ${entries.length} entries but no trusted head at ${this.trustedHeadPath} — without its pin the journal cannot prove nothing was deleted`);
676
+ if (requirePin && entries.length > 0) throw this.codec.integrityError(`${this.codec.subject} ${this.path} has ${entries.length} entries but no trusted head at ${this.trustedHeadPath} — without its pin the journal cannot prove nothing was deleted. Restore the pin file, or adopt the current head with pinTrustedHead().`);
562
677
  return;
563
678
  }
564
- verifyEntriesAgainstTrustedHead(entries, pinned, this.codec, `${this.codec.subject} ${this.path}`);
679
+ verifyEntriesAgainstTrustedHead(entries, pinned, this.codec, {
680
+ subject: `${this.codec.subject} ${this.path}`,
681
+ trustedHeadPath: this.trustedHeadPath
682
+ });
565
683
  }
566
684
  readEntries() {
567
685
  if (!existsSync(this.path)) return [];
@@ -615,6 +733,6 @@ var FileLedgerJournal = class {
615
733
  }
616
734
  };
617
735
  //#endregion
618
- export { tryWithLedgerFileLock as a, LEDGER_HASH_PATTERN as c, hashCanonical as d, AtomicFileLockError as f, appendLedgerLine as i, LedgerCanonicalizationError as l, tryAcquireAtomicFileLock as m, trustedHeadPathFor as n, withLedgerFileLock as o, probeAtomicFileLock as p, verifyEntriesAgainstTrustedHead as r, writeLedgerFileAtomically as s, FileLedgerJournal as t, canonicalString as u };
736
+ export { appendLedgerLine as a, writeLedgerFileAtomically as c, canonicalString as d, hashCanonical as f, tryAcquireAtomicFileLock as h, verifyEntriesAgainstTrustedHead as i, LEDGER_HASH_PATTERN as l, probeAtomicFileLock as m, readTrustedHeadFile as n, tryWithLedgerFileLock as o, AtomicFileLockError as p, trustedHeadPathFor as r, withLedgerFileLock as s, FileLedgerJournal as t, LedgerCanonicalizationError as u };
619
737
 
620
- //# sourceMappingURL=ledger-core-CPZfcrC2.js.map
738
+ //# sourceMappingURL=ledger-core-DAKFKRzi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ledger-core-DAKFKRzi.js","names":[],"sources":["../src/ledger-core/atomic-file-lock.ts","../src/ledger-core/canonical.ts","../src/ledger-core/journal-file.ts","../src/ledger-core/trusted-head.ts","../src/ledger-core/journal.ts"],"sourcesContent":["/** Crash-safe filesystem lock shared by campaign persistence and run exclusion. */\n\nimport { randomUUID } from 'node:crypto'\nimport {\n closeSync,\n constants,\n existsSync,\n fsyncSync,\n linkSync,\n openSync,\n readFileSync,\n renameSync,\n unlinkSync,\n writeFileSync,\n} from 'node:fs'\nimport { hostname } from 'node:os'\n\nexport interface AtomicFileLockOwner {\n readonly pid: number\n readonly host: string\n readonly nonce: string\n}\n\nexport interface AtomicFileLock {\n readonly owner: AtomicFileLockOwner\n release(): void\n}\n\nexport type AtomicFileLockUnavailable =\n | { readonly acquired: false; readonly reason: 'held'; readonly holder: AtomicFileLockOwner }\n | { readonly acquired: false; readonly reason: 'recovery' }\n\nexport type AtomicFileLockAcquisition =\n | { readonly acquired: true; readonly lock: AtomicFileLock }\n | AtomicFileLockUnavailable\n\nexport interface AtomicFileLockOptions {\n readonly lockPath: string\n readonly pid?: number\n}\n\nexport class AtomicFileLockError extends Error {\n override readonly name = 'AtomicFileLockError'\n}\n\ntype OwnerState =\n | { readonly state: 'missing' }\n | { readonly state: 'stale'; readonly owner: AtomicFileLockOwner }\n | { readonly state: 'held'; readonly owner: AtomicFileLockOwner }\n\n/** Report whether another lock path prevents acquisition without modifying it. */\nexport function probeAtomicFileLock(\n options: AtomicFileLockOptions,\n): AtomicFileLockUnavailable | null {\n if (existsSync(recoveryPath(options.lockPath))) return { acquired: false, reason: 'recovery' }\n const state = ownerState(options.lockPath)\n if (state.state === 'held') {\n return { acquired: false, reason: 'held', holder: state.owner }\n }\n return null\n}\n\n/**\n * Try to acquire a complete, uniquely-owned lock inode.\n *\n * A hard link publishes fully-written owner metadata atomically. Stale-owner\n * removal is serialized so one reclaimer cannot delete a new owner's lock.\n */\nexport function tryAcquireAtomicFileLock(\n options: AtomicFileLockOptions,\n): AtomicFileLockAcquisition {\n const pid = options.pid ?? process.pid\n if (!Number.isSafeInteger(pid) || pid <= 0) {\n throw new AtomicFileLockError('atomic file lock pid must be a positive integer')\n }\n const owner: AtomicFileLockOwner = { pid, host: hostname(), nonce: randomUUID() }\n for (let attempt = 0; attempt < 8; attempt += 1) {\n if (existsSync(recoveryPath(options.lockPath))) {\n return { acquired: false, reason: 'recovery' }\n }\n if (tryLinkOwner(options.lockPath, owner, `acquire.${attempt}`)) {\n return acquired(options.lockPath, owner)\n }\n\n const holder = ownerState(options.lockPath)\n if (holder.state === 'missing') continue\n if (holder.state === 'held') {\n return { acquired: false, reason: 'held', holder: holder.owner }\n }\n\n const reclaimPath = recoveryPath(options.lockPath)\n if (!tryLinkOwner(reclaimPath, owner, `reclaim.${attempt}`)) {\n return { acquired: false, reason: 'recovery' }\n }\n try {\n const current = ownerState(options.lockPath)\n if (current.state === 'held') {\n return { acquired: false, reason: 'held', holder: current.owner }\n }\n if (current.state === 'stale') {\n const tombstone = `${options.lockPath}.stale.${owner.nonce}.${attempt}`\n try {\n renameSync(options.lockPath, tombstone)\n unlinkSync(tombstone)\n } catch (error) {\n if (!isMissing(error)) throw error\n }\n }\n if (tryLinkOwner(options.lockPath, owner, `recovered.${attempt}`)) {\n return acquired(options.lockPath, owner)\n }\n } finally {\n releaseOwnedPath(reclaimPath, owner)\n }\n }\n\n throw new AtomicFileLockError(`could not acquire atomic file lock ${options.lockPath}`)\n}\n\nfunction acquired(lockPath: string, owner: AtomicFileLockOwner): AtomicFileLockAcquisition {\n return {\n acquired: true,\n lock: {\n owner,\n release: () => releaseOwnedPath(lockPath, owner),\n },\n }\n}\n\nfunction tryLinkOwner(lockPath: string, owner: AtomicFileLockOwner, suffix: string): boolean {\n const ownerPath = `${lockPath}.${owner.pid}.${owner.nonce}.${suffix}.owner`\n const descriptor = openSync(\n ownerPath,\n constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,\n 0o600,\n )\n try {\n writeFileSync(descriptor, `${canonicalOwner(owner)}\\n`, 'utf8')\n fsyncSync(descriptor)\n } catch (error) {\n closeSync(descriptor)\n unlinkIfExists(ownerPath)\n throw error\n }\n closeSync(descriptor)\n\n try {\n linkSync(ownerPath, lockPath)\n return true\n } catch (error) {\n if (!isAlreadyExists(error)) throw error\n return false\n } finally {\n unlinkIfExists(ownerPath)\n }\n}\n\nfunction ownerState(lockPath: string): OwnerState {\n const owner = readOwner(lockPath)\n if (!owner) return { state: 'missing' }\n if (owner.host !== hostname()) return { state: 'held', owner }\n try {\n process.kill(owner.pid, 0)\n return { state: 'held', owner }\n } catch (error) {\n if (isNoSuchProcess(error)) return { state: 'stale', owner }\n return { state: 'held', owner }\n }\n}\n\nfunction readOwner(lockPath: string): AtomicFileLockOwner | undefined {\n let contents: string\n try {\n contents = readFileSync(lockPath, 'utf8').trim()\n } catch (error) {\n if (isMissing(error)) return undefined\n throw error\n }\n\n let value: unknown\n try {\n value = JSON.parse(contents)\n } catch (error) {\n throw invalidOwner(lockPath, error)\n }\n if (!isOwner(value)) throw invalidOwner(lockPath)\n return value\n}\n\nfunction isOwner(value: unknown): value is AtomicFileLockOwner {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n const record = value as Record<string, unknown>\n return (\n Object.keys(record).length === 3 &&\n Number.isSafeInteger(record.pid) &&\n Number(record.pid) > 0 &&\n typeof record.host === 'string' &&\n record.host.trim().length > 0 &&\n typeof record.nonce === 'string' &&\n record.nonce.trim().length > 0\n )\n}\n\nfunction invalidOwner(lockPath: string, cause?: unknown): AtomicFileLockError {\n const error = new AtomicFileLockError(\n `atomic file lock has an invalid owner (${lockPath}); refusing unsafe recovery`,\n )\n if (cause !== undefined) Object.defineProperty(error, 'cause', { value: cause })\n return error\n}\n\nfunction releaseOwnedPath(lockPath: string, owner: AtomicFileLockOwner): void {\n const current = readOwner(lockPath)\n if (!current) return\n if (canonicalOwner(current) !== canonicalOwner(owner)) {\n throw new AtomicFileLockError(`atomic file lock owner changed before release (${lockPath})`)\n }\n try {\n unlinkSync(lockPath)\n } catch (error) {\n if (!isMissing(error)) throw error\n }\n}\n\nfunction canonicalOwner(owner: AtomicFileLockOwner): string {\n return JSON.stringify({ host: owner.host, nonce: owner.nonce, pid: owner.pid })\n}\n\nfunction recoveryPath(lockPath: string): string {\n return `${lockPath}.reclaim`\n}\n\nfunction isAlreadyExists(error: unknown): boolean {\n return error instanceof Error && 'code' in error && error.code === 'EEXIST'\n}\n\nfunction isMissing(error: unknown): boolean {\n return error instanceof Error && 'code' in error && error.code === 'ENOENT'\n}\n\nfunction isNoSuchProcess(error: unknown): boolean {\n return error instanceof Error && 'code' in error && error.code === 'ESRCH'\n}\n\nfunction unlinkIfExists(path: string): void {\n try {\n unlinkSync(path)\n } catch (error) {\n if (!isMissing(error)) throw error\n }\n}\n","/**\n * Canonical bytes and digests for durable journal rows.\n *\n * Serialization is RFC 8785 through `@tangle-network/agent-interface` — the\n * stack's single identity scheme — so a value carries the same digest in every\n * package that produces or verifies one.\n *\n * Values canonical JSON cannot represent faithfully are refused, never coerced.\n * `NaN`, `Infinity`, `undefined`, class instances, cycles, and unpaired\n * surrogates each have a lossy encoding that maps distinct records onto one\n * digest, and in a hash chain the digest IS the record's identity: a coercion\n * there is a collision, not a convenience.\n */\n\nimport {\n canonicalCandidateBytes,\n canonicalCandidateJson,\n sha256Bytes,\n} from '@tangle-network/agent-interface'\nimport { ValidationError } from '../errors'\n\nexport type LedgerHash = `sha256:${string}`\n\n/** Shape of every ledger digest: `sha256:` followed by 64 lowercase hex chars. */\nexport const LEDGER_HASH_PATTERN = /^sha256:[a-f0-9]{64}$/\n\n/** A value has no faithful canonical-JSON form, so it cannot be stored or hashed. */\nexport class LedgerCanonicalizationError extends ValidationError {}\n\n/** Canonical JSON encoding (RFC 8785): object keys sorted by UTF-16 code unit,\n * recursively. This is the byte form that is hashed and stored, so equal values\n * always encode identically. */\nexport function canonicalString(value: unknown): string {\n try {\n return canonicalCandidateJson(value)\n } catch (error) {\n throw canonicalizationError(value, error)\n }\n}\n\nexport function hashCanonical(value: unknown): LedgerHash {\n try {\n return sha256Bytes(canonicalCandidateBytes(value))\n } catch (error) {\n throw canonicalizationError(value, error)\n }\n}\n\n/** Name the offending value and path so a refusal is actionable. The RFC 8785\n * encoder above is the sole authority on acceptance; this walk only explains a\n * rejection it already made. */\nfunction canonicalizationError(value: unknown, cause: unknown): LedgerCanonicalizationError {\n const defect = firstNonCanonicalDefect(value, '$', new Set())\n const reason = defect !== '' ? defect : cause instanceof Error ? cause.message : String(cause)\n return new LedgerCanonicalizationError(`value has no canonical JSON form: ${reason}`, {\n cause,\n })\n}\n\nfunction firstNonCanonicalDefect(value: unknown, path: string, ancestors: Set<object>): string {\n if (value === null) return ''\n if (value === undefined) return `${path} is undefined`\n const type = typeof value\n if (type === 'number') {\n return Number.isFinite(value) ? '' : `${path} is ${String(value)}`\n }\n if (type === 'string') {\n return isWellFormedUtf16(value as string) ? '' : `${path} contains an unpaired surrogate`\n }\n if (type === 'boolean') return ''\n if (type !== 'object') return `${path} is a ${type}`\n const object = value as object\n if (ancestors.has(object)) return `${path} closes a reference cycle`\n const prototype = Object.getPrototypeOf(object)\n if (!Array.isArray(object) && prototype !== Object.prototype && prototype !== null) {\n return `${path} is a ${object.constructor?.name ?? 'non-plain'} instance, not plain JSON data`\n }\n const nested = new Set(ancestors).add(object)\n if (Array.isArray(object)) {\n for (let index = 0; index < object.length; index += 1) {\n const defect = firstNonCanonicalDefect(object[index], `${path}[${index}]`, nested)\n if (defect !== '') return defect\n }\n return ''\n }\n for (const [key, entry] of Object.entries(object)) {\n if (!isWellFormedUtf16(key)) return `${path}.${key} has a key with an unpaired surrogate`\n const defect = firstNonCanonicalDefect(entry, `${path}.${key}`, nested)\n if (defect !== '') return defect\n }\n return ''\n}\n\nfunction isWellFormedUtf16(value: string): boolean {\n for (let index = 0; index < value.length; index += 1) {\n const code = value.charCodeAt(index)\n if (code >= 0xd800 && code <= 0xdbff) {\n const next = value.charCodeAt(index + 1)\n if (!(next >= 0xdc00 && next <= 0xdfff)) return false\n index += 1\n } else if (code >= 0xdc00 && code <= 0xdfff) {\n return false\n }\n }\n return true\n}\n","/** Filesystem durability and cross-process exclusion for append-only journal files. */\n\nimport {\n closeSync,\n constants,\n fsyncSync,\n mkdirSync,\n openSync,\n renameSync,\n rmSync,\n writeSync,\n} from 'node:fs'\nimport { dirname } from 'node:path'\nimport { AtomicFileLockError, tryAcquireAtomicFileLock } from './atomic-file-lock'\n\n/** How a journal binding reports file-layer faults in its own error taxonomy. */\nexport interface LedgerFileContext {\n /** Noun used in error messages, e.g. 'search ledger'. */\n subject: string\n integrityError(message: string, options?: { cause?: unknown }): Error\n}\n\n/** Append one already-serialized line; the write is fsynced (file and directory)\n * before returning so an acknowledged append survives a crash. */\nexport function appendLedgerLine(path: string, line: string, context: LedgerFileContext): void {\n mkdirSync(dirname(path), { recursive: true })\n const fd = openSync(path, constants.O_CREAT | constants.O_WRONLY | constants.O_APPEND, 0o600)\n try {\n writeAll(fd, Buffer.from(line, 'utf8'), context)\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n fsyncDirectory(dirname(path))\n}\n\n/** Replace a small sidecar file whole: write a temporary sibling, fsync it,\n * then rename over the target. A reader therefore sees the previous contents\n * or the new ones, never a partial file. */\nexport function writeLedgerFileAtomically(\n path: string,\n contents: string,\n context: LedgerFileContext,\n): void {\n const directory = dirname(path)\n mkdirSync(directory, { recursive: true })\n const temporaryPath = `${path}.tmp`\n const fd = openSync(\n temporaryPath,\n constants.O_CREAT | constants.O_WRONLY | constants.O_TRUNC,\n 0o600,\n )\n try {\n writeAll(fd, Buffer.from(contents, 'utf8'), context)\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n renameSync(temporaryPath, path)\n fsyncDirectory(directory)\n}\n\n/** Remove whatever occupies a sidecar path and fsync its directory, so the\n * removal is as durable as the write that created it. False when the path was\n * already empty. Recursive because a sidecar path holding a directory is one of\n * the states this has to be able to clear, not one it can refuse. */\nexport function removeLedgerFile(path: string, context: LedgerFileContext): boolean {\n try {\n rmSync(path, { recursive: true })\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false\n throw context.integrityError(`${context.subject} ${path} could not be removed`, {\n cause: error,\n })\n }\n fsyncDirectory(dirname(path))\n return true\n}\n\nexport function withLedgerFileLock<T>(path: string, context: LedgerFileContext, run: () => T): T {\n const result = tryWithLedgerFileLock(path, context, run)\n if (!result.acquired) {\n throw context.integrityError(`${context.subject} lock is held (${path})`)\n }\n return result.value\n}\n\nexport type FileLockResult<T> = { acquired: true; value: T } | { acquired: false }\n\nexport function tryWithLedgerFileLock<T>(\n path: string,\n context: LedgerFileContext,\n run: () => T,\n): FileLockResult<T> {\n mkdirSync(dirname(path), { recursive: true })\n const lockPath = `${path}.lock`\n try {\n const acquisition = tryAcquireAtomicFileLock({ lockPath })\n if (!acquisition.acquired) return { acquired: false }\n try {\n return { acquired: true, value: run() }\n } finally {\n acquisition.lock.release()\n }\n } catch (error) {\n if (error instanceof AtomicFileLockError) {\n throw context.integrityError(error.message, { cause: error })\n }\n throw error\n }\n}\n\nfunction writeAll(fd: number, bytes: Buffer, context: LedgerFileContext): void {\n let offset = 0\n while (offset < bytes.byteLength) {\n const written = writeSync(fd, bytes, offset, bytes.byteLength - offset)\n if (written <= 0) throw context.integrityError('filesystem wrote zero bytes')\n offset += written\n }\n}\n\nfunction fsyncDirectory(path: string): void {\n const fd = openSync(path, constants.O_RDONLY)\n try {\n fsyncSync(fd)\n } finally {\n closeSync(fd)\n }\n}\n","/**\n * Trusted head — the pin that makes deletion detectable.\n *\n * ## What the chain binds\n *\n * Every entry hashes the journal header, its sequence, the previous entry's\n * hash, and its event, so editing, reordering, or removing an entry in the\n * middle of a journal invalidates every hash after it. What a hash chain\n * cannot bind is its own length. Dropping entries off the END leaves a shorter\n * journal that is internally perfect, and rebuilding a journal from sequence 0\n * with recomputed hashes produces a different, equally perfect chain. For an\n * audit log of candidate registrations and promotion decisions that is the\n * difference between a record of a search that happened and a record of one\n * that did not.\n *\n * ## What the pin binds\n *\n * `<journal>.head` holds `{sequence, entryHash}` for one entry a writer\n * actually appended, in a file separate from the journal it attests. Each\n * entry hash commits to the entire prefix before it, so checking that the\n * entry at exactly `sequence` still carries `entryHash` proves two things at\n * once: the journal is at least `sequence + 1` entries long, and those entries\n * are byte-for-byte the ones the writer saw. A journal that merely grew past\n * the pin still verifies; a truncated, rolled-back, or wholesale-rewritten one\n * cannot.\n *\n * ## What the pin does NOT defend against\n *\n * The pin is a second file, not a signature. A writer with access to BOTH the\n * journal and its pin can rewrite both consistently, and the result verifies.\n * Pinning raises the cost of undetected tampering from one file to two and\n * nothing more; defending against a writer who holds both requires an anchor\n * outside this store entirely — a signed head, or a digest published to an\n * append-only service the tamperer does not control.\n *\n * Deleting the pin file downgrades a pinned journal to an unpinned one, which\n * cannot be distinguished from a journal written before pinning existed. That\n * is what `requireTrustedHead` is for: a journal opened under it refuses to\n * read at all once a non-empty journal has no pin.\n *\n * ## Abandoning a pin\n *\n * A pin outlives the journal it attests: deleting or rebuilding the journal\n * leaves a pin naming history the file no longer carries, and every read is\n * refused because that is precisely the deletion the pin exists to catch. The\n * refusal names the pin file, and `clearTrustedHeadFile` is the explicit way to\n * discard it — the same downgrade a writer with filesystem access could already\n * perform with `rm`, made reachable through the API so it is a decision in the\n * code rather than an undocumented manual step. It returns the guarantee it\n * gave up and writes no record of its own; a caller that needs the discard on\n * the record has to write one.\n */\n\nimport { readFileSync } from 'node:fs'\nimport { canonicalString, LEDGER_HASH_PATTERN, type LedgerHash } from './canonical'\nimport { type LedgerFileContext, removeLedgerFile, writeLedgerFileAtomically } from './journal-file'\n\n/** A `(sequence, entryHash)` pair a writer pinned outside the journal. */\nexport interface LedgerTrustedHead {\n sequence: number\n entryHash: LedgerHash\n}\n\n/** Minimum entry shape the anchor check needs. */\nexport interface LedgerAnchoredEntry {\n sequence: number\n entryHash: LedgerHash\n}\n\n/** Sibling pin file for a journal — deliberately NOT the journal, so rewriting\n * the log is not enough to rewrite the trust recorded about it. */\nexport function trustedHeadPathFor(journalPath: string): string {\n return `${journalPath}.head`\n}\n\nconst TRUSTED_HEAD_RECOVERY =\n ' Restore the file, or discard the pin it held with clearTrustedHead().'\n\n/** The pin, or null when this journal has never been pinned. A pin that exists\n * but does not parse is corruption or tamper of the trust record itself and\n * fails loudly: reporting it as \"no pin\" would silently downgrade what the\n * journal can prove. */\nexport function readTrustedHeadFile(\n path: string,\n context: LedgerFileContext,\n): LedgerTrustedHead | null {\n let text: string\n try {\n text = readFileSync(path, 'utf8')\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null\n throw context.integrityError(\n `${context.subject} trusted head ${path} could not be read.${TRUSTED_HEAD_RECOVERY}`,\n { cause: error },\n )\n }\n let raw: unknown\n try {\n raw = JSON.parse(text)\n } catch (error) {\n throw context.integrityError(\n `${context.subject} trusted head ${path} is not valid JSON.${TRUSTED_HEAD_RECOVERY}`,\n { cause: error },\n )\n }\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw context.integrityError(\n `${context.subject} trusted head ${path} is not an object.${TRUSTED_HEAD_RECOVERY}`,\n )\n }\n const record = raw as Record<string, unknown>\n const keys = Object.keys(record).sort()\n if (keys.length !== 2 || keys[0] !== 'entryHash' || keys[1] !== 'sequence') {\n throw context.integrityError(\n `${context.subject} trusted head ${path} has keys [${keys.join(', ')}], expected [entryHash, sequence].${TRUSTED_HEAD_RECOVERY}`,\n )\n }\n const { sequence, entryHash } = record\n if (typeof sequence !== 'number' || !Number.isSafeInteger(sequence) || sequence < 0) {\n throw context.integrityError(\n `${context.subject} trusted head ${path} sequence is not a non-negative integer.${TRUSTED_HEAD_RECOVERY}`,\n )\n }\n if (typeof entryHash !== 'string' || !LEDGER_HASH_PATTERN.test(entryHash)) {\n throw context.integrityError(\n `${context.subject} trusted head ${path} entryHash is not a sha256 digest.${TRUSTED_HEAD_RECOVERY}`,\n )\n }\n return { sequence, entryHash: entryHash as LedgerHash }\n}\n\n/** Publish a pin by write-then-rename, so a crash leaves either the previous\n * pin or the new one and never a torn file that would lock a journal out of\n * its own trust record.\n *\n * Not re-exported from the package's public surface. It takes no lock, checks no\n * chain, and enforces no monotonicity, so a public form would be the one call\n * able to move a pin BACKWARD onto a journal it never read — which is the\n * truncation the pin exists to refuse. `FileLedgerJournal.pinTrustedHead` is\n * the public writer: locked, chain-verified, forward-only. */\nexport function writeTrustedHeadFile(\n path: string,\n head: LedgerTrustedHead,\n context: LedgerFileContext,\n): void {\n try {\n writeLedgerFileAtomically(path, `${canonicalString(head)}\\n`, context)\n } catch (error) {\n throw context.integrityError(`${context.subject} trusted head ${path} could not be written`, {\n cause: error,\n })\n }\n}\n\n/** Outcome of discarding a pin: the guarantee that was given up. `unreadable`\n * carries why the discarded pin could not be named — corruption, a permission\n * fault, a wrong file type — so a caller recording the decision never has to\n * treat \"no pin was there\" and \"the pin could not be read\" as one state.\n * `cleanupError` reports a stale atomic-write sibling that remains after the\n * actual pin was cleared. */\nexport type LedgerTrustedHeadRemoval =\n | { removed: true; head: LedgerTrustedHead; cleanupError?: string }\n | { removed: true; head: null; unreadable: string; cleanupError?: string }\n | { removed: false; cleanupError?: string }\n\n/** Discard a journal's pin, reporting what was discarded. The journal file is\n * untouched and reads as unpinned afterwards, so the next pinning append opens\n * a new pin at the entry it writes. This gives up the deletion guarantee for\n * every entry the pin covered; it is the recovery for a pin whose journal was\n * deliberately deleted or rebuilt, and the only way past a pin file that can no\n * longer be read at all. */\nexport function clearTrustedHeadFile(\n path: string,\n context: LedgerFileContext,\n): LedgerTrustedHeadRemoval {\n let head: LedgerTrustedHead | null = null\n let unreadable = ''\n try {\n head = readTrustedHeadFile(path, context)\n } catch (error) {\n // A pin nobody can read is precisely what clearing has to get past, so the\n // fault is reported on a removal that still happens rather than raised —\n // but it is reported, never flattened into \"there was no pin\".\n unreadable = error instanceof Error ? error.message : String(error)\n }\n const removed = removeLedgerFile(path, context)\n let cleanupError: string | undefined\n try {\n // Cleanup cannot block the requested trust-state change. Report a stale\n // atomic-write sibling so the caller can repair it before the next write.\n removeLedgerFile(`${path}.tmp`, context)\n } catch (error) {\n cleanupError = error instanceof Error ? error.message : String(error)\n }\n const cleanup = cleanupError === undefined ? {} : { cleanupError }\n if (!removed) return { removed: false, ...cleanup }\n if (head === null) return { removed: true, head: null, unreadable, ...cleanup }\n return { removed: true, head, ...cleanup }\n}\n\n/** How a journal is named when a trusted-head check refuses it. */\nexport interface LedgerTrustedHeadSubject {\n /** The journal in error messages, e.g. `search ledger /runs/a/ledger.jsonl`. */\n subject: string\n /** The sibling pin file, named in every refusal so the operator can see which\n * of the two files carries the claim being enforced. */\n trustedHeadPath: string\n}\n\n/** The entry at exactly `head.sequence` must still carry `head.entryHash`.\n * Entries must already have passed chain verification, which is what makes\n * index and sequence interchangeable here. */\nexport function verifyEntriesAgainstTrustedHead(\n entries: readonly LedgerAnchoredEntry[],\n head: LedgerTrustedHead,\n context: LedgerFileContext,\n naming: LedgerTrustedHeadSubject,\n): void {\n // A caller passing the bare subject string an untyped build would accept\n // destructures into two undefineds, which would corrupt the text of a\n // security refusal rather than fail. Refuse instead.\n if (typeof naming !== 'object' || naming === null) {\n throw new TypeError(\n 'verifyEntriesAgainstTrustedHead expects { subject, trustedHeadPath } as its fourth argument',\n )\n }\n const { subject, trustedHeadPath } = naming\n const pinned = entries[head.sequence]\n if (pinned === undefined) {\n throw context.integrityError(\n `${subject} trusted head ${trustedHeadPath} pins sequence ${head.sequence} (${head.entryHash}) but the journal has ${entries.length} entries — pinned history is missing (truncation, rollback, or rewrite). Restore the journal, or abandon the pinned history on purpose with clearTrustedHead().`,\n )\n }\n if (pinned.sequence !== head.sequence) {\n throw context.integrityError(\n `${subject} entry at index ${head.sequence} carries sequence ${pinned.sequence}; the chain was not verified before the trusted head ${trustedHeadPath}`,\n )\n }\n if (pinned.entryHash !== head.entryHash) {\n throw context.integrityError(\n `${subject} entry at pinned sequence ${head.sequence} carries ${pinned.entryHash} but the trusted head ${trustedHeadPath} recorded ${head.entryHash} — the journal was rewritten. Restore the journal, or abandon the pinned history on purpose with clearTrustedHead().`,\n )\n }\n}\n","/**\n * Generic durable append-only journal with a SHA-256 hash chain.\n *\n * The file format is canonical JSONL: every row is the canonical JSON encoding\n * (RFC 8785, object keys sorted recursively) of one entry, and every entry\n * hashes the previous entry's hash so the log is tamper-evident. Appends are\n * serialized in-process per path and across processes via a lock file, fsynced\n * before acknowledgement, and idempotent by `eventId`. A malformed,\n * non-canonical, truncated, reordered, or conflicting log fails loudly; a bad\n * row is never skipped.\n *\n * A hash chain binds each entry to its predecessor, which cannot prove that no\n * entry was removed from the end: a valid shorter prefix is still a valid\n * chain. `pinHead` records the appended entry in a sibling `<journal>.head`\n * file and every read re-verifies the journal against a pin that is present,\n * so truncation and wholesale rewrite are refused. `trusted-head.ts` carries\n * the full threat model, including what a pin does not defend against.\n *\n * Domain vocabulary lives entirely in the consumer's codec: entry schema\n * validation, the constant header fields stamped into every entry, the error\n * taxonomy, and the state machine replayed over verified entries.\n */\n\nimport { existsSync, readFileSync } from 'node:fs'\nimport { resolve } from 'node:path'\nimport { Mutex } from '../concurrency'\nimport { canonicalString, hashCanonical, type LedgerHash } from './canonical'\nimport { appendLedgerLine, type LedgerFileContext, withLedgerFileLock } from './journal-file'\nimport {\n clearTrustedHeadFile,\n type LedgerTrustedHead,\n type LedgerTrustedHeadRemoval,\n readTrustedHeadFile,\n trustedHeadPathFor,\n verifyEntriesAgainstTrustedHead,\n writeTrustedHeadFile,\n} from './trusted-head'\n\n/** Minimum shape of a journal event: the idempotency key. */\nexport interface LedgerEventBase {\n eventId: string\n}\n\n/** Chain envelope every entry carries in addition to the journal's constant\n * header fields. `entryHash` covers the header, sequence, previous hash, and\n * event, so any rewrite of history invalidates every later entry. */\nexport interface LedgerChainFields<Event extends LedgerEventBase> {\n sequence: number\n previousHash: LedgerHash | null\n event: Event\n entryHash: LedgerHash\n}\n\nexport type LedgerEntryOf<Header extends object, Event extends LedgerEventBase> = Header &\n LedgerChainFields<Event>\n\nexport interface LedgerLineContext {\n path: string\n line: number\n}\n\n/** Domain state machine replayed over chain-verified entries in order.\n * `apply` throws (via the codec's error taxonomy) on an invalid transition;\n * `finish` builds the consumer's projection once every entry was applied. */\nexport interface LedgerProjector<Entry, Projection> {\n apply(entry: Entry, index: number): void\n finish(entries: Entry[]): Projection\n}\n\nexport interface LedgerJournalCodec<\n Header extends object,\n Event extends LedgerEventBase,\n Projection,\n> extends LedgerFileContext {\n /** Constant fields stamped into every entry and covered by its hash. */\n header: Header\n conflictError(message: string): Error\n /** Validate one parsed JSON row into a typed entry. Must reject malformed\n * rows and non-canonical event encodings with the binding's own errors;\n * the journal itself verifies the stored bytes and the hash chain. */\n parseEntry(raw: unknown, context: LedgerLineContext): LedgerEntryOf<Header, Event>\n /** Reject an entry whose constant header fields do not match this journal. */\n checkEntryHeader(entry: LedgerEntryOf<Header, Event>, index: number): void\n createProjector(): LedgerProjector<LedgerEntryOf<Header, Event>, Projection>\n}\n\nexport interface LedgerReplayResult<Entry, Projection> {\n entries: Entry[]\n projection: Projection\n}\n\nexport interface LedgerAppendResult<Entry, Projection> {\n entry: Entry\n /** False when the exact event was already durably present. */\n appended: boolean\n projection: Projection\n}\n\nexport interface LedgerAppendOptions {\n /** Record the appended entry as this journal's trusted head, so a later read\n * can prove nothing was deleted from the end.\n *\n * On the idempotent path — the event is already durable — the pin moves up to\n * that entry only when the entry is ahead of it, along a chain this call has\n * just verified. So a pin write that failed after its row was fsynced is\n * repaired by the retry that idempotency already asks the caller for, and the\n * pin still only ever moves forward over history it has checked.\n *\n * A strict journal whose first pin write failed still refuses the retry\n * because it cannot distinguish that failure from a deleted pin. Recover it\n * explicitly with `pinTrustedHead()`; silently adopting the current file\n * would defeat `requireTrustedHead`. */\n pinHead?: boolean\n}\n\nexport interface FileLedgerJournalOptions {\n /** Refuse to read a non-empty journal that has no trusted head.\n *\n * Off by default because journals written before pinning have no pin and\n * must keep opening. Turn it on for a journal this process pins: without it,\n * deleting the sibling pin file silently downgrades the journal back to a\n * chain that cannot detect deletion. */\n requireTrustedHead?: boolean\n}\n\n// One async mutex per resolved journal path so concurrent appends from a single\n// process queue up instead of failing on the held cross-process lock file.\nconst journalMutexes = new Map<string, Mutex>()\n\nfunction mutexFor(path: string): Mutex {\n const existing = journalMutexes.get(path)\n if (existing) return existing\n const mutex = new Mutex()\n journalMutexes.set(path, mutex)\n return mutex\n}\n\n/** Durable filesystem journal. Construction performs no I/O; `append` and\n * `replay` validate the complete existing file under the locks. */\nexport class FileLedgerJournal<Header extends object, Event extends LedgerEventBase, Projection> {\n readonly path: string\n /** Sibling file holding this journal's trusted head. */\n readonly trustedHeadPath: string\n private readonly codec: LedgerJournalCodec<Header, Event, Projection>\n private readonly mutex: Mutex\n private readonly requireTrustedHead: boolean\n\n constructor(\n path: string,\n codec: LedgerJournalCodec<Header, Event, Projection>,\n options: FileLedgerJournalOptions = {},\n ) {\n this.path = resolve(path)\n this.trustedHeadPath = trustedHeadPathFor(this.path)\n this.codec = codec\n this.mutex = mutexFor(this.path)\n this.requireTrustedHead = options.requireTrustedHead === true\n }\n\n async replay(): Promise<LedgerReplayResult<LedgerEntryOf<Header, Event>, Projection>> {\n return this.mutex.runExclusive(() =>\n withLedgerFileLock(this.path, this.codec, () => this.replayLocked()),\n )\n }\n\n async append(\n event: Event,\n options: LedgerAppendOptions = {},\n ): Promise<LedgerAppendResult<LedgerEntryOf<Header, Event>, Projection>> {\n return this.mutex.runExclusive(() =>\n withLedgerFileLock(this.path, this.codec, () => {\n const before = this.replayLocked()\n const existing = before.entries.find((entry) => entry.event.eventId === event.eventId)\n if (existing) {\n if (canonicalString(existing.event) !== canonicalString(event)) {\n throw this.codec.conflictError(\n `eventId ${event.eventId} already exists with different content`,\n )\n }\n if (options.pinHead === true) this.pinAcknowledged(existing, before.entries)\n return { entry: existing, appended: false, projection: before.projection }\n }\n\n const material = {\n ...this.codec.header,\n sequence: before.entries.length,\n previousHash: before.entries.at(-1)?.entryHash ?? null,\n event,\n }\n const entry = { ...material, entryHash: hashCanonical(material) }\n\n // Apply the state transition before spending an append: an entry the\n // projector rejects must never reach the durable file.\n const projection = this.project([...before.entries, entry])\n appendLedgerLine(this.path, `${canonicalString(entry)}\\n`, this.codec)\n if (options.pinHead === true) {\n // Journal first, pin second: a crash or a failed pin write between\n // them leaves the pin one entry behind, which still verifies and is\n // repaired by the next pinning append, including the idempotent\n // retry of this same event. The reverse order would leave a pin\n // naming an entry the journal does not carry, locking the journal\n // out of its own history.\n writeTrustedHeadFile(\n this.trustedHeadPath,\n { sequence: entry.sequence, entryHash: entry.entryHash },\n this.codec,\n )\n }\n return { entry, appended: true, projection }\n }),\n )\n }\n\n /** The pinned head, or null when this journal has never been pinned. */\n async trustedHead(): Promise<LedgerTrustedHead | null> {\n return this.mutex.runExclusive(() =>\n withLedgerFileLock(this.path, this.codec, () =>\n readTrustedHeadFile(this.trustedHeadPath, this.codec),\n ),\n )\n }\n\n /** Pin the current verified head. The chain and any existing pin are checked\n * first, so the pin only ever moves forward along a journal that still\n * carries the history it already recorded. */\n async pinTrustedHead(): Promise<LedgerTrustedHead> {\n return this.mutex.runExclusive(() =>\n withLedgerFileLock(this.path, this.codec, () => {\n const entries = this.readEntries()\n this.project(entries)\n // `requireTrustedHead` is deliberately not applied here: pinning is how\n // a journal that lacks a pin acquires one.\n this.verifyTrustedHead(entries, false)\n const last = entries.at(-1)\n if (last === undefined) {\n throw this.codec.integrityError(\n `${this.codec.subject} ${this.path} is empty; there is no head to pin`,\n )\n }\n const head: LedgerTrustedHead = { sequence: last.sequence, entryHash: last.entryHash }\n writeTrustedHeadFile(this.trustedHeadPath, head, this.codec)\n return head\n }),\n )\n }\n\n /** Discard this journal's pin, reporting what was discarded. The journal file\n * is untouched and reads as unpinned afterwards, so the next pinning append\n * opens a new pin at the entry it writes. It is the way out of a pin whose\n * journal was deliberately deleted or rebuilt — every read of such a path is\n * refused, correctly, because a pin naming history the file no longer carries\n * is indistinguishable from a deletion. Clearing removes the deletion\n * guarantee for every entry the pin covered. */\n async clearTrustedHead(): Promise<LedgerTrustedHeadRemoval> {\n return this.mutex.runExclusive(() =>\n withLedgerFileLock(this.path, this.codec, () =>\n clearTrustedHeadFile(this.trustedHeadPath, this.codec),\n ),\n )\n }\n\n /** Move the pin up to an entry this caller just acknowledged on the\n * idempotent path, when a pin write that failed after its row was already\n * fsynced left the pin behind. The move is safe because it is forward-only\n * along a chain `replayLocked` has just verified: every entry up to this one\n * is bound to it by hash, so the pin can only ever come to name more history\n * than it did, never different history.\n *\n * With no pin at all this only pins the journal's current head. An entry in\n * the middle would publish a pin that reads as protection while leaving every\n * later entry silently truncatable, which is a worse state to hand an auditor\n * than the honest absence of a pin; `pinTrustedHead()` is the way to pin an\n * unpinned journal on purpose. */\n private pinAcknowledged(\n entry: LedgerEntryOf<Header, Event>,\n entries: LedgerEntryOf<Header, Event>[],\n ): void {\n const pinned = readTrustedHeadFile(this.trustedHeadPath, this.codec)\n if (pinned === null) {\n if (entry.sequence !== entries.length - 1) return\n } else if (pinned.sequence >= entry.sequence) {\n return\n }\n writeTrustedHeadFile(\n this.trustedHeadPath,\n { sequence: entry.sequence, entryHash: entry.entryHash },\n this.codec,\n )\n }\n\n private replayLocked(): LedgerReplayResult<LedgerEntryOf<Header, Event>, Projection> {\n const entries = this.readEntries()\n const projection = this.project(entries)\n // The anchor is meaningful only over a chain that already verified, so it\n // is checked last — and on every read path, so a violated pin is refused\n // before an append can extend the journal over it.\n this.verifyTrustedHead(entries, this.requireTrustedHead)\n return { entries, projection }\n }\n\n private verifyTrustedHead(entries: LedgerEntryOf<Header, Event>[], requirePin: boolean): void {\n const pinned = readTrustedHeadFile(this.trustedHeadPath, this.codec)\n if (pinned === null) {\n if (requirePin && entries.length > 0) {\n throw this.codec.integrityError(\n `${this.codec.subject} ${this.path} has ${entries.length} entries but no trusted head at ${this.trustedHeadPath} — without its pin the journal cannot prove nothing was deleted. Restore the pin file, or adopt the current head with pinTrustedHead().`,\n )\n }\n return\n }\n verifyEntriesAgainstTrustedHead(entries, pinned, this.codec, {\n subject: `${this.codec.subject} ${this.path}`,\n trustedHeadPath: this.trustedHeadPath,\n })\n }\n\n private readEntries(): LedgerEntryOf<Header, Event>[] {\n if (!existsSync(this.path)) return []\n const text = readFileSync(this.path, 'utf8')\n if (text.length === 0) return []\n if (!text.endsWith('\\n')) {\n throw this.codec.integrityError(\n `${this.codec.subject} ${this.path} has a truncated final record (missing newline)`,\n )\n }\n\n const lines = text.slice(0, -1).split('\\n')\n const entries: LedgerEntryOf<Header, Event>[] = []\n for (let index = 0; index < lines.length; index += 1) {\n const line = lines[index]!\n if (line.length === 0) {\n throw this.codec.integrityError(\n `${this.codec.subject} ${this.path} has a blank row at line ${index + 1}`,\n )\n }\n let raw: unknown\n try {\n raw = JSON.parse(line)\n } catch (error) {\n throw this.codec.integrityError(\n `${this.codec.subject} ${this.path} has invalid JSON at line ${index + 1}`,\n { cause: error },\n )\n }\n const entry = this.codec.parseEntry(raw, { path: this.path, line: index + 1 })\n let canonical: string\n try {\n canonical = canonicalString(entry)\n } catch (error) {\n throw this.codec.integrityError(\n `${this.codec.subject} ${this.path} has a row with no canonical JSON form at line ${index + 1}`,\n { cause: error },\n )\n }\n if (line !== canonical) {\n throw this.codec.integrityError(\n `${this.codec.subject} ${this.path} has non-canonical bytes at line ${index + 1}`,\n )\n }\n entries.push(entry)\n }\n return entries\n }\n\n private project(entries: LedgerEntryOf<Header, Event>[]): Projection {\n const projector = this.codec.createProjector()\n const eventIds = new Set<string>()\n let expectedPrevious: LedgerHash | null = null\n for (let index = 0; index < entries.length; index += 1) {\n const entry = entries[index]!\n this.codec.checkEntryHeader(entry, index)\n if (entry.sequence !== index) {\n throw this.codec.integrityError(\n `entry ${entry.event.eventId} has sequence ${entry.sequence}, expected ${index}`,\n )\n }\n if (entry.previousHash !== expectedPrevious) {\n throw this.codec.integrityError(\n `entry ${entry.event.eventId} does not extend the previous hash`,\n )\n }\n const { entryHash: _entryHash, ...material } = entry\n const expectedHash = hashCanonical(material)\n if (entry.entryHash !== expectedHash) {\n throw this.codec.integrityError(\n `entry ${entry.event.eventId} hash mismatch: expected ${expectedHash}, got ${entry.entryHash}`,\n )\n }\n expectedPrevious = entry.entryHash\n if (eventIds.has(entry.event.eventId)) {\n throw this.codec.integrityError(\n `duplicate eventId ${entry.event.eventId} in durable ledger`,\n )\n }\n eventIds.add(entry.event.eventId)\n projector.apply(entry, index)\n }\n return projector.finish(entries)\n }\n}\n"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  import { t as DefaultVerdict } from "../verdict-Dps8_okt.js";
2
- import { a as MatrixCell, i as MatrixAxis, n as AxisSummary, o as MatrixResult, r as CellResult, s as RunAgentMatrixOptions, t as runAgentMatrix } from "../index-DSC51roc.js";
2
+ import { a as MatrixCell, i as MatrixAxis, n as AxisSummary, o as MatrixResult, r as CellResult, s as RunAgentMatrixOptions, t as runAgentMatrix } from "../index-DSC51roc2.js";
3
3
  export { type AxisSummary, type CellResult, type DefaultVerdict, type MatrixAxis, type MatrixCell, type MatrixResult, type RunAgentMatrixOptions, runAgentMatrix };
@@ -1,7 +1,7 @@
1
1
  import { f as Run } from "../schema-BtVldJ3T.js";
2
2
  import { s as TraceStore } from "../store-CT9YIIve.js";
3
3
  import { a as ContinuousCalibrationResult, n as CandidateScore, o as GoldenItem, r as ContinuousAgreement, t as CalibrationResult } from "../judge-calibration-DFtEMlde.js";
4
- import { i as CorpusAgreementReport } from "../statistics-DbvkkDPa.js";
4
+ import { a as CorpusAgreementReport } from "../statistics-D_4Snl-5.js";
5
5
  import { a as OutcomeFilter, i as InMemoryOutcomeStore, n as FileSystemOutcomeStore, o as OutcomeStore, r as FileSystemOutcomeStoreOptions, t as DeploymentOutcome } from "../outcome-store-BYHIuO0e.js";
6
6
  import { n as SeriesConvergenceResult, t as SeriesConvergenceOptions } from "../series-convergence-ofsqPWhs.js";
7
7
  import { a as rubricPredictiveValidity, i as RubricRanking, n as RubricPredictiveValidityInput, r as RubricPredictiveValidityReport, t as RubricOutcomePair } from "../rubric-predictive-validity-C1dCLcvb.js";
@@ -1,8 +1,8 @@
1
1
  import { s as ValidationError } from "../errors-8YnH8WlF.js";
2
- import { A as spearmanR, E as pearsonR } from "../statistics-DWM_AyLe.js";
2
+ import { F as spearmanR, j as pearsonR } from "../statistics-RwRNu2__.js";
3
3
  import { o as llmSpans, t as aggregateLlm } from "../query-Di7eEQ79.js";
4
4
  import { t as analyzeSeries } from "../series-convergence-CjO2QdRW.js";
5
- import { t as rubricPredictiveValidity } from "../rubric-predictive-validity-QG7ydk0s.js";
5
+ import { t as rubricPredictiveValidity } from "../rubric-predictive-validity-D6Q6n9oq.js";
6
6
  import { n as InMemoryOutcomeStore, t as FileSystemOutcomeStore } from "../outcome-store-ChBKlTd_.js";
7
7
  //#region src/meta-eval/calibration.ts
8
8
  async function calibrationCurve(traceStore, outcomeStore, evalMetric, outcomeMetric, options = {}) {
@@ -1,5 +1,5 @@
1
1
  import { w as JudgeScore } from "../types-BokuXvOG.js";
2
- import { o as MatrixResult } from "../index-DSC51roc.js";
2
+ import { o as MatrixResult } from "../index-DSC51roc2.js";
3
3
  import { AgentProfile } from "@tangle-network/agent-interface";
4
4
  //#region src/multishot/types.d.ts
5
5
  interface MultishotMessage {
package/dist/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "@tangle-network/agent-eval — wire protocol",
5
- "version": "0.133.2",
5
+ "version": "0.133.3",
6
6
  "description": "HTTP and stdio RPC interface to agent-eval. The TypeScript runtime is the source of truth; this spec is the contract that cross-language clients (Python, Rust, Go) generate from.\n\nWire-protocol version: 1.0.0. Bumps on breaking changes to request/response schemas.",
7
7
  "contact": {
8
8
  "name": "Tangle Network",
@@ -1,5 +1,5 @@
1
1
  import { s as ValidationError } from "./errors-8YnH8WlF.js";
2
- import { N as wilcoxonSignedRank, p as mcnemar, v as pairedBootstrap, x as pairedRiskDifference } from "./statistics-DWM_AyLe.js";
2
+ import { C as pairedBootstrap, E as pairedRiskDifference, R as wilcoxonSignedRank, v as mcnemar } from "./statistics-RwRNu2__.js";
3
3
  //#region src/paired-arms.ts
4
4
  /**
5
5
  * Matched-pair arm comparison — "did the treatment arm beat the baseline arm
@@ -257,4 +257,4 @@ function metricValue(row, name) {
257
257
  //#endregion
258
258
  export { pairArms as n, pairRunRecords as r, comparePairedArms as t };
259
259
 
260
- //# sourceMappingURL=paired-arms-6XItKzd1.js.map
260
+ //# sourceMappingURL=paired-arms-CA_8pN01.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"paired-arms-6XItKzd1.js","names":[],"sources":["../src/paired-arms.ts"],"sourcesContent":["/**\n * Matched-pair arm comparison — \"did the treatment arm beat the baseline arm\n * on the SAME work items?\"\n *\n * An arm A/B over run records is only trustworthy when it is PAIRED: the same\n * task/scenario/seed evaluated under both arms, compared item-by-item, so\n * inter-item difficulty variance cancels instead of masquerading as an arm\n * effect. This module owns the two error-prone steps every consumer otherwise\n * hand-rolls:\n *\n * 1. Pairing — matching rows across arms by `pairKey` (and by `repKey`\n * within multi-rep items), with leftovers REPORTED rather than silently\n * dropped (a silently unbalanced pairing biases every paired statistic\n * downstream). Pairing never keys on outcome content: matching reps by\n * their outcomes deflates discordant-pair counts and makes McNemar\n * anti-conservative, so reps pair only by (`pairKey`, `repKey`) identity.\n * 2. Composition — feeding the matched pairs to the correct paired\n * estimators that already live in `statistics`: `mcnemar` +\n * `pairedRiskDifference` for pass/fail, `pairedBootstrap` +\n * `wilcoxonSignedRank` for continuous metrics. No statistic is\n * re-implemented here.\n *\n * The row shape is deliberately structural — callers project a `RunRecord`\n * (or any record) into `{ pairKey, arm, pass?, metrics? }`. Arm names are\n * caller-supplied parameters; the module ships no domain literal.\n */\n\nimport { ValidationError } from './errors'\nimport type { RunRecord } from './run-record'\nimport type { McNemarResult, PairedBootstrapOptions, PairedBootstrapResult } from './statistics'\nimport {\n mcnemar,\n pairedBootstrap,\n pairedRiskDifference,\n type RiskDifferenceResult,\n wilcoxonSignedRank,\n} from './statistics'\n\n/** One arm observation of one work item. Structural on purpose: callers\n * project their own record type (e.g. a `RunRecord`) into this shape. */\nexport interface PairedArmRow {\n /** Matching key — rows sharing a `pairKey` across both arms form pairs\n * (typically the task/scenario/seed identity). */\n pairKey: string\n /** Rep identity within a `pairKey` (e.g. a seed or rep number). Required on\n * every row of a `pairKey` that has more than one rep in either arm; reps\n * then pair only on exact (`pairKey`, `repKey`) match, never on outcome\n * content. Optional when each arm has at most one rep of the item. */\n repKey?: string\n /** Arm label this row was produced under. */\n arm: string\n /** Binary outcome; omit when the comparison has no pass/fail notion. */\n pass?: boolean\n /** Named numeric measurements (score, cost, latency, …). */\n metrics?: Record<string, number>\n}\n\nexport interface PairArmsOptions {\n /** Arm treated as the control side of every pair. */\n baselineArm: string\n /** Arm treated as the treatment side of every pair. */\n treatmentArm: string\n}\n\n/** One matched (baseline, treatment) observation of the same work item. */\nexport interface MatchedPair {\n pairKey: string\n /** 0-based position of this pair within its `pairKey`, ordered by sorted\n * `repKey` (always 0 for a single-rep item). The rep identity itself is on\n * the rows (`baseline.repKey` / `treatment.repKey`). */\n repIndex: number\n baseline: PairedArmRow\n treatment: PairedArmRow\n}\n\nexport interface PairArmsResult {\n /** Matched pairs, ordered by (`pairKey`, `repIndex`). */\n pairs: MatchedPair[]\n /** Baseline rows left without a treatment counterpart — reported, never\n * silently dropped. */\n unpairedBaseline: PairedArmRow[]\n /** Treatment rows left without a baseline counterpart. */\n unpairedTreatment: PairedArmRow[]\n}\n\n/**\n * Match rows across two arms into (baseline, treatment) pairs by `pairKey`.\n *\n * A `pairKey` with at most one row per arm pairs directly, no `repKey`\n * needed. A `pairKey` with multiple reps in either arm requires `repKey` on\n * every one of its rows, and reps pair only on exact (`pairKey`, `repKey`)\n * match — pairing is keyed purely on row identity, never on outcome content\n * (outcome-keyed matching deflates discordant counts and biases McNemar), and\n * is therefore independent of input order. Reps whose `repKey` has no\n * counterpart in the other arm, and items present in only one arm, land in\n * the unpaired lists — reported, never truncated.\n *\n * Fail-loud: throws when either named arm has zero rows (an unknown arm\n * name would otherwise read as \"everything unpaired\"), when the two arm\n * names are equal, when a multi-rep `pairKey` has a row without `repKey`, or\n * when a (`pairKey`, arm) group repeats a `repKey` (the match would be\n * ambiguous).\n */\nexport function pairArms(rows: readonly PairedArmRow[], opts: PairArmsOptions): PairArmsResult {\n const { baselineArm, treatmentArm } = opts\n if (baselineArm === treatmentArm) {\n throw new ValidationError(\n `pairArms: baselineArm and treatmentArm are both '${baselineArm}' — an arm cannot be compared to itself`,\n )\n }\n\n // arm → pairKey → rows\n const byArm = new Map<string, Map<string, PairedArmRow[]>>()\n const armsSeen = new Set<string>()\n for (const row of rows) {\n armsSeen.add(row.arm)\n if (row.arm !== baselineArm && row.arm !== treatmentArm) continue\n const byKey = byArm.get(row.arm) ?? new Map<string, PairedArmRow[]>()\n const group = byKey.get(row.pairKey) ?? []\n group.push(row)\n byKey.set(row.pairKey, group)\n byArm.set(row.arm, byKey)\n }\n\n for (const arm of [baselineArm, treatmentArm]) {\n if (!byArm.has(arm)) {\n const seen = [...armsSeen].sort().join(', ') || '<none>'\n throw new ValidationError(`pairArms: no rows for arm '${arm}' (arms present: ${seen})`)\n }\n }\n\n const baselineByKey = byArm.get(baselineArm)!\n const treatmentByKey = byArm.get(treatmentArm)!\n\n const allKeys = [...new Set([...baselineByKey.keys(), ...treatmentByKey.keys()])].sort()\n const pairs: MatchedPair[] = []\n const unpairedBaseline: PairedArmRow[] = []\n const unpairedTreatment: PairedArmRow[] = []\n for (const pairKey of allKeys) {\n const b = baselineByKey.get(pairKey) ?? []\n const t = treatmentByKey.get(pairKey) ?? []\n\n if (b.length <= 1 && t.length <= 1) {\n if (b.length === 1 && t.length === 1) {\n const baseline = b[0]!\n const treatment = t[0]!\n if (baseline.repKey !== undefined || treatment.repKey !== undefined) {\n if (\n baseline.repKey === undefined ||\n treatment.repKey === undefined ||\n baseline.repKey !== treatment.repKey\n ) {\n unpairedBaseline.push(baseline)\n unpairedTreatment.push(treatment)\n continue\n }\n }\n pairs.push({ pairKey, repIndex: 0, baseline, treatment })\n } else {\n unpairedBaseline.push(...b)\n unpairedTreatment.push(...t)\n }\n continue\n }\n\n const bByRep = indexByRepKey(b, pairKey, baselineArm)\n const tByRep = indexByRepKey(t, pairKey, treatmentArm)\n const repKeys = [...new Set([...bByRep.keys(), ...tByRep.keys()])].sort()\n let repIndex = 0\n for (const repKey of repKeys) {\n const baseline = bByRep.get(repKey)\n const treatment = tByRep.get(repKey)\n if (baseline !== undefined && treatment !== undefined) {\n pairs.push({ pairKey, repIndex: repIndex++, baseline, treatment })\n } else if (baseline !== undefined) {\n unpairedBaseline.push(baseline)\n } else if (treatment !== undefined) {\n unpairedTreatment.push(treatment)\n }\n }\n }\n\n return { pairs, unpairedBaseline, unpairedTreatment }\n}\n\n/** Index a multi-rep (pairKey, arm) group by `repKey`, enforcing that every\n * row carries one and that no repKey repeats within the group. */\nfunction indexByRepKey(\n group: readonly PairedArmRow[],\n pairKey: string,\n arm: string,\n): Map<string, PairedArmRow> {\n const byRep = new Map<string, PairedArmRow>()\n for (const row of group) {\n if (row.repKey === undefined) {\n throw new ValidationError(\n `pairArms: pairKey '${pairKey}' has multiple reps in an arm, but a row in arm '${arm}' ` +\n `is missing repKey — multi-rep items require an explicit repKey on every row so reps ` +\n `pair by identity (pairing reps by outcome or by index would bias the paired statistics)`,\n )\n }\n if (byRep.has(row.repKey)) {\n throw new ValidationError(\n `pairArms: duplicate repKey '${row.repKey}' for pairKey '${pairKey}' in arm '${arm}' — ` +\n `(pairKey, repKey) must uniquely identify a rep within an arm`,\n )\n }\n byRep.set(row.repKey, row)\n }\n return byRep\n}\n\n/** Paired pass/fail comparison over the pairs where BOTH sides carry `pass`. */\nexport interface PairedCorrectness {\n /** Discordant pairs where the treatment passed and the baseline failed. */\n b10: number\n /** Discordant pairs where the baseline passed and the treatment failed. */\n b01: number\n /** Exact McNemar significance over the paired outcomes (`b === b10`, `c === b01`). */\n mcnemar: McNemarResult\n /** Paired effect size: p(treatment) − p(baseline) with a paired-variance CI. */\n riskDifference: RiskDifferenceResult\n}\n\n/** Paired delta summary for one named metric (delta = treatment − baseline). */\nexport interface PairedMetricDelta {\n name: string\n /** Pairs where BOTH sides carry a finite value for this metric. */\n n: number\n /** Pairs where at least one side does not carry the metric. */\n nMissing: number\n /** Median paired delta, or null when `n === 0`. */\n medianDelta: number | null\n /** Mean paired delta, or null when `n === 0`. */\n meanDelta: number | null\n /** Bootstrap CI on the paired delta (`pairedBootstrap`); null when\n * `n === 0` — a zero-width [0, 0] interval on no data would read as a\n * measured tight null. */\n bootstrapCi: PairedBootstrapResult | null\n /** Wilcoxon signed-rank test on the paired deltas; null when `n === 0`. */\n wilcoxon: { w: number; p: number } | null\n}\n\nexport interface ComparePairedArmsOptions extends PairArmsOptions {\n /** Metrics to compare. Default: every metric name observed on any matched\n * pair, sorted. A name that appears on no pair is still reported (with\n * `n = 0`) so a misspelled metric is visible instead of vanishing. */\n metricNames?: string[]\n /** Passed through to `pairedBootstrap` — set `seed` for reproducible CIs. */\n bootstrap?: PairedBootstrapOptions\n}\n\nexport interface PairedArmsComparison {\n nPairs: number\n nUnpairedBaseline: number\n nUnpairedTreatment: number\n /** null when no matched pair carries `pass` on both sides — a pass/fail\n * verdict over rows that never measured pass/fail would be fabricated. */\n correctness: PairedCorrectness | null\n metricDeltas: PairedMetricDelta[]\n}\n\n/**\n * Full matched-pair arm comparison: pair via {@link pairArms}, then compose\n * the paired estimators from `statistics` over the matched pairs.\n *\n * Correctness uses only the pairs where both sides carry `pass` (`mcnemar.n`\n * is that subset's size); each metric uses only the pairs where both sides\n * carry a finite value for it, with the remainder counted in `nMissing`.\n * Deltas are treatment − baseline throughout.\n *\n * Fail-loud: inherits {@link pairArms}'s unknown-arm throw, and throws on a\n * non-finite metric value — silently treating corrupt telemetry as \"metric\n * absent\" would misreport it as missing coverage.\n */\nexport function comparePairedArms(\n rows: readonly PairedArmRow[],\n opts: ComparePairedArmsOptions,\n): PairedArmsComparison {\n const { pairs, unpairedBaseline, unpairedTreatment } = pairArms(rows, opts)\n\n let correctness: PairedCorrectness | null = null\n const baselinePass: number[] = []\n const treatmentPass: number[] = []\n for (const pair of pairs) {\n if (pair.baseline.pass === undefined || pair.treatment.pass === undefined) continue\n baselinePass.push(pair.baseline.pass ? 1 : 0)\n treatmentPass.push(pair.treatment.pass ? 1 : 0)\n }\n if (baselinePass.length > 0) {\n const mc = mcnemar(baselinePass, treatmentPass)\n correctness = {\n b10: mc.b,\n b01: mc.c,\n mcnemar: mc,\n riskDifference: pairedRiskDifference(baselinePass, treatmentPass),\n }\n }\n\n const metricNames =\n opts.metricNames ??\n [\n ...new Set(\n pairs.flatMap((p) => [\n ...Object.keys(p.baseline.metrics ?? {}),\n ...Object.keys(p.treatment.metrics ?? {}),\n ]),\n ),\n ].sort()\n\n const metricDeltas: PairedMetricDelta[] = metricNames.map((name) => {\n const before: number[] = []\n const after: number[] = []\n let nMissing = 0\n for (const pair of pairs) {\n const b = metricValue(pair.baseline, name)\n const t = metricValue(pair.treatment, name)\n if (b === undefined || t === undefined) {\n nMissing++\n continue\n }\n before.push(b)\n after.push(t)\n }\n const bootstrapCi = before.length === 0 ? null : pairedBootstrap(before, after, opts.bootstrap)\n return {\n name,\n n: before.length,\n nMissing,\n medianDelta: bootstrapCi?.median ?? null,\n meanDelta: bootstrapCi?.mean ?? null,\n bootstrapCi,\n wilcoxon: before.length === 0 ? null : wilcoxonSignedRank(before, after),\n }\n })\n\n return {\n nPairs: pairs.length,\n nUnpairedBaseline: unpairedBaseline.length,\n nUnpairedTreatment: unpairedTreatment.length,\n correctness,\n metricDeltas,\n }\n}\n\nexport interface MatchedRunRecordPair {\n pairKey: string\n repKey: string\n baseline: RunRecord\n treatment: RunRecord\n}\n\nexport interface PairRunRecordsResult {\n pairs: MatchedRunRecordPair[]\n unpairedBaseline: RunRecord[]\n unpairedTreatment: RunRecord[]\n}\n\ninterface RunRecordArmRow extends PairedArmRow {\n run: RunRecord\n repKey: string\n}\n\n/**\n * Pair two RunRecord arms by the identity of the evaluated work:\n * `(experimentId, scenarioId, seed)`.\n *\n * Falling back to array order, candidate id, or experiment id can compare\n * different tasks and fabricate lift. Duplicate identities throw.\n */\nexport function pairRunRecords(\n baselineRuns: readonly RunRecord[],\n treatmentRuns: readonly RunRecord[],\n): PairRunRecordsResult {\n const baselineRows = runRecordArmRows(baselineRuns, 'baseline')\n const treatmentRows = runRecordArmRows(treatmentRuns, 'treatment')\n validateRunRecordArmRows(baselineRows, 'baseline')\n validateRunRecordArmRows(treatmentRows, 'treatment')\n if (baselineRows.length === 0 || treatmentRows.length === 0) {\n return {\n pairs: [],\n unpairedBaseline: baselineRows.map((row) => row.run),\n unpairedTreatment: treatmentRows.map((row) => row.run),\n }\n }\n\n const result = pairArms([...baselineRows, ...treatmentRows], {\n baselineArm: 'baseline',\n treatmentArm: 'treatment',\n })\n return {\n pairs: result.pairs.map((pair) => {\n const baseline = pair.baseline as RunRecordArmRow\n const treatment = pair.treatment as RunRecordArmRow\n return {\n pairKey: pair.pairKey,\n repKey: baseline.repKey,\n baseline: baseline.run,\n treatment: treatment.run,\n }\n }),\n unpairedBaseline: result.unpairedBaseline.map((row) => (row as RunRecordArmRow).run),\n unpairedTreatment: result.unpairedTreatment.map((row) => (row as RunRecordArmRow).run),\n }\n}\n\nfunction runRecordArmRows(runs: readonly RunRecord[], arm: string): RunRecordArmRow[] {\n return runs.map((run) => {\n const scenarioId = run.scenarioId.trim()\n if (!scenarioId) {\n throw new ValidationError(\n `pairRunRecords: run '${run.runId}' is missing scenarioId; paired comparisons require explicit scenario identity`,\n )\n }\n return {\n pairKey: JSON.stringify([run.experimentId, scenarioId]),\n repKey: String(run.seed),\n arm,\n run,\n }\n })\n}\n\nfunction validateRunRecordArmRows(rows: readonly RunRecordArmRow[], arm: string): void {\n const byPairKey = new Map<string, RunRecordArmRow[]>()\n for (const row of rows) {\n const group = byPairKey.get(row.pairKey) ?? []\n group.push(row)\n byPairKey.set(row.pairKey, group)\n }\n for (const [pairKey, group] of byPairKey) {\n if (group.length > 1) indexByRepKey(group, pairKey, arm)\n }\n}\n\nfunction metricValue(row: PairedArmRow, name: string): number | undefined {\n const v = row.metrics?.[name]\n if (v === undefined) return undefined\n if (!Number.isFinite(v)) {\n throw new ValidationError(\n `comparePairedArms: non-finite value for metric '${name}' on pairKey '${row.pairKey}' (arm '${row.arm}'): ${v}`,\n )\n }\n return v\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"paired-arms-CA_8pN01.js","names":[],"sources":["../src/paired-arms.ts"],"sourcesContent":["/**\n * Matched-pair arm comparison — \"did the treatment arm beat the baseline arm\n * on the SAME work items?\"\n *\n * An arm A/B over run records is only trustworthy when it is PAIRED: the same\n * task/scenario/seed evaluated under both arms, compared item-by-item, so\n * inter-item difficulty variance cancels instead of masquerading as an arm\n * effect. This module owns the two error-prone steps every consumer otherwise\n * hand-rolls:\n *\n * 1. Pairing — matching rows across arms by `pairKey` (and by `repKey`\n * within multi-rep items), with leftovers REPORTED rather than silently\n * dropped (a silently unbalanced pairing biases every paired statistic\n * downstream). Pairing never keys on outcome content: matching reps by\n * their outcomes deflates discordant-pair counts and makes McNemar\n * anti-conservative, so reps pair only by (`pairKey`, `repKey`) identity.\n * 2. Composition — feeding the matched pairs to the correct paired\n * estimators that already live in `statistics`: `mcnemar` +\n * `pairedRiskDifference` for pass/fail, `pairedBootstrap` +\n * `wilcoxonSignedRank` for continuous metrics. No statistic is\n * re-implemented here.\n *\n * The row shape is deliberately structural — callers project a `RunRecord`\n * (or any record) into `{ pairKey, arm, pass?, metrics? }`. Arm names are\n * caller-supplied parameters; the module ships no domain literal.\n */\n\nimport { ValidationError } from './errors'\nimport type { RunRecord } from './run-record'\nimport type { McNemarResult, PairedBootstrapOptions, PairedBootstrapResult } from './statistics'\nimport {\n mcnemar,\n pairedBootstrap,\n pairedRiskDifference,\n type RiskDifferenceResult,\n wilcoxonSignedRank,\n} from './statistics'\n\n/** One arm observation of one work item. Structural on purpose: callers\n * project their own record type (e.g. a `RunRecord`) into this shape. */\nexport interface PairedArmRow {\n /** Matching key — rows sharing a `pairKey` across both arms form pairs\n * (typically the task/scenario/seed identity). */\n pairKey: string\n /** Rep identity within a `pairKey` (e.g. a seed or rep number). Required on\n * every row of a `pairKey` that has more than one rep in either arm; reps\n * then pair only on exact (`pairKey`, `repKey`) match, never on outcome\n * content. Optional when each arm has at most one rep of the item. */\n repKey?: string\n /** Arm label this row was produced under. */\n arm: string\n /** Binary outcome; omit when the comparison has no pass/fail notion. */\n pass?: boolean\n /** Named numeric measurements (score, cost, latency, …). */\n metrics?: Record<string, number>\n}\n\nexport interface PairArmsOptions {\n /** Arm treated as the control side of every pair. */\n baselineArm: string\n /** Arm treated as the treatment side of every pair. */\n treatmentArm: string\n}\n\n/** One matched (baseline, treatment) observation of the same work item. */\nexport interface MatchedPair {\n pairKey: string\n /** 0-based position of this pair within its `pairKey`, ordered by sorted\n * `repKey` (always 0 for a single-rep item). The rep identity itself is on\n * the rows (`baseline.repKey` / `treatment.repKey`). */\n repIndex: number\n baseline: PairedArmRow\n treatment: PairedArmRow\n}\n\nexport interface PairArmsResult {\n /** Matched pairs, ordered by (`pairKey`, `repIndex`). */\n pairs: MatchedPair[]\n /** Baseline rows left without a treatment counterpart — reported, never\n * silently dropped. */\n unpairedBaseline: PairedArmRow[]\n /** Treatment rows left without a baseline counterpart. */\n unpairedTreatment: PairedArmRow[]\n}\n\n/**\n * Match rows across two arms into (baseline, treatment) pairs by `pairKey`.\n *\n * A `pairKey` with at most one row per arm pairs directly, no `repKey`\n * needed. A `pairKey` with multiple reps in either arm requires `repKey` on\n * every one of its rows, and reps pair only on exact (`pairKey`, `repKey`)\n * match — pairing is keyed purely on row identity, never on outcome content\n * (outcome-keyed matching deflates discordant counts and biases McNemar), and\n * is therefore independent of input order. Reps whose `repKey` has no\n * counterpart in the other arm, and items present in only one arm, land in\n * the unpaired lists — reported, never truncated.\n *\n * Fail-loud: throws when either named arm has zero rows (an unknown arm\n * name would otherwise read as \"everything unpaired\"), when the two arm\n * names are equal, when a multi-rep `pairKey` has a row without `repKey`, or\n * when a (`pairKey`, arm) group repeats a `repKey` (the match would be\n * ambiguous).\n */\nexport function pairArms(rows: readonly PairedArmRow[], opts: PairArmsOptions): PairArmsResult {\n const { baselineArm, treatmentArm } = opts\n if (baselineArm === treatmentArm) {\n throw new ValidationError(\n `pairArms: baselineArm and treatmentArm are both '${baselineArm}' — an arm cannot be compared to itself`,\n )\n }\n\n // arm → pairKey → rows\n const byArm = new Map<string, Map<string, PairedArmRow[]>>()\n const armsSeen = new Set<string>()\n for (const row of rows) {\n armsSeen.add(row.arm)\n if (row.arm !== baselineArm && row.arm !== treatmentArm) continue\n const byKey = byArm.get(row.arm) ?? new Map<string, PairedArmRow[]>()\n const group = byKey.get(row.pairKey) ?? []\n group.push(row)\n byKey.set(row.pairKey, group)\n byArm.set(row.arm, byKey)\n }\n\n for (const arm of [baselineArm, treatmentArm]) {\n if (!byArm.has(arm)) {\n const seen = [...armsSeen].sort().join(', ') || '<none>'\n throw new ValidationError(`pairArms: no rows for arm '${arm}' (arms present: ${seen})`)\n }\n }\n\n const baselineByKey = byArm.get(baselineArm)!\n const treatmentByKey = byArm.get(treatmentArm)!\n\n const allKeys = [...new Set([...baselineByKey.keys(), ...treatmentByKey.keys()])].sort()\n const pairs: MatchedPair[] = []\n const unpairedBaseline: PairedArmRow[] = []\n const unpairedTreatment: PairedArmRow[] = []\n for (const pairKey of allKeys) {\n const b = baselineByKey.get(pairKey) ?? []\n const t = treatmentByKey.get(pairKey) ?? []\n\n if (b.length <= 1 && t.length <= 1) {\n if (b.length === 1 && t.length === 1) {\n const baseline = b[0]!\n const treatment = t[0]!\n if (baseline.repKey !== undefined || treatment.repKey !== undefined) {\n if (\n baseline.repKey === undefined ||\n treatment.repKey === undefined ||\n baseline.repKey !== treatment.repKey\n ) {\n unpairedBaseline.push(baseline)\n unpairedTreatment.push(treatment)\n continue\n }\n }\n pairs.push({ pairKey, repIndex: 0, baseline, treatment })\n } else {\n unpairedBaseline.push(...b)\n unpairedTreatment.push(...t)\n }\n continue\n }\n\n const bByRep = indexByRepKey(b, pairKey, baselineArm)\n const tByRep = indexByRepKey(t, pairKey, treatmentArm)\n const repKeys = [...new Set([...bByRep.keys(), ...tByRep.keys()])].sort()\n let repIndex = 0\n for (const repKey of repKeys) {\n const baseline = bByRep.get(repKey)\n const treatment = tByRep.get(repKey)\n if (baseline !== undefined && treatment !== undefined) {\n pairs.push({ pairKey, repIndex: repIndex++, baseline, treatment })\n } else if (baseline !== undefined) {\n unpairedBaseline.push(baseline)\n } else if (treatment !== undefined) {\n unpairedTreatment.push(treatment)\n }\n }\n }\n\n return { pairs, unpairedBaseline, unpairedTreatment }\n}\n\n/** Index a multi-rep (pairKey, arm) group by `repKey`, enforcing that every\n * row carries one and that no repKey repeats within the group. */\nfunction indexByRepKey(\n group: readonly PairedArmRow[],\n pairKey: string,\n arm: string,\n): Map<string, PairedArmRow> {\n const byRep = new Map<string, PairedArmRow>()\n for (const row of group) {\n if (row.repKey === undefined) {\n throw new ValidationError(\n `pairArms: pairKey '${pairKey}' has multiple reps in an arm, but a row in arm '${arm}' ` +\n `is missing repKey — multi-rep items require an explicit repKey on every row so reps ` +\n `pair by identity (pairing reps by outcome or by index would bias the paired statistics)`,\n )\n }\n if (byRep.has(row.repKey)) {\n throw new ValidationError(\n `pairArms: duplicate repKey '${row.repKey}' for pairKey '${pairKey}' in arm '${arm}' — ` +\n `(pairKey, repKey) must uniquely identify a rep within an arm`,\n )\n }\n byRep.set(row.repKey, row)\n }\n return byRep\n}\n\n/** Paired pass/fail comparison over the pairs where BOTH sides carry `pass`. */\nexport interface PairedCorrectness {\n /** Discordant pairs where the treatment passed and the baseline failed. */\n b10: number\n /** Discordant pairs where the baseline passed and the treatment failed. */\n b01: number\n /** Exact McNemar significance over the paired outcomes (`b === b10`, `c === b01`). */\n mcnemar: McNemarResult\n /** Paired effect size: p(treatment) − p(baseline) with a paired-variance CI. */\n riskDifference: RiskDifferenceResult\n}\n\n/** Paired delta summary for one named metric (delta = treatment − baseline). */\nexport interface PairedMetricDelta {\n name: string\n /** Pairs where BOTH sides carry a finite value for this metric. */\n n: number\n /** Pairs where at least one side does not carry the metric. */\n nMissing: number\n /** Median paired delta, or null when `n === 0`. */\n medianDelta: number | null\n /** Mean paired delta, or null when `n === 0`. */\n meanDelta: number | null\n /** Bootstrap CI on the paired delta (`pairedBootstrap`); null when\n * `n === 0` — a zero-width [0, 0] interval on no data would read as a\n * measured tight null. */\n bootstrapCi: PairedBootstrapResult | null\n /** Wilcoxon signed-rank test on the paired deltas; null when `n === 0`. */\n wilcoxon: { w: number; p: number } | null\n}\n\nexport interface ComparePairedArmsOptions extends PairArmsOptions {\n /** Metrics to compare. Default: every metric name observed on any matched\n * pair, sorted. A name that appears on no pair is still reported (with\n * `n = 0`) so a misspelled metric is visible instead of vanishing. */\n metricNames?: string[]\n /** Passed through to `pairedBootstrap` — set `seed` for reproducible CIs. */\n bootstrap?: PairedBootstrapOptions\n}\n\nexport interface PairedArmsComparison {\n nPairs: number\n nUnpairedBaseline: number\n nUnpairedTreatment: number\n /** null when no matched pair carries `pass` on both sides — a pass/fail\n * verdict over rows that never measured pass/fail would be fabricated. */\n correctness: PairedCorrectness | null\n metricDeltas: PairedMetricDelta[]\n}\n\n/**\n * Full matched-pair arm comparison: pair via {@link pairArms}, then compose\n * the paired estimators from `statistics` over the matched pairs.\n *\n * Correctness uses only the pairs where both sides carry `pass` (`mcnemar.n`\n * is that subset's size); each metric uses only the pairs where both sides\n * carry a finite value for it, with the remainder counted in `nMissing`.\n * Deltas are treatment − baseline throughout.\n *\n * Fail-loud: inherits {@link pairArms}'s unknown-arm throw, and throws on a\n * non-finite metric value — silently treating corrupt telemetry as \"metric\n * absent\" would misreport it as missing coverage.\n */\nexport function comparePairedArms(\n rows: readonly PairedArmRow[],\n opts: ComparePairedArmsOptions,\n): PairedArmsComparison {\n const { pairs, unpairedBaseline, unpairedTreatment } = pairArms(rows, opts)\n\n let correctness: PairedCorrectness | null = null\n const baselinePass: number[] = []\n const treatmentPass: number[] = []\n for (const pair of pairs) {\n if (pair.baseline.pass === undefined || pair.treatment.pass === undefined) continue\n baselinePass.push(pair.baseline.pass ? 1 : 0)\n treatmentPass.push(pair.treatment.pass ? 1 : 0)\n }\n if (baselinePass.length > 0) {\n const mc = mcnemar(baselinePass, treatmentPass)\n correctness = {\n b10: mc.b,\n b01: mc.c,\n mcnemar: mc,\n riskDifference: pairedRiskDifference(baselinePass, treatmentPass),\n }\n }\n\n const metricNames =\n opts.metricNames ??\n [\n ...new Set(\n pairs.flatMap((p) => [\n ...Object.keys(p.baseline.metrics ?? {}),\n ...Object.keys(p.treatment.metrics ?? {}),\n ]),\n ),\n ].sort()\n\n const metricDeltas: PairedMetricDelta[] = metricNames.map((name) => {\n const before: number[] = []\n const after: number[] = []\n let nMissing = 0\n for (const pair of pairs) {\n const b = metricValue(pair.baseline, name)\n const t = metricValue(pair.treatment, name)\n if (b === undefined || t === undefined) {\n nMissing++\n continue\n }\n before.push(b)\n after.push(t)\n }\n const bootstrapCi = before.length === 0 ? null : pairedBootstrap(before, after, opts.bootstrap)\n return {\n name,\n n: before.length,\n nMissing,\n medianDelta: bootstrapCi?.median ?? null,\n meanDelta: bootstrapCi?.mean ?? null,\n bootstrapCi,\n wilcoxon: before.length === 0 ? null : wilcoxonSignedRank(before, after),\n }\n })\n\n return {\n nPairs: pairs.length,\n nUnpairedBaseline: unpairedBaseline.length,\n nUnpairedTreatment: unpairedTreatment.length,\n correctness,\n metricDeltas,\n }\n}\n\nexport interface MatchedRunRecordPair {\n pairKey: string\n repKey: string\n baseline: RunRecord\n treatment: RunRecord\n}\n\nexport interface PairRunRecordsResult {\n pairs: MatchedRunRecordPair[]\n unpairedBaseline: RunRecord[]\n unpairedTreatment: RunRecord[]\n}\n\ninterface RunRecordArmRow extends PairedArmRow {\n run: RunRecord\n repKey: string\n}\n\n/**\n * Pair two RunRecord arms by the identity of the evaluated work:\n * `(experimentId, scenarioId, seed)`.\n *\n * Falling back to array order, candidate id, or experiment id can compare\n * different tasks and fabricate lift. Duplicate identities throw.\n */\nexport function pairRunRecords(\n baselineRuns: readonly RunRecord[],\n treatmentRuns: readonly RunRecord[],\n): PairRunRecordsResult {\n const baselineRows = runRecordArmRows(baselineRuns, 'baseline')\n const treatmentRows = runRecordArmRows(treatmentRuns, 'treatment')\n validateRunRecordArmRows(baselineRows, 'baseline')\n validateRunRecordArmRows(treatmentRows, 'treatment')\n if (baselineRows.length === 0 || treatmentRows.length === 0) {\n return {\n pairs: [],\n unpairedBaseline: baselineRows.map((row) => row.run),\n unpairedTreatment: treatmentRows.map((row) => row.run),\n }\n }\n\n const result = pairArms([...baselineRows, ...treatmentRows], {\n baselineArm: 'baseline',\n treatmentArm: 'treatment',\n })\n return {\n pairs: result.pairs.map((pair) => {\n const baseline = pair.baseline as RunRecordArmRow\n const treatment = pair.treatment as RunRecordArmRow\n return {\n pairKey: pair.pairKey,\n repKey: baseline.repKey,\n baseline: baseline.run,\n treatment: treatment.run,\n }\n }),\n unpairedBaseline: result.unpairedBaseline.map((row) => (row as RunRecordArmRow).run),\n unpairedTreatment: result.unpairedTreatment.map((row) => (row as RunRecordArmRow).run),\n }\n}\n\nfunction runRecordArmRows(runs: readonly RunRecord[], arm: string): RunRecordArmRow[] {\n return runs.map((run) => {\n const scenarioId = run.scenarioId.trim()\n if (!scenarioId) {\n throw new ValidationError(\n `pairRunRecords: run '${run.runId}' is missing scenarioId; paired comparisons require explicit scenario identity`,\n )\n }\n return {\n pairKey: JSON.stringify([run.experimentId, scenarioId]),\n repKey: String(run.seed),\n arm,\n run,\n }\n })\n}\n\nfunction validateRunRecordArmRows(rows: readonly RunRecordArmRow[], arm: string): void {\n const byPairKey = new Map<string, RunRecordArmRow[]>()\n for (const row of rows) {\n const group = byPairKey.get(row.pairKey) ?? []\n group.push(row)\n byPairKey.set(row.pairKey, group)\n }\n for (const [pairKey, group] of byPairKey) {\n if (group.length > 1) indexByRepKey(group, pairKey, arm)\n }\n}\n\nfunction metricValue(row: PairedArmRow, name: string): number | undefined {\n const v = row.metrics?.[name]\n if (v === undefined) return undefined\n if (!Number.isFinite(v)) {\n throw new ValidationError(\n `comparePairedArms: non-finite value for metric '${name}' on pairKey '${row.pairKey}' (arm '${row.arm}'): ${v}`,\n )\n }\n return v\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuGA,SAAgB,SAAS,MAA+B,MAAuC;CAC7F,MAAM,EAAE,aAAa,iBAAiB;CACtC,IAAI,gBAAgB,cAClB,MAAM,IAAI,gBACR,oDAAoD,YAAY,wCAClE;CAIF,MAAM,wBAAQ,IAAI,IAAyC;CAC3D,MAAM,2BAAW,IAAI,IAAY;CACjC,KAAK,MAAM,OAAO,MAAM;EACtB,SAAS,IAAI,IAAI,GAAG;EACpB,IAAI,IAAI,QAAQ,eAAe,IAAI,QAAQ,cAAc;EACzD,MAAM,QAAQ,MAAM,IAAI,IAAI,GAAG,qBAAK,IAAI,IAA4B;EACpE,MAAM,QAAQ,MAAM,IAAI,IAAI,OAAO,KAAK,CAAC;EACzC,MAAM,KAAK,GAAG;EACd,MAAM,IAAI,IAAI,SAAS,KAAK;EAC5B,MAAM,IAAI,IAAI,KAAK,KAAK;CAC1B;CAEA,KAAK,MAAM,OAAO,CAAC,aAAa,YAAY,GAC1C,IAAI,CAAC,MAAM,IAAI,GAAG,GAEhB,MAAM,IAAI,gBAAgB,8BAA8B,IAAI,mBAD/C,CAAC,GAAG,QAAQ,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,KAAK,SACoC,EAAE;CAI1F,MAAM,gBAAgB,MAAM,IAAI,WAAW;CAC3C,MAAM,iBAAiB,MAAM,IAAI,YAAY;CAE7C,MAAM,UAAU,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,cAAc,KAAK,GAAG,GAAG,eAAe,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK;CACvF,MAAM,QAAuB,CAAC;CAC9B,MAAM,mBAAmC,CAAC;CAC1C,MAAM,oBAAoC,CAAC;CAC3C,KAAK,MAAM,WAAW,SAAS;EAC7B,MAAM,IAAI,cAAc,IAAI,OAAO,KAAK,CAAC;EACzC,MAAM,IAAI,eAAe,IAAI,OAAO,KAAK,CAAC;EAE1C,IAAI,EAAE,UAAU,KAAK,EAAE,UAAU,GAAG;GAClC,IAAI,EAAE,WAAW,KAAK,EAAE,WAAW,GAAG;IACpC,MAAM,WAAW,EAAE;IACnB,MAAM,YAAY,EAAE;IACpB,IAAI,SAAS,WAAW,KAAA,KAAa,UAAU,WAAW,KAAA,GAEtD;SAAA,SAAS,WAAW,KAAA,KACpB,UAAU,WAAW,KAAA,KACrB,SAAS,WAAW,UAAU,QAC9B;MACA,iBAAiB,KAAK,QAAQ;MAC9B,kBAAkB,KAAK,SAAS;MAChC;KACF;;IAEF,MAAM,KAAK;KAAE;KAAS,UAAU;KAAG;KAAU;IAAU,CAAC;GAC1D,OAAO;IACL,iBAAiB,KAAK,GAAG,CAAC;IAC1B,kBAAkB,KAAK,GAAG,CAAC;GAC7B;GACA;EACF;EAEA,MAAM,SAAS,cAAc,GAAG,SAAS,WAAW;EACpD,MAAM,SAAS,cAAc,GAAG,SAAS,YAAY;EACrD,MAAM,UAAU,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,OAAO,KAAK,GAAG,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK;EACxE,IAAI,WAAW;EACf,KAAK,MAAM,UAAU,SAAS;GAC5B,MAAM,WAAW,OAAO,IAAI,MAAM;GAClC,MAAM,YAAY,OAAO,IAAI,MAAM;GACnC,IAAI,aAAa,KAAA,KAAa,cAAc,KAAA,GAC1C,MAAM,KAAK;IAAE;IAAS,UAAU;IAAY;IAAU;GAAU,CAAC;QAC5D,IAAI,aAAa,KAAA,GACtB,iBAAiB,KAAK,QAAQ;QACzB,IAAI,cAAc,KAAA,GACvB,kBAAkB,KAAK,SAAS;EAEpC;CACF;CAEA,OAAO;EAAE;EAAO;EAAkB;CAAkB;AACtD;;;AAIA,SAAS,cACP,OACA,SACA,KAC2B;CAC3B,MAAM,wBAAQ,IAAI,IAA0B;CAC5C,KAAK,MAAM,OAAO,OAAO;EACvB,IAAI,IAAI,WAAW,KAAA,GACjB,MAAM,IAAI,gBACR,sBAAsB,QAAQ,mDAAmD,IAAI,8KAGvF;EAEF,IAAI,MAAM,IAAI,IAAI,MAAM,GACtB,MAAM,IAAI,gBACR,+BAA+B,IAAI,OAAO,iBAAiB,QAAQ,YAAY,IAAI,iEAErF;EAEF,MAAM,IAAI,IAAI,QAAQ,GAAG;CAC3B;CACA,OAAO;AACT;;;;;;;;;;;;;;AAiEA,SAAgB,kBACd,MACA,MACsB;CACtB,MAAM,EAAE,OAAO,kBAAkB,sBAAsB,SAAS,MAAM,IAAI;CAE1E,IAAI,cAAwC;CAC5C,MAAM,eAAyB,CAAC;CAChC,MAAM,gBAA0B,CAAC;CACjC,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,SAAS,SAAS,KAAA,KAAa,KAAK,UAAU,SAAS,KAAA,GAAW;EAC3E,aAAa,KAAK,KAAK,SAAS,OAAO,IAAI,CAAC;EAC5C,cAAc,KAAK,KAAK,UAAU,OAAO,IAAI,CAAC;CAChD;CACA,IAAI,aAAa,SAAS,GAAG;EAC3B,MAAM,KAAK,QAAQ,cAAc,aAAa;EAC9C,cAAc;GACZ,KAAK,GAAG;GACR,KAAK,GAAG;GACR,SAAS;GACT,gBAAgB,qBAAqB,cAAc,aAAa;EAClE;CACF;CAaA,MAAM,gBAVJ,KAAK,eACL,CACE,GAAG,IAAI,IACL,MAAM,SAAS,MAAM,CACnB,GAAG,OAAO,KAAK,EAAE,SAAS,WAAW,CAAC,CAAC,GACvC,GAAG,OAAO,KAAK,EAAE,UAAU,WAAW,CAAC,CAAC,CAC1C,CAAC,CACH,CACF,CAAC,CAAC,KAAK,EAAA,CAE6C,KAAK,SAAS;EAClE,MAAM,SAAmB,CAAC;EAC1B,MAAM,QAAkB,CAAC;EACzB,IAAI,WAAW;EACf,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,IAAI,YAAY,KAAK,UAAU,IAAI;GACzC,MAAM,IAAI,YAAY,KAAK,WAAW,IAAI;GAC1C,IAAI,MAAM,KAAA,KAAa,MAAM,KAAA,GAAW;IACtC;IACA;GACF;GACA,OAAO,KAAK,CAAC;GACb,MAAM,KAAK,CAAC;EACd;EACA,MAAM,cAAc,OAAO,WAAW,IAAI,OAAO,gBAAgB,QAAQ,OAAO,KAAK,SAAS;EAC9F,OAAO;GACL;GACA,GAAG,OAAO;GACV;GACA,aAAa,aAAa,UAAU;GACpC,WAAW,aAAa,QAAQ;GAChC;GACA,UAAU,OAAO,WAAW,IAAI,OAAO,mBAAmB,QAAQ,KAAK;EACzE;CACF,CAAC;CAED,OAAO;EACL,QAAQ,MAAM;EACd,mBAAmB,iBAAiB;EACpC,oBAAoB,kBAAkB;EACtC;EACA;CACF;AACF;;;;;;;;AA2BA,SAAgB,eACd,cACA,eACsB;CACtB,MAAM,eAAe,iBAAiB,cAAc,UAAU;CAC9D,MAAM,gBAAgB,iBAAiB,eAAe,WAAW;CACjE,yBAAyB,cAAc,UAAU;CACjD,yBAAyB,eAAe,WAAW;CACnD,IAAI,aAAa,WAAW,KAAK,cAAc,WAAW,GACxD,OAAO;EACL,OAAO,CAAC;EACR,kBAAkB,aAAa,KAAK,QAAQ,IAAI,GAAG;EACnD,mBAAmB,cAAc,KAAK,QAAQ,IAAI,GAAG;CACvD;CAGF,MAAM,SAAS,SAAS,CAAC,GAAG,cAAc,GAAG,aAAa,GAAG;EAC3D,aAAa;EACb,cAAc;CAChB,CAAC;CACD,OAAO;EACL,OAAO,OAAO,MAAM,KAAK,SAAS;GAChC,MAAM,WAAW,KAAK;GACtB,MAAM,YAAY,KAAK;GACvB,OAAO;IACL,SAAS,KAAK;IACd,QAAQ,SAAS;IACjB,UAAU,SAAS;IACnB,WAAW,UAAU;GACvB;EACF,CAAC;EACD,kBAAkB,OAAO,iBAAiB,KAAK,QAAS,IAAwB,GAAG;EACnF,mBAAmB,OAAO,kBAAkB,KAAK,QAAS,IAAwB,GAAG;CACvF;AACF;AAEA,SAAS,iBAAiB,MAA4B,KAAgC;CACpF,OAAO,KAAK,KAAK,QAAQ;EACvB,MAAM,aAAa,IAAI,WAAW,KAAK;EACvC,IAAI,CAAC,YACH,MAAM,IAAI,gBACR,wBAAwB,IAAI,MAAM,+EACpC;EAEF,OAAO;GACL,SAAS,KAAK,UAAU,CAAC,IAAI,cAAc,UAAU,CAAC;GACtD,QAAQ,OAAO,IAAI,IAAI;GACvB;GACA;EACF;CACF,CAAC;AACH;AAEA,SAAS,yBAAyB,MAAkC,KAAmB;CACrF,MAAM,4BAAY,IAAI,IAA+B;CACrD,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,UAAU,IAAI,IAAI,OAAO,KAAK,CAAC;EAC7C,MAAM,KAAK,GAAG;EACd,UAAU,IAAI,IAAI,SAAS,KAAK;CAClC;CACA,KAAK,MAAM,CAAC,SAAS,UAAU,WAC7B,IAAI,MAAM,SAAS,GAAG,cAAc,OAAO,SAAS,GAAG;AAE3D;AAEA,SAAS,YAAY,KAAmB,MAAkC;CACxE,MAAM,IAAI,IAAI,UAAU;CACxB,IAAI,MAAM,KAAA,GAAW,OAAO,KAAA;CAC5B,IAAI,CAAC,OAAO,SAAS,CAAC,GACpB,MAAM,IAAI,gBACR,mDAAmD,KAAK,gBAAgB,IAAI,QAAQ,UAAU,IAAI,IAAI,MAAM,GAC9G;CAEF,OAAO;AACT"}
@@ -2,7 +2,7 @@ import { S as ToolSpan, f as Run, r as BudgetSpec } from "../schema-BtVldJ3T.js"
2
2
  import { a as RunFilter, s as TraceStore } from "../store-CT9YIIve.js";
3
3
  import { o as llmSpans } from "../query-CJ_DX8vl.js";
4
4
  import { n as FailureClusterReport, r as failureClusterView, t as FailureCluster } from "../failure-cluster-CqcvCcdR.js";
5
- import { l as TrajectoryStep, m as computeToolUseMetrics, n as BaselineReport, t as BaselineOptions } from "../baseline-hG3K85h4.js";
5
+ import { d as TrajectoryStep, g as computeToolUseMetrics, n as BaselineReport, t as BaselineOptions } from "../baseline-D_fT6277.js";
6
6
  //#region src/pipelines/budget-breach.d.ts
7
7
  interface BudgetBreachFinding {
8
8
  runId: string;
@@ -1,9 +1,10 @@
1
1
  import { t as executionTrackByLane } from "../execution-tracks-CpgFPpS5.js";
2
- import { E as pearsonR, u as interRaterReliability } from "../statistics-DWM_AyLe.js";
2
+ import { h as interRaterReliability, j as pearsonR } from "../statistics-RwRNu2__.js";
3
3
  import { s as isToolSpan } from "../schema-CRhEY1SO.js";
4
4
  import { t as buildTrajectory } from "../trajectory-D_7rLrvE.js";
5
+ import { t as compareToBaseline } from "../baseline-BaPxoROc.js";
5
6
  import { i as hasCapturedToolArgs, l as toolSpans, n as argHash, o as llmSpans, s as runFailureClass, t as aggregateLlm } from "../query-Di7eEQ79.js";
6
- import { a as DEFAULT_RULES, i as computeToolUseMetrics, o as classifyFailure, t as compareToBaseline } from "../baseline-DcX5hQDv.js";
7
+ import { n as DEFAULT_RULES, r as classifyFailure, t as computeToolUseMetrics } from "../tool-use-metrics-DEGMKycK.js";
7
8
  //#region src/pipelines/budget-breach.ts
8
9
  async function budgetBreachView(store, options = {}) {
9
10
  const runs = await store.listRuns({