@tangle-network/agent-eval 0.200.0 → 0.201.0

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 (231) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/adapters/http.d.ts +7 -8
  3. package/dist/adapters/http.d.ts.map +1 -1
  4. package/dist/adapters/http.js +2 -2
  5. package/dist/adapters/http.js.map +1 -1
  6. package/dist/agent-profile-DDfIjdyS.d.ts.map +1 -1
  7. package/dist/analyst/index.d.ts +76 -76
  8. package/dist/analyst/index.d.ts.map +1 -1
  9. package/dist/analyst/index.js +6 -6
  10. package/dist/{analyze-runs-DFbp2gB8.js → analyze-runs-BrIibNd0.js} +47 -42
  11. package/dist/{analyze-runs-DFbp2gB8.js.map → analyze-runs-BrIibNd0.js.map} +1 -1
  12. package/dist/{ast-CP9ae9B0.js → ast-DJ6Bc7ab.js} +5 -4
  13. package/dist/{ast-CP9ae9B0.js.map → ast-DJ6Bc7ab.js.map} +1 -1
  14. package/dist/authenticity/index.d.ts +14 -15
  15. package/dist/authenticity/index.d.ts.map +1 -1
  16. package/dist/{baseline-BC-eBZ7U.js → baseline-CVKsfWTg.js} +2 -2
  17. package/dist/{baseline-BC-eBZ7U.js.map → baseline-CVKsfWTg.js.map} +1 -1
  18. package/dist/{benchmark-BeQJyrRC.js → benchmark-BywPeAUo.js} +4 -3
  19. package/dist/{benchmark-BeQJyrRC.js.map → benchmark-BywPeAUo.js.map} +1 -1
  20. package/dist/{benchmark-command-EBmuMUBo.js → benchmark-command-DFs_i62N.js} +24 -23
  21. package/dist/{benchmark-command-EBmuMUBo.js.map → benchmark-command-DFs_i62N.js.map} +1 -1
  22. package/dist/benchmarks/index.d.ts +12 -12
  23. package/dist/benchmarks/index.d.ts.map +1 -1
  24. package/dist/benchmarks/index.js +3 -3
  25. package/dist/{bounded-process-BBZob7vl.js → bounded-process-Cj9YjXbE.js} +3 -3
  26. package/dist/{bounded-process-BBZob7vl.js.map → bounded-process-Cj9YjXbE.js.map} +1 -1
  27. package/dist/builder-eval/index.d.ts +16 -17
  28. package/dist/builder-eval/index.d.ts.map +1 -1
  29. package/dist/builder-eval/index.js +12 -10
  30. package/dist/builder-eval/index.js.map +1 -1
  31. package/dist/campaign/index.js +11 -11
  32. package/dist/{campaign-L391bmir.js → campaign-PmTWZi72.js} +30 -25
  33. package/dist/{campaign-L391bmir.js.map → campaign-PmTWZi72.js.map} +1 -1
  34. package/dist/{campaign-evidence-jL_U7Ikv.js → campaign-evidence-Dt_rWPk2.js} +4 -4
  35. package/dist/{campaign-evidence-jL_U7Ikv.js.map → campaign-evidence-Dt_rWPk2.js.map} +1 -1
  36. package/dist/{chat-client-CJORSG_J.js → chat-client-DUhWNQQS.js} +5 -4
  37. package/dist/{chat-client-CJORSG_J.js.map → chat-client-DUhWNQQS.js.map} +1 -1
  38. package/dist/{chat-json-call-DnlDjpco.js → chat-json-call-Dx_sHzqm.js} +2 -2
  39. package/dist/{chat-json-call-DnlDjpco.js.map → chat-json-call-Dx_sHzqm.js.map} +1 -1
  40. package/dist/{claim-V9Zqbq0J.js → claim-BXcZw-jo.js} +2 -2
  41. package/dist/{claim-V9Zqbq0J.js.map → claim-BXcZw-jo.js.map} +1 -1
  42. package/dist/cli.js +12 -11
  43. package/dist/cli.js.map +1 -1
  44. package/dist/contract/index.js +22 -19
  45. package/dist/contract/index.js.map +1 -1
  46. package/dist/{default-registry-xMLX160E.js → default-registry-D4qXx2aG.js} +2 -2
  47. package/dist/{default-registry-xMLX160E.js.map → default-registry-D4qXx2aG.js.map} +1 -1
  48. package/dist/define-agent-eval-1YuvJpOs.d.ts.map +1 -1
  49. package/dist/{define-agent-eval-7q_UwsTt.js → define-agent-eval-CJN9dTQw.js} +10 -9
  50. package/dist/{define-agent-eval-7q_UwsTt.js.map → define-agent-eval-CJN9dTQw.js.map} +1 -1
  51. package/dist/{descriptive-1V17A-qa.js → descriptive-D9LUFk1-.js} +2 -2
  52. package/dist/{descriptive-1V17A-qa.js.map → descriptive-D9LUFk1-.js.map} +1 -1
  53. package/dist/diagnosis/index.d.ts +7 -7
  54. package/dist/diagnosis/index.d.ts.map +1 -1
  55. package/dist/diagnosis/index.js +2 -2
  56. package/dist/diagnosis/index.js.map +1 -1
  57. package/dist/{dspy-rlm-engine-B5T43BkZ.js → dspy-rlm-engine-US8YsYGq.js} +5 -5
  58. package/dist/{dspy-rlm-engine-B5T43BkZ.js.map → dspy-rlm-engine-US8YsYGq.js.map} +1 -1
  59. package/dist/{eval-campaign-GapmPHIA.js → eval-campaign-ha_aWtk7.js} +14 -12
  60. package/dist/{eval-campaign-GapmPHIA.js.map → eval-campaign-ha_aWtk7.js.map} +1 -1
  61. package/dist/evaluation.d.ts +9 -10
  62. package/dist/evaluation.d.ts.map +1 -1
  63. package/dist/experiment/index.d.ts +29 -29
  64. package/dist/experiment/index.d.ts.map +1 -1
  65. package/dist/experiment/index.js +13 -12
  66. package/dist/experiment/index.js.map +1 -1
  67. package/dist/{exporters-DEcXbvgO.js → exporters-Coiv-qO2.js} +2 -2
  68. package/dist/{exporters-DEcXbvgO.js.map → exporters-Coiv-qO2.js.map} +1 -1
  69. package/dist/{external-optimizer-process-CEXcCKqd.js → external-optimizer-process-KLcw0Vnc.js} +2 -2
  70. package/dist/{external-optimizer-process-CEXcCKqd.js.map → external-optimizer-process-KLcw0Vnc.js.map} +1 -1
  71. package/dist/{external-optimizer-subprocess-laM1-UDV.js → external-optimizer-subprocess-CkyUUV7h.js} +4 -4
  72. package/dist/{external-optimizer-subprocess-laM1-UDV.js.map → external-optimizer-subprocess-CkyUUV7h.js.map} +1 -1
  73. package/dist/fuzz.d.ts +15 -15
  74. package/dist/fuzz.d.ts.map +1 -1
  75. package/dist/{hf-dataset-CRqtSjnR.js → hf-dataset-kjIfIK_e.js} +3 -3
  76. package/dist/{hf-dataset-CRqtSjnR.js.map → hf-dataset-kjIfIK_e.js.map} +1 -1
  77. package/dist/hosted/index.d.ts +8 -8
  78. package/dist/hosted/index.d.ts.map +1 -1
  79. package/dist/hosted/index.js +1 -1
  80. package/dist/{index-D4pC7Tvd2.d.ts → index-D4pC7Tvd.d.ts} +1 -1
  81. package/dist/index-D4pC7Tvd.d.ts.map +1 -0
  82. package/dist/index-DKmrHA02.d.ts.map +1 -1
  83. package/dist/{index-DNgf5gyG2.d.ts → index-DNgf5gyG.d.ts} +1 -1
  84. package/dist/index-DNgf5gyG.d.ts.map +1 -0
  85. package/dist/index.d.ts +124 -124
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +52 -42
  88. package/dist/index.js.map +1 -1
  89. package/dist/{integrity-BFP4kjGj.js → integrity-BvnVARVl.js} +30 -29
  90. package/dist/{integrity-BFP4kjGj.js.map → integrity-BvnVARVl.js.map} +1 -1
  91. package/dist/{integrity-BvHANvnp.js → integrity-uwK7XC-8.js} +2 -2
  92. package/dist/{integrity-BvHANvnp.js.map → integrity-uwK7XC-8.js.map} +1 -1
  93. package/dist/{internal-BMFSR8Ns.js → internal-BOe3zmjQ.js} +2 -2
  94. package/dist/{internal-BMFSR8Ns.js.map → internal-BOe3zmjQ.js.map} +1 -1
  95. package/dist/{jev-review-Cu9xtIu4.js → jev-review-DZEjDzlF.js} +7 -5
  96. package/dist/{jev-review-Cu9xtIu4.js.map → jev-review-DZEjDzlF.js.map} +1 -1
  97. package/dist/jev.d.ts +10 -10
  98. package/dist/jev.d.ts.map +1 -1
  99. package/dist/jev.js +2 -2
  100. package/dist/{judge-calibration-DYmaBtJr.js → judge-calibration-C5zgtiJT.js} +2 -2
  101. package/dist/{judge-calibration-DYmaBtJr.js.map → judge-calibration-C5zgtiJT.js.map} +1 -1
  102. package/dist/kind-factory-CdPPHtGZ.js.map +1 -1
  103. package/dist/{llm-client-c_bKHTXz.js → llm-client-Bhqoglar.js} +6 -4
  104. package/dist/{llm-client-c_bKHTXz.js.map → llm-client-Bhqoglar.js.map} +1 -1
  105. package/dist/{llm-judge-CL6f3utD.js → llm-judge-C-0P1WiT.js} +26 -21
  106. package/dist/{llm-judge-CL6f3utD.js.map → llm-judge-C-0P1WiT.js.map} +1 -1
  107. package/dist/matrix/index.d.ts +1 -1
  108. package/dist/matrix/index.js +1 -1
  109. package/dist/{matrix-_NLSWsKM.d.ts → matrix--IZKROxD.d.ts} +2 -2
  110. package/dist/{matrix-_NLSWsKM.d.ts.map → matrix--IZKROxD.d.ts.map} +1 -1
  111. package/dist/{matrix-pWinRMPl.js → matrix-DnlHn5x1.js} +8 -6
  112. package/dist/{matrix-pWinRMPl.js.map → matrix-DnlHn5x1.js.map} +1 -1
  113. package/dist/meta-eval/index.d.ts +55 -55
  114. package/dist/meta-eval/index.d.ts.map +1 -1
  115. package/dist/meta-eval/index.js +4 -4
  116. package/dist/{mint-iCD-W43s.js → mint-vlRckJEX.js} +2 -2
  117. package/dist/{mint-iCD-W43s.js.map → mint-vlRckJEX.js.map} +1 -1
  118. package/dist/multishot/golden/index.d.ts +23 -23
  119. package/dist/multishot/golden/index.d.ts.map +1 -1
  120. package/dist/multishot/golden/index.js +1 -1
  121. package/dist/multishot/index.d.ts +9 -9
  122. package/dist/multishot/index.d.ts.map +1 -1
  123. package/dist/multishot/index.js +1 -1
  124. package/dist/openapi.json +1 -1
  125. package/dist/{otel-spans-CQmSxtcS.js → otel-spans-C8YIJWy1.js} +9 -4
  126. package/dist/{otel-spans-CQmSxtcS.js.map → otel-spans-C8YIJWy1.js.map} +1 -1
  127. package/dist/{outcome-store-BXlkwMPR.js → outcome-store-DUcMj-3D.js} +2 -2
  128. package/dist/{outcome-store-BXlkwMPR.js.map → outcome-store-DUcMj-3D.js.map} +1 -1
  129. package/dist/package-version-D7lQHt_-.js.map +1 -1
  130. package/dist/{paired-arms-DKZFVPt8.js → paired-arms-DpS_h8ON.js} +8 -5
  131. package/dist/{paired-arms-DKZFVPt8.js.map → paired-arms-DpS_h8ON.js.map} +1 -1
  132. package/dist/{paired-binary-fzkwENI4.js → paired-binary-OMBVRtc-.js} +2 -2
  133. package/dist/{paired-binary-fzkwENI4.js.map → paired-binary-OMBVRtc-.js.map} +1 -1
  134. package/dist/{paired-promotion-decision-D5B1Cxc2.js → paired-promotion-decision-DhrmjdV8.js} +5 -4
  135. package/dist/{paired-promotion-decision-D5B1Cxc2.js.map → paired-promotion-decision-DhrmjdV8.js.map} +1 -1
  136. package/dist/{paired-tests-C8iCsioC.js → paired-tests-Ccr16gcW.js} +3 -3
  137. package/dist/{paired-tests-C8iCsioC.js.map → paired-tests-Ccr16gcW.js.map} +1 -1
  138. package/dist/pipelines/index.d.ts +19 -19
  139. package/dist/pipelines/index.d.ts.map +1 -1
  140. package/dist/pipelines/index.js +5 -4
  141. package/dist/pipelines/index.js.map +1 -1
  142. package/dist/{power-and-mde-K_CmO3dc.js → power-and-mde-bie6i965.js} +9 -6
  143. package/dist/{power-and-mde-K_CmO3dc.js.map → power-and-mde-bie6i965.js.map} +1 -1
  144. package/dist/{produced-state-DZMW8cuF.js → produced-state-CR2tAPLf.js} +8 -5
  145. package/dist/{produced-state-DZMW8cuF.js.map → produced-state-CR2tAPLf.js.map} +1 -1
  146. package/dist/{promotion-policy-C6FAJvmZ.js → promotion-policy-BI1RxzwD.js} +4 -4
  147. package/dist/{promotion-policy-C6FAJvmZ.js.map → promotion-policy-BI1RxzwD.js.map} +1 -1
  148. package/dist/{raw-provider-sink-CXuOhLvT.js → raw-provider-sink-rB2DYxr6.js} +2 -2
  149. package/dist/{raw-provider-sink-CXuOhLvT.js.map → raw-provider-sink-rB2DYxr6.js.map} +1 -1
  150. package/dist/{release-confidence-XQm_xjhR.js → release-confidence-CiFg1EPq.js} +3 -2
  151. package/dist/{release-confidence-XQm_xjhR.js.map → release-confidence-CiFg1EPq.js.map} +1 -1
  152. package/dist/{report-command-DKJ_yAIj.js → report-command-DzPpFgjB.js} +20 -18
  153. package/dist/{report-command-DKJ_yAIj.js.map → report-command-DzPpFgjB.js.map} +1 -1
  154. package/dist/reporting.js +5 -5
  155. package/dist/{reward-hacking-D0XwhVWE.js → reward-hacking-C-a__ShZ.js} +5 -3
  156. package/dist/{reward-hacking-D0XwhVWE.js.map → reward-hacking-C-a__ShZ.js.map} +1 -1
  157. package/dist/rl.d.ts +199 -202
  158. package/dist/rl.d.ts.map +1 -1
  159. package/dist/rl.js +48 -49
  160. package/dist/rl.js.map +1 -1
  161. package/dist/rollout/index.d.ts +1 -1
  162. package/dist/rollout/index.js +5 -5
  163. package/dist/{rollout-B-XBReTC.js → rollout-BUQixCoc.js} +5 -5
  164. package/dist/{rollout-B-XBReTC.js.map → rollout-BUQixCoc.js.map} +1 -1
  165. package/dist/{rubric-predictive-validity-CCK-1B7w.js → rubric-predictive-validity-B0Pg-3I5.js} +3 -3
  166. package/dist/{rubric-predictive-validity-CCK-1B7w.js.map → rubric-predictive-validity-B0Pg-3I5.js.map} +1 -1
  167. package/dist/{run-record-tNnlQIrD.js → run-record-BB290Y9A.js} +5 -3
  168. package/dist/{run-record-tNnlQIrD.js.map → run-record-BB290Y9A.js.map} +1 -1
  169. package/dist/{schema-C1aaAxTf.js → schema-DAMp77zB.js} +24 -20
  170. package/dist/{schema-C1aaAxTf.js.map → schema-DAMp77zB.js.map} +1 -1
  171. package/dist/search/index.d.ts +34 -34
  172. package/dist/search/index.d.ts.map +1 -1
  173. package/dist/search/index.js +2 -2
  174. package/dist/{search-kernel-AVgIjXaI.js → search-kernel-Dpniloks.js} +33 -26
  175. package/dist/{search-kernel-AVgIjXaI.js.map → search-kernel-Dpniloks.js.map} +1 -1
  176. package/dist/{search-ledger-CL0-mXQ8.js → search-ledger-CXQPwdWz.js} +4 -3
  177. package/dist/{search-ledger-CL0-mXQ8.js.map → search-ledger-CXQPwdWz.js.map} +1 -1
  178. package/dist/{search-shipper-Cs6K4km9.js → search-shipper-DhhofkDX.js} +3 -6
  179. package/dist/{search-shipper-Cs6K4km9.js.map → search-shipper-DhhofkDX.js.map} +1 -1
  180. package/dist/{search-summary-DvWk0hb3.js → search-summary-C1nAHgkB.js} +4 -4
  181. package/dist/{search-summary-DvWk0hb3.js.map → search-summary-C1nAHgkB.js.map} +1 -1
  182. package/dist/{semantic-concept-judge-2SE_kXGF.js → semantic-concept-judge-BcjYg37z.js} +4 -4
  183. package/dist/{semantic-concept-judge-2SE_kXGF.js.map → semantic-concept-judge-BcjYg37z.js.map} +1 -1
  184. package/dist/{sequential-Yz3OSskl.js → sequential-DWn_np6P.js} +2 -3
  185. package/dist/{sequential-Yz3OSskl.js.map → sequential-DWn_np6P.js.map} +1 -1
  186. package/dist/{server-D56r1lhp.js → server-B4jMhqQn.js} +2 -2
  187. package/dist/{server-D56r1lhp.js.map → server-B4jMhqQn.js.map} +1 -1
  188. package/dist/{skillopt-optimization-method-B2syNik0.js → skillopt-optimization-method-Bls9t-G_.js} +9 -9
  189. package/dist/{skillopt-optimization-method-B2syNik0.js.map → skillopt-optimization-method-Bls9t-G_.js.map} +1 -1
  190. package/dist/{statistical-heldout-BAFDCo07.js → statistical-heldout-CNLOvRq_.js} +3 -3
  191. package/dist/{statistical-heldout-BAFDCo07.js.map → statistical-heldout-CNLOvRq_.js.map} +1 -1
  192. package/dist/statistical-heldout-HswMvzbP.d.ts.map +1 -1
  193. package/dist/{store-DNe_Uv1Q.js → store-BhNuLdJw.js} +3 -5
  194. package/dist/{store-DNe_Uv1Q.js.map → store-BhNuLdJw.js.map} +1 -1
  195. package/dist/store-C4avFjIi.d.ts.map +1 -1
  196. package/dist/{store-otlp-BPowgPm6.js → store-otlp-yd9lDziN.js} +2 -2
  197. package/dist/{store-otlp-BPowgPm6.js.map → store-otlp-yd9lDziN.js.map} +1 -1
  198. package/dist/{store-tool-spans-BaGakFc2.js → store-tool-spans-Biio4_Nw.js} +4 -4
  199. package/dist/{store-tool-spans-BaGakFc2.js.map → store-tool-spans-Biio4_Nw.js.map} +1 -1
  200. package/dist/storyboard/index.d.ts +20 -21
  201. package/dist/storyboard/index.d.ts.map +1 -1
  202. package/dist/{student-t-BA-Uy51p.js → student-t-BBZYka0E.js} +4 -3
  203. package/dist/{student-t-BA-Uy51p.js.map → student-t-BBZYka0E.js.map} +1 -1
  204. package/dist/{summary-report-BeYnvddy.js → summary-report-DLzptLSA.js} +8 -7
  205. package/dist/{summary-report-BeYnvddy.js.map → summary-report-DLzptLSA.js.map} +1 -1
  206. package/dist/supervisor-run/index.d.ts +25 -25
  207. package/dist/supervisor-run/index.d.ts.map +1 -1
  208. package/dist/supervisor-run/index.js +2 -2
  209. package/dist/{tool-waste-CJDfxH45.js → tool-waste-B-u3rzCz.js} +11 -6
  210. package/dist/{tool-waste-CJDfxH45.js.map → tool-waste-B-u3rzCz.js.map} +1 -1
  211. package/dist/trace-repair/index.d.ts +96 -96
  212. package/dist/trace-repair/index.d.ts.map +1 -1
  213. package/dist/trace-repair/index.js +1 -1
  214. package/dist/trace-repair/index.js.map +1 -1
  215. package/dist/traces.d.ts +29 -29
  216. package/dist/traces.d.ts.map +1 -1
  217. package/dist/traces.js +9 -7
  218. package/dist/traces.js.map +1 -1
  219. package/dist/trajectory-replay/index.d.ts +35 -35
  220. package/dist/trajectory-replay/index.d.ts.map +1 -1
  221. package/dist/trajectory-replay/index.js +5 -5
  222. package/dist/trajectory-replay/index.js.map +1 -1
  223. package/dist/{tree-Ki_gW7z8.js → tree-D48IJUK0.js} +39 -31
  224. package/dist/{tree-Ki_gW7z8.js.map → tree-D48IJUK0.js.map} +1 -1
  225. package/dist/wire/index.d.ts +48 -48
  226. package/dist/wire/index.d.ts.map +1 -1
  227. package/dist/wire/index.js +1 -1
  228. package/docs/jev-behavior-review.md +4 -3
  229. package/package.json +18 -36
  230. package/dist/index-D4pC7Tvd2.d.ts.map +0 -1
  231. package/dist/index-DNgf5gyG2.d.ts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"paired-promotion-decision-D5B1Cxc2.js","names":[],"sources":["../src/paired-delta-test.ts","../src/paired-promotion-decision.ts"],"sourcesContent":["import {\n type PairedBootstrapOptions,\n type PairedBootstrapResult,\n pairedBootstrap,\n pairedSignTest,\n} from './statistics'\n\nexport interface PairedDeltaTestOptions extends PairedBootstrapOptions {\n /** Smallest candidate-minus-baseline delta that counts as improvement. Default 0. */\n threshold?: number\n /** Caller-required paired observations. The exact test may impose a higher minimum. */\n minPairs?: number\n}\n\nexport interface PairedDeltaTestResult {\n bootstrap: PairedBootstrapResult\n method: 'bootstrap-ci' | 'exact-sign'\n /** Exact one-sided p-value below the bootstrap minimum; otherwise null. */\n pValue: number | null\n /** Effective observation minimum after accounting for confidence. */\n minimumPairs: number\n sufficient: boolean\n /**\n * The bootstrap interval has zero width (or is non-finite), so it carries no\n * information about how far the estimate could be wrong and cannot support a\n * decision in either direction. See {@link pairedDeltaTest}.\n */\n indeterminate: boolean\n significant: boolean\n}\n\n/** Smallest all-positive sample that can clear a one-sided exact sign test. */\nexport function minimumPairsForPairedDeltaTest(confidence = 0.95): number {\n if (!Number.isFinite(confidence) || confidence <= 0 || confidence >= 1) {\n throw new Error(\n `minimumPairsForPairedDeltaTest: confidence must be in (0,1), got ${confidence}`,\n )\n }\n const oneSidedAlpha = (1 - confidence) / 2\n return Math.ceil(Math.log2(1 / oneSidedAlpha))\n}\n\n/**\n * Tests whether a paired candidate-minus-baseline delta clears a threshold.\n *\n * At 20 or more pairs, the percentile bootstrap lower bound carries the\n * decision. Below that point the interval is descriptive only, so the function\n * switches to a pre-registered one-sided exact sign test. The exact path is\n * deliberately conservative: it requires both a point estimate above the\n * threshold and enough consistently positive paired differences.\n *\n * ## A zero-width interval is never significant\n *\n * When every paired delta is identical the resample distribution is a point\n * mass and the interval collapses: `[0, 0]` when all pairs tie, `[g, g]` on n\n * identical deltas of g. Neither says the effect is certain — both say the\n * sample carries no information about how far the estimate could be wrong, and\n * `low > threshold` then answers on the point estimate alone. It fails in both\n * directions: `[0, 0]` clears every NEGATIVE threshold, which is how a\n * tie-dominated pass/fail comparison laundered a regression into a\n * noninferiority pass, and `[g, g]` clears every threshold below g with no\n * spread behind it. Under a bounded asymmetric null whose true mean paired\n * delta is exactly 0 — 2 % of pairs dropping by 1.0, the rest gaining 0.0204 —\n * every sample that misses the drop is exactly that shape, and deciding on\n * `low > 0` promoted 65.65 % of samples at n = 20 against a nominal 5 %.\n *\n * So `indeterminate` is reported and `significant` is false whenever the\n * interval has zero width, on BOTH paths: at small n the exact sign test is a\n * test of the MEDIAN and a zero-spread sample is precisely where it stops\n * saying anything about the mean the caller is thresholding.\n *\n * `threshold` may be negative — that is a noninferiority margin, and it is the\n * regime the zero-width hole is worst in. For a two-point (pass/fail) outcome\n * the percentile bootstrap is not a valid interval at a nonzero margin at all;\n * use {@link decidePairedPromotion}, which routes those to Tango's score\n * interval, rather than thresholding this function's bootstrap directly.\n */\nexport function pairedDeltaTest(\n before: number[],\n after: number[],\n options: PairedDeltaTestOptions = {},\n): PairedDeltaTestResult {\n const threshold = options.threshold ?? 0\n if (!Number.isFinite(threshold)) {\n throw new Error(`pairedDeltaTest: threshold must be finite, got ${threshold}`)\n }\n const confidence = options.confidence ?? 0.95\n const exactMinimum = minimumPairsForPairedDeltaTest(confidence)\n const requestedMinimum = options.minPairs ?? exactMinimum\n if (!Number.isInteger(requestedMinimum) || requestedMinimum < 1) {\n throw new Error(`pairedDeltaTest: minPairs must be a positive integer, got ${requestedMinimum}`)\n }\n const minimumPairs = Math.max(requestedMinimum, exactMinimum)\n const bootstrap = pairedBootstrap(before, after, options)\n const sufficient = bootstrap.n >= minimumPairs\n // A point-mass resample distribution is an absence of evidence, not a\n // certainty, and it is the shape that clears every negative threshold. Both\n // paths refuse it: see the \"zero-width\" section of this function's docs.\n const indeterminate =\n bootstrap.n > 0 &&\n (!Number.isFinite(bootstrap.low) ||\n !Number.isFinite(bootstrap.high) ||\n bootstrap.low === bootstrap.high)\n\n if (bootstrap.gateEligible) {\n return {\n bootstrap,\n method: 'bootstrap-ci',\n pValue: null,\n minimumPairs,\n sufficient,\n indeterminate,\n significant: sufficient && !indeterminate && bootstrap.low > threshold,\n }\n }\n\n const differences = before.map((value, index) => after[index]! - value - threshold)\n const exact = pairedSignTest(differences, 'greater')\n const estimate = options.statistic === 'mean' ? bootstrap.mean : bootstrap.median\n return {\n bootstrap,\n method: 'exact-sign',\n pValue: exact.pValue,\n minimumPairs,\n sufficient,\n indeterminate,\n significant:\n sufficient &&\n !indeterminate &&\n estimate > threshold &&\n exact.pValue <= (1 - bootstrap.confidence) / 2,\n }\n}\n","/**\n * @module\n * ONE rule for \"does this paired interval clear a promotion threshold\".\n *\n * The rule below was derived on `HeldOutGate` (#479) after the same estimator\n * bug shipped twice. It then turned out that a SECOND gate — the composable\n * `heldOutGate`, plus everything else routed through `heldoutSignificance` —\n * still carried the original defect, because the rule had been written into one\n * gate's method body rather than into a shared function. Two copies of a\n * statistical rule is how a defect survives in one of them, so there is now\n * exactly one copy and both gates call it.\n *\n * Three things the rule does that a bare `pairedBootstrap(...).low > threshold`\n * does not:\n *\n * 1. **Two-point (pass/fail) outcomes decide on Tango's SCORE interval.** On a\n * pass/fail eval the paired delta vector is dominated by ties, so the\n * bootstrap of the mean is a resample of a lattice with three atoms and its\n * percentile interval is not valid at a nonzero margin. The score interval\n * (`pairedRiskDifferenceScore`) re-estimates the nuisance loss rate under\n * each hypothesised margin instead of fixing it at the observed value, which\n * is the only construction that stays a confidence interval as the margin\n * moves off zero — the regime every noninferiority threshold lives in.\n * Measured on the composable gate before this change, at a true risk\n * difference sitting exactly on the production caller's -0.05 margin and a\n * nominal 5 %: 14.60 % false promotion at n = 40 and 10.10 % at n = 76.\n * 2. **McNemar's exact test holds a VETO at every non-negative threshold.**\n * Redundant with the interval by construction and kept anyway, so that\n * swapping the estimator for one without that duality cannot silently\n * reintroduce \"promotes what the exact test refuses\". Witness: n = 6, b = 5,\n * c = 0 — no exact argument reaches alpha = 0.05 with 5 discordant pairs\n * (two-sided floor 2/2^5 = 0.0625), whatever an interval says. A NEGATIVE\n * threshold is a noninferiority question, which McNemar's test of \"no\n * difference\" is not the right test for, so the veto does not apply there.\n * 3. **A ZERO-WIDTH interval is refused, wherever it sits.** At [0, 0] it\n * cannot tell a gain from a regression and clears every negative threshold.\n * Away from zero it fails the opposite way: n identical positive deltas give\n * [g, g], which clears threshold 0 on no spread at all. Both are an absence\n * of evidence. Measured on the composable gate before this change, under a\n * bounded asymmetric null whose true mean paired delta is exactly 0: 88.50 %\n * false promotion at n = 6 and 65.65 % at n = 20 against a nominal 5 %.\n *\n * Eligibility follows the requested target. A continuous mean needs the\n * bootstrap minimum; binary outcomes and explicit median targets use the\n * exact-test minimum. A small-sample sign diagnostic cannot certify a mean\n * effect. These floors establish estimator eligibility, not adequate power.\n */\n\nimport { minimumPairsForPairedDeltaTest, pairedDeltaTest } from './paired-delta-test'\nimport {\n BOOTSTRAP_GATE_MIN_N,\n type PairedBootstrapResult,\n pairedBinaryScale,\n pairedDeltaTieFraction,\n pairedRiskDifferenceExact,\n pairedRiskDifferenceScore,\n} from './statistics'\n\n/** Which paired estimator produced the deciding interval. */\nexport type PairedDecisionStatistic =\n | 'paired_risk_difference'\n | 'mean_bootstrap'\n | 'median_bootstrap'\n\n/** Which test carried the decision, given the estimator and the sample size. */\nexport type PairedDecisionMethod = 'score-interval' | 'bootstrap-ci' | 'exact-sign'\n\n/** McNemar's exact paired-binary evidence, on the two-point path only. */\nexport interface PairedMcNemarEvidence {\n /** Discordant pairs the treatment won. */\n b: number\n /** Discordant pairs the control won. */\n c: number\n /** b + c — the only pairs carrying information. */\n nDiscordant: number\n /** Two-sided exact p-value. */\n pValue: number\n}\n\nexport interface PairedPromotionDecisionOptions {\n /** Smallest candidate-minus-baseline delta that counts as improvement, in the\n * caller's native units. May be negative (a noninferiority margin). Default 0. */\n threshold?: number\n /** Confidence level. Default 0.95. */\n confidence?: number\n /** Bootstrap resamples, on the paths where a bootstrap decides. Default 2000. */\n resamples?: number\n /** Deterministic bootstrap seed. Omitted ⇒ derived from the deltas. */\n seed?: number\n /** Caller-required paired observations. The test also imposes its own\n * minimum: bootstrap eligibility for a continuous mean, or the exact-test\n * minimum for binary and explicit median targets. This is not a power guarantee. */\n minPairs?: number\n /**\n * `'mean'` (default) routes by SHAPE: a two-point (pass/fail) outcome on any\n * encoding decides on the score interval, everything else on the mean\n * bootstrap. `'median'` forces the median bootstrap on every input, including\n * shapes where it is structurally blind — kept for callers who want outlier\n * robustness on genuinely continuous outcomes and accept that cost.\n */\n statistic?: 'mean' | 'median'\n /** Declared binary support {0, binaryScale}, including when all observations\n * are zero. Must be finite and positive; every observation must use that\n * support. Uses the risk-difference mean, so cannot accompany 'median'.\n * Omitted: infer binary support from observed positive values. */\n binaryScale?: number\n /**\n * Declare the outcome continuous: never infer a two-point support from the\n * observed values, so the estimator is fixed before the data is seen. A\n * sealed call on a continuous measure must route to the bootstrap whatever\n * the sample happens to look like; with inference on, a sample whose values\n * all land on {0, s} is silently decided on the score interval instead.\n * Measured under discovery's E1 alternative (deltas in {+1, -0.5, 0} on a\n * zero baseline) at 20 pairs: 5.4 % of 2,000 simulated samples (seed 11)\n * re-route without this flag, 0 with it; the exact rate is 5.54 %\n * (`paired-promotion-power.test.ts`). Cannot accompany `binaryScale`.\n */\n continuous?: boolean\n}\n\nexport interface PairedPromotionDecision {\n /** Paired observations supplied. */\n n: number\n /** Threshold the interval was judged against, native units. */\n threshold: number\n confidence: number\n statistic: PairedDecisionStatistic\n method: PairedDecisionMethod\n /** Declared or inferred positive level ({0,1} ⇒ 1, {0,100} ⇒ 100),\n * or null when the outcome is not two-point. Non-null is exactly the\n * condition for the `paired_risk_difference` path, and it is the factor\n * `delta` / `low` / `high` were rescaled by. */\n binaryScale: number | null\n /** Exact-tie fraction over the paired deltas; null when there are no pairs. */\n tieFraction: number | null\n /** Point estimate of the DECIDING statistic, in the caller's native units. */\n delta: number\n /** Lower bound of the DECIDING interval, native units. */\n low: number\n /** Upper bound of the DECIDING interval, native units. */\n high: number\n /** The bootstrap that decided, or null when the score interval did. Callers\n * that need a bootstrap as a diagnostic on the two-point path compute their\n * own — it is not computed here, so the binary path costs no resamples. */\n bootstrap: PairedBootstrapResult | null\n /** McNemar's exact evidence, or null off the two-point path. */\n mcnemar: PairedMcNemarEvidence | null\n /** Exact one-sided sign-test p-value on the small-sample bootstrap path;\n * null otherwise. */\n pValue: number | null\n /** Effective observation minimum for the target, confidence, and caller floor. */\n minimumPairs: number\n /** n >= minimumPairs. */\n sufficient: boolean\n /** The deciding interval is zero-width at numeric precision or non-finite — no evidence in either\n * direction, so it cannot clear any threshold on evidence. */\n indeterminate: boolean\n /** McNemar's exact test refuses at a non-negative threshold. */\n exactTestVetoes: boolean\n /** The deciding interval clears the threshold, ignoring the other two guards. */\n clearsThreshold: boolean\n /** `sufficient && !indeterminate && clearsThreshold && !exactTestVetoes` —\n * the whole rule. */\n promote: boolean\n /** What `delta` measures, for a reason string. */\n label: 'success-rate' | 'mean' | 'median'\n /** Why a zero-width interval is zero-width; empty when it is not. */\n indeterminateCause: string\n /** Sentence naming the test when the exact sign test decided; else empty. */\n methodDetail: string\n}\n\n/** The shape facts that pick the estimator, without computing an interval. */\nexport interface PairedDecisionShape {\n statistic: PairedDecisionStatistic\n /** Declared or inferred positive binary level; null off the binary path. */\n binaryScale: number | null\n /** Exact-tie fraction over the paired deltas; null when there are no pairs. */\n tieFraction: number | null\n}\n\n/**\n * Which estimator {@link decidePairedPromotion} would use on this data, and the\n * shape facts behind it — for callers that must report the shape on a path\n * where no interval is computed at all (an early rejection, or zero pairs).\n * Cheap: no bootstrap, no interval.\n */\nexport function pairedDecisionShape(\n before: number[],\n after: number[],\n statistic: 'mean' | 'median' = 'mean',\n declaredBinaryScale?: number,\n continuous = false,\n): PairedDecisionShape {\n if (continuous && declaredBinaryScale !== undefined) {\n throw new Error('pairedDecisionShape: continuous cannot accompany binaryScale')\n }\n if (declaredBinaryScale !== undefined) {\n if (!Number.isFinite(declaredBinaryScale) || declaredBinaryScale <= 0) {\n throw new Error('pairedDecisionShape: binaryScale must be finite and positive')\n }\n if (statistic === 'median') {\n throw new Error('pairedDecisionShape: binaryScale requires the mean statistic, not median')\n }\n for (const [name, arm] of [\n ['before', before],\n ['after', after],\n ] as const) {\n for (let i = 0; i < arm.length; i++) {\n const value = arm[i]!\n if (value !== 0 && value !== declaredBinaryScale) {\n throw new Error(\n `pairedDecisionShape: ${name}[${i}] must be 0 or binaryScale (${declaredBinaryScale}); got ${value}`,\n )\n }\n }\n }\n }\n const tieFraction = before.length === 0 ? null : pairedDeltaTieFraction(before, after)\n if (statistic === 'median') {\n return { statistic: 'median_bootstrap', binaryScale: null, tieFraction }\n }\n if (continuous) {\n return { statistic: 'mean_bootstrap', binaryScale: null, tieFraction }\n }\n const binaryScale = declaredBinaryScale ?? pairedBinaryScale(before, after)\n if (binaryScale !== null) {\n return { statistic: 'paired_risk_difference', binaryScale, tieFraction }\n }\n return { statistic: 'mean_bootstrap', binaryScale: null, tieFraction }\n}\n\n/**\n * Decide whether a paired candidate-minus-baseline delta clears a promotion\n * threshold. `before` is the baseline arm, `after` the candidate arm, paired by\n * position. Throws on unequal lengths.\n */\nexport function decidePairedPromotion(\n before: number[],\n after: number[],\n options: PairedPromotionDecisionOptions = {},\n): PairedPromotionDecision {\n if (before.length !== after.length) {\n throw new Error(\n `decidePairedPromotion: unequal sample sizes (${before.length} vs ${after.length})`,\n )\n }\n const threshold = options.threshold ?? 0\n if (!Number.isFinite(threshold)) {\n throw new Error(`decidePairedPromotion: threshold must be finite, got ${threshold}`)\n }\n const confidence = options.confidence ?? 0.95\n const exactMinimum = minimumPairsForPairedDeltaTest(confidence)\n const requestedMinimum = options.minPairs ?? exactMinimum\n if (!Number.isInteger(requestedMinimum) || requestedMinimum < 1) {\n throw new Error(\n `decidePairedPromotion: minPairs must be a positive integer, got ${requestedMinimum}`,\n )\n }\n const { binaryScale, tieFraction } = pairedDecisionShape(\n before,\n after,\n options.statistic,\n options.binaryScale,\n options.continuous ?? false,\n )\n const estimatorMinimum =\n binaryScale === null && options.statistic !== 'median' ? BOOTSTRAP_GATE_MIN_N : exactMinimum\n const minimumPairs = Math.max(requestedMinimum, exactMinimum, estimatorMinimum)\n const n = before.length\n const sufficient = n >= minimumPairs\n\n let core: {\n statistic: PairedDecisionStatistic\n method: PairedDecisionMethod\n delta: number\n low: number\n high: number\n bootstrap: PairedBootstrapResult | null\n mcnemar: PairedMcNemarEvidence | null\n pValue: number | null\n clearsThreshold: boolean\n label: PairedPromotionDecision['label']\n methodDetail: string\n }\n\n if (binaryScale !== null) {\n // Normalise the two-point encoding to {0,1} so the estimators see the\n // pass/fail structure, then rescale the answer back into the caller's\n // native units — the threshold is read in the units of the scores, so a\n // 0-100 pass/fail dimension must be gated in points, not in a rate.\n const unitControl = before.map((v) => v / binaryScale)\n const unitTreatment = after.map((v) => v / binaryScale)\n // TWO estimators with different jobs, because no single one does both.\n // `exact` is the authority on RD = 0 and supplies the veto; its\n // Clopper-Pearson interval conditions on the discordant count and is\n // therefore NOT a confidence interval at a nonzero margin. `score` is\n // Tango's, which is.\n const exact = pairedRiskDifferenceExact(unitControl, unitTreatment, confidence)\n const score = pairedRiskDifferenceScore(unitControl, unitTreatment, confidence)\n const low = score.lower * binaryScale\n core = {\n statistic: 'paired_risk_difference',\n method: 'score-interval',\n delta: score.riskDifference * binaryScale,\n low,\n high: score.upper * binaryScale,\n bootstrap: null,\n mcnemar: {\n b: exact.b,\n c: exact.c,\n nDiscordant: exact.nDiscordant,\n pValue: exact.pValue,\n },\n pValue: null,\n clearsThreshold: low > threshold,\n label: 'success-rate',\n methodDetail: '',\n }\n } else {\n const bootstrapStatistic = options.statistic === 'median' ? 'median' : 'mean'\n const test = pairedDeltaTest(before, after, {\n confidence,\n resamples: options.resamples,\n statistic: bootstrapStatistic,\n seed: options.seed,\n threshold,\n minPairs: minimumPairs,\n })\n const ci = test.bootstrap\n core = {\n statistic: bootstrapStatistic === 'mean' ? 'mean_bootstrap' : 'median_bootstrap',\n method: test.method,\n delta: bootstrapStatistic === 'mean' ? ci.mean : ci.median,\n low: ci.low,\n high: ci.high,\n bootstrap: ci,\n mcnemar: null,\n pValue: test.pValue,\n clearsThreshold: test.significant,\n label: bootstrapStatistic,\n methodDetail:\n test.method === 'exact-sign'\n ? bootstrapStatistic === 'mean'\n ? ` The mean requires ${minimumPairs} pairs for bootstrap eligibility;` +\n ` the exact sign-test p=${fmt(test.pValue ?? 1)} does not establish a mean effect.`\n : ` Below ${BOOTSTRAP_GATE_MIN_N} pairs the interval is descriptive only;` +\n ` the median decision uses the exact one-sided sign test, p=${fmt(test.pValue ?? 1)}.`\n : '',\n }\n }\n\n // Equivalent deltas can differ by rounding after subtraction and averaging.\n // Use the interval's own scale so tiny score units retain the same decision\n // as a rescaled copy of the evidence.\n const intervalScale = Math.max(Math.abs(core.low), Math.abs(core.high))\n const intervalTolerance = intervalScale * Number.EPSILON * 8\n const indeterminate =\n !Number.isFinite(core.low) ||\n !Number.isFinite(core.high) ||\n core.high - core.low <= intervalTolerance\n const indeterminateCause = !indeterminate\n ? ''\n : tieFraction === 1\n ? 'every paired delta is an exact tie'\n : core.mcnemar !== null && core.mcnemar.nDiscordant === 0\n ? 'every pair is concordant (0 discordant pairs)'\n : `the ${core.label} CI collapsed to a point at ${fmt(core.low)}`\n // Only at a non-negative threshold: a negative threshold asks a\n // noninferiority question, which McNemar's test of \"no difference\" does not\n // answer.\n const exactTestVetoes =\n core.mcnemar !== null && threshold >= 0 && !(core.mcnemar.pValue < 1 - confidence)\n\n return {\n n,\n threshold,\n confidence,\n binaryScale,\n tieFraction,\n minimumPairs,\n sufficient,\n indeterminate,\n indeterminateCause,\n exactTestVetoes,\n promote: sufficient && !indeterminate && core.clearsThreshold && !exactTestVetoes,\n ...core,\n }\n}\n\nfunction fmt(x: number): string {\n return x.toFixed(4)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"paired-promotion-decision-DhrmjdV8.js","names":[],"sources":["../src/paired-delta-test.ts","../src/paired-promotion-decision.ts"],"sourcesContent":["import {\n type PairedBootstrapOptions,\n type PairedBootstrapResult,\n pairedBootstrap,\n pairedSignTest,\n} from './statistics'\n\nexport interface PairedDeltaTestOptions extends PairedBootstrapOptions {\n /** Smallest candidate-minus-baseline delta that counts as improvement. Default 0. */\n threshold?: number\n /** Caller-required paired observations. The exact test may impose a higher minimum. */\n minPairs?: number\n}\n\nexport interface PairedDeltaTestResult {\n bootstrap: PairedBootstrapResult\n method: 'bootstrap-ci' | 'exact-sign'\n /** Exact one-sided p-value below the bootstrap minimum; otherwise null. */\n pValue: number | null\n /** Effective observation minimum after accounting for confidence. */\n minimumPairs: number\n sufficient: boolean\n /**\n * The bootstrap interval has zero width (or is non-finite), so it carries no\n * information about how far the estimate could be wrong and cannot support a\n * decision in either direction. See {@link pairedDeltaTest}.\n */\n indeterminate: boolean\n significant: boolean\n}\n\n/** Smallest all-positive sample that can clear a one-sided exact sign test. */\nexport function minimumPairsForPairedDeltaTest(confidence = 0.95): number {\n if (!Number.isFinite(confidence) || confidence <= 0 || confidence >= 1) {\n throw new Error(\n `minimumPairsForPairedDeltaTest: confidence must be in (0,1), got ${confidence}`,\n )\n }\n const oneSidedAlpha = (1 - confidence) / 2\n return Math.ceil(Math.log2(1 / oneSidedAlpha))\n}\n\n/**\n * Tests whether a paired candidate-minus-baseline delta clears a threshold.\n *\n * At 20 or more pairs, the percentile bootstrap lower bound carries the\n * decision. Below that point the interval is descriptive only, so the function\n * switches to a pre-registered one-sided exact sign test. The exact path is\n * deliberately conservative: it requires both a point estimate above the\n * threshold and enough consistently positive paired differences.\n *\n * ## A zero-width interval is never significant\n *\n * When every paired delta is identical the resample distribution is a point\n * mass and the interval collapses: `[0, 0]` when all pairs tie, `[g, g]` on n\n * identical deltas of g. Neither says the effect is certain — both say the\n * sample carries no information about how far the estimate could be wrong, and\n * `low > threshold` then answers on the point estimate alone. It fails in both\n * directions: `[0, 0]` clears every NEGATIVE threshold, which is how a\n * tie-dominated pass/fail comparison laundered a regression into a\n * noninferiority pass, and `[g, g]` clears every threshold below g with no\n * spread behind it. Under a bounded asymmetric null whose true mean paired\n * delta is exactly 0 — 2 % of pairs dropping by 1.0, the rest gaining 0.0204 —\n * every sample that misses the drop is exactly that shape, and deciding on\n * `low > 0` promoted 65.65 % of samples at n = 20 against a nominal 5 %.\n *\n * So `indeterminate` is reported and `significant` is false whenever the\n * interval has zero width, on BOTH paths: at small n the exact sign test is a\n * test of the MEDIAN and a zero-spread sample is precisely where it stops\n * saying anything about the mean the caller is thresholding.\n *\n * `threshold` may be negative — that is a noninferiority margin, and it is the\n * regime the zero-width hole is worst in. For a two-point (pass/fail) outcome\n * the percentile bootstrap is not a valid interval at a nonzero margin at all;\n * use {@link decidePairedPromotion}, which routes those to Tango's score\n * interval, rather than thresholding this function's bootstrap directly.\n */\nexport function pairedDeltaTest(\n before: number[],\n after: number[],\n options: PairedDeltaTestOptions = {},\n): PairedDeltaTestResult {\n const threshold = options.threshold ?? 0\n if (!Number.isFinite(threshold)) {\n throw new Error(`pairedDeltaTest: threshold must be finite, got ${threshold}`)\n }\n const confidence = options.confidence ?? 0.95\n const exactMinimum = minimumPairsForPairedDeltaTest(confidence)\n const requestedMinimum = options.minPairs ?? exactMinimum\n if (!Number.isInteger(requestedMinimum) || requestedMinimum < 1) {\n throw new Error(`pairedDeltaTest: minPairs must be a positive integer, got ${requestedMinimum}`)\n }\n const minimumPairs = Math.max(requestedMinimum, exactMinimum)\n const bootstrap = pairedBootstrap(before, after, options)\n const sufficient = bootstrap.n >= minimumPairs\n // A point-mass resample distribution is an absence of evidence, not a\n // certainty, and it is the shape that clears every negative threshold. Both\n // paths refuse it: see the \"zero-width\" section of this function's docs.\n const indeterminate =\n bootstrap.n > 0 &&\n (!Number.isFinite(bootstrap.low) ||\n !Number.isFinite(bootstrap.high) ||\n bootstrap.low === bootstrap.high)\n\n if (bootstrap.gateEligible) {\n return {\n bootstrap,\n method: 'bootstrap-ci',\n pValue: null,\n minimumPairs,\n sufficient,\n indeterminate,\n significant: sufficient && !indeterminate && bootstrap.low > threshold,\n }\n }\n\n const differences = before.map((value, index) => after[index]! - value - threshold)\n const exact = pairedSignTest(differences, 'greater')\n const estimate = options.statistic === 'mean' ? bootstrap.mean : bootstrap.median\n return {\n bootstrap,\n method: 'exact-sign',\n pValue: exact.pValue,\n minimumPairs,\n sufficient,\n indeterminate,\n significant:\n sufficient &&\n !indeterminate &&\n estimate > threshold &&\n exact.pValue <= (1 - bootstrap.confidence) / 2,\n }\n}\n","/**\n * @module\n * ONE rule for \"does this paired interval clear a promotion threshold\".\n *\n * The rule below was derived on `HeldOutGate` (#479) after the same estimator\n * bug shipped twice. It then turned out that a SECOND gate — the composable\n * `heldOutGate`, plus everything else routed through `heldoutSignificance` —\n * still carried the original defect, because the rule had been written into one\n * gate's method body rather than into a shared function. Two copies of a\n * statistical rule is how a defect survives in one of them, so there is now\n * exactly one copy and both gates call it.\n *\n * Three things the rule does that a bare `pairedBootstrap(...).low > threshold`\n * does not:\n *\n * 1. **Two-point (pass/fail) outcomes decide on Tango's SCORE interval.** On a\n * pass/fail eval the paired delta vector is dominated by ties, so the\n * bootstrap of the mean is a resample of a lattice with three atoms and its\n * percentile interval is not valid at a nonzero margin. The score interval\n * (`pairedRiskDifferenceScore`) re-estimates the nuisance loss rate under\n * each hypothesised margin instead of fixing it at the observed value, which\n * is the only construction that stays a confidence interval as the margin\n * moves off zero — the regime every noninferiority threshold lives in.\n * Measured on the composable gate before this change, at a true risk\n * difference sitting exactly on the production caller's -0.05 margin and a\n * nominal 5 %: 14.60 % false promotion at n = 40 and 10.10 % at n = 76.\n * 2. **McNemar's exact test holds a VETO at every non-negative threshold.**\n * Redundant with the interval by construction and kept anyway, so that\n * swapping the estimator for one without that duality cannot silently\n * reintroduce \"promotes what the exact test refuses\". Witness: n = 6, b = 5,\n * c = 0 — no exact argument reaches alpha = 0.05 with 5 discordant pairs\n * (two-sided floor 2/2^5 = 0.0625), whatever an interval says. A NEGATIVE\n * threshold is a noninferiority question, which McNemar's test of \"no\n * difference\" is not the right test for, so the veto does not apply there.\n * 3. **A ZERO-WIDTH interval is refused, wherever it sits.** At [0, 0] it\n * cannot tell a gain from a regression and clears every negative threshold.\n * Away from zero it fails the opposite way: n identical positive deltas give\n * [g, g], which clears threshold 0 on no spread at all. Both are an absence\n * of evidence. Measured on the composable gate before this change, under a\n * bounded asymmetric null whose true mean paired delta is exactly 0: 88.50 %\n * false promotion at n = 6 and 65.65 % at n = 20 against a nominal 5 %.\n *\n * Eligibility follows the requested target. A continuous mean needs the\n * bootstrap minimum; binary outcomes and explicit median targets use the\n * exact-test minimum. A small-sample sign diagnostic cannot certify a mean\n * effect. These floors establish estimator eligibility, not adequate power.\n */\n\nimport { minimumPairsForPairedDeltaTest, pairedDeltaTest } from './paired-delta-test'\nimport {\n BOOTSTRAP_GATE_MIN_N,\n type PairedBootstrapResult,\n pairedBinaryScale,\n pairedDeltaTieFraction,\n pairedRiskDifferenceExact,\n pairedRiskDifferenceScore,\n} from './statistics'\n\n/** Which paired estimator produced the deciding interval. */\nexport type PairedDecisionStatistic =\n | 'paired_risk_difference'\n | 'mean_bootstrap'\n | 'median_bootstrap'\n\n/** Which test carried the decision, given the estimator and the sample size. */\nexport type PairedDecisionMethod = 'score-interval' | 'bootstrap-ci' | 'exact-sign'\n\n/** McNemar's exact paired-binary evidence, on the two-point path only. */\nexport interface PairedMcNemarEvidence {\n /** Discordant pairs the treatment won. */\n b: number\n /** Discordant pairs the control won. */\n c: number\n /** b + c — the only pairs carrying information. */\n nDiscordant: number\n /** Two-sided exact p-value. */\n pValue: number\n}\n\nexport interface PairedPromotionDecisionOptions {\n /** Smallest candidate-minus-baseline delta that counts as improvement, in the\n * caller's native units. May be negative (a noninferiority margin). Default 0. */\n threshold?: number\n /** Confidence level. Default 0.95. */\n confidence?: number\n /** Bootstrap resamples, on the paths where a bootstrap decides. Default 2000. */\n resamples?: number\n /** Deterministic bootstrap seed. Omitted ⇒ derived from the deltas. */\n seed?: number\n /** Caller-required paired observations. The test also imposes its own\n * minimum: bootstrap eligibility for a continuous mean, or the exact-test\n * minimum for binary and explicit median targets. This is not a power guarantee. */\n minPairs?: number\n /**\n * `'mean'` (default) routes by SHAPE: a two-point (pass/fail) outcome on any\n * encoding decides on the score interval, everything else on the mean\n * bootstrap. `'median'` forces the median bootstrap on every input, including\n * shapes where it is structurally blind — kept for callers who want outlier\n * robustness on genuinely continuous outcomes and accept that cost.\n */\n statistic?: 'mean' | 'median'\n /** Declared binary support {0, binaryScale}, including when all observations\n * are zero. Must be finite and positive; every observation must use that\n * support. Uses the risk-difference mean, so cannot accompany 'median'.\n * Omitted: infer binary support from observed positive values. */\n binaryScale?: number\n /**\n * Declare the outcome continuous: never infer a two-point support from the\n * observed values, so the estimator is fixed before the data is seen. A\n * sealed call on a continuous measure must route to the bootstrap whatever\n * the sample happens to look like; with inference on, a sample whose values\n * all land on {0, s} is silently decided on the score interval instead.\n * Measured under discovery's E1 alternative (deltas in {+1, -0.5, 0} on a\n * zero baseline) at 20 pairs: 5.4 % of 2,000 simulated samples (seed 11)\n * re-route without this flag, 0 with it; the exact rate is 5.54 %\n * (`paired-promotion-power.test.ts`). Cannot accompany `binaryScale`.\n */\n continuous?: boolean\n}\n\nexport interface PairedPromotionDecision {\n /** Paired observations supplied. */\n n: number\n /** Threshold the interval was judged against, native units. */\n threshold: number\n confidence: number\n statistic: PairedDecisionStatistic\n method: PairedDecisionMethod\n /** Declared or inferred positive level ({0,1} ⇒ 1, {0,100} ⇒ 100),\n * or null when the outcome is not two-point. Non-null is exactly the\n * condition for the `paired_risk_difference` path, and it is the factor\n * `delta` / `low` / `high` were rescaled by. */\n binaryScale: number | null\n /** Exact-tie fraction over the paired deltas; null when there are no pairs. */\n tieFraction: number | null\n /** Point estimate of the DECIDING statistic, in the caller's native units. */\n delta: number\n /** Lower bound of the DECIDING interval, native units. */\n low: number\n /** Upper bound of the DECIDING interval, native units. */\n high: number\n /** The bootstrap that decided, or null when the score interval did. Callers\n * that need a bootstrap as a diagnostic on the two-point path compute their\n * own — it is not computed here, so the binary path costs no resamples. */\n bootstrap: PairedBootstrapResult | null\n /** McNemar's exact evidence, or null off the two-point path. */\n mcnemar: PairedMcNemarEvidence | null\n /** Exact one-sided sign-test p-value on the small-sample bootstrap path;\n * null otherwise. */\n pValue: number | null\n /** Effective observation minimum for the target, confidence, and caller floor. */\n minimumPairs: number\n /** n >= minimumPairs. */\n sufficient: boolean\n /** The deciding interval is zero-width at numeric precision or non-finite — no evidence in either\n * direction, so it cannot clear any threshold on evidence. */\n indeterminate: boolean\n /** McNemar's exact test refuses at a non-negative threshold. */\n exactTestVetoes: boolean\n /** The deciding interval clears the threshold, ignoring the other two guards. */\n clearsThreshold: boolean\n /** `sufficient && !indeterminate && clearsThreshold && !exactTestVetoes` —\n * the whole rule. */\n promote: boolean\n /** What `delta` measures, for a reason string. */\n label: 'success-rate' | 'mean' | 'median'\n /** Why a zero-width interval is zero-width; empty when it is not. */\n indeterminateCause: string\n /** Sentence naming the test when the exact sign test decided; else empty. */\n methodDetail: string\n}\n\n/** The shape facts that pick the estimator, without computing an interval. */\nexport interface PairedDecisionShape {\n statistic: PairedDecisionStatistic\n /** Declared or inferred positive binary level; null off the binary path. */\n binaryScale: number | null\n /** Exact-tie fraction over the paired deltas; null when there are no pairs. */\n tieFraction: number | null\n}\n\n/**\n * Which estimator {@link decidePairedPromotion} would use on this data, and the\n * shape facts behind it — for callers that must report the shape on a path\n * where no interval is computed at all (an early rejection, or zero pairs).\n * Cheap: no bootstrap, no interval.\n */\nexport function pairedDecisionShape(\n before: number[],\n after: number[],\n statistic: 'mean' | 'median' = 'mean',\n declaredBinaryScale?: number,\n continuous = false,\n): PairedDecisionShape {\n if (continuous && declaredBinaryScale !== undefined) {\n throw new Error('pairedDecisionShape: continuous cannot accompany binaryScale')\n }\n if (declaredBinaryScale !== undefined) {\n if (!Number.isFinite(declaredBinaryScale) || declaredBinaryScale <= 0) {\n throw new Error('pairedDecisionShape: binaryScale must be finite and positive')\n }\n if (statistic === 'median') {\n throw new Error('pairedDecisionShape: binaryScale requires the mean statistic, not median')\n }\n for (const [name, arm] of [\n ['before', before],\n ['after', after],\n ] as const) {\n for (let i = 0; i < arm.length; i++) {\n const value = arm[i]!\n if (value !== 0 && value !== declaredBinaryScale) {\n throw new Error(\n `pairedDecisionShape: ${name}[${i}] must be 0 or binaryScale (${declaredBinaryScale}); got ${value}`,\n )\n }\n }\n }\n }\n const tieFraction = before.length === 0 ? null : pairedDeltaTieFraction(before, after)\n if (statistic === 'median') {\n return { statistic: 'median_bootstrap', binaryScale: null, tieFraction }\n }\n if (continuous) {\n return { statistic: 'mean_bootstrap', binaryScale: null, tieFraction }\n }\n const binaryScale = declaredBinaryScale ?? pairedBinaryScale(before, after)\n if (binaryScale !== null) {\n return { statistic: 'paired_risk_difference', binaryScale, tieFraction }\n }\n return { statistic: 'mean_bootstrap', binaryScale: null, tieFraction }\n}\n\n/**\n * Decide whether a paired candidate-minus-baseline delta clears a promotion\n * threshold. `before` is the baseline arm, `after` the candidate arm, paired by\n * position. Throws on unequal lengths.\n */\nexport function decidePairedPromotion(\n before: number[],\n after: number[],\n options: PairedPromotionDecisionOptions = {},\n): PairedPromotionDecision {\n if (before.length !== after.length) {\n throw new Error(\n `decidePairedPromotion: unequal sample sizes (${before.length} vs ${after.length})`,\n )\n }\n const threshold = options.threshold ?? 0\n if (!Number.isFinite(threshold)) {\n throw new Error(`decidePairedPromotion: threshold must be finite, got ${threshold}`)\n }\n const confidence = options.confidence ?? 0.95\n const exactMinimum = minimumPairsForPairedDeltaTest(confidence)\n const requestedMinimum = options.minPairs ?? exactMinimum\n if (!Number.isInteger(requestedMinimum) || requestedMinimum < 1) {\n throw new Error(\n `decidePairedPromotion: minPairs must be a positive integer, got ${requestedMinimum}`,\n )\n }\n const { binaryScale, tieFraction } = pairedDecisionShape(\n before,\n after,\n options.statistic,\n options.binaryScale,\n options.continuous ?? false,\n )\n const estimatorMinimum =\n binaryScale === null && options.statistic !== 'median' ? BOOTSTRAP_GATE_MIN_N : exactMinimum\n const minimumPairs = Math.max(requestedMinimum, exactMinimum, estimatorMinimum)\n const n = before.length\n const sufficient = n >= minimumPairs\n\n let core: {\n statistic: PairedDecisionStatistic\n method: PairedDecisionMethod\n delta: number\n low: number\n high: number\n bootstrap: PairedBootstrapResult | null\n mcnemar: PairedMcNemarEvidence | null\n pValue: number | null\n clearsThreshold: boolean\n label: PairedPromotionDecision['label']\n methodDetail: string\n }\n\n if (binaryScale !== null) {\n // Normalise the two-point encoding to {0,1} so the estimators see the\n // pass/fail structure, then rescale the answer back into the caller's\n // native units — the threshold is read in the units of the scores, so a\n // 0-100 pass/fail dimension must be gated in points, not in a rate.\n const unitControl = before.map((v) => v / binaryScale)\n const unitTreatment = after.map((v) => v / binaryScale)\n // TWO estimators with different jobs, because no single one does both.\n // `exact` is the authority on RD = 0 and supplies the veto; its\n // Clopper-Pearson interval conditions on the discordant count and is\n // therefore NOT a confidence interval at a nonzero margin. `score` is\n // Tango's, which is.\n const exact = pairedRiskDifferenceExact(unitControl, unitTreatment, confidence)\n const score = pairedRiskDifferenceScore(unitControl, unitTreatment, confidence)\n const low = score.lower * binaryScale\n core = {\n statistic: 'paired_risk_difference',\n method: 'score-interval',\n delta: score.riskDifference * binaryScale,\n low,\n high: score.upper * binaryScale,\n bootstrap: null,\n mcnemar: {\n b: exact.b,\n c: exact.c,\n nDiscordant: exact.nDiscordant,\n pValue: exact.pValue,\n },\n pValue: null,\n clearsThreshold: low > threshold,\n label: 'success-rate',\n methodDetail: '',\n }\n } else {\n const bootstrapStatistic = options.statistic === 'median' ? 'median' : 'mean'\n const test = pairedDeltaTest(before, after, {\n confidence,\n resamples: options.resamples,\n statistic: bootstrapStatistic,\n seed: options.seed,\n threshold,\n minPairs: minimumPairs,\n })\n const ci = test.bootstrap\n core = {\n statistic: bootstrapStatistic === 'mean' ? 'mean_bootstrap' : 'median_bootstrap',\n method: test.method,\n delta: bootstrapStatistic === 'mean' ? ci.mean : ci.median,\n low: ci.low,\n high: ci.high,\n bootstrap: ci,\n mcnemar: null,\n pValue: test.pValue,\n clearsThreshold: test.significant,\n label: bootstrapStatistic,\n methodDetail:\n test.method === 'exact-sign'\n ? bootstrapStatistic === 'mean'\n ? ` The mean requires ${minimumPairs} pairs for bootstrap eligibility;` +\n ` the exact sign-test p=${fmt(test.pValue ?? 1)} does not establish a mean effect.`\n : ` Below ${BOOTSTRAP_GATE_MIN_N} pairs the interval is descriptive only;` +\n ` the median decision uses the exact one-sided sign test, p=${fmt(test.pValue ?? 1)}.`\n : '',\n }\n }\n\n // Equivalent deltas can differ by rounding after subtraction and averaging.\n // Use the interval's own scale so tiny score units retain the same decision\n // as a rescaled copy of the evidence.\n const intervalScale = Math.max(Math.abs(core.low), Math.abs(core.high))\n const intervalTolerance = intervalScale * Number.EPSILON * 8\n const indeterminate =\n !Number.isFinite(core.low) ||\n !Number.isFinite(core.high) ||\n core.high - core.low <= intervalTolerance\n const indeterminateCause = !indeterminate\n ? ''\n : tieFraction === 1\n ? 'every paired delta is an exact tie'\n : core.mcnemar !== null && core.mcnemar.nDiscordant === 0\n ? 'every pair is concordant (0 discordant pairs)'\n : `the ${core.label} CI collapsed to a point at ${fmt(core.low)}`\n // Only at a non-negative threshold: a negative threshold asks a\n // noninferiority question, which McNemar's test of \"no difference\" does not\n // answer.\n const exactTestVetoes =\n core.mcnemar !== null && threshold >= 0 && !(core.mcnemar.pValue < 1 - confidence)\n\n return {\n n,\n threshold,\n confidence,\n binaryScale,\n tieFraction,\n minimumPairs,\n sufficient,\n indeterminate,\n indeterminateCause,\n exactTestVetoes,\n promote: sufficient && !indeterminate && core.clearsThreshold && !exactTestVetoes,\n ...core,\n }\n}\n\nfunction fmt(x: number): string {\n return x.toFixed(4)\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { s as ValidationError } from "./errors-Dngq5h35.js";
2
- import { a as medianInPlace, i as makeRng, n as binomialHalfUpperTail, t as assertFiniteSample } from "./internal-BMFSR8Ns.js";
3
- import { t as studentTCdf } from "./student-t-BA-Uy51p.js";
2
+ import { a as medianInPlace, i as makeRng, n as binomialHalfUpperTail, t as assertFiniteSample } from "./internal-BOe3zmjQ.js";
3
+ import { t as studentTCdf } from "./student-t-BBZYka0E.js";
4
4
  //#region src/statistics/paired-tests.ts
5
5
  /**
6
6
  * Paired significance tests on continuous scores: the paired t-test, the
@@ -210,4 +210,4 @@ const DECISION_PAIRED_DELTA_STATISTIC = "mean";
210
210
  //#endregion
211
211
  export { pairedSignTest as a, pairedDeltaTieFraction as i, DECISION_PAIRED_DELTA_STATISTIC as n, pairedTTest as o, pairedBootstrap as r, BOOTSTRAP_GATE_MIN_N as t };
212
212
 
213
- //# sourceMappingURL=paired-tests-C8iCsioC.js.map
213
+ //# sourceMappingURL=paired-tests-Ccr16gcW.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"paired-tests-C8iCsioC.js","names":[],"sources":["../src/statistics/paired-tests.ts"],"sourcesContent":["/**\n * Paired significance tests on continuous scores: the paired t-test, the\n * promotion-gate paired bootstrap, the exact sign test, and the package-wide\n * paired-delta decision statistic.\n */\n\nimport { ValidationError } from '../errors'\nimport { studentTCdf } from '../math/student-t'\nimport { assertFiniteSample, binomialHalfUpperTail, makeRng, medianInPlace } from './internal'\n\nexport interface PairedTTestResult {\n /** Null when the statistic is undefined — see {@link pairedTTest}. */\n t: number | null\n df: number\n /** Null exactly when `t` is null. */\n p: number | null\n}\n\n/**\n * Paired t-test — before/after measurements on the SAME items.\n * Pairing removes inter-item variance, giving tighter significance than\n * an unpaired test when comparing prompt v1 vs prompt v2 on identical\n * scenarios.\n *\n * Returns `t = p = null` where the statistic is undefined: fewer than two\n * pairs, or a non-zero constant delta whose observed variance is zero. A\n * constant shift carries no information about the variance it would have to\n * be compared against, so the honest answer is \"undefined\", not `p = 0` —\n * three observations cannot buy absolute certainty. This is the same contract\n * {@link pairedCohensDz} states for the same condition. An all-zero delta is\n * different: it is a measured null, and returns `t = 0, p = 1`.\n */\nexport function pairedTTest(before: number[], after: number[]): PairedTTestResult {\n if (before.length !== after.length) {\n throw new ValidationError(\n `pairedTTest: unequal sample sizes (${before.length} vs ${after.length})`,\n )\n }\n assertFiniteSample('pairedTTest', 'before', before)\n assertFiniteSample('pairedTTest', 'after', after)\n const n = before.length\n if (n < 2) return { t: null, df: 0, p: null }\n\n const diffs = before.map((b, i) => after[i]! - b)\n const mean = diffs.reduce((a, b) => a + b, 0) / n\n const variance = diffs.reduce((acc, d) => acc + (d - mean) ** 2, 0) / (n - 1)\n const se = Math.sqrt(variance / n)\n if (se === 0) {\n return mean === 0 ? { t: 0, df: n - 1, p: 1 } : { t: null, df: n - 1, p: null }\n }\n\n const t = mean / se\n const df = n - 1\n const p = 2 * (1 - studentTCdf(Math.abs(t), df))\n return { t, df, p }\n}\n\n// ── Paired bootstrap (promotion-gate effect size) ────────────────────\n\nexport interface PairedBootstrapResult {\n /** Number of paired observations. */\n n: number\n /** Median of paired deltas (after − before). */\n median: number\n /** Mean of paired deltas. */\n mean: number\n /** Lower bound of the bootstrap CI on the chosen statistic. */\n low: number\n /** Upper bound of the bootstrap CI on the chosen statistic. */\n high: number\n /** Confidence level used (e.g. 0.95). */\n confidence: number\n /** Number of bootstrap resamples used. */\n resamples: number\n /** False below {@link BOOTSTRAP_GATE_MIN_N}. See {@link pairedBootstrap}. */\n gateEligible: boolean\n}\n\n/**\n * Pairs below which a percentile bootstrap interval is descriptive spread only.\n *\n * `P(low > 0)` under a true null, against a nominal 2.5 %, measured over 4000\n * seeded trials: 13.53 % at n = 3, 3.52 % at n = 10, 3.10 % at n = 20 on the\n * median; 13.85 %, 4.90 %, 3.80 % on the mean. This is intrinsic to resampling\n * three points, not an implementation error — scipy's BCa gives 16.0 % on the\n * same n = 3 data — so no change to the estimator moves it. Below this floor\n * the decision belongs to the exact sign test or exact signed-rank test.\n */\nexport const BOOTSTRAP_GATE_MIN_N = 20\n\nexport interface PairedBootstrapOptions {\n /** Confidence level. Default 0.95. */\n confidence?: number\n /** Bootstrap resample count. Default 2000. */\n resamples?: number\n /** Statistic to bootstrap. Default 'median'. */\n statistic?: 'median' | 'mean'\n /** Deterministic seed. If omitted, derived from the deltas so the interval\n * is reproducible regardless. */\n seed?: number\n}\n\n/**\n * Paired bootstrap on (after − before) deltas. Returns a CI on the chosen\n * statistic (median by default); pairs are resampled with replacement. Throws\n * on unequal sample sizes.\n *\n * `low > threshold` carries the stated confidence ONLY at `n ≥\n * {@link BOOTSTRAP_GATE_MIN_N}`, which `gateEligible` reports. Below it the\n * check fires under a true null several times more often than nominal, so the\n * interval is descriptive spread and a promotion must not turn on it.\n */\nexport function pairedBootstrap(\n before: number[],\n after: number[],\n opts: PairedBootstrapOptions = {},\n): PairedBootstrapResult {\n if (before.length !== after.length) {\n throw new Error(`pairedBootstrap: unequal sample sizes (${before.length} vs ${after.length})`)\n }\n const confidence = opts.confidence ?? 0.95\n const resamples = opts.resamples ?? 2000\n const statistic = opts.statistic ?? 'median'\n if (confidence <= 0 || confidence >= 1) {\n throw new Error(`pairedBootstrap: confidence must be in (0,1), got ${confidence}`)\n }\n\n const n = before.length\n const deltas = before.map((b, i) => after[i]! - b)\n const gateEligible = n >= BOOTSTRAP_GATE_MIN_N\n if (n === 0) {\n return { n: 0, median: 0, mean: 0, low: 0, high: 0, confidence, resamples, gateEligible }\n }\n if (n === 1) {\n const d = deltas[0]!\n return { n: 1, median: d, mean: d, low: d, high: d, confidence, resamples, gateEligible }\n }\n\n const rng = makeRng(opts.seed, deltas)\n const samples = new Array<number>(resamples)\n for (let b = 0; b < resamples; b++) {\n if (statistic === 'mean') {\n let sum = 0\n for (let k = 0; k < n; k++) {\n sum += deltas[Math.floor(rng() * n)]!\n }\n samples[b] = sum / n\n } else {\n const acc = new Array<number>(n)\n for (let k = 0; k < n; k++) {\n acc[k] = deltas[Math.floor(rng() * n)]!\n }\n samples[b] = medianInPlace(acc)\n }\n }\n samples.sort((a, b) => a - b)\n\n const alpha = 1 - confidence\n const lowIdx = Math.floor((alpha / 2) * resamples)\n const highIdx = Math.min(resamples - 1, Math.ceil((1 - alpha / 2) * resamples) - 1)\n\n return {\n n,\n median: medianInPlace([...deltas]),\n mean: deltas.reduce((s, x) => s + x, 0) / n,\n low: samples[lowIdx]!,\n high: samples[Math.max(highIdx, lowIdx)]!,\n confidence,\n resamples,\n gateEligible,\n }\n}\n\n/** Pre-registered direction for a one-sided paired sign test. */\nexport type SignTestAlternative = 'greater' | 'less'\n\n/** Exact one-sided sign-test result for paired numeric differences. */\nexport interface PairedSignTestResult {\n /** Total supplied differences, including zero ties. */\n n: number\n /** Strictly positive differences. */\n positive: number\n /** Strictly negative differences. */\n negative: number\n /** Zero differences excluded from the binomial test. */\n ties: number\n /** Non-zero differences used by the binomial test. */\n nNonTies: number\n /** Direction of the pre-registered alternative hypothesis. */\n alternative: SignTestAlternative\n /** Exact one-sided p-value under P(positive) = P(negative) = 0.5. */\n pValue: number\n}\n\n/**\n * Exact one-sided sign test over paired differences.\n *\n * Pass `after[i] - before[i]` for each matched item. `alternative = 'greater'`\n * tests whether positive signs are more likely than negative signs and returns\n * `P(Binomial(nNonTies, 0.5) >= positive)`. `alternative = 'less'` treats\n * negative signs as successes instead. With a continuous difference\n * distribution this is the usual directional median test. Exact zero\n * differences are ties and do not enter the binomial denominator. All-tie and\n * empty inputs return p = 1. Every input difference must be finite, and the\n * direction must be chosen explicitly so a caller cannot select it after\n * seeing the signs.\n */\nexport function pairedSignTest(\n differences: readonly number[],\n alternative: SignTestAlternative,\n): PairedSignTestResult {\n if (alternative !== 'greater' && alternative !== 'less') {\n throw new ValidationError(\n `pairedSignTest: alternative must be 'greater' or 'less', got ${alternative}`,\n )\n }\n\n let positive = 0\n let negative = 0\n let ties = 0\n for (let i = 0; i < differences.length; i++) {\n const difference = differences[i]!\n if (!Number.isFinite(difference)) {\n throw new ValidationError(\n `pairedSignTest: difference at index ${i} must be finite, got ${difference}`,\n )\n }\n if (difference > 0) positive++\n else if (difference < 0) negative++\n else ties++\n }\n\n const nNonTies = positive + negative\n const successes = alternative === 'greater' ? positive : negative\n return {\n n: differences.length,\n positive,\n negative,\n ties,\n nNonTies,\n alternative,\n pValue: binomialHalfUpperTail(successes, nNonTies),\n }\n}\n\n/** Fraction of paired observations whose delta is an exact tie (|after − before|\n * < 1e-9). Throws on unequal sample sizes; 0 pairs ⇒ 0. */\nexport function pairedDeltaTieFraction(\n before: ArrayLike<number>,\n after: ArrayLike<number>,\n): number {\n if (before.length !== after.length) {\n throw new Error(\n `pairedDeltaTieFraction: unequal sample sizes (${before.length} vs ${after.length})`,\n )\n }\n const n = before.length\n if (n === 0) return 0\n let ties = 0\n for (let i = 0; i < n; i++) {\n if (Math.abs(after[i]! - before[i]!) < 1e-9) ties++\n }\n return ties / n\n}\n\n/**\n * The paired-delta statistic a DECISION is computed on, package-wide.\n *\n * The mean paired delta is the estimator that answers the question a promotion\n * gate asks — \"by how much did the candidate move the score\" — in the caller's\n * own units, and it equals the aggregate lift everyone quotes. The MEDIAN\n * answers a different question and loses the answer to this one in every regime\n * eval data actually lands in:\n * - TWO-POINT (pass/fail) outcomes on any encoding: the delta vector lives in\n * {−s, 0, +s} dominated by zeros, so the median and its whole bootstrap CI\n * are pinned at exactly 0 however large the shift. (Decide these on\n * {@link pairedRiskDifferenceExact} instead — same estimand, exact interval.)\n * - TIE-DOMINATED outcomes: at half the pairs tied the sample median is 0 by\n * construction, and `ci.low > threshold` then answers \"no\" forever at a\n * non-negative threshold and \"yes\" forever at a negative one.\n * - LOW-CARDINALITY outcomes, even well below half ties: judge dimensions on\n * integer 0-100, and block scores like {⅔, 1} from averaging pass/fail\n * leaves, put the median on a coarse lattice whose bootstrap percentiles\n * land on atoms. Measured: 26 blocks of 3 pass/fail leaves carrying a real\n * +12.8pp lift, only 23% of pairs tied, gives a median CI of [0, 0.333] —\n * lower bound exactly 0, so a gate at threshold 0 refuses a real lift.\n * That last case is why there is no tie-fraction threshold here: any cutoff on\n * ties leaves the lattice case open on the other side of it.\n *\n * `heldoutSignificance` has defaulted to the mean since #316 for the same\n * reason. The median remains available per call site for callers who\n * specifically want outlier robustness and accept the blindness.\n */\nexport const DECISION_PAIRED_DELTA_STATISTIC: 'mean' = 'mean'\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,YAAY,QAAkB,OAAoC;CAChF,IAAI,OAAO,WAAW,MAAM,QAC1B,MAAM,IAAI,gBACR,sCAAsC,OAAO,OAAO,MAAM,MAAM,OAAO,EACzE;CAEF,mBAAmB,eAAe,UAAU,MAAM;CAClD,mBAAmB,eAAe,SAAS,KAAK;CAChD,MAAM,IAAI,OAAO;CACjB,IAAI,IAAI,GAAG,OAAO;EAAE,GAAG;EAAM,IAAI;EAAG,GAAG;CAAK;CAE5C,MAAM,QAAQ,OAAO,KAAK,GAAG,MAAM,MAAM,KAAM,CAAC;CAChD,MAAM,OAAO,MAAM,QAAQ,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI;CAChD,MAAM,WAAW,MAAM,QAAQ,KAAK,MAAM,OAAO,IAAI,SAAS,GAAG,CAAC,KAAK,IAAI;CAC3E,MAAM,KAAK,KAAK,KAAK,WAAW,CAAC;CACjC,IAAI,OAAO,GACT,OAAO,SAAS,IAAI;EAAE,GAAG;EAAG,IAAI,IAAI;EAAG,GAAG;CAAE,IAAI;EAAE,GAAG;EAAM,IAAI,IAAI;EAAG,GAAG;CAAK;CAGhF,MAAM,IAAI,OAAO;CACjB,MAAM,KAAK,IAAI;CAEf,OAAO;EAAE;EAAG;EAAI,GADN,KAAK,IAAI,YAAY,KAAK,IAAI,CAAC,GAAG,EAAE;CAC5B;AACpB;;;;;;;;;;;AAiCA,MAAa,uBAAuB;;;;;;;;;;;AAwBpC,SAAgB,gBACd,QACA,OACA,OAA+B,CAAC,GACT;CACvB,IAAI,OAAO,WAAW,MAAM,QAC1B,MAAM,IAAI,MAAM,0CAA0C,OAAO,OAAO,MAAM,MAAM,OAAO,EAAE;CAE/F,MAAM,aAAa,KAAK,cAAc;CACtC,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,YAAY,KAAK,aAAa;CACpC,IAAI,cAAc,KAAK,cAAc,GACnC,MAAM,IAAI,MAAM,qDAAqD,YAAY;CAGnF,MAAM,IAAI,OAAO;CACjB,MAAM,SAAS,OAAO,KAAK,GAAG,MAAM,MAAM,KAAM,CAAC;CACjD,MAAM,eAAe,KAAA;CACrB,IAAI,MAAM,GACR,OAAO;EAAE,GAAG;EAAG,QAAQ;EAAG,MAAM;EAAG,KAAK;EAAG,MAAM;EAAG;EAAY;EAAW;CAAa;CAE1F,IAAI,MAAM,GAAG;EACX,MAAM,IAAI,OAAO;EACjB,OAAO;GAAE,GAAG;GAAG,QAAQ;GAAG,MAAM;GAAG,KAAK;GAAG,MAAM;GAAG;GAAY;GAAW;EAAa;CAC1F;CAEA,MAAM,MAAM,QAAQ,KAAK,MAAM,MAAM;CACrC,MAAM,UAAU,IAAI,MAAc,SAAS;CAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,WAAW,KAC7B,IAAI,cAAc,QAAQ;EACxB,IAAI,MAAM;EACV,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,OAAO,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;EAEpC,QAAQ,KAAK,MAAM;CACrB,OAAO;EACL,MAAM,MAAM,IAAI,MAAc,CAAC;EAC/B,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,IAAI,KAAK,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;EAEtC,QAAQ,KAAK,cAAc,GAAG;CAChC;CAEF,QAAQ,MAAM,GAAG,MAAM,IAAI,CAAC;CAE5B,MAAM,QAAQ,IAAI;CAClB,MAAM,SAAS,KAAK,MAAO,QAAQ,IAAK,SAAS;CACjD,MAAM,UAAU,KAAK,IAAI,YAAY,GAAG,KAAK,MAAM,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC;CAElF,OAAO;EACL;EACA,QAAQ,cAAc,CAAC,GAAG,MAAM,CAAC;EACjC,MAAM,OAAO,QAAQ,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI;EAC1C,KAAK,QAAQ;EACb,MAAM,QAAQ,KAAK,IAAI,SAAS,MAAM;EACtC;EACA;EACA;CACF;AACF;;;;;;;;;;;;;;AAoCA,SAAgB,eACd,aACA,aACsB;CACtB,IAAI,gBAAgB,aAAa,gBAAgB,QAC/C,MAAM,IAAI,gBACR,gEAAgE,aAClE;CAGF,IAAI,WAAW;CACf,IAAI,WAAW;CACf,IAAI,OAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;EAC3C,MAAM,aAAa,YAAY;EAC/B,IAAI,CAAC,OAAO,SAAS,UAAU,GAC7B,MAAM,IAAI,gBACR,uCAAuC,EAAE,uBAAuB,YAClE;EAEF,IAAI,aAAa,GAAG;OACf,IAAI,aAAa,GAAG;OACpB;CACP;CAEA,MAAM,WAAW,WAAW;CAC5B,MAAM,YAAY,gBAAgB,YAAY,WAAW;CACzD,OAAO;EACL,GAAG,YAAY;EACf;EACA;EACA;EACA;EACA;EACA,QAAQ,sBAAsB,WAAW,QAAQ;CACnD;AACF;;;AAIA,SAAgB,uBACd,QACA,OACQ;CACR,IAAI,OAAO,WAAW,MAAM,QAC1B,MAAM,IAAI,MACR,iDAAiD,OAAO,OAAO,MAAM,MAAM,OAAO,EACpF;CAEF,MAAM,IAAI,OAAO;CACjB,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,OAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,IAAI,KAAK,IAAI,MAAM,KAAM,OAAO,EAAG,IAAI,MAAM;CAE/C,OAAO,OAAO;AAChB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAa,kCAA0C"}
1
+ {"version":3,"file":"paired-tests-Ccr16gcW.js","names":[],"sources":["../src/statistics/paired-tests.ts"],"sourcesContent":["/**\n * Paired significance tests on continuous scores: the paired t-test, the\n * promotion-gate paired bootstrap, the exact sign test, and the package-wide\n * paired-delta decision statistic.\n */\n\nimport { ValidationError } from '../errors'\nimport { studentTCdf } from '../math/student-t'\nimport { assertFiniteSample, binomialHalfUpperTail, makeRng, medianInPlace } from './internal'\n\nexport interface PairedTTestResult {\n /** Null when the statistic is undefined — see {@link pairedTTest}. */\n t: number | null\n df: number\n /** Null exactly when `t` is null. */\n p: number | null\n}\n\n/**\n * Paired t-test — before/after measurements on the SAME items.\n * Pairing removes inter-item variance, giving tighter significance than\n * an unpaired test when comparing prompt v1 vs prompt v2 on identical\n * scenarios.\n *\n * Returns `t = p = null` where the statistic is undefined: fewer than two\n * pairs, or a non-zero constant delta whose observed variance is zero. A\n * constant shift carries no information about the variance it would have to\n * be compared against, so the honest answer is \"undefined\", not `p = 0` —\n * three observations cannot buy absolute certainty. This is the same contract\n * {@link pairedCohensDz} states for the same condition. An all-zero delta is\n * different: it is a measured null, and returns `t = 0, p = 1`.\n */\nexport function pairedTTest(before: number[], after: number[]): PairedTTestResult {\n if (before.length !== after.length) {\n throw new ValidationError(\n `pairedTTest: unequal sample sizes (${before.length} vs ${after.length})`,\n )\n }\n assertFiniteSample('pairedTTest', 'before', before)\n assertFiniteSample('pairedTTest', 'after', after)\n const n = before.length\n if (n < 2) return { t: null, df: 0, p: null }\n\n const diffs = before.map((b, i) => after[i]! - b)\n const mean = diffs.reduce((a, b) => a + b, 0) / n\n const variance = diffs.reduce((acc, d) => acc + (d - mean) ** 2, 0) / (n - 1)\n const se = Math.sqrt(variance / n)\n if (se === 0) {\n return mean === 0 ? { t: 0, df: n - 1, p: 1 } : { t: null, df: n - 1, p: null }\n }\n\n const t = mean / se\n const df = n - 1\n const p = 2 * (1 - studentTCdf(Math.abs(t), df))\n return { t, df, p }\n}\n\n// ── Paired bootstrap (promotion-gate effect size) ────────────────────\n\nexport interface PairedBootstrapResult {\n /** Number of paired observations. */\n n: number\n /** Median of paired deltas (after − before). */\n median: number\n /** Mean of paired deltas. */\n mean: number\n /** Lower bound of the bootstrap CI on the chosen statistic. */\n low: number\n /** Upper bound of the bootstrap CI on the chosen statistic. */\n high: number\n /** Confidence level used (e.g. 0.95). */\n confidence: number\n /** Number of bootstrap resamples used. */\n resamples: number\n /** False below {@link BOOTSTRAP_GATE_MIN_N}. See {@link pairedBootstrap}. */\n gateEligible: boolean\n}\n\n/**\n * Pairs below which a percentile bootstrap interval is descriptive spread only.\n *\n * `P(low > 0)` under a true null, against a nominal 2.5 %, measured over 4000\n * seeded trials: 13.53 % at n = 3, 3.52 % at n = 10, 3.10 % at n = 20 on the\n * median; 13.85 %, 4.90 %, 3.80 % on the mean. This is intrinsic to resampling\n * three points, not an implementation error — scipy's BCa gives 16.0 % on the\n * same n = 3 data — so no change to the estimator moves it. Below this floor\n * the decision belongs to the exact sign test or exact signed-rank test.\n */\nexport const BOOTSTRAP_GATE_MIN_N = 20\n\nexport interface PairedBootstrapOptions {\n /** Confidence level. Default 0.95. */\n confidence?: number\n /** Bootstrap resample count. Default 2000. */\n resamples?: number\n /** Statistic to bootstrap. Default 'median'. */\n statistic?: 'median' | 'mean'\n /** Deterministic seed. If omitted, derived from the deltas so the interval\n * is reproducible regardless. */\n seed?: number\n}\n\n/**\n * Paired bootstrap on (after − before) deltas. Returns a CI on the chosen\n * statistic (median by default); pairs are resampled with replacement. Throws\n * on unequal sample sizes.\n *\n * `low > threshold` carries the stated confidence ONLY at `n ≥\n * {@link BOOTSTRAP_GATE_MIN_N}`, which `gateEligible` reports. Below it the\n * check fires under a true null several times more often than nominal, so the\n * interval is descriptive spread and a promotion must not turn on it.\n */\nexport function pairedBootstrap(\n before: number[],\n after: number[],\n opts: PairedBootstrapOptions = {},\n): PairedBootstrapResult {\n if (before.length !== after.length) {\n throw new Error(`pairedBootstrap: unequal sample sizes (${before.length} vs ${after.length})`)\n }\n const confidence = opts.confidence ?? 0.95\n const resamples = opts.resamples ?? 2000\n const statistic = opts.statistic ?? 'median'\n if (confidence <= 0 || confidence >= 1) {\n throw new Error(`pairedBootstrap: confidence must be in (0,1), got ${confidence}`)\n }\n\n const n = before.length\n const deltas = before.map((b, i) => after[i]! - b)\n const gateEligible = n >= BOOTSTRAP_GATE_MIN_N\n if (n === 0) {\n return { n: 0, median: 0, mean: 0, low: 0, high: 0, confidence, resamples, gateEligible }\n }\n if (n === 1) {\n const d = deltas[0]!\n return { n: 1, median: d, mean: d, low: d, high: d, confidence, resamples, gateEligible }\n }\n\n const rng = makeRng(opts.seed, deltas)\n const samples = new Array<number>(resamples)\n for (let b = 0; b < resamples; b++) {\n if (statistic === 'mean') {\n let sum = 0\n for (let k = 0; k < n; k++) {\n sum += deltas[Math.floor(rng() * n)]!\n }\n samples[b] = sum / n\n } else {\n const acc = new Array<number>(n)\n for (let k = 0; k < n; k++) {\n acc[k] = deltas[Math.floor(rng() * n)]!\n }\n samples[b] = medianInPlace(acc)\n }\n }\n samples.sort((a, b) => a - b)\n\n const alpha = 1 - confidence\n const lowIdx = Math.floor((alpha / 2) * resamples)\n const highIdx = Math.min(resamples - 1, Math.ceil((1 - alpha / 2) * resamples) - 1)\n\n return {\n n,\n median: medianInPlace([...deltas]),\n mean: deltas.reduce((s, x) => s + x, 0) / n,\n low: samples[lowIdx]!,\n high: samples[Math.max(highIdx, lowIdx)]!,\n confidence,\n resamples,\n gateEligible,\n }\n}\n\n/** Pre-registered direction for a one-sided paired sign test. */\nexport type SignTestAlternative = 'greater' | 'less'\n\n/** Exact one-sided sign-test result for paired numeric differences. */\nexport interface PairedSignTestResult {\n /** Total supplied differences, including zero ties. */\n n: number\n /** Strictly positive differences. */\n positive: number\n /** Strictly negative differences. */\n negative: number\n /** Zero differences excluded from the binomial test. */\n ties: number\n /** Non-zero differences used by the binomial test. */\n nNonTies: number\n /** Direction of the pre-registered alternative hypothesis. */\n alternative: SignTestAlternative\n /** Exact one-sided p-value under P(positive) = P(negative) = 0.5. */\n pValue: number\n}\n\n/**\n * Exact one-sided sign test over paired differences.\n *\n * Pass `after[i] - before[i]` for each matched item. `alternative = 'greater'`\n * tests whether positive signs are more likely than negative signs and returns\n * `P(Binomial(nNonTies, 0.5) >= positive)`. `alternative = 'less'` treats\n * negative signs as successes instead. With a continuous difference\n * distribution this is the usual directional median test. Exact zero\n * differences are ties and do not enter the binomial denominator. All-tie and\n * empty inputs return p = 1. Every input difference must be finite, and the\n * direction must be chosen explicitly so a caller cannot select it after\n * seeing the signs.\n */\nexport function pairedSignTest(\n differences: readonly number[],\n alternative: SignTestAlternative,\n): PairedSignTestResult {\n if (alternative !== 'greater' && alternative !== 'less') {\n throw new ValidationError(\n `pairedSignTest: alternative must be 'greater' or 'less', got ${alternative}`,\n )\n }\n\n let positive = 0\n let negative = 0\n let ties = 0\n for (let i = 0; i < differences.length; i++) {\n const difference = differences[i]!\n if (!Number.isFinite(difference)) {\n throw new ValidationError(\n `pairedSignTest: difference at index ${i} must be finite, got ${difference}`,\n )\n }\n if (difference > 0) positive++\n else if (difference < 0) negative++\n else ties++\n }\n\n const nNonTies = positive + negative\n const successes = alternative === 'greater' ? positive : negative\n return {\n n: differences.length,\n positive,\n negative,\n ties,\n nNonTies,\n alternative,\n pValue: binomialHalfUpperTail(successes, nNonTies),\n }\n}\n\n/** Fraction of paired observations whose delta is an exact tie (|after − before|\n * < 1e-9). Throws on unequal sample sizes; 0 pairs ⇒ 0. */\nexport function pairedDeltaTieFraction(\n before: ArrayLike<number>,\n after: ArrayLike<number>,\n): number {\n if (before.length !== after.length) {\n throw new Error(\n `pairedDeltaTieFraction: unequal sample sizes (${before.length} vs ${after.length})`,\n )\n }\n const n = before.length\n if (n === 0) return 0\n let ties = 0\n for (let i = 0; i < n; i++) {\n if (Math.abs(after[i]! - before[i]!) < 1e-9) ties++\n }\n return ties / n\n}\n\n/**\n * The paired-delta statistic a DECISION is computed on, package-wide.\n *\n * The mean paired delta is the estimator that answers the question a promotion\n * gate asks — \"by how much did the candidate move the score\" — in the caller's\n * own units, and it equals the aggregate lift everyone quotes. The MEDIAN\n * answers a different question and loses the answer to this one in every regime\n * eval data actually lands in:\n * - TWO-POINT (pass/fail) outcomes on any encoding: the delta vector lives in\n * {−s, 0, +s} dominated by zeros, so the median and its whole bootstrap CI\n * are pinned at exactly 0 however large the shift. (Decide these on\n * {@link pairedRiskDifferenceExact} instead — same estimand, exact interval.)\n * - TIE-DOMINATED outcomes: at half the pairs tied the sample median is 0 by\n * construction, and `ci.low > threshold` then answers \"no\" forever at a\n * non-negative threshold and \"yes\" forever at a negative one.\n * - LOW-CARDINALITY outcomes, even well below half ties: judge dimensions on\n * integer 0-100, and block scores like {⅔, 1} from averaging pass/fail\n * leaves, put the median on a coarse lattice whose bootstrap percentiles\n * land on atoms. Measured: 26 blocks of 3 pass/fail leaves carrying a real\n * +12.8pp lift, only 23% of pairs tied, gives a median CI of [0, 0.333] —\n * lower bound exactly 0, so a gate at threshold 0 refuses a real lift.\n * That last case is why there is no tie-fraction threshold here: any cutoff on\n * ties leaves the lattice case open on the other side of it.\n *\n * `heldoutSignificance` has defaulted to the mean since #316 for the same\n * reason. The median remains available per call site for callers who\n * specifically want outlier robustness and accept the blindness.\n */\nexport const DECISION_PAIRED_DELTA_STATISTIC: 'mean' = 'mean'\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,YAAY,QAAkB,OAAoC;CAChF,IAAI,OAAO,WAAW,MAAM,QAC1B,MAAM,IAAI,gBACR,sCAAsC,OAAO,OAAO,MAAM,MAAM,OAAO,EACzE;CAEF,mBAAmB,eAAe,UAAU,MAAM;CAClD,mBAAmB,eAAe,SAAS,KAAK;CAChD,MAAM,IAAI,OAAO;CACjB,IAAI,IAAI,GAAG,OAAO;EAAE,GAAG;EAAM,IAAI;EAAG,GAAG;CAAK;CAE5C,MAAM,QAAQ,OAAO,KAAK,GAAG,MAAM,MAAM,KAAM,CAAC;CAChD,MAAM,OAAO,MAAM,QAAQ,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI;CAChD,MAAM,WAAW,MAAM,QAAQ,KAAK,MAAM,OAAO,IAAI,SAAS,GAAG,CAAC,KAAK,IAAI;CAC3E,MAAM,KAAK,KAAK,KAAK,WAAW,CAAC;CACjC,IAAI,OAAO,GACT,OAAO,SAAS,IAAI;EAAE,GAAG;EAAG,IAAI,IAAI;EAAG,GAAG;CAAE,IAAI;EAAE,GAAG;EAAM,IAAI,IAAI;EAAG,GAAG;CAAK;CAGhF,MAAM,IAAI,OAAO;CACjB,MAAM,KAAK,IAAI;CAEf,OAAO;EAAE;EAAG;EAAI,GADN,KAAK,IAAI,YAAY,KAAK,IAAI,CAAC,GAAG,EAAE;CAC5B;AACpB;;;;;;;;;;;AAiCA,MAAa,uBAAuB;;;;;;;;;;;AAwBpC,SAAgB,gBACd,QACA,OACA,OAA+B,CAAC,GACT;CACvB,IAAI,OAAO,WAAW,MAAM,QAC1B,MAAM,IAAI,MAAM,0CAA0C,OAAO,OAAO,MAAM,MAAM,OAAO,EAAE;CAE/F,MAAM,aAAa,KAAK,cAAc;CACtC,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,YAAY,KAAK,aAAa;CACpC,IAAI,cAAc,KAAK,cAAc,GACnC,MAAM,IAAI,MAAM,qDAAqD,YAAY;CAGnF,MAAM,IAAI,OAAO;CACjB,MAAM,SAAS,OAAO,KAAK,GAAG,MAAM,MAAM,KAAM,CAAC;CACjD,MAAM,eAAe,KAAA;CACrB,IAAI,MAAM,GACR,OAAO;EAAE,GAAG;EAAG,QAAQ;EAAG,MAAM;EAAG,KAAK;EAAG,MAAM;EAAG;EAAY;EAAW;CAAa;CAE1F,IAAI,MAAM,GAAG;EACX,MAAM,IAAI,OAAO;EACjB,OAAO;GAAE,GAAG;GAAG,QAAQ;GAAG,MAAM;GAAG,KAAK;GAAG,MAAM;GAAG;GAAY;GAAW;EAAa;CAC1F;CAEA,MAAM,MAAM,QAAQ,KAAK,MAAM,MAAM;CACrC,MAAM,UAAU,IAAI,MAAc,SAAS;CAC3C,KAAK,IAAI,IAAI,GAAG,IAAI,WAAW,KAC7B,IAAI,cAAc,QAAQ;EACxB,IAAI,MAAM;EACV,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,OAAO,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;EAEpC,QAAQ,KAAK,MAAM;CACrB,OAAO;EACL,MAAM,MAAM,IAAI,MAAc,CAAC;EAC/B,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,IAAI,KAAK,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC;EAEtC,QAAQ,KAAK,cAAc,GAAG;CAChC;CAEF,QAAQ,MAAM,GAAG,MAAM,IAAI,CAAC;CAE5B,MAAM,QAAQ,IAAI;CAClB,MAAM,SAAS,KAAK,MAAO,QAAQ,IAAK,SAAS;CACjD,MAAM,UAAU,KAAK,IAAI,YAAY,GAAG,KAAK,MAAM,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC;CAElF,OAAO;EACL;EACA,QAAQ,cAAc,CAAC,GAAG,MAAM,CAAC;EACjC,MAAM,OAAO,QAAQ,GAAG,MAAM,IAAI,GAAG,CAAC,IAAI;EAC1C,KAAK,QAAQ;EACb,MAAM,QAAQ,KAAK,IAAI,SAAS,MAAM;EACtC;EACA;EACA;CACF;AACF;;;;;;;;;;;;;;AAoCA,SAAgB,eACd,aACA,aACsB;CACtB,IAAI,gBAAgB,aAAa,gBAAgB,QAC/C,MAAM,IAAI,gBACR,gEAAgE,aAClE;CAGF,IAAI,WAAW;CACf,IAAI,WAAW;CACf,IAAI,OAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,YAAY,QAAQ,KAAK;EAC3C,MAAM,aAAa,YAAY;EAC/B,IAAI,CAAC,OAAO,SAAS,UAAU,GAC7B,MAAM,IAAI,gBACR,uCAAuC,EAAE,uBAAuB,YAClE;EAEF,IAAI,aAAa,GAAG;OACf,IAAI,aAAa,GAAG;OACpB;CACP;CAEA,MAAM,WAAW,WAAW;CAC5B,MAAM,YAAY,gBAAgB,YAAY,WAAW;CACzD,OAAO;EACL,GAAG,YAAY;EACf;EACA;EACA;EACA;EACA;EACA,QAAQ,sBAAsB,WAAW,QAAQ;CACnD;AACF;;;AAIA,SAAgB,uBACd,QACA,OACQ;CACR,IAAI,OAAO,WAAW,MAAM,QAC1B,MAAM,IAAI,MACR,iDAAiD,OAAO,OAAO,MAAM,MAAM,OAAO,EACpF;CAEF,MAAM,IAAI,OAAO;CACjB,IAAI,MAAM,GAAG,OAAO;CACpB,IAAI,OAAO;CACX,KAAK,IAAI,IAAI,GAAG,IAAI,GAAG,KACrB,IAAI,KAAK,IAAI,MAAM,KAAM,OAAO,EAAG,IAAI,MAAM;CAE/C,OAAO,OAAO;AAChB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAa,kCAA0C"}
@@ -6,7 +6,7 @@ import { n as TrajectoryStep } from "../trajectory-Y1fmdlw7.js";
6
6
  import { c as computeToolUseMetrics, d as judgeAgreementView, f as BudgetBreachFinding, g as BaselineReport, h as BaselineOptions, i as toolWasteView, l as JudgeAgreementReport, m as budgetBreachView, n as ToolWasteOptions, p as BudgetBreachReport, r as ToolWasteReport, t as ToolWasteFinding, u as JudgePair } from "../tool-waste-CP5DO383.js";
7
7
  //#region src/pipelines/first-divergence.d.ts
8
8
  /** One step of a run, as the diff reads it. */
9
- interface DiffStep {
9
+ export interface DiffStep {
10
10
  /** The step's id within its run. */
11
11
  id: string;
12
12
  name: string;
@@ -14,13 +14,13 @@ interface DiffStep {
14
14
  /** Fields compared once two steps pair, such as status, tool or model. */
15
15
  fields: Readonly<Record<string, string | number | boolean | null>>;
16
16
  }
17
- type StepPairing = 'id' | 'position' | 'name';
18
- interface FieldDifference {
17
+ export type StepPairing = 'id' | 'position' | 'name';
18
+ export interface FieldDifference {
19
19
  field: string;
20
20
  a: string | number | boolean | null;
21
21
  b: string | number | boolean | null;
22
22
  }
23
- interface StepPair {
23
+ export interface StepPair {
24
24
  /** Index in run A. */
25
25
  a: number;
26
26
  /** Index in run B. */
@@ -29,8 +29,8 @@ interface StepPair {
29
29
  /** Empty when the paired steps agree on name, kind and every field. */
30
30
  differences: FieldDifference[];
31
31
  }
32
- type StepDivergenceKind = 'changed' | 'replaced' | 'only-in-a' | 'only-in-b' | 'reordered';
33
- interface StepDivergence {
32
+ export type StepDivergenceKind = 'changed' | 'replaced' | 'only-in-a' | 'only-in-b' | 'reordered';
33
+ export interface StepDivergence {
34
34
  /** The first index at which the runs stop agreeing; equals `commonPrefixLen`. */
35
35
  index: number;
36
36
  kind: StepDivergenceKind;
@@ -41,7 +41,7 @@ interface StepDivergence {
41
41
  differences: FieldDifference[];
42
42
  reason: string;
43
43
  }
44
- interface StepDiff {
44
+ export interface StepDiff {
45
45
  /** Paired steps in run A order. */
46
46
  pairs: StepPair[];
47
47
  /** Indexes of run A steps with no partner in run B. */
@@ -53,8 +53,8 @@ interface StepDiff {
53
53
  /** Null when the runs agree step for step. */
54
54
  firstDivergence: StepDivergence | null;
55
55
  }
56
- declare function diffSteps(a: readonly DiffStep[], b: readonly DiffStep[]): StepDiff;
57
- interface DivergenceReport {
56
+ export declare function diffSteps(a: readonly DiffStep[], b: readonly DiffStep[]): StepDiff;
57
+ export interface DivergenceReport {
58
58
  runA: string;
59
59
  runB: string;
60
60
  /** Null when the runs agree step for step. */
@@ -66,31 +66,31 @@ interface DivergenceReport {
66
66
  /** The step diff behind the first divergence, over trajectory step indexes. */
67
67
  diff: StepDiff;
68
68
  }
69
- declare function firstDivergenceView(store: TraceStore, runA: string, runB: string): Promise<DivergenceReport>;
69
+ export declare function firstDivergenceView(store: TraceStore, runA: string, runB: string): Promise<DivergenceReport>;
70
70
  /**
71
71
  * One run's spans as diff steps, in containment order: depth first, siblings
72
72
  * by start time, the same order `buildTrajectory` gives a trace store run.
73
73
  * Spans whose parent is absent, or whose parents form a cycle, start their own
74
74
  * subtree, so every span appears exactly once.
75
75
  */
76
- declare function diffStepsFromSpans(spans: readonly DiagnosisSpan[]): DiffStep[];
76
+ export declare function diffStepsFromSpans(spans: readonly DiagnosisSpan[]): DiffStep[];
77
77
  //#endregion
78
78
  //#region src/pipelines/regression.d.ts
79
- interface RegressionSpec {
79
+ export interface RegressionSpec {
80
80
  metric: string;
81
81
  higherIsBetter: boolean;
82
82
  /** Extract a scalar from a run. Omit it and `metric` must name one of
83
83
  * `RUN_METRICS`; any other name is refused. */
84
84
  extract?: (run: Run, store: TraceStore) => Promise<number | null>;
85
85
  }
86
- interface RegressionOptions extends BaselineOptions {
86
+ export interface RegressionOptions extends BaselineOptions {
87
87
  baseline: RunFilter;
88
88
  candidate: RunFilter;
89
89
  }
90
- declare function regressionView(store: TraceStore, metrics: RegressionSpec[], options: RegressionOptions): Promise<BaselineReport>;
90
+ export declare function regressionView(store: TraceStore, metrics: RegressionSpec[], options: RegressionOptions): Promise<BaselineReport>;
91
91
  //#endregion
92
92
  //#region src/pipelines/stuck-loop.d.ts
93
- interface StuckLoopFinding {
93
+ export interface StuckLoopFinding {
94
94
  runId: string;
95
95
  toolName: string;
96
96
  argHash: string;
@@ -102,12 +102,12 @@ interface StuckLoopFinding {
102
102
  /** Milliseconds between first and last call in the loop. */
103
103
  windowMs: number;
104
104
  }
105
- interface StuckLoopReport {
105
+ export interface StuckLoopReport {
106
106
  findings: StuckLoopFinding[];
107
107
  affectedRunRatio: number;
108
108
  totalRuns: number;
109
109
  }
110
- interface StuckLoopOptions {
110
+ export interface StuckLoopOptions {
111
111
  /** Minimum call count to flag a loop (default 3). */
112
112
  minOccurrences?: number;
113
113
  /** Maximum time between the first and last repeated call (default 60 seconds). */
@@ -120,7 +120,7 @@ interface StuckLoopOptions {
120
120
  /** Filter to a specific runId; omit to scan the entire corpus. */
121
121
  runId?: string;
122
122
  }
123
- declare function stuckLoopView(store: TraceStore, options?: StuckLoopOptions): Promise<StuckLoopReport>;
123
+ export declare function stuckLoopView(store: TraceStore, options?: StuckLoopOptions): Promise<StuckLoopReport>;
124
124
  //#endregion
125
- export { BudgetBreachFinding, BudgetBreachReport, DiffStep, DivergenceReport, FailureCluster, FailureClusterReport, FieldDifference, JudgeAgreementReport, JudgePair, RegressionOptions, RegressionSpec, StepDiff, StepDivergence, StepDivergenceKind, StepPair, StepPairing, StuckLoopFinding, StuckLoopOptions, StuckLoopReport, ToolWasteFinding, ToolWasteOptions, ToolWasteReport, budgetBreachView, computeToolUseMetrics, diffSteps, diffStepsFromSpans, failureClusterView, firstDivergenceView, judgeAgreementView, regressionView, stuckLoopView, toolWasteView };
125
+ export { BudgetBreachFinding, BudgetBreachReport, FailureCluster, FailureClusterReport, JudgeAgreementReport, JudgePair, ToolWasteFinding, ToolWasteOptions, ToolWasteReport, budgetBreachView, computeToolUseMetrics, failureClusterView, judgeAgreementView, toolWasteView };
126
126
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/pipelines/first-divergence.ts","../../src/pipelines/regression.ts","../../src/pipelines/stuck-loop.ts"],"mappings":";;;;;;;;UAyBiB;;EAEf;EACA;EACA;;EAEA,QAAQ,SAAS;;KAGP;UAEK;EACf;EACA;EACA;;UAGe;;EAEf;;EAEA;EACA,UAAU;;EAEV,aAAa;;KAGH;UAEK;;EAEf;EACA,MAAM;;EAEN;EACA;;EAEA,aAAa;EACb;;UAGe;;EAEf,OAAO;;EAEP;;EAEA;;EAEA;;EAEA,iBAAiB;;iBAGH,UAAU,YAAY,YAAY,YAAY,aAAa;UAwI1D;EACf;EACA;;EAEA;EACA,QAAQ;EACR,QAAQ;EACR;EACA;;EAEA,MAAM;;iBAGc,oBACpB,OAAO,YACP,cACA,eACC,QAAQ;;;;;;;iBAmCK,mBAAmB,gBAAgB,kBAAkB;;;UC7PpD;EACf;EACA;;;EAGA,WAAW,KAAK,KAAK,OAAO,eAAe;;UAG5B,0BAA0B;EACzC,UAAU;EACV,WAAW;;iBAGS,eACpB,OAAO,YACP,SAAS,kBACT,SAAS,oBACR,QAAQ;;;UCFM;EACf;EACA;EACA;;EAEA;EACA;;EAEA;;EAEA;;UAGe;EACf,UAAU;EACV;EACA;;UAGe;;EAEf;;EAEA;;;;;EAKA;;EAEA;;iBAGoB,cACpB,OAAO,YACP,UAAS,mBACR,QAAQ"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/pipelines/first-divergence.ts","../../src/pipelines/regression.ts","../../src/pipelines/stuck-loop.ts"],"mappings":";;;;;;;;iBAyBiB;;EAEf;EACA;EACA;;EAEA,QAAQ,SAAS;;YAGP;iBAEK;EACf;EACA;EACA;;iBAGe;;EAEf;;EAEA;EACA,UAAU;;EAEV,aAAa;;YAGH;iBAEK;;EAEf;EACA,MAAM;;EAEN;EACA;;EAEA,aAAa;EACb;;iBAGe;;EAEf,OAAO;;EAEP;;EAEA;;EAEA;;EAEA,iBAAiB;;wBAGH,UAAU,YAAY,YAAY,YAAY,aAAa;iBAwI1D;EACf;EACA;;EAEA;EACA,QAAQ;EACR,QAAQ;EACR;EACA;;EAEA,MAAM;;wBAGc,oBACpB,OAAO,YACP,cACA,eACC,QAAQ;;;;;;;wBAmCK,mBAAmB,gBAAgB,kBAAkB;;;iBC7PpD;EACf;EACA;;;EAGA,WAAW,KAAK,KAAK,OAAO,eAAe;;iBAG5B,0BAA0B;EACzC,UAAU;EACV,WAAW;;wBAGS,eACpB,OAAO,YACP,SAAS,kBACT,SAAS,oBACR,QAAQ;;;iBCFM;EACf;EACA;EACA;;EAEA;EACA;;EAEA;;EAEA;;iBAGe;EACf,UAAU;EACV;EACA;;iBAGe;;EAEf;;EAEA;;;;;EAKA;;EAEA;;wBAGoB,cACpB,OAAO,YACP,UAAS,mBACR,QAAQ"}
@@ -1,5 +1,5 @@
1
- import { a as budgetBreachView, i as failureClusterView, n as computeToolUseMetrics, r as judgeAgreementView, t as toolWasteView } from "../tool-waste-CJDfxH45.js";
2
- import { t as compareToBaseline } from "../baseline-BC-eBZ7U.js";
1
+ import { a as budgetBreachView, i as failureClusterView, n as computeToolUseMetrics, r as judgeAgreementView, t as toolWasteView } from "../tool-waste-B-u3rzCz.js";
2
+ import { t as compareToBaseline } from "../baseline-CVKsfWTg.js";
3
3
  import { a as isToolSpan } from "../schema-CSf6qWgZ.js";
4
4
  import { a as hasCapturedToolArgs, r as argHash, u as runMetricExtractor } from "../query-DlV0pdx4.js";
5
5
  import { t as buildTrajectory } from "../trajectory-D_7rLrvE.js";
@@ -203,7 +203,7 @@ function diffStepsFromSpans(spans) {
203
203
  async function regressionView(store, metrics, options) {
204
204
  const baselineRuns = await store.listRuns(options.baseline);
205
205
  const candidateRuns = await store.listRuns(options.candidate);
206
- return compareToBaseline(await Promise.all(metrics.map(async (m) => {
206
+ const samples = await Promise.all(metrics.map(async (m) => {
207
207
  const extract = m.extract ?? runMetricExtractor(m.metric);
208
208
  const baseline = await extractAll(baselineRuns, extract, store);
209
209
  const candidate = await extractAll(candidateRuns, extract, store);
@@ -213,7 +213,8 @@ async function regressionView(store, metrics, options) {
213
213
  baseline,
214
214
  candidate
215
215
  };
216
- })), options);
216
+ }));
217
+ return compareToBaseline(samples, options);
217
218
  }
218
219
  async function extractAll(runs, extract, store) {
219
220
  const out = [];
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/pipelines/first-divergence.ts","../../src/pipelines/regression.ts","../../src/pipelines/stuck-loop.ts"],"sourcesContent":["/**\n * Step diff between two runs, with the first divergence marked.\n *\n * Steps pair in three passes, each over the steps still unpaired:\n * 1. `id`: the same step id in both runs, e.g. a forked run that kept its\n * parent's span ids.\n * 2. `position`: the same index with the same name and kind.\n * 3. `name`: the same name and kind anywhere, first unpaired step in order.\n * A step left unpaired exists in only one run. The first divergence is the\n * first index at which the two runs stop agreeing step for step, and it says\n * how: a paired step changed, one run replaced, added or removed a step, or the\n * runs ran the same steps in a different order.\n *\n * `diffSteps` reads plain `DiffStep` lists, so the same diff serves agent-eval\n * trace stores (`firstDivergenceView`) and flat OTLP spans\n * (`diffStepsFromSpans`). Use it to attribute \"why is variant B better?\" to a\n * step, and to compare sibling or parent and child runs of a search tree.\n */\n\nimport type { DiagnosisSpan } from '../diagnosis/spans'\nimport type { Span } from '../trace/schema'\nimport type { TraceStore } from '../trace/store'\nimport { buildTrajectory, type TrajectoryStep } from '../trajectory'\n\n/** One step of a run, as the diff reads it. */\nexport interface DiffStep {\n /** The step's id within its run. */\n id: string\n name: string\n kind: string\n /** Fields compared once two steps pair, such as status, tool or model. */\n fields: Readonly<Record<string, string | number | boolean | null>>\n}\n\nexport type StepPairing = 'id' | 'position' | 'name'\n\nexport interface FieldDifference {\n field: string\n a: string | number | boolean | null\n b: string | number | boolean | null\n}\n\nexport interface StepPair {\n /** Index in run A. */\n a: number\n /** Index in run B. */\n b: number\n pairedBy: StepPairing\n /** Empty when the paired steps agree on name, kind and every field. */\n differences: FieldDifference[]\n}\n\nexport type StepDivergenceKind = 'changed' | 'replaced' | 'only-in-a' | 'only-in-b' | 'reordered'\n\nexport interface StepDivergence {\n /** The first index at which the runs stop agreeing; equals `commonPrefixLen`. */\n index: number\n kind: StepDivergenceKind\n /** Index of the step at `index` in each run, or null when that run has ended. */\n a: number | null\n b: number | null\n /** Set when `kind` is `changed`. */\n differences: FieldDifference[]\n reason: string\n}\n\nexport interface StepDiff {\n /** Paired steps in run A order. */\n pairs: StepPair[]\n /** Indexes of run A steps with no partner in run B. */\n onlyInA: number[]\n /** Indexes of run B steps with no partner in run A. */\n onlyInB: number[]\n /** Leading steps paired index for index with no difference. */\n commonPrefixLen: number\n /** Null when the runs agree step for step. */\n firstDivergence: StepDivergence | null\n}\n\nexport function diffSteps(a: readonly DiffStep[], b: readonly DiffStep[]): StepDiff {\n const partnerOfA = new Map<number, { b: number; pairedBy: StepPairing }>()\n const pairedB = new Set<number>()\n const pair = (i: number, j: number, pairedBy: StepPairing) => {\n partnerOfA.set(i, { b: j, pairedBy })\n pairedB.add(j)\n }\n\n const bById = new Map<string, number[]>()\n b.forEach((step, j) => {\n const list = bById.get(step.id)\n if (list) list.push(j)\n else bById.set(step.id, [j])\n })\n a.forEach((step, i) => {\n const j = bById.get(step.id)?.find((candidate) => !pairedB.has(candidate))\n if (j !== undefined) pair(i, j, 'id')\n })\n a.forEach((step, i) => {\n if (partnerOfA.has(i) || i >= b.length || pairedB.has(i)) return\n if (sameShape(step, b[i]!)) pair(i, i, 'position')\n })\n a.forEach((step, i) => {\n if (partnerOfA.has(i)) return\n const j = b.findIndex((candidate, index) => !pairedB.has(index) && sameShape(step, candidate))\n if (j >= 0) pair(i, j, 'name')\n })\n\n const pairs: StepPair[] = [...partnerOfA.entries()]\n .sort(([x], [y]) => x - y)\n .map(([i, partner]) => ({\n a: i,\n b: partner.b,\n pairedBy: partner.pairedBy,\n differences: differencesBetween(a[i]!, b[partner.b]!),\n }))\n const pairByA = new Map(pairs.map((entry) => [entry.a, entry]))\n const partnerOfB = new Map(pairs.map((entry) => [entry.b, entry.a]))\n\n let prefix = 0\n while (prefix < a.length && prefix < b.length) {\n const entry = pairByA.get(prefix)\n if (!entry || entry.b !== prefix || entry.differences.length > 0) break\n prefix += 1\n }\n\n return {\n pairs,\n onlyInA: a.map((_, i) => i).filter((i) => !partnerOfA.has(i)),\n onlyInB: b.map((_, j) => j).filter((j) => !pairedB.has(j)),\n commonPrefixLen: prefix,\n firstDivergence:\n prefix === a.length && prefix === b.length\n ? null\n : divergenceAt(prefix, a, b, pairByA.get(prefix), partnerOfB),\n }\n}\n\nfunction divergenceAt(\n index: number,\n a: readonly DiffStep[],\n b: readonly DiffStep[],\n pairOfA: StepPair | undefined,\n partnerOfB: ReadonlyMap<number, number>,\n): StepDivergence {\n const stepA = a[index]\n const stepB = b[index]\n const base = {\n index,\n a: stepA ? index : null,\n b: stepB ? index : null,\n differences: [] as FieldDifference[],\n }\n if (stepA && stepB && pairOfA?.b === index) {\n return {\n ...base,\n kind: 'changed',\n differences: pairOfA.differences,\n reason: `at index ${index}, ${describe(stepA)} changed: ${pairOfA.differences\n .map((d) => `${d.field} ${String(d.a)} vs ${String(d.b)}`)\n .join('; ')}`,\n }\n }\n const aUnpaired = stepA !== undefined && pairOfA === undefined\n const bUnpaired = stepB !== undefined && !partnerOfB.has(index)\n if (stepA && stepB && aUnpaired && bUnpaired) {\n return {\n ...base,\n kind: 'replaced',\n reason: `at index ${index}, A ran ${describe(stepA)} and B ran ${describe(stepB)}`,\n }\n }\n if (stepA && aUnpaired) {\n return {\n ...base,\n kind: 'only-in-a',\n reason: `at index ${index}, only A has ${describe(stepA)}`,\n }\n }\n if (stepB && bUnpaired) {\n return {\n ...base,\n kind: 'only-in-b',\n reason: `at index ${index}, only B has ${describe(stepB)}`,\n }\n }\n return {\n ...base,\n kind: 'reordered',\n reason: `at index ${index}, A ran ${describe(stepA!)} and B ran ${describe(stepB!)}; both steps occur in the other run at a different position`,\n }\n}\n\nfunction sameShape(x: DiffStep, y: DiffStep): boolean {\n return x.name === y.name && x.kind === y.kind\n}\n\nfunction differencesBetween(x: DiffStep, y: DiffStep): FieldDifference[] {\n const differences: FieldDifference[] = []\n if (x.kind !== y.kind) differences.push({ field: 'kind', a: x.kind, b: y.kind })\n if (x.name !== y.name) differences.push({ field: 'name', a: x.name, b: y.name })\n const fields = [...new Set([...Object.keys(x.fields), ...Object.keys(y.fields)])].sort()\n for (const field of fields) {\n const left = x.fields[field] ?? null\n const right = y.fields[field] ?? null\n if (left !== right) differences.push({ field, a: left, b: right })\n }\n return differences\n}\n\nfunction describe(step: DiffStep): string {\n return `${step.kind} \"${step.name}\"`\n}\n\n// ── agent-eval trace store ────────────────────────────────────────────\n\nexport interface DivergenceReport {\n runA: string\n runB: string\n /** Null when the runs agree step for step. */\n firstDivergenceIndex: number | null\n aStep?: TrajectoryStep\n bStep?: TrajectoryStep\n reason?: string\n commonPrefixLen: number\n /** The step diff behind the first divergence, over trajectory step indexes. */\n diff: StepDiff\n}\n\nexport async function firstDivergenceView(\n store: TraceStore,\n runA: string,\n runB: string,\n): Promise<DivergenceReport> {\n const [a, b] = await Promise.all([buildTrajectory(store, runA), buildTrajectory(store, runB)])\n const diff = diffSteps(a.steps.map(trajectoryDiffStep), b.steps.map(trajectoryDiffStep))\n const divergence = diff.firstDivergence\n return {\n runA,\n runB,\n firstDivergenceIndex: divergence?.index ?? null,\n ...(divergence?.a != null ? { aStep: a.steps[divergence.a] } : {}),\n ...(divergence?.b != null ? { bStep: b.steps[divergence.b] } : {}),\n ...(divergence ? { reason: divergence.reason } : {}),\n commonPrefixLen: diff.commonPrefixLen,\n diff,\n }\n}\n\nfunction trajectoryDiffStep(step: TrajectoryStep): DiffStep {\n const span: Span = step.span\n const fields: Record<string, string | number | boolean | null> = {\n status: span.status ?? null,\n }\n if (span.kind === 'tool') fields.tool = span.toolName\n if (span.kind === 'llm') fields.model = span.model\n if (span.kind === 'judge') fields.dimension = span.dimension\n return { id: span.spanId, name: span.name, kind: span.kind, fields }\n}\n\n// ── flat OTLP spans ───────────────────────────────────────────────────\n\n/**\n * One run's spans as diff steps, in containment order: depth first, siblings\n * by start time, the same order `buildTrajectory` gives a trace store run.\n * Spans whose parent is absent, or whose parents form a cycle, start their own\n * subtree, so every span appears exactly once.\n */\nexport function diffStepsFromSpans(spans: readonly DiagnosisSpan[]): DiffStep[] {\n const ids = new Set(spans.map((span) => span.spanId))\n const children = new Map<string | null, DiagnosisSpan[]>()\n for (const span of spans) {\n const parent =\n span.parentSpanId !== null && ids.has(span.parentSpanId) ? span.parentSpanId : null\n const list = children.get(parent)\n if (list) list.push(span)\n else children.set(parent, [span])\n }\n // Span ids are random per run, so two runs of the same logical trace can carry\n // different ids for \"the same\" span. Siblings that share a start time (a common\n // case: parallel steps a driver starts together) must still sort the same way\n // in both runs, or the diff below reports a false `reordered` divergence purely\n // from spanId noise. End time, kind and name are content, not per-run identity,\n // so they break the tie before spanId, which is the last resort.\n const byStart = (x: DiagnosisSpan, y: DiagnosisSpan) =>\n (x.startMs ?? 0) - (y.startMs ?? 0) ||\n (x.endMs ?? 0) - (y.endMs ?? 0) ||\n x.kind.localeCompare(y.kind) ||\n x.name.localeCompare(y.name) ||\n x.spanId.localeCompare(y.spanId)\n for (const list of children.values()) list.sort(byStart)\n const ordered: DiffStep[] = []\n const visited = new Set<string>()\n const visit = (span: DiagnosisSpan) => {\n if (visited.has(span.spanId)) return\n visited.add(span.spanId)\n ordered.push({\n id: span.spanId,\n name: span.name,\n kind: span.kind,\n fields: { status: span.status, tool: span.toolName, model: span.model },\n })\n for (const child of children.get(span.spanId) ?? []) visit(child)\n }\n for (const root of children.get(null) ?? []) visit(root)\n for (const span of [...spans].sort(byStart)) visit(span)\n return ordered\n}\n","/**\n * RegressionView — compares a candidate slice to a baseline slice on a\n * named metric. Delegates the statistics (Welch's t-test, Cohen's d,\n * IQR stability) to `baseline.ts`.\n *\n * This is the entry point for CI regression gates: \"given runs tagged\n * release=A and release=B, did any metric regress?\"\n */\n\nimport { type BaselineOptions, type BaselineReport, compareToBaseline } from '../baseline'\nimport { runMetricExtractor } from '../trace/query'\nimport type { Run } from '../trace/schema'\nimport type { RunFilter, TraceStore } from '../trace/store'\n\nexport interface RegressionSpec {\n metric: string\n higherIsBetter: boolean\n /** Extract a scalar from a run. Omit it and `metric` must name one of\n * `RUN_METRICS`; any other name is refused. */\n extract?: (run: Run, store: TraceStore) => Promise<number | null>\n}\n\nexport interface RegressionOptions extends BaselineOptions {\n baseline: RunFilter\n candidate: RunFilter\n}\n\nexport async function regressionView(\n store: TraceStore,\n metrics: RegressionSpec[],\n options: RegressionOptions,\n): Promise<BaselineReport> {\n const baselineRuns = await store.listRuns(options.baseline)\n const candidateRuns = await store.listRuns(options.candidate)\n const samples = await Promise.all(\n metrics.map(async (m) => {\n const extract = m.extract ?? runMetricExtractor(m.metric)\n const baseline = await extractAll(baselineRuns, extract, store)\n const candidate = await extractAll(candidateRuns, extract, store)\n return { metric: m.metric, higherIsBetter: m.higherIsBetter, baseline, candidate }\n }),\n )\n return compareToBaseline(samples, options)\n}\n\nasync function extractAll(\n runs: Run[],\n extract: (r: Run, s: TraceStore) => Promise<number | null>,\n store: TraceStore,\n): Promise<number[]> {\n const out: number[] = []\n for (const r of runs) {\n const v = await extract(r, store)\n if (v !== null && Number.isFinite(v)) out.push(v)\n }\n return out\n}\n","/**\n * StuckLoopView — detects when an agent calls the same tool with the\n * same (or structurally similar) arguments ≥ N times in a short window.\n *\n * Rationale: agents that loop are the number-one production failure\n * mode on long-horizon flows. The view returns (runId, toolName,\n * argHash, occurrences, windowMs) for each detected loop plus a\n * fraction of runs affected.\n */\n\nimport { executionTrackByLane } from '../trace/execution-tracks'\nimport { argHash, hasCapturedToolArgs } from '../trace/query'\nimport { isToolSpan, type Span, type ToolSpan } from '../trace/schema'\nimport type { TraceStore } from '../trace/store'\n\nconst DEFAULT_MAX_WINDOW_MS = 60_000\nconst DEFAULT_MAX_INTERVENING_TOOL_CALLS = 0\n\ninterface ScopedToolCall {\n span: ToolSpan\n scopeSpanId: string | null\n laneSpanId: string | null\n}\n\ninterface IndexedToolCall extends ScopedToolCall {\n toolCallIndex: number\n trackId: string\n}\n\nexport interface StuckLoopFinding {\n runId: string\n toolName: string\n argHash: string\n /** Calls in this episode's densest qualifying interval, not the whole-run total. */\n occurrences: number\n spanIds: string[]\n /** Nearest agent ancestor, or the direct parent when ancestry is incomplete. */\n scopeSpanId?: string\n /** Milliseconds between first and last call in the loop. */\n windowMs: number\n}\n\nexport interface StuckLoopReport {\n findings: StuckLoopFinding[]\n affectedRunRatio: number\n totalRuns: number\n}\n\nexport interface StuckLoopOptions {\n /** Minimum call count to flag a loop (default 3). */\n minOccurrences?: number\n /** Maximum time between the first and last repeated call (default 60 seconds). */\n maxWindowMs?: number\n /**\n * Maximum other tool calls allowed between adjacent repeats (default 0).\n * Set to 1 to detect alternating patterns such as A,B,A,B,A.\n */\n maxInterveningToolCalls?: number\n /** Filter to a specific runId; omit to scan the entire corpus. */\n runId?: string\n}\n\nexport async function stuckLoopView(\n store: TraceStore,\n options: StuckLoopOptions = {},\n): Promise<StuckLoopReport> {\n const minOccurrences = options.minOccurrences ?? 3\n const maxWindowMs = options.maxWindowMs ?? DEFAULT_MAX_WINDOW_MS\n const maxInterveningToolCalls =\n options.maxInterveningToolCalls ?? DEFAULT_MAX_INTERVENING_TOOL_CALLS\n if (!Number.isInteger(minOccurrences) || minOccurrences < 1) {\n throw new RangeError('minOccurrences must be a positive integer')\n }\n if (!Number.isFinite(maxWindowMs) || maxWindowMs < 0) {\n throw new RangeError('maxWindowMs must be a finite non-negative number')\n }\n if (!Number.isInteger(maxInterveningToolCalls) || maxInterveningToolCalls < 0) {\n throw new RangeError('maxInterveningToolCalls must be a non-negative integer')\n }\n const runs = options.runId\n ? [{ runId: options.runId }]\n : (await store.listRuns()).map((r) => ({ runId: r.runId }))\n\n const findings: StuckLoopFinding[] = []\n for (const { runId } of runs) {\n const spans = await store.spans({ runId })\n const spansById = new Map(spans.map((span) => [span.spanId, span]))\n const scopedTools: ScopedToolCall[] = spans\n .filter(isToolSpan)\n .map((span, sourceIndex) => ({ span, sourceIndex }))\n .sort((a, b) => a.span.startedAt - b.span.startedAt || a.sourceIndex - b.sourceIndex)\n .map(({ span }) => ({ span, ...executionScope(span, spansById) }))\n const trackByLane = executionTrackByLane(\n scopedTools.map((call) => {\n const direct = call.laneSpanId === null || call.laneSpanId === call.scopeSpanId\n const timed = direct\n ? call.span\n : call.laneSpanId\n ? spansById.get(call.laneSpanId)\n : undefined\n return {\n key: executionKey(call),\n scopeKey: JSON.stringify(call.scopeSpanId),\n start: timed?.startedAt ?? null,\n end: timed?.endedAt ?? null,\n }\n }),\n )\n const nextToolIndexByTrack = new Map<string, number>()\n const orderedTools: IndexedToolCall[] = scopedTools.map((call) => {\n const trackId = trackByLane.get(executionKey(call))!\n const toolCallIndex = nextToolIndexByTrack.get(trackId) ?? 0\n nextToolIndexByTrack.set(trackId, toolCallIndex + 1)\n return { ...call, toolCallIndex, trackId }\n })\n const byKey = new Map<\n string,\n {\n calls: IndexedToolCall[]\n argHash: string\n toolName: string\n scopeSpanId: string | null\n }\n >()\n for (const call of orderedTools) {\n if (!hasCapturedToolArgs(call.span)) continue\n const h = argHash(call.span.args)\n const key = JSON.stringify([call.trackId, call.span.toolName, h])\n const bucket = byKey.get(key) ?? {\n calls: [],\n argHash: h,\n toolName: call.span.toolName,\n scopeSpanId: call.scopeSpanId,\n }\n bucket.calls.push(call)\n byKey.set(key, bucket)\n }\n for (const { calls, argHash: h, toolName, scopeSpanId } of byKey.values()) {\n if (calls.length < minOccurrences) continue\n let episodeStart = 0\n for (let episodeEnd = 1; episodeEnd <= calls.length; episodeEnd += 1) {\n const previous = calls[episodeEnd - 1]!\n const next = calls[episodeEnd]\n const episodeEnded =\n next === undefined ||\n next.span.startedAt - previous.span.startedAt > maxWindowMs ||\n next.toolCallIndex - previous.toolCallIndex - 1 > maxInterveningToolCalls ||\n !callsAreSerial(previous, next)\n if (!episodeEnded) continue\n\n const episode = calls.slice(episodeStart, episodeEnd)\n let left = 0\n let bestStart = 0\n let bestEnd = -1\n for (let right = 0; right < episode.length; right += 1) {\n while (episode[right]!.span.startedAt - episode[left]!.span.startedAt > maxWindowMs) {\n left += 1\n }\n if (right - left > bestEnd - bestStart) {\n bestStart = left\n bestEnd = right\n }\n }\n if (bestEnd - bestStart + 1 >= minOccurrences) {\n const loop = episode.slice(bestStart, bestEnd + 1)\n const first = loop[0]!.span.startedAt\n const last = loop[loop.length - 1]!.span.startedAt\n findings.push({\n runId,\n toolName,\n argHash: h,\n occurrences: loop.length,\n spanIds: loop.map((call) => call.span.spanId),\n ...(scopeSpanId ? { scopeSpanId } : {}),\n windowMs: last - first,\n })\n }\n episodeStart = episodeEnd\n }\n }\n }\n\n const affectedRuns = new Set(findings.map((f) => f.runId))\n return {\n findings,\n affectedRunRatio: runs.length > 0 ? affectedRuns.size / runs.length : 0,\n totalRuns: runs.length,\n }\n}\n\nfunction laneKey(call: Pick<ScopedToolCall, 'scopeSpanId' | 'laneSpanId'>): string {\n return JSON.stringify([call.scopeSpanId, call.laneSpanId])\n}\n\nfunction executionKey(call: ScopedToolCall): string {\n return call.laneSpanId === null || call.laneSpanId === call.scopeSpanId\n ? JSON.stringify([call.scopeSpanId, call.span.spanId])\n : laneKey(call)\n}\n\nfunction executionScope(\n span: ToolSpan,\n spansById: ReadonlyMap<string, Span>,\n): { scopeSpanId: string | null; laneSpanId: string | null } {\n const directParent = span.parentSpanId\n if (!directParent) return { scopeSpanId: null, laneSpanId: null }\n\n let currentId: string | undefined = directParent\n let laneSpanId: string | null = null\n const seen = new Set<string>()\n while (currentId && !seen.has(currentId)) {\n seen.add(currentId)\n const current = spansById.get(currentId)\n if (!current) return { scopeSpanId: directParent, laneSpanId: directParent }\n if (current.kind === 'agent') {\n return { scopeSpanId: current.spanId, laneSpanId: laneSpanId ?? current.spanId }\n }\n laneSpanId = current.spanId\n currentId = current.parentSpanId\n }\n return { scopeSpanId: directParent, laneSpanId: directParent }\n}\n\nfunction callsAreSerial(previous: IndexedToolCall, next: IndexedToolCall): boolean {\n return previous.span.endedAt !== undefined && previous.span.endedAt <= next.span.startedAt\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/pipelines/first-divergence.ts","../../src/pipelines/regression.ts","../../src/pipelines/stuck-loop.ts"],"sourcesContent":["/**\n * Step diff between two runs, with the first divergence marked.\n *\n * Steps pair in three passes, each over the steps still unpaired:\n * 1. `id`: the same step id in both runs, e.g. a forked run that kept its\n * parent's span ids.\n * 2. `position`: the same index with the same name and kind.\n * 3. `name`: the same name and kind anywhere, first unpaired step in order.\n * A step left unpaired exists in only one run. The first divergence is the\n * first index at which the two runs stop agreeing step for step, and it says\n * how: a paired step changed, one run replaced, added or removed a step, or the\n * runs ran the same steps in a different order.\n *\n * `diffSteps` reads plain `DiffStep` lists, so the same diff serves agent-eval\n * trace stores (`firstDivergenceView`) and flat OTLP spans\n * (`diffStepsFromSpans`). Use it to attribute \"why is variant B better?\" to a\n * step, and to compare sibling or parent and child runs of a search tree.\n */\n\nimport type { DiagnosisSpan } from '../diagnosis/spans'\nimport type { Span } from '../trace/schema'\nimport type { TraceStore } from '../trace/store'\nimport { buildTrajectory, type TrajectoryStep } from '../trajectory'\n\n/** One step of a run, as the diff reads it. */\nexport interface DiffStep {\n /** The step's id within its run. */\n id: string\n name: string\n kind: string\n /** Fields compared once two steps pair, such as status, tool or model. */\n fields: Readonly<Record<string, string | number | boolean | null>>\n}\n\nexport type StepPairing = 'id' | 'position' | 'name'\n\nexport interface FieldDifference {\n field: string\n a: string | number | boolean | null\n b: string | number | boolean | null\n}\n\nexport interface StepPair {\n /** Index in run A. */\n a: number\n /** Index in run B. */\n b: number\n pairedBy: StepPairing\n /** Empty when the paired steps agree on name, kind and every field. */\n differences: FieldDifference[]\n}\n\nexport type StepDivergenceKind = 'changed' | 'replaced' | 'only-in-a' | 'only-in-b' | 'reordered'\n\nexport interface StepDivergence {\n /** The first index at which the runs stop agreeing; equals `commonPrefixLen`. */\n index: number\n kind: StepDivergenceKind\n /** Index of the step at `index` in each run, or null when that run has ended. */\n a: number | null\n b: number | null\n /** Set when `kind` is `changed`. */\n differences: FieldDifference[]\n reason: string\n}\n\nexport interface StepDiff {\n /** Paired steps in run A order. */\n pairs: StepPair[]\n /** Indexes of run A steps with no partner in run B. */\n onlyInA: number[]\n /** Indexes of run B steps with no partner in run A. */\n onlyInB: number[]\n /** Leading steps paired index for index with no difference. */\n commonPrefixLen: number\n /** Null when the runs agree step for step. */\n firstDivergence: StepDivergence | null\n}\n\nexport function diffSteps(a: readonly DiffStep[], b: readonly DiffStep[]): StepDiff {\n const partnerOfA = new Map<number, { b: number; pairedBy: StepPairing }>()\n const pairedB = new Set<number>()\n const pair = (i: number, j: number, pairedBy: StepPairing) => {\n partnerOfA.set(i, { b: j, pairedBy })\n pairedB.add(j)\n }\n\n const bById = new Map<string, number[]>()\n b.forEach((step, j) => {\n const list = bById.get(step.id)\n if (list) list.push(j)\n else bById.set(step.id, [j])\n })\n a.forEach((step, i) => {\n const j = bById.get(step.id)?.find((candidate) => !pairedB.has(candidate))\n if (j !== undefined) pair(i, j, 'id')\n })\n a.forEach((step, i) => {\n if (partnerOfA.has(i) || i >= b.length || pairedB.has(i)) return\n if (sameShape(step, b[i]!)) pair(i, i, 'position')\n })\n a.forEach((step, i) => {\n if (partnerOfA.has(i)) return\n const j = b.findIndex((candidate, index) => !pairedB.has(index) && sameShape(step, candidate))\n if (j >= 0) pair(i, j, 'name')\n })\n\n const pairs: StepPair[] = [...partnerOfA.entries()]\n .sort(([x], [y]) => x - y)\n .map(([i, partner]) => ({\n a: i,\n b: partner.b,\n pairedBy: partner.pairedBy,\n differences: differencesBetween(a[i]!, b[partner.b]!),\n }))\n const pairByA = new Map(pairs.map((entry) => [entry.a, entry]))\n const partnerOfB = new Map(pairs.map((entry) => [entry.b, entry.a]))\n\n let prefix = 0\n while (prefix < a.length && prefix < b.length) {\n const entry = pairByA.get(prefix)\n if (!entry || entry.b !== prefix || entry.differences.length > 0) break\n prefix += 1\n }\n\n return {\n pairs,\n onlyInA: a.map((_, i) => i).filter((i) => !partnerOfA.has(i)),\n onlyInB: b.map((_, j) => j).filter((j) => !pairedB.has(j)),\n commonPrefixLen: prefix,\n firstDivergence:\n prefix === a.length && prefix === b.length\n ? null\n : divergenceAt(prefix, a, b, pairByA.get(prefix), partnerOfB),\n }\n}\n\nfunction divergenceAt(\n index: number,\n a: readonly DiffStep[],\n b: readonly DiffStep[],\n pairOfA: StepPair | undefined,\n partnerOfB: ReadonlyMap<number, number>,\n): StepDivergence {\n const stepA = a[index]\n const stepB = b[index]\n const base = {\n index,\n a: stepA ? index : null,\n b: stepB ? index : null,\n differences: [] as FieldDifference[],\n }\n if (stepA && stepB && pairOfA?.b === index) {\n return {\n ...base,\n kind: 'changed',\n differences: pairOfA.differences,\n reason: `at index ${index}, ${describe(stepA)} changed: ${pairOfA.differences\n .map((d) => `${d.field} ${String(d.a)} vs ${String(d.b)}`)\n .join('; ')}`,\n }\n }\n const aUnpaired = stepA !== undefined && pairOfA === undefined\n const bUnpaired = stepB !== undefined && !partnerOfB.has(index)\n if (stepA && stepB && aUnpaired && bUnpaired) {\n return {\n ...base,\n kind: 'replaced',\n reason: `at index ${index}, A ran ${describe(stepA)} and B ran ${describe(stepB)}`,\n }\n }\n if (stepA && aUnpaired) {\n return {\n ...base,\n kind: 'only-in-a',\n reason: `at index ${index}, only A has ${describe(stepA)}`,\n }\n }\n if (stepB && bUnpaired) {\n return {\n ...base,\n kind: 'only-in-b',\n reason: `at index ${index}, only B has ${describe(stepB)}`,\n }\n }\n return {\n ...base,\n kind: 'reordered',\n reason: `at index ${index}, A ran ${describe(stepA!)} and B ran ${describe(stepB!)}; both steps occur in the other run at a different position`,\n }\n}\n\nfunction sameShape(x: DiffStep, y: DiffStep): boolean {\n return x.name === y.name && x.kind === y.kind\n}\n\nfunction differencesBetween(x: DiffStep, y: DiffStep): FieldDifference[] {\n const differences: FieldDifference[] = []\n if (x.kind !== y.kind) differences.push({ field: 'kind', a: x.kind, b: y.kind })\n if (x.name !== y.name) differences.push({ field: 'name', a: x.name, b: y.name })\n const fields = [...new Set([...Object.keys(x.fields), ...Object.keys(y.fields)])].sort()\n for (const field of fields) {\n const left = x.fields[field] ?? null\n const right = y.fields[field] ?? null\n if (left !== right) differences.push({ field, a: left, b: right })\n }\n return differences\n}\n\nfunction describe(step: DiffStep): string {\n return `${step.kind} \"${step.name}\"`\n}\n\n// ── agent-eval trace store ────────────────────────────────────────────\n\nexport interface DivergenceReport {\n runA: string\n runB: string\n /** Null when the runs agree step for step. */\n firstDivergenceIndex: number | null\n aStep?: TrajectoryStep\n bStep?: TrajectoryStep\n reason?: string\n commonPrefixLen: number\n /** The step diff behind the first divergence, over trajectory step indexes. */\n diff: StepDiff\n}\n\nexport async function firstDivergenceView(\n store: TraceStore,\n runA: string,\n runB: string,\n): Promise<DivergenceReport> {\n const [a, b] = await Promise.all([buildTrajectory(store, runA), buildTrajectory(store, runB)])\n const diff = diffSteps(a.steps.map(trajectoryDiffStep), b.steps.map(trajectoryDiffStep))\n const divergence = diff.firstDivergence\n return {\n runA,\n runB,\n firstDivergenceIndex: divergence?.index ?? null,\n ...(divergence?.a != null ? { aStep: a.steps[divergence.a] } : {}),\n ...(divergence?.b != null ? { bStep: b.steps[divergence.b] } : {}),\n ...(divergence ? { reason: divergence.reason } : {}),\n commonPrefixLen: diff.commonPrefixLen,\n diff,\n }\n}\n\nfunction trajectoryDiffStep(step: TrajectoryStep): DiffStep {\n const span: Span = step.span\n const fields: Record<string, string | number | boolean | null> = {\n status: span.status ?? null,\n }\n if (span.kind === 'tool') fields.tool = span.toolName\n if (span.kind === 'llm') fields.model = span.model\n if (span.kind === 'judge') fields.dimension = span.dimension\n return { id: span.spanId, name: span.name, kind: span.kind, fields }\n}\n\n// ── flat OTLP spans ───────────────────────────────────────────────────\n\n/**\n * One run's spans as diff steps, in containment order: depth first, siblings\n * by start time, the same order `buildTrajectory` gives a trace store run.\n * Spans whose parent is absent, or whose parents form a cycle, start their own\n * subtree, so every span appears exactly once.\n */\nexport function diffStepsFromSpans(spans: readonly DiagnosisSpan[]): DiffStep[] {\n const ids = new Set(spans.map((span) => span.spanId))\n const children = new Map<string | null, DiagnosisSpan[]>()\n for (const span of spans) {\n const parent =\n span.parentSpanId !== null && ids.has(span.parentSpanId) ? span.parentSpanId : null\n const list = children.get(parent)\n if (list) list.push(span)\n else children.set(parent, [span])\n }\n // Span ids are random per run, so two runs of the same logical trace can carry\n // different ids for \"the same\" span. Siblings that share a start time (a common\n // case: parallel steps a driver starts together) must still sort the same way\n // in both runs, or the diff below reports a false `reordered` divergence purely\n // from spanId noise. End time, kind and name are content, not per-run identity,\n // so they break the tie before spanId, which is the last resort.\n const byStart = (x: DiagnosisSpan, y: DiagnosisSpan) =>\n (x.startMs ?? 0) - (y.startMs ?? 0) ||\n (x.endMs ?? 0) - (y.endMs ?? 0) ||\n x.kind.localeCompare(y.kind) ||\n x.name.localeCompare(y.name) ||\n x.spanId.localeCompare(y.spanId)\n for (const list of children.values()) list.sort(byStart)\n const ordered: DiffStep[] = []\n const visited = new Set<string>()\n const visit = (span: DiagnosisSpan) => {\n if (visited.has(span.spanId)) return\n visited.add(span.spanId)\n ordered.push({\n id: span.spanId,\n name: span.name,\n kind: span.kind,\n fields: { status: span.status, tool: span.toolName, model: span.model },\n })\n for (const child of children.get(span.spanId) ?? []) visit(child)\n }\n for (const root of children.get(null) ?? []) visit(root)\n for (const span of [...spans].sort(byStart)) visit(span)\n return ordered\n}\n","/**\n * RegressionView — compares a candidate slice to a baseline slice on a\n * named metric. Delegates the statistics (Welch's t-test, Cohen's d,\n * IQR stability) to `baseline.ts`.\n *\n * This is the entry point for CI regression gates: \"given runs tagged\n * release=A and release=B, did any metric regress?\"\n */\n\nimport { type BaselineOptions, type BaselineReport, compareToBaseline } from '../baseline'\nimport { runMetricExtractor } from '../trace/query'\nimport type { Run } from '../trace/schema'\nimport type { RunFilter, TraceStore } from '../trace/store'\n\nexport interface RegressionSpec {\n metric: string\n higherIsBetter: boolean\n /** Extract a scalar from a run. Omit it and `metric` must name one of\n * `RUN_METRICS`; any other name is refused. */\n extract?: (run: Run, store: TraceStore) => Promise<number | null>\n}\n\nexport interface RegressionOptions extends BaselineOptions {\n baseline: RunFilter\n candidate: RunFilter\n}\n\nexport async function regressionView(\n store: TraceStore,\n metrics: RegressionSpec[],\n options: RegressionOptions,\n): Promise<BaselineReport> {\n const baselineRuns = await store.listRuns(options.baseline)\n const candidateRuns = await store.listRuns(options.candidate)\n const samples = await Promise.all(\n metrics.map(async (m) => {\n const extract = m.extract ?? runMetricExtractor(m.metric)\n const baseline = await extractAll(baselineRuns, extract, store)\n const candidate = await extractAll(candidateRuns, extract, store)\n return { metric: m.metric, higherIsBetter: m.higherIsBetter, baseline, candidate }\n }),\n )\n return compareToBaseline(samples, options)\n}\n\nasync function extractAll(\n runs: Run[],\n extract: (r: Run, s: TraceStore) => Promise<number | null>,\n store: TraceStore,\n): Promise<number[]> {\n const out: number[] = []\n for (const r of runs) {\n const v = await extract(r, store)\n if (v !== null && Number.isFinite(v)) out.push(v)\n }\n return out\n}\n","/**\n * StuckLoopView — detects when an agent calls the same tool with the\n * same (or structurally similar) arguments ≥ N times in a short window.\n *\n * Rationale: agents that loop are the number-one production failure\n * mode on long-horizon flows. The view returns (runId, toolName,\n * argHash, occurrences, windowMs) for each detected loop plus a\n * fraction of runs affected.\n */\n\nimport { executionTrackByLane } from '../trace/execution-tracks'\nimport { argHash, hasCapturedToolArgs } from '../trace/query'\nimport { isToolSpan, type Span, type ToolSpan } from '../trace/schema'\nimport type { TraceStore } from '../trace/store'\n\nconst DEFAULT_MAX_WINDOW_MS = 60_000\nconst DEFAULT_MAX_INTERVENING_TOOL_CALLS = 0\n\ninterface ScopedToolCall {\n span: ToolSpan\n scopeSpanId: string | null\n laneSpanId: string | null\n}\n\ninterface IndexedToolCall extends ScopedToolCall {\n toolCallIndex: number\n trackId: string\n}\n\nexport interface StuckLoopFinding {\n runId: string\n toolName: string\n argHash: string\n /** Calls in this episode's densest qualifying interval, not the whole-run total. */\n occurrences: number\n spanIds: string[]\n /** Nearest agent ancestor, or the direct parent when ancestry is incomplete. */\n scopeSpanId?: string\n /** Milliseconds between first and last call in the loop. */\n windowMs: number\n}\n\nexport interface StuckLoopReport {\n findings: StuckLoopFinding[]\n affectedRunRatio: number\n totalRuns: number\n}\n\nexport interface StuckLoopOptions {\n /** Minimum call count to flag a loop (default 3). */\n minOccurrences?: number\n /** Maximum time between the first and last repeated call (default 60 seconds). */\n maxWindowMs?: number\n /**\n * Maximum other tool calls allowed between adjacent repeats (default 0).\n * Set to 1 to detect alternating patterns such as A,B,A,B,A.\n */\n maxInterveningToolCalls?: number\n /** Filter to a specific runId; omit to scan the entire corpus. */\n runId?: string\n}\n\nexport async function stuckLoopView(\n store: TraceStore,\n options: StuckLoopOptions = {},\n): Promise<StuckLoopReport> {\n const minOccurrences = options.minOccurrences ?? 3\n const maxWindowMs = options.maxWindowMs ?? DEFAULT_MAX_WINDOW_MS\n const maxInterveningToolCalls =\n options.maxInterveningToolCalls ?? DEFAULT_MAX_INTERVENING_TOOL_CALLS\n if (!Number.isInteger(minOccurrences) || minOccurrences < 1) {\n throw new RangeError('minOccurrences must be a positive integer')\n }\n if (!Number.isFinite(maxWindowMs) || maxWindowMs < 0) {\n throw new RangeError('maxWindowMs must be a finite non-negative number')\n }\n if (!Number.isInteger(maxInterveningToolCalls) || maxInterveningToolCalls < 0) {\n throw new RangeError('maxInterveningToolCalls must be a non-negative integer')\n }\n const runs = options.runId\n ? [{ runId: options.runId }]\n : (await store.listRuns()).map((r) => ({ runId: r.runId }))\n\n const findings: StuckLoopFinding[] = []\n for (const { runId } of runs) {\n const spans = await store.spans({ runId })\n const spansById = new Map(spans.map((span) => [span.spanId, span]))\n const scopedTools: ScopedToolCall[] = spans\n .filter(isToolSpan)\n .map((span, sourceIndex) => ({ span, sourceIndex }))\n .sort((a, b) => a.span.startedAt - b.span.startedAt || a.sourceIndex - b.sourceIndex)\n .map(({ span }) => ({ span, ...executionScope(span, spansById) }))\n const trackByLane = executionTrackByLane(\n scopedTools.map((call) => {\n const direct = call.laneSpanId === null || call.laneSpanId === call.scopeSpanId\n const timed = direct\n ? call.span\n : call.laneSpanId\n ? spansById.get(call.laneSpanId)\n : undefined\n return {\n key: executionKey(call),\n scopeKey: JSON.stringify(call.scopeSpanId),\n start: timed?.startedAt ?? null,\n end: timed?.endedAt ?? null,\n }\n }),\n )\n const nextToolIndexByTrack = new Map<string, number>()\n const orderedTools: IndexedToolCall[] = scopedTools.map((call) => {\n const trackId = trackByLane.get(executionKey(call))!\n const toolCallIndex = nextToolIndexByTrack.get(trackId) ?? 0\n nextToolIndexByTrack.set(trackId, toolCallIndex + 1)\n return { ...call, toolCallIndex, trackId }\n })\n const byKey = new Map<\n string,\n {\n calls: IndexedToolCall[]\n argHash: string\n toolName: string\n scopeSpanId: string | null\n }\n >()\n for (const call of orderedTools) {\n if (!hasCapturedToolArgs(call.span)) continue\n const h = argHash(call.span.args)\n const key = JSON.stringify([call.trackId, call.span.toolName, h])\n const bucket = byKey.get(key) ?? {\n calls: [],\n argHash: h,\n toolName: call.span.toolName,\n scopeSpanId: call.scopeSpanId,\n }\n bucket.calls.push(call)\n byKey.set(key, bucket)\n }\n for (const { calls, argHash: h, toolName, scopeSpanId } of byKey.values()) {\n if (calls.length < minOccurrences) continue\n let episodeStart = 0\n for (let episodeEnd = 1; episodeEnd <= calls.length; episodeEnd += 1) {\n const previous = calls[episodeEnd - 1]!\n const next = calls[episodeEnd]\n const episodeEnded =\n next === undefined ||\n next.span.startedAt - previous.span.startedAt > maxWindowMs ||\n next.toolCallIndex - previous.toolCallIndex - 1 > maxInterveningToolCalls ||\n !callsAreSerial(previous, next)\n if (!episodeEnded) continue\n\n const episode = calls.slice(episodeStart, episodeEnd)\n let left = 0\n let bestStart = 0\n let bestEnd = -1\n for (let right = 0; right < episode.length; right += 1) {\n while (episode[right]!.span.startedAt - episode[left]!.span.startedAt > maxWindowMs) {\n left += 1\n }\n if (right - left > bestEnd - bestStart) {\n bestStart = left\n bestEnd = right\n }\n }\n if (bestEnd - bestStart + 1 >= minOccurrences) {\n const loop = episode.slice(bestStart, bestEnd + 1)\n const first = loop[0]!.span.startedAt\n const last = loop[loop.length - 1]!.span.startedAt\n findings.push({\n runId,\n toolName,\n argHash: h,\n occurrences: loop.length,\n spanIds: loop.map((call) => call.span.spanId),\n ...(scopeSpanId ? { scopeSpanId } : {}),\n windowMs: last - first,\n })\n }\n episodeStart = episodeEnd\n }\n }\n }\n\n const affectedRuns = new Set(findings.map((f) => f.runId))\n return {\n findings,\n affectedRunRatio: runs.length > 0 ? affectedRuns.size / runs.length : 0,\n totalRuns: runs.length,\n }\n}\n\nfunction laneKey(call: Pick<ScopedToolCall, 'scopeSpanId' | 'laneSpanId'>): string {\n return JSON.stringify([call.scopeSpanId, call.laneSpanId])\n}\n\nfunction executionKey(call: ScopedToolCall): string {\n return call.laneSpanId === null || call.laneSpanId === call.scopeSpanId\n ? JSON.stringify([call.scopeSpanId, call.span.spanId])\n : laneKey(call)\n}\n\nfunction executionScope(\n span: ToolSpan,\n spansById: ReadonlyMap<string, Span>,\n): { scopeSpanId: string | null; laneSpanId: string | null } {\n const directParent = span.parentSpanId\n if (!directParent) return { scopeSpanId: null, laneSpanId: null }\n\n let currentId: string | undefined = directParent\n let laneSpanId: string | null = null\n const seen = new Set<string>()\n while (currentId && !seen.has(currentId)) {\n seen.add(currentId)\n const current = spansById.get(currentId)\n if (!current) return { scopeSpanId: directParent, laneSpanId: directParent }\n if (current.kind === 'agent') {\n return { scopeSpanId: current.spanId, laneSpanId: laneSpanId ?? current.spanId }\n }\n laneSpanId = current.spanId\n currentId = current.parentSpanId\n }\n return { scopeSpanId: directParent, laneSpanId: directParent }\n}\n\nfunction callsAreSerial(previous: IndexedToolCall, next: IndexedToolCall): boolean {\n return previous.span.endedAt !== undefined && previous.span.endedAt <= next.span.startedAt\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { s as zQuantile } from "./internal-BMFSR8Ns.js";
2
- import { u as normalCdf } from "./paired-arms-DKZFVPt8.js";
1
+ import { s as zQuantile } from "./internal-BOe3zmjQ.js";
2
+ import { u as normalCdf } from "./paired-arms-DpS_h8ON.js";
3
3
  //#region src/statistics/power-and-mde.ts
4
4
  /**
5
5
  * Required N per arm for a two-sample comparison at target effect size,
@@ -12,7 +12,8 @@ function requiredSampleSize(opts) {
12
12
  if (!Number.isFinite(effect) || effect <= 0) return Infinity;
13
13
  const alpha = opts.alpha ?? .05;
14
14
  const power = opts.power ?? .8;
15
- const n = 2 * ((zQuantile(opts.twoSided ?? true ? 1 - alpha / 2 : 1 - alpha) + zQuantile(power)) / effect) ** 2;
15
+ const twoSided = opts.twoSided ?? true;
16
+ const n = 2 * ((zQuantile(twoSided ? 1 - alpha / 2 : 1 - alpha) + zQuantile(power)) / effect) ** 2;
16
17
  return Math.ceil(n);
17
18
  }
18
19
  /**
@@ -29,7 +30,8 @@ function requiredPairedSampleSize(opts) {
29
30
  if (!Number.isFinite(effect) || effect <= 0) return Infinity;
30
31
  const alpha = opts.alpha ?? .05;
31
32
  const power = opts.power ?? .8;
32
- const zAlpha = zQuantile(opts.twoSided ?? true ? 1 - alpha / 2 : 1 - alpha);
33
+ const twoSided = opts.twoSided ?? true;
34
+ const zAlpha = zQuantile(twoSided ? 1 - alpha / 2 : 1 - alpha);
33
35
  const zBeta = zQuantile(power);
34
36
  return Math.ceil(((zAlpha + zBeta) / effect) ** 2);
35
37
  }
@@ -43,7 +45,8 @@ function pairedMde(opts) {
43
45
  if (!Number.isFinite(opts.nPaired) || opts.nPaired <= 0) return Infinity;
44
46
  const alpha = opts.alpha ?? .05;
45
47
  const power = opts.power ?? .8;
46
- return (zQuantile(opts.twoSided ?? true ? 1 - alpha / 2 : 1 - alpha) + zQuantile(power)) / Math.sqrt(opts.nPaired);
48
+ const twoSided = opts.twoSided ?? true;
49
+ return (zQuantile(twoSided ? 1 - alpha / 2 : 1 - alpha) + zQuantile(power)) / Math.sqrt(opts.nPaired);
47
50
  }
48
51
  /**
49
52
  * Number of paired observations needed for a McNemar test to reach a target
@@ -94,4 +97,4 @@ function mcnemarPower(opts) {
94
97
  //#endregion
95
98
  export { requiredSampleSize as a, requiredPairedSampleSize as i, mcnemarRequiredN as n, pairedMde as r, mcnemarPower as t };
96
99
 
97
- //# sourceMappingURL=power-and-mde-K_CmO3dc.js.map
100
+ //# sourceMappingURL=power-and-mde-bie6i965.js.map