@tangle-network/agent-eval 0.188.0 → 0.190.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 (240) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +3 -2
  3. package/dist/adapters/http.d.ts +11 -2
  4. package/dist/adapters/http.d.ts.map +1 -1
  5. package/dist/adapters/http.js +75 -7
  6. package/dist/adapters/http.js.map +1 -1
  7. package/dist/{agent-profile-CfbuZk8b.d.ts → agent-profile-DNV_U6QR.d.ts} +3 -3
  8. package/dist/{agent-profile-CfbuZk8b.d.ts.map → agent-profile-DNV_U6QR.d.ts.map} +1 -1
  9. package/dist/analyst/index.d.ts +75 -12
  10. package/dist/analyst/index.d.ts.map +1 -1
  11. package/dist/analyst/index.js +213 -8
  12. package/dist/analyst/index.js.map +1 -1
  13. package/dist/{analyze-runs-CLz5O01x.d.ts → analyze-runs-BvK4-wSs.d.ts} +5 -5
  14. package/dist/{analyze-runs-CLz5O01x.d.ts.map → analyze-runs-BvK4-wSs.d.ts.map} +1 -1
  15. package/dist/{analyze-runs-DCY7LMt4.js → analyze-runs-SCSWYpy0.js} +17 -9
  16. package/dist/analyze-runs-SCSWYpy0.js.map +1 -0
  17. package/dist/{benchmark-command-CdUGku3W.js → benchmark-command-TZlsuAh9.js} +12 -12
  18. package/dist/{benchmark-command-CdUGku3W.js.map → benchmark-command-TZlsuAh9.js.map} +1 -1
  19. package/dist/benchmarks/index.d.ts +4 -4
  20. package/dist/benchmarks/index.js +3 -3
  21. package/dist/campaign/index.d.ts +7 -7
  22. package/dist/campaign/index.js +8 -8
  23. package/dist/{campaign-DZuysBbS.js → campaign-_29Qpc9Z.js} +258 -13
  24. package/dist/campaign-_29Qpc9Z.js.map +1 -0
  25. package/dist/{campaign-evidence-IF1p0oDa.js → campaign-evidence-CWF8pEIk.js} +9 -8
  26. package/dist/campaign-evidence-CWF8pEIk.js.map +1 -0
  27. package/dist/{chat-client-C4dg7Ng_.js → chat-client-DbH06Ek6.js} +2 -2
  28. package/dist/{chat-client-C4dg7Ng_.js.map → chat-client-DbH06Ek6.js.map} +1 -1
  29. package/dist/{chat-json-call-JVGbfx95.js → chat-json-call-B0xTEY0I.js} +2 -2
  30. package/dist/{chat-json-call-JVGbfx95.js.map → chat-json-call-B0xTEY0I.js.map} +1 -1
  31. package/dist/claim-BGOUqoMq.d.ts +38 -0
  32. package/dist/claim-BGOUqoMq.d.ts.map +1 -0
  33. package/dist/cli.js +3 -3
  34. package/dist/{client-CFj9dfcn.d.ts → client-BhEyAdfe.d.ts} +4 -4
  35. package/dist/{client-CFj9dfcn.d.ts.map → client-BhEyAdfe.d.ts.map} +1 -1
  36. package/dist/{client-CXE-U1SA.js → client-QwJpsNYU.js} +5 -1
  37. package/dist/client-QwJpsNYU.js.map +1 -0
  38. package/dist/contract/index.d.ts +10 -10
  39. package/dist/contract/index.js +22 -13
  40. package/dist/contract/index.js.map +1 -1
  41. package/dist/deep-freeze-CurcOQg3.js +16 -0
  42. package/dist/deep-freeze-CurcOQg3.js.map +1 -0
  43. package/dist/{default-registry-BtZ_t-9b.js → default-registry-16I-Bxlf.js} +4 -3
  44. package/dist/default-registry-16I-Bxlf.js.map +1 -0
  45. package/dist/{default-registry-BLsGfnoO.d.ts → default-registry-B5HrwrPM.d.ts} +6 -6
  46. package/dist/{default-registry-BLsGfnoO.d.ts.map → default-registry-B5HrwrPM.d.ts.map} +1 -1
  47. package/dist/{define-agent-eval-CsicmDxt.d.ts → define-agent-eval-BWeyIpny.d.ts} +8 -7
  48. package/dist/define-agent-eval-BWeyIpny.d.ts.map +1 -0
  49. package/dist/{define-agent-eval-ChtddasD.js → define-agent-eval-DrGfrCTZ.js} +8 -8
  50. package/dist/{define-agent-eval-ChtddasD.js.map → define-agent-eval-DrGfrCTZ.js.map} +1 -1
  51. package/dist/diagnosis/index.d.ts +3 -3
  52. package/dist/diagnosis/index.d.ts.map +1 -1
  53. package/dist/diagnosis/index.js +4 -4
  54. package/dist/diagnosis/index.js.map +1 -1
  55. package/dist/{dspy-rlm-engine-sPX7B5ac.js → dspy-rlm-engine-CTIGomgr.js} +3 -3
  56. package/dist/{dspy-rlm-engine-sPX7B5ac.js.map → dspy-rlm-engine-CTIGomgr.js.map} +1 -1
  57. package/dist/{engine-Gw2n1irt.d.ts → engine-BkLCnZQX.d.ts} +4 -4
  58. package/dist/{engine-Gw2n1irt.d.ts.map → engine-BkLCnZQX.d.ts.map} +1 -1
  59. package/dist/{eval-campaign-CeotQ1ZR.js → eval-campaign-CMKRjWl0.js} +3 -3
  60. package/dist/{eval-campaign-CeotQ1ZR.js.map → eval-campaign-CMKRjWl0.js.map} +1 -1
  61. package/dist/evaluation.d.ts +2 -2
  62. package/dist/{exact-types-CrhPoDp8.d.ts → exact-types-CfpP2A4Z.d.ts} +2 -2
  63. package/dist/{exact-types-CrhPoDp8.d.ts.map → exact-types-CfpP2A4Z.d.ts.map} +1 -1
  64. package/dist/experiment/index.d.ts +2 -1
  65. package/dist/experiment/index.d.ts.map +1 -1
  66. package/dist/experiment/index.js +1 -1
  67. package/dist/exporters-XMCYJlqT.d.ts +161 -0
  68. package/dist/exporters-XMCYJlqT.d.ts.map +1 -0
  69. package/dist/{external-optimizer-process-D3BLpZyR.js → external-optimizer-process-Mc83UoWl.js} +3 -3
  70. package/dist/{external-optimizer-process-D3BLpZyR.js.map → external-optimizer-process-Mc83UoWl.js.map} +1 -1
  71. package/dist/{external-optimizer-subprocess-BrfsgL92.js → external-optimizer-subprocess-C0dHXH8e.js} +3 -3
  72. package/dist/{external-optimizer-subprocess-BrfsgL92.js.map → external-optimizer-subprocess-C0dHXH8e.js.map} +1 -1
  73. package/dist/{feedback-trajectory-BJi4t3Vf.d.ts → feedback-trajectory-BYjh7NEM.d.ts} +4 -4
  74. package/dist/{feedback-trajectory-BJi4t3Vf.d.ts.map → feedback-trajectory-BYjh7NEM.d.ts.map} +1 -1
  75. package/dist/hosted/index.d.ts +2 -2
  76. package/dist/hosted/index.d.ts.map +1 -1
  77. package/dist/hosted/index.js +1 -1
  78. package/dist/{index-Ca0_IjFu.d.ts → index-2e_MTznj.d.ts} +7 -7
  79. package/dist/{index-Ca0_IjFu.d.ts.map → index-2e_MTznj.d.ts.map} +1 -1
  80. package/dist/{index-BD1oSbHr.d.ts → index-BR_R1eBE.d.ts} +11 -11
  81. package/dist/{index-BD1oSbHr.d.ts.map → index-BR_R1eBE.d.ts.map} +1 -1
  82. package/dist/{index-u0d1Jp4F.d.ts → index-BqDJ6fUK.d.ts} +45 -24
  83. package/dist/index-BqDJ6fUK.d.ts.map +1 -0
  84. package/dist/{index-eTTnPe3r.d.ts → index-Ci0-EYzc.d.ts} +678 -249
  85. package/dist/index-Ci0-EYzc.d.ts.map +1 -0
  86. package/dist/{index-B-s8qDRJ.d.ts → index-D4pC7Tvd.d.ts} +4 -3
  87. package/dist/index-D4pC7Tvd.d.ts.map +1 -0
  88. package/dist/index.d.ts +81 -32
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +385 -41
  91. package/dist/index.js.map +1 -1
  92. package/dist/{insight-report-DeE1solA.d.ts → insight-report-CvPkEIcC.d.ts} +13 -6
  93. package/dist/insight-report-CvPkEIcC.d.ts.map +1 -0
  94. package/dist/jev.d.ts +2 -2
  95. package/dist/jev.js +2 -1
  96. package/dist/jev.js.map +1 -1
  97. package/dist/{journal-BuRLm6ho.js → journal-CDP0Vhx_.js} +262 -115
  98. package/dist/journal-CDP0Vhx_.js.map +1 -0
  99. package/dist/{kind-factory-g1ROw67A.js → kind-factory-ByZeXDqF.js} +4 -3
  100. package/dist/{kind-factory-g1ROw67A.js.map → kind-factory-ByZeXDqF.js.map} +1 -1
  101. package/dist/ledger-core/index.d.ts +2 -2
  102. package/dist/ledger-core/index.js +1 -1
  103. package/dist/{llm-client-DF9bPNYA.js → llm-client-BqmRnibA.js} +2 -2
  104. package/dist/{llm-client-DF9bPNYA.js.map → llm-client-BqmRnibA.js.map} +1 -1
  105. package/dist/{llm-judge-DF18J3CR.js → llm-judge-bwbgDMP0.js} +1895 -1379
  106. package/dist/llm-judge-bwbgDMP0.js.map +1 -0
  107. package/dist/{matrix-BvA1WUpj.d.ts → matrix-CNypzP-v.d.ts} +2 -2
  108. package/dist/{matrix-BvA1WUpj.d.ts.map → matrix-CNypzP-v.d.ts.map} +1 -1
  109. package/dist/meta-eval/index.d.ts +1 -1
  110. package/dist/mint-C9Lojt1V.d.ts +147 -0
  111. package/dist/mint-C9Lojt1V.d.ts.map +1 -0
  112. package/dist/{mint-B4zgSAJW.js → mint-iCD-W43s.js} +30 -12
  113. package/dist/mint-iCD-W43s.js.map +1 -0
  114. package/dist/multishot/golden/index.d.ts +1 -1
  115. package/dist/multishot/index.d.ts +2 -2
  116. package/dist/openapi.json +1 -1
  117. package/dist/pipelines/index.d.ts +1 -1
  118. package/dist/{prime-protocol-CF8Z43Fj.d.ts → prime-protocol-Fwvctmhm.d.ts} +2 -2
  119. package/dist/{prime-protocol-CF8Z43Fj.d.ts.map → prime-protocol-Fwvctmhm.d.ts.map} +1 -1
  120. package/dist/{produced-state-xsMMjYVc.js → produced-state-zSaGDC_Q.js} +4 -4
  121. package/dist/{produced-state-xsMMjYVc.js.map → produced-state-zSaGDC_Q.js.map} +1 -1
  122. package/dist/{raw-provider-sink-C96CcqeG.js → raw-provider-sink-DIRCZjP_.js} +2 -2
  123. package/dist/{raw-provider-sink-C96CcqeG.js.map → raw-provider-sink-DIRCZjP_.js.map} +1 -1
  124. package/dist/{redact-DxwlUAvh.js → redact-8fX8-uKr.js} +16 -7
  125. package/dist/redact-8fX8-uKr.js.map +1 -0
  126. package/dist/{redact-DPoH1-OW.d.ts → redact-Du3WKUOP.d.ts} +7 -5
  127. package/dist/redact-Du3WKUOP.d.ts.map +1 -0
  128. package/dist/{registry-BM2Kdk8T.d.ts → registry-mAQVtuLx.d.ts} +3 -3
  129. package/dist/{registry-BM2Kdk8T.d.ts.map → registry-mAQVtuLx.d.ts.map} +1 -1
  130. package/dist/{release-confidence-CG4Z9QLB.d.ts → release-confidence-CkoXFx9I.d.ts} +3 -3
  131. package/dist/{release-confidence-CG4Z9QLB.d.ts.map → release-confidence-CkoXFx9I.d.ts.map} +1 -1
  132. package/dist/{release-confidence-RanRUXRW.js → release-confidence-XQm_xjhR.js} +3 -3
  133. package/dist/release-confidence-XQm_xjhR.js.map +1 -0
  134. package/dist/reporting.d.ts +3 -3
  135. package/dist/reporting.js +1 -1
  136. package/dist/{researcher-Bjcd_8T_.d.ts → researcher-DojEMb7E.d.ts} +3 -3
  137. package/dist/{researcher-Bjcd_8T_.d.ts.map → researcher-DojEMb7E.d.ts.map} +1 -1
  138. package/dist/{reward-hacking-DrKXJtgH.d.ts → reward-hacking-Cyk-QHk9.d.ts} +2 -2
  139. package/dist/{reward-hacking-DrKXJtgH.d.ts.map → reward-hacking-Cyk-QHk9.d.ts.map} +1 -1
  140. package/dist/rl.d.ts +10 -8
  141. package/dist/rl.d.ts.map +1 -1
  142. package/dist/rl.js +7 -6
  143. package/dist/rl.js.map +1 -1
  144. package/dist/rollout/index.d.ts +5 -4
  145. package/dist/rollout/index.js +2 -2
  146. package/dist/{rollout-C0Ifk3vK.js → rollout-B-XBReTC.js} +2 -2
  147. package/dist/{rollout-C0Ifk3vK.js.map → rollout-B-XBReTC.js.map} +1 -1
  148. package/dist/{rubric-predictive-validity-B14yBAmM.d.ts → rubric-predictive-validity-DIUwJJ9Y.d.ts} +2 -2
  149. package/dist/{rubric-predictive-validity-B14yBAmM.d.ts.map → rubric-predictive-validity-DIUwJJ9Y.d.ts.map} +1 -1
  150. package/dist/{run-record-tg_5H2IK.js → run-record-BRlM99rX.js} +41 -3
  151. package/dist/run-record-BRlM99rX.js.map +1 -0
  152. package/dist/{run-record-xEULpgML.d.ts → run-record-DF-fuu31.d.ts} +58 -6
  153. package/dist/run-record-DF-fuu31.d.ts.map +1 -0
  154. package/dist/{run-record-CNNzWGpB.js → run-record-DnMFyg_g.js} +26 -13
  155. package/dist/run-record-DnMFyg_g.js.map +1 -0
  156. package/dist/{schema-BzWDXhOR.d.ts → schema-B5LtYvn9.d.ts} +7 -5
  157. package/dist/{schema-BzWDXhOR.d.ts.map → schema-B5LtYvn9.d.ts.map} +1 -1
  158. package/dist/schema-C1aaAxTf.js.map +1 -1
  159. package/dist/search-ledger-types-C9anU2DX.d.ts +489 -0
  160. package/dist/search-ledger-types-C9anU2DX.d.ts.map +1 -0
  161. package/dist/{semantic-concept-judge-70LxrCSL.js → semantic-concept-judge-BY0me-Jq.js} +6 -6
  162. package/dist/{semantic-concept-judge-70LxrCSL.js.map → semantic-concept-judge-BY0me-Jq.js.map} +1 -1
  163. package/dist/{server-C8XvqCg8.js → server-DnJCDLf8.js} +2 -2
  164. package/dist/{server-C8XvqCg8.js.map → server-DnJCDLf8.js.map} +1 -1
  165. package/dist/{skillopt-optimization-method-C60h0Got.js → skillopt-optimization-method-CfnQEjgT.js} +364 -19
  166. package/dist/skillopt-optimization-method-CfnQEjgT.js.map +1 -0
  167. package/dist/{spans-W0o8pbEg.d.ts → spans-D1RftP_w.d.ts} +2 -2
  168. package/dist/{spans-W0o8pbEg.d.ts.map → spans-D1RftP_w.d.ts.map} +1 -1
  169. package/dist/{statistical-heldout-CPv-y6sK.d.ts → statistical-heldout-CLpjYffJ.d.ts} +4 -38
  170. package/dist/statistical-heldout-CLpjYffJ.d.ts.map +1 -0
  171. package/dist/{store-otlp-DPgzSQKS.js → store-otlp-84DH0OEH.js} +2 -2
  172. package/dist/{store-otlp-DPgzSQKS.js.map → store-otlp-84DH0OEH.js.map} +1 -1
  173. package/dist/{store-tool-spans-C2lLwDzg.d.ts → store-tool-spans-BBXs8hDP.d.ts} +6 -6
  174. package/dist/{store-tool-spans-C2lLwDzg.d.ts.map → store-tool-spans-BBXs8hDP.d.ts.map} +1 -1
  175. package/dist/{store-tool-spans-ClPxcvh2.js → store-tool-spans-DSbYpl7f.js} +5 -5
  176. package/dist/{store-tool-spans-ClPxcvh2.js.map → store-tool-spans-DSbYpl7f.js.map} +1 -1
  177. package/dist/{summary-report-SNvJoAvD.d.ts → summary-report-DvvesgR_.d.ts} +5 -4
  178. package/dist/summary-report-DvvesgR_.d.ts.map +1 -0
  179. package/dist/supervisor-run/index.d.ts +2 -2
  180. package/dist/{tool-groups-CCQqAa16.d.ts → tool-groups-W7MJobX_.d.ts} +3 -3
  181. package/dist/tool-groups-W7MJobX_.d.ts.map +1 -0
  182. package/dist/trace-repair/index.d.ts +2 -2
  183. package/dist/traces.d.ts +6 -66
  184. package/dist/traces.d.ts.map +1 -1
  185. package/dist/traces.js +7 -196
  186. package/dist/traces.js.map +1 -1
  187. package/dist/{types-DDp2Mb8Y.d.ts → types-Bay1dh2o.d.ts} +5 -5
  188. package/dist/{types-DDp2Mb8Y.d.ts.map → types-Bay1dh2o.d.ts.map} +1 -1
  189. package/dist/{types-B1vDZ694.d.ts → types-BvmWNjjs.d.ts} +3 -3
  190. package/dist/{types-B1vDZ694.d.ts.map → types-BvmWNjjs.d.ts.map} +1 -1
  191. package/dist/{types-C8VBbiw6.js → types-CW5b90JH.js} +2 -15
  192. package/dist/types-CW5b90JH.js.map +1 -0
  193. package/dist/{types-5ak2PqnJ.d.ts → types-Cvqy7asY.d.ts} +3 -3
  194. package/dist/{types-5ak2PqnJ.d.ts.map → types-Cvqy7asY.d.ts.map} +1 -1
  195. package/dist/{types-Tp5WhYL9.d.ts → types-DRNlLbBR.d.ts} +2 -2
  196. package/dist/{types-Tp5WhYL9.d.ts.map → types-DRNlLbBR.d.ts.map} +1 -1
  197. package/dist/wire/index.d.ts +1 -1
  198. package/dist/wire/index.js +1 -1
  199. package/docs/adapters-observability.md +31 -27
  200. package/docs/charter.md +1 -1
  201. package/docs/code-agent-intake.md +1 -1
  202. package/docs/design/mlbenchmarks-book-review.md +1 -1
  203. package/docs/distributed-driver.md +1 -5
  204. package/docs/experiment.md +15 -0
  205. package/docs/insight-report.md +3 -2
  206. package/docs/public-api.md +174 -68
  207. package/docs/redaction.md +7 -1
  208. package/docs/rollout.md +5 -5
  209. package/docs/search-ledger.md +174 -0
  210. package/docs/trace-contracts.md +48 -3
  211. package/package.json +1 -1
  212. package/dist/analyze-runs-DCY7LMt4.js.map +0 -1
  213. package/dist/campaign-DZuysBbS.js.map +0 -1
  214. package/dist/campaign-evidence-IF1p0oDa.js.map +0 -1
  215. package/dist/client-CXE-U1SA.js.map +0 -1
  216. package/dist/default-registry-BtZ_t-9b.js.map +0 -1
  217. package/dist/define-agent-eval-CsicmDxt.d.ts.map +0 -1
  218. package/dist/index-B-s8qDRJ.d.ts.map +0 -1
  219. package/dist/index-eTTnPe3r.d.ts.map +0 -1
  220. package/dist/index-u0d1Jp4F.d.ts.map +0 -1
  221. package/dist/insight-report-DeE1solA.d.ts.map +0 -1
  222. package/dist/journal-BuRLm6ho.js.map +0 -1
  223. package/dist/llm-judge-DF18J3CR.js.map +0 -1
  224. package/dist/mint-B4zgSAJW.js.map +0 -1
  225. package/dist/mint-CJFSYBIU.d.ts +0 -285
  226. package/dist/mint-CJFSYBIU.d.ts.map +0 -1
  227. package/dist/redact-DPoH1-OW.d.ts.map +0 -1
  228. package/dist/redact-DxwlUAvh.js.map +0 -1
  229. package/dist/release-confidence-RanRUXRW.js.map +0 -1
  230. package/dist/run-record-CNNzWGpB.js.map +0 -1
  231. package/dist/run-record-tg_5H2IK.js.map +0 -1
  232. package/dist/run-record-xEULpgML.d.ts.map +0 -1
  233. package/dist/search-ledger-types-D7KDVg-o.d.ts +0 -302
  234. package/dist/search-ledger-types-D7KDVg-o.d.ts.map +0 -1
  235. package/dist/skillopt-optimization-method-C60h0Got.js.map +0 -1
  236. package/dist/statistical-heldout-CPv-y6sK.d.ts.map +0 -1
  237. package/dist/summary-report-SNvJoAvD.d.ts.map +0 -1
  238. package/dist/tool-groups-CCQqAa16.d.ts.map +0 -1
  239. package/dist/types-C8VBbiw6.js.map +0 -1
  240. package/docs/search-history-receipts.md +0 -232
@@ -1 +1 @@
1
- {"version":3,"file":"produced-state-xsMMjYVc.js","names":[],"sources":["../src/agent-profile.ts","../src/completion-verifier.ts","../src/produced-state.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport type { AgentProfile } from '@tangle-network/agent-interface'\nimport { type HarnessType, harnessSupportsModel } from '@tangle-network/agent-interface'\nimport { ValidationError } from './errors'\nimport { canonicalString } from './ledger-core/canonical'\n\nexport type { AgentProfile, HarnessType } from '@tangle-network/agent-interface'\n\n/**\n * The agentic coding harnesses an eval sweeps by default — the ones we care about\n * ranking. This is the SINGLE source of that list; consumers import it instead of\n * re-declaring their own (a re-declared list is how the fleet drifts). Pass an\n * explicit `harnesses` (e.g. `harnessTypeSchema.options` for literally every known\n * harness) to widen beyond these.\n */\nexport const CODING_HARNESSES: readonly HarnessType[] = [\n 'opencode',\n 'claude-code',\n 'codex',\n 'kimi-code',\n]\n\nexport interface ProfileAxisSpec {\n /** The domain profile to sweep. Its prompt/tools/skills are held fixed; only the\n * harness and model vary. `model.default` is the fallback model. */\n base: AgentProfile\n /** Harnesses to cross. Default: {@link CODING_HARNESSES}. */\n harnesses?: readonly HarnessType[]\n /** Models to cross. Default: `[base.model.default]` — one model, i.e. today's\n * single-model behaviour, so omitting this never changes an existing run. */\n models?: readonly string[]\n /** Force every (harness, model) pair verbatim, even ones the harness can't run —\n * for deliberately testing failure modes. Default (false): SNAP instead — a\n * vendor-locked harness runs only the swept models in its family, or its native\n * default when it supports none, so no harness is dropped and none gets a\n * guaranteed-failing foreign-model cell. */\n keepIncompatible?: boolean\n}\n\n/** Model sentinel for a vendor-locked harness that supports none of the swept models:\n * it carries no provider prefix, so `harnessSupportsModel` accepts it and the harness\n * resolves it to its own native default model at runtime (e.g. kimi-code → its Kimi\n * model). Lets `expandProfileAxes` snap-instead-of-drop without a per-harness flagship\n * table that would rot as router catalogs change. */\nexport const HARNESS_NATIVE_MODEL = 'default'\n\n/**\n * Expand a base profile across the harness × model matrix into the `AgentProfile[]`\n * that `runProfileMatrix` / `selfImprove` score — the ONE place \"which harnesses ×\n * which models do we evaluate\" lives, so no product hand-rolls its own harness list\n * or column→profile mapping (the pattern that let those copies drift and silently\n * break the harness pivot).\n *\n * Each cell clones `base`, sets the canonical top-level `harness` and `model.default`,\n * and stamps `metadata.harness` + `metadata.harnessModel` for matrix grouping. Both\n * metadata fields are hash-bearing, so every cell gets a distinct `agentProfileId` row\n * and results join back by harness/model via {@link harnessAxisOf} with no\n * hand-recomputed key. A vendor-locked harness snaps to its family's swept models — or\n * its native default ({@link HARNESS_NATIVE_MODEL}) when it supports none — so every\n * requested harness runs; `keepIncompatible` forces every pair verbatim.\n *\n * Omit `harnesses`/`models` to sweep the full default set — the \"turn it on for\n * everything we care about\" switch, identical in shape whether one harness or all.\n */\nexport function expandProfileAxes(spec: ProfileAxisSpec): AgentProfile[] {\n const harnesses = spec.harnesses ?? CODING_HARNESSES\n if (harnesses.length === 0) throw new ValidationError('expandProfileAxes: no harnesses to sweep')\n const baseModel = spec.base.model?.default\n const models = spec.models ?? (baseModel ? [baseModel] : [])\n if (models.length === 0) {\n throw new ValidationError(\n 'expandProfileAxes: no models to sweep — base profile has no model.default and none were supplied',\n )\n }\n const out: AgentProfile[] = []\n const seen = new Set<string>()\n for (const harness of harnesses) {\n // A universal (router-backed) harness — opencode/pi/claudish — runs every swept\n // model. A vendor-locked harness — codex/claude-code/kimi-code — runs only the\n // swept models in its own family; when it supports NONE of them it snaps to its\n // native default (the `HARNESS_NATIVE_MODEL` sentinel it resolves at runtime)\n // rather than being dropped, so every requested harness still appears in the\n // sweep on a model it can actually run — e.g. sweeping `deepseek/x` puts opencode\n // on deepseek and kimi-code on its own Kimi model, a real head-to-head.\n // `keepIncompatible` forces every (harness, model) pair verbatim (failure-mode runs).\n const supported = spec.keepIncompatible\n ? models\n : models.filter((model) => harnessSupportsModel(harness, model))\n const effective = supported.length > 0 ? supported : [HARNESS_NATIVE_MODEL]\n for (const model of effective) {\n const profile: AgentProfile = {\n ...spec.base,\n harness,\n name: `${spec.base.name ?? 'agent'}/${harness}/${model}`,\n model: { ...spec.base.model, default: model },\n metadata: { ...(spec.base.metadata ?? {}), harness, harnessModel: model },\n }\n const id = agentProfileId(profile)\n if (seen.has(id)) continue\n seen.add(id)\n out.push(profile)\n }\n }\n if (out.length === 0) {\n // Unreachable in normal use — snapping guarantees ≥1 cell per harness — but keep a\n // fail-closed guard so a future refactor can't silently produce an empty sweep.\n throw new ValidationError(\n `expandProfileAxes: produced no profiles (harnesses=[${harnesses.join(', ')}], models=[${models.join(', ')}]).`,\n )\n }\n return out\n}\n\n/**\n * Read the (harness, model) a matrix cell ran under, off a profile or a result row's\n * profile — the join-back for a `byHarness` pivot. Returns undefined when the profile\n * wasn't produced by {@link expandProfileAxes}. Callers group `result.byProfile` by\n * this instead of recomputing an id (recomputing the wrong key is what broke the pivot\n * in the hand-rolled copies).\n */\nexport function harnessAxisOf(\n profile: Pick<AgentProfile, 'metadata'>,\n): { harness: HarnessType; model: string } | undefined {\n const m = profile.metadata as Record<string, unknown> | undefined\n const harness = m?.harness\n const model = m?.harnessModel\n if (typeof harness === 'string' && typeof model === 'string') {\n return { harness: harness as HarnessType, model }\n }\n return undefined\n}\n\n/**\n * Collision-resistant, path-safe, human-readable profile id for eval artifacts.\n * Scorecard joins still use `agentProfileHash`; this id is for run ids, matrix\n * keys, and directory names where two profiles must not collapse onto one row.\n * The suffix is the first 64 bits of the behaviour hash, enough for ordinary\n * eval matrices while keeping filenames readable.\n */\nexport function agentProfileId(profile: AgentProfile): string {\n const label = pathSafeProfileLabel(agentProfileDisplayLabel(profile)) ?? 'profile'\n return `${label}-${agentProfileHash(profile).slice(0, 16)}`\n}\n\n/**\n * Model snapshot used for `RunRecord.model`. Eval surfaces require a concrete\n * model id because run records reject bare/missing model aliases.\n */\nexport function agentProfileModelId(profile: AgentProfile): string {\n const model = profile.model?.default?.trim()\n if (!model) {\n const label = agentProfileDisplayLabel(profile) ?? 'unnamed profile'\n throw new ValidationError(\n `AgentProfile \"${label}\" has no model.default — cannot record eval run`,\n )\n }\n return model\n}\n\nfunction agentProfileDisplayLabel(profile: AgentProfile): string | undefined {\n return profile.name?.trim() || profile.version?.trim() || undefined\n}\n\nfunction pathSafeProfileLabel(label: string | undefined): string | undefined {\n const safe = label\n ?.trim()\n .replace(/[^A-Za-z0-9._-]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n return safe || undefined\n}\n\nfunction compact<T extends Record<string, unknown>>(input: T): Partial<T> {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(input)) {\n if (value !== undefined) out[key] = value\n }\n return out as Partial<T>\n}\n\n/**\n * Deterministic behaviour identity for the canonical\n * `@tangle-network/agent-interface` AgentProfile.\n *\n * `name` and `description` are labels and do not affect the hash. Profile\n * `version`, prompt, model hints, tools, resources, hooks, modes, permissions,\n * and extensions do affect the hash. Resource array order is hash-bearing\n * because mount order can change agent behaviour. Undefined fields are treated\n * as absent; explicit `null` fields remain hash-bearing.\n */\nexport function agentProfileHash(profile: AgentProfile): string {\n const model = agentProfileModelId(profile)\n const behaviour = compact({\n ...profile,\n name: undefined,\n description: undefined,\n tags: profile.tags ? [...profile.tags].sort() : undefined,\n model: compact({ ...profile.model, default: model }),\n })\n return createHash('sha256').update(canonicalString(behaviour)).digest('hex')\n}\n","/**\n * Completion verifier — the task-completion oracle.\n *\n * Answers the only eval question that is not a proxy: did the agent actually\n * COMPLETE the task — produce every required deliverable, persisted and\n * correct — rather than describe what should be done. A fluent transcript\n * that never produces the artifact scores zero here.\n *\n * Per requirement, a two-stage check:\n * 1. Structural — a produced item (vault artifact / approved proposal /\n * tool call) of the right kind is matched against the requirement and\n * carries non-empty content. Deterministic; no LLM.\n * 2. Correctness — only if structurally present AND the matched item\n * carries content, one targeted check decides whether that item\n * actually fulfils the requirement. A hallucinated artifact fails here;\n * an absent one already failed stage 1.\n *\n * `completionRate` is satisfied / MEASURABLE requirements (unmeasured rows —\n * checker failures — are excluded from the denominator, never scored as\n * zeros). Quality dimensions are meaningless on an incomplete task — callers\n * gate on `fullyComplete` / `completionRate` before scoring quality.\n */\n\nimport { randomUUID } from 'node:crypto'\nimport type { ChatClient, ChatRequest } from './analyst/chat-client'\nimport type { Artifact } from './artifact-validator'\nimport { CostLedger, type CostLedgerHandle } from './cost-ledger'\nimport { assertServedModel, ModelSubstitutionError } from './integrity/served-model'\nimport { recoverTruncatedJson } from './json-recovery'\nimport { JudgeParseError } from './judges'\nimport {\n costReceiptFromLlm,\n costReceiptFromLlmError,\n maximumChargeForLlmRequest,\n} from './llm-client'\nimport { packageVersion } from './package-version'\nimport type { RawProviderEvent, RawProviderSink } from './trace/raw-provider-sink'\nimport {\n certificationEvidenceDigest,\n type DefaultVerdict,\n type VerdictCertification,\n} from './verdict'\nimport type { CheckerIdentity, VerificationStrategySource } from './verification-strategy'\n\n/** What kind of produced state can satisfy a requirement structurally. */\nexport type SatisfiedBy = 'artifact' | 'proposal' | 'tool-call' | 'any'\n\nexport interface CompletionRequirement {\n /** Stable id from the task gold (e.g. a persona's `expected_requirements[].req_id`). */\n reqId: string\n /** Human-readable description of the required deliverable. */\n title: string\n /** Optional kind/category hint, matched against a produced item's kind. */\n category?: string\n /** What produced state satisfies this requirement. Defaults to 'any'. */\n satisfiedBy?: SatisfiedBy\n}\n\nexport interface TaskGold {\n taskId: string\n requirements: CompletionRequirement[]\n}\n\nexport interface ProducedProposal {\n id: string\n title: string\n status: 'pending' | 'approved' | 'rejected'\n /** Optional persisted body — when present, enables a correctness check. */\n content?: string\n}\n\n/** Everything observable about what a run actually produced. */\nexport interface ProducedState {\n /** Persisted vault artifacts. Reuses the shared `Artifact` shape. */\n artifacts: Artifact[]\n /** Proposals / filings the agent created. */\n proposals: ProducedProposal[]\n /** Names of tools the agent invoked. */\n toolCalls: string[]\n}\n\nexport interface RequirementCheck {\n reqId: string\n title: string\n /** A produced item of the right kind matched the requirement, non-empty. */\n structurallyPresent: boolean\n /**\n * Whether the matched item actually fulfils the requirement. `null` when\n * not structurally present, when the matched item carries no content\n * to assess, or when the correctness check itself failed (`unmeasured`).\n */\n correct: boolean | null\n /** structurallyPresent && !unmeasured && correct !== false. */\n satisfied: boolean\n /**\n * Set when the correctness check itself errored (LLM call failure or an\n * unparseable response after retry). The requirement's fulfilment is\n * UNKNOWN — `correct` stays null, `satisfied` is false, and\n * `completionVerdict` excludes the row from `completionRate`'s\n * denominator. Never folded into a zero: a synthetic zero is\n * indistinguishable from a real failure (see `JudgeParseError`).\n */\n unmeasured?: true\n /** Why the correctness check could not be measured (present iff `unmeasured`). */\n unmeasuredReason?: string\n /** Human-readable evidence for the verdict. */\n evidence: string[]\n}\n\n/** Extends the substrate verdict spine: `valid` = `fullyComplete` and\n * `score` = `completionRate` — derived in `completionVerdict()`, the one\n * place those equalities hold by construction. */\nexport interface CompletionVerdict extends DefaultVerdict {\n taskId: string\n requirements: RequirementCheck[]\n /** satisfied / MEASURABLE requirements (unmeasured rows leave the denominator). */\n completionRate: number\n /** Every measurable requirement satisfied (false when anything is unmeasured). */\n fullyComplete: boolean\n /** Requirements whose correctness check errored — reported, never scored as zero. */\n unmeasuredCount: number\n}\n\n/**\n * Construct a `CompletionVerdict` from the per-requirement checks, deriving\n * `completionRate` / `fullyComplete` and the spine fields (`valid` =\n * `fullyComplete`, `score` = `completionRate`) in one place. Throws on zero\n * requirements — a verdict over nothing is a misconfiguration, mirroring\n * `verifyCompletion`'s gold-spec guard.\n */\nexport function completionVerdict(input: {\n taskId: string\n requirements: RequirementCheck[]\n /** What certified the correctness stage, when anything did. Omitted =\n * an uncertified verdict — the honest default for a bare checker. */\n certification?: VerdictCertification\n}): CompletionVerdict {\n if (input.requirements.length === 0) {\n throw new Error(\n `completionVerdict: task '${input.taskId}' has no requirement checks — nothing to derive a verdict from`,\n )\n }\n const measurable = input.requirements.filter((r) => !r.unmeasured)\n const unmeasuredCount = input.requirements.length - measurable.length\n if (measurable.length === 0) {\n // Every check errored: this is an infrastructure failure, not a scored\n // run. A 0-rate verdict here would be a fabricated measurement.\n throw new Error(\n `completionVerdict: task '${input.taskId}' has no measurable requirements — all ${input.requirements.length} correctness checks failed (${input.requirements[0]?.unmeasuredReason ?? 'unknown reason'})`,\n )\n }\n const satisfiedCount = measurable.filter((r) => r.satisfied).length\n const completionRate = satisfiedCount / measurable.length\n // A run with unmeasured rows can still report a rate over what WAS\n // measured, but must not claim full completion.\n const fullyComplete = unmeasuredCount === 0 && satisfiedCount === measurable.length\n return {\n taskId: input.taskId,\n requirements: input.requirements,\n completionRate,\n fullyComplete,\n unmeasuredCount,\n valid: fullyComplete,\n score: completionRate,\n ...(input.certification === undefined ? {} : { certification: input.certification }),\n }\n}\n\n/**\n * What a correctness checker declares about itself so the completion\n * verdict can carry a certification: which strategy member it discharges,\n * its exact identity, and the steps its answers rest on unverified.\n */\nexport interface CorrectnessCheckerAttestation {\n strategy: VerificationStrategySource\n checker: CheckerIdentity\n assumptions: string[]\n}\n\n/**\n * Decides whether a produced item's content actually fulfils a requirement.\n * Injected so the structural verifier stays pure and unit-testable; the\n * production implementation is `createLlmCorrectnessChecker`.\n *\n * `attestation` is optional metadata on the function value: a checker that\n * carries one yields certified completion verdicts; a bare function yields\n * the same verdict uncertified. A plain arrow function remains a valid\n * checker.\n */\nexport interface CorrectnessChecker {\n (\n requirement: CompletionRequirement,\n content: string,\n ): Promise<{ correct: boolean; reason: string }>\n attestation?: CorrectnessCheckerAttestation\n}\n\nconst STOPWORDS = new Set([\n 'the',\n 'a',\n 'an',\n 'of',\n 'for',\n 'and',\n 'or',\n 'to',\n 'in',\n 'on',\n 'with',\n 'by',\n])\n\n// Deliverable-FORM vocabulary — words that name the SHAPE of an output, not its\n// domain content. A correct \"swap-comparison view persisted as a ui/** artifact\"\n// is an OpenUI JSON whose body says nothing about \"artifact\" / \"persisted\" /\n// \"view\"; the discriminative tokens are the domain nouns (swap, comparison).\n// Stripped from the REQUIREMENT side of structural recall so a deliverable is\n// matched on what it IS about, not on the boilerplate describing its form. The\n// correctness checker strips the same class via TITLE_STOPWORDS. Anti-game holds:\n// the distinctive domain tokens remain, so an off-topic item still fails.\nconst REQUIREMENT_FORM_STOPWORDS = new Set([\n 'generated',\n 'generate',\n 'view',\n 'render',\n 'rendered',\n 'persisted',\n 'persist',\n 'artifact',\n 'file',\n 'document',\n 'note',\n 'proposal',\n 'deliverable',\n 'output',\n 'created',\n 'create',\n 'produce',\n 'produced',\n 'flag',\n])\n\nconst MATCH_THRESHOLD = 0.5\nconst MIN_CONTENT_CHARS = 50\n\nfunction tokens(s: string, extraStop?: Set<string>): Set<string> {\n return new Set(\n s\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length > 1 && !STOPWORDS.has(t) && !extraStop?.has(t)),\n )\n}\n\n/**\n * Recall of the requirement's tokens within a candidate's identifying text.\n * Recall, not Jaccard — a candidate's path/id legitimately carries extra\n * tokens the requirement does not name. The requirement side drops\n * deliverable-FORM vocabulary so recall keys on the distinctive domain tokens.\n */\nfunction tokenRecall(requirementText: string, candidateText: string): number {\n const req = tokens(requirementText, REQUIREMENT_FORM_STOPWORDS)\n if (req.size === 0) return 0\n const cand = tokens(candidateText)\n let hit = 0\n for (const t of req) if (cand.has(t)) hit++\n return hit / req.size\n}\n\ninterface Candidate {\n reqIndex: number\n /** Unique key for a produced item — each item satisfies at most one requirement. */\n itemKey: string\n score: number\n evidence: string\n /** Content to correctness-check, or null when the matched item has none. */\n content: string | null\n}\n\nfunction artifactCandidates(\n req: CompletionRequirement,\n reqIndex: number,\n artifacts: Artifact[],\n): Candidate[] {\n const reqText = `${req.title} ${req.category ?? ''}`\n const out: Candidate[] = []\n artifacts.forEach((a, i) => {\n if ((a.content ?? '').trim().length < MIN_CONTENT_CHARS) return\n // Match against the artifact CONTENT too, not just its path + kind — a\n // generated view / note whose path is generic still satisfies a requirement\n // when its body covers it (e.g. an OpenUI comparison grounded in the on-file\n // figures). Bounded slice keeps the recall text cheap; MATCH_THRESHOLD holds.\n let score = tokenRecall(\n reqText,\n `${a.path ?? ''} ${a.kind} ${(a.content ?? '').slice(0, 4000)}`,\n )\n if (req.category && a.kind && req.category.toLowerCase() === a.kind.toLowerCase()) {\n score = Math.max(score, 1)\n }\n if (score < MATCH_THRESHOLD) return\n out.push({\n reqIndex,\n itemKey: `artifact:${i}`,\n score,\n evidence: `artifact '${a.path ?? a.kind}' matched (token recall ${score.toFixed(2)})`,\n content: a.content ?? null,\n })\n })\n return out\n}\n\nfunction proposalCandidates(\n req: CompletionRequirement,\n reqIndex: number,\n proposals: ProducedProposal[],\n): Candidate[] {\n const reqText = `${req.title} ${req.category ?? ''}`\n const out: Candidate[] = []\n for (const p of proposals) {\n // Pending or rejected work is not a completed deliverable.\n if (p.status !== 'approved') continue\n // A proposal needs an assessable BODY to be a deliverable. A bare title is\n // not completion: correctness cannot be judged on it, so a title-only match\n // would auto-pass the oracle (structurallyPresent && correct===null →\n // satisfied) with no verifiable content. Tool calls are the only\n // legitimately content-less deliverable (`toolCallCandidates`).\n const body = (p.content ?? '').trim()\n if (body.length < MIN_CONTENT_CHARS) continue\n // Match against the body as well as the (often short) title — a refusal /\n // flag / analysis proposal whose title is a label still satisfies a\n // descriptively-worded requirement when its content covers it. MATCH_THRESHOLD\n // + the requirement's distinctive tokens keep an off-topic proposal out;\n // correctness (a SEMANTIC checker, NOT this lexical pass) then judges\n // polarity/fulfilment, so a negation that merely contains the tokens fails.\n // Structural and correctness must use different evidence or the two-stage\n // check collapses to one lexical gate.\n const score = tokenRecall(reqText, `${p.title} ${body}`)\n if (score < MATCH_THRESHOLD) continue\n out.push({\n reqIndex,\n itemKey: `proposal:${p.id}`,\n score,\n evidence: `approved proposal '${p.title}' matched (token recall ${score.toFixed(2)})`,\n content: body,\n })\n }\n return out\n}\n\nfunction toolCallCandidates(\n req: CompletionRequirement,\n reqIndex: number,\n toolCalls: string[],\n): Candidate[] {\n const out: Candidate[] = []\n toolCalls.forEach((name, i) => {\n const score = tokenRecall(req.title, name)\n if (score < MATCH_THRESHOLD) return\n out.push({\n reqIndex,\n itemKey: `tool:${i}`,\n score,\n evidence: `tool call '${name}' matched (token recall ${score.toFixed(2)})`,\n content: null,\n })\n })\n return out\n}\n\n/**\n * Verify whether a run completed the task. `checkCorrectness` is injected —\n * `createLlmCorrectnessChecker` for production, a deterministic stub in tests.\n *\n * Throws on a gold spec with no requirements: an eval task that requires\n * nothing is a misconfiguration, not a vacuously-complete task.\n */\nexport async function verifyCompletion(\n gold: TaskGold,\n state: ProducedState,\n checkCorrectness: CorrectnessChecker,\n): Promise<CompletionVerdict> {\n if (gold.requirements.length === 0) {\n throw new Error(\n `verifyCompletion: task '${gold.taskId}' has no requirements — malformed gold spec`,\n )\n }\n\n // Collect every above-threshold (requirement, produced-item) candidate, then\n // assign greedily by descending score: each requirement and each produced\n // item is used at most once. One deliverable fulfils one requirement.\n const candidates: Candidate[] = []\n gold.requirements.forEach((req, i) => {\n const by = req.satisfiedBy ?? 'any'\n if (by === 'artifact' || by === 'any') {\n candidates.push(...artifactCandidates(req, i, state.artifacts))\n }\n if (by === 'proposal' || by === 'any') {\n candidates.push(...proposalCandidates(req, i, state.proposals))\n }\n if (by === 'tool-call' || by === 'any') {\n candidates.push(...toolCallCandidates(req, i, state.toolCalls))\n }\n })\n candidates.sort((a, b) => b.score - a.score)\n\n const assigned = new Map<number, Candidate>()\n const itemTaken = new Set<string>()\n for (const c of candidates) {\n if (assigned.has(c.reqIndex) || itemTaken.has(c.itemKey)) continue\n assigned.set(c.reqIndex, c)\n itemTaken.add(c.itemKey)\n }\n\n const requirements: RequirementCheck[] = []\n for (let i = 0; i < gold.requirements.length; i++) {\n const req = gold.requirements[i]!\n const match = assigned.get(i)\n const evidence: string[] = []\n let correct: boolean | null = null\n let unmeasuredReason: string | undefined\n\n if (match) {\n evidence.push(match.evidence)\n if (match.content !== null) {\n try {\n const r = await checkCorrectness(req, match.content)\n correct = r.correct\n evidence.push(`correctness: ${r.correct ? 'pass' : 'fail'} — ${r.reason}`)\n } catch (err) {\n // The CHECKER failed, not the requirement. Recording this as a\n // zero would fabricate a model failure out of an infrastructure\n // one; the requirement is unmeasured and leaves the denominator.\n unmeasuredReason =\n err instanceof JudgeParseError\n ? `checker response unparseable after retry: ${err.raw.slice(0, 200)}`\n : `checker call failed: ${err instanceof Error ? err.message : String(err)}`\n evidence.push(`correctness: UNMEASURED — ${unmeasuredReason}`)\n }\n } else {\n evidence.push('correctness: not assessed — matched item carries no content')\n }\n } else {\n const by = req.satisfiedBy ?? 'any'\n const kind = by === 'any' ? 'artifact/proposal/tool-call' : by\n evidence.push(`no produced ${kind} matched this requirement`)\n }\n\n const structurallyPresent = match !== undefined\n const unmeasured = unmeasuredReason !== undefined\n const satisfied = structurallyPresent && !unmeasured && correct !== false\n requirements.push({\n reqId: req.reqId,\n title: req.title,\n structurallyPresent,\n correct,\n satisfied,\n ...(unmeasured ? { unmeasured: true as const, unmeasuredReason } : {}),\n evidence,\n })\n }\n\n // The verdict blends the deterministic structural stage with the injected\n // correctness stage, so the certification names the checker's own member\n // and carries the structural stage's lexical nature as an assumption. A\n // checker without an attestation yields an uncertified verdict.\n const attestation = checkCorrectness.attestation\n const certification: VerdictCertification | undefined = attestation\n ? {\n strategy: attestation.strategy,\n checker: attestation.checker,\n assumptions: [\n 'structural matching is lexical token recall over produced items — the correctness stage only sees items it matched',\n ...attestation.assumptions,\n ],\n evidenceDigest: certificationEvidenceDigest({ taskId: gold.taskId, requirements }),\n }\n : undefined\n\n return completionVerdict({\n taskId: gold.taskId,\n requirements,\n ...(certification === undefined ? {} : { certification }),\n })\n}\n\nexport interface LlmCorrectnessCheckerOpts {\n model?: string\n /** Optional ledger for direct use. */\n costLedger?: CostLedgerHandle\n costPhase?: string\n costTags?: Record<string, string>\n signal?: AbortSignal\n /** Max chars of artifact content sent to the checker. */\n maxContentChars?: number\n /**\n * Checker LLM calls per requirement before giving up (parse failures and\n * call errors both consume attempts). The failure then surfaces as an\n * `unmeasured` requirement, never a zero.\n */\n maxAttempts?: number\n /**\n * Forensic capture of every checker request/response/error — without it a\n * checker failure is unauditable (the agent-turn raws never contain the\n * checker's own calls). Same sink contract as `LlmClient`.\n */\n rawSink?: RawProviderSink\n}\n\n/**\n * Parse the correctness checker's model response. Tolerates a response\n * truncated mid-JSON (max_tokens cap) by auto-closing the prefix — the\n * verdict boolean usually lands in the first few tokens, so a recovered\n * prefix with a boolean `correct` is a real measurement, not a guess.\n * Fails loud (JudgeParseError) when no boolean verdict is recoverable.\n */\nexport function parseCorrectnessResponse(raw: string): { correct: boolean; reason: string } {\n const readVerdict = (candidate: unknown): { correct: boolean; reason: string } | null => {\n if (candidate === null || typeof candidate !== 'object') return null\n const { correct, reason } = candidate as { correct?: unknown; reason?: unknown }\n if (typeof correct !== 'boolean') return null\n return { correct, reason: typeof reason === 'string' ? reason : '' }\n }\n\n const match = raw.match(/\\{[\\s\\S]*\\}/)\n if (match) {\n try {\n const strict = readVerdict(JSON.parse(match[0]))\n if (strict) return strict\n } catch {\n // fall through to truncation recovery\n }\n }\n // The strict path needs a closing `}`; a cap-hit response has none. Take\n // everything from the first `{` and auto-close it.\n const start = raw.indexOf('{')\n if (start !== -1) {\n const recovered = readVerdict(recoverTruncatedJson(raw.slice(start)))\n if (recovered) return recovered\n }\n throw new JudgeParseError('correctness-checker', raw)\n}\n\n/**\n * Production `CorrectnessChecker` — one LLM call per matched artifact,\n * deterministic (temperature 0), structured JSON out. Judges fulfilment\n * only: a plan, a gesture, or a description of what should be done does not\n * fulfil a requirement — the artifact must BE the deliverable.\n */\nexport function createLlmCorrectnessChecker(\n chat: ChatClient,\n opts: LlmCorrectnessCheckerOpts = {},\n): CorrectnessChecker {\n const model = opts.model ?? 'claude-sonnet-4-6'\n const maxContentChars = opts.maxContentChars ?? 8000\n const maxAttempts = opts.maxAttempts ?? 2\n const costLedger = opts.costLedger ?? new CostLedger()\n const sink = opts.rawSink\n const record = async (event: RawProviderEvent): Promise<void> => {\n // Forensic capture is best-effort; the verdict is the system of record.\n try {\n await sink?.record(event)\n } catch {\n // Intentionally swallowed.\n }\n }\n const checker: CorrectnessChecker = async (requirement, content) => {\n const request = {\n model,\n messages: [\n {\n role: 'system' as const,\n content:\n 'You verify whether a produced work artifact actually fulfils a stated requirement. Judge fulfilment only — is the deliverable substantively present and on-point — not polish. A plan to do it later, a vague gesture, or a description of what should be done does NOT fulfil a requirement; the artifact must BE the deliverable. Respond with a single JSON object: {\"correct\": boolean, \"reason\": string (<= 30 words)}.',\n },\n {\n role: 'user' as const,\n content: `Requirement: ${requirement.title}\\n${\n requirement.category ? `Category: ${requirement.category}\\n` : ''\n }\\nProduced artifact:\\n${content.slice(0, maxContentChars)}`,\n },\n ],\n temperature: 0,\n maxTokens: 200,\n } satisfies ChatRequest\n let lastErr: unknown\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const started = Date.now()\n await record({\n eventId: randomUUID(),\n provider: chat.transport,\n model,\n endpoint: '/chat',\n baseUrl: '',\n attemptIndex: attempt,\n direction: 'request',\n timestamp: started,\n requestBody: request,\n redactedFields: [],\n })\n try {\n const paid = await costLedger.runPaidCall({\n channel: 'verifier',\n phase: opts.costPhase ?? 'completion.correctness',\n actor: 'correctness-checker',\n model,\n maximumCharge:\n chat.maximumAttempts === undefined\n ? undefined\n : maximumChargeForLlmRequest(request, { maximumAttempts: chat.maximumAttempts }),\n tags: {\n ...opts.costTags,\n requirementId: requirement.reqId,\n attempt: String(attempt),\n },\n signal: opts.signal,\n execute: (signal, callId) => chat.chat(request, { signal, idempotencyKey: callId }),\n receipt: costReceiptFromLlm,\n receiptFromError: costReceiptFromLlmError,\n })\n if (!paid.succeeded) throw paid.error\n const resp = paid.value\n // Hold the transport to its own word: a verdict produced by a model\n // other than the one requested is not this model's verdict. A\n // transport that echoes no id cannot be made to prove identity here —\n // callers needing that proof enable it at the client\n // (`LlmClientOptions.assertServedModel`) or gate on `assertModelsServed`.\n assertServedModel(model, resp.servedModel, {\n allowUnreported: true,\n context: `correctness checker for requirement ${requirement.reqId}`,\n })\n const raw = resp.content\n await record({\n eventId: randomUUID(),\n provider: chat.transport,\n model,\n endpoint: '/chat',\n baseUrl: '',\n attemptIndex: attempt,\n direction: 'response',\n timestamp: Date.now(),\n durationMs: Date.now() - started,\n responseBody: resp,\n redactedFields: [],\n })\n return parseCorrectnessResponse(raw)\n } catch (err) {\n lastErr = err\n await record({\n eventId: randomUUID(),\n provider: chat.transport,\n model,\n endpoint: '/chat',\n baseUrl: '',\n attemptIndex: attempt,\n direction: 'error',\n timestamp: Date.now(),\n durationMs: Date.now() - started,\n errorMessage: err instanceof Error ? err.message : String(err),\n redactedFields: [],\n })\n // Substitution is a verdict about identity, not a transient fault:\n // another attempt reaches the same wrong model and spends more tokens.\n if (err instanceof ModelSubstitutionError) throw err\n }\n }\n throw lastErr instanceof Error ? lastErr : new Error(String(lastErr))\n }\n checker.attestation = {\n strategy: 'judge',\n checker: { name: 'llm-correctness-checker', version: model },\n assumptions: [\n 'served-model identity is accepted when the transport reports none (assertServedModel allowUnreported)',\n ],\n }\n return checker\n}\n\n/** Stopwords for requirement-title tokenization — drops the imperative verbs\n * ('review', 'update', …) common to deliverable titles so recall keys on the\n * substantive nouns, not the boilerplate ask. */\nconst TITLE_STOPWORDS = new Set([\n 'the',\n 'a',\n 'an',\n 'and',\n 'or',\n 'for',\n 'to',\n 'of',\n 'in',\n 'on',\n 'with',\n 'review',\n 'update',\n 'new',\n 'proposed',\n])\n\n/**\n * Deterministic `CorrectnessChecker` — the no-LLM counterpart to\n * `createLlmCorrectnessChecker`. A produced item fulfils a requirement when its\n * content is substantive (≥ `minContentLength` chars) AND recalls ≥ `minRecall`\n * of the requirement title's significant tokens. No network.\n *\n * Polarity-blind: token recall credits a negation that contains the\n * requirement's tokens (\"I will NOT produce the comparison\" recalls every token\n * of \"produce the comparison\"). The structural match stage is ALSO lexical, so\n * pairing the two collapses to a single gameable gate. Use this only as an\n * opt-in structural pre-filter or for tasks whose requirements have no polarity\n * to invert; for produced-state grading the correctness checker MUST be semantic\n * (`createLlmCorrectnessChecker`). See the anti-game fixtures in the test suite.\n */\nexport function createTokenRecallChecker(\n opts: { minRecall?: number; minContentLength?: number } = {},\n): CorrectnessChecker {\n const minRecall = opts.minRecall ?? 0.5\n const minLen = opts.minContentLength ?? 120\n const checker: CorrectnessChecker = async (requirement, content) => {\n const body = content.trim()\n if (body.length < minLen)\n return {\n correct: false,\n reason: `content too thin (${body.length} chars) to be the deliverable`,\n }\n const titleTokens = requirement.title\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length > 2 && !TITLE_STOPWORDS.has(t))\n if (titleTokens.length === 0)\n return {\n correct: true,\n reason: 'requirement title has no significant tokens — structural match accepted',\n }\n const lower = body.toLowerCase()\n const hits = titleTokens.filter((t) => lower.includes(t)).length\n const recall = hits / titleTokens.length\n return recall >= minRecall\n ? {\n correct: true,\n reason: `content recalls ${hits}/${titleTokens.length} requirement tokens`,\n }\n : {\n correct: false,\n reason: `content recalls only ${hits}/${titleTokens.length} requirement tokens`,\n }\n }\n // 'schema': token recall checks shape, not meaning — the member whose\n // documented failure mode (\"a well-formed wrong answer passes\") is\n // exactly this checker's polarity blindness.\n checker.attestation = {\n strategy: 'schema',\n checker: { name: 'token-recall-checker', version: packageVersion() },\n assumptions: ['polarity-blind: a negation that recalls the requirement tokens passes'],\n }\n return checker\n}\n","/**\n * Produced-state extraction — normalize a run's runtime event stream into the\n * typed `ProducedState` the completion oracle consumes.\n *\n * `ProducedState` answers \"what did the agent actually produce\" — vault\n * artifacts, proposals, tool calls. The runtime emits these as a stream of\n * events; this module is the single normalization point from that stream to\n * the shape `verifyCompletion` expects.\n *\n * Input is structurally typed (`RuntimeEventLike`) so this module does not\n * depend on agent-runtime — agent-runtime's `RuntimeStreamEvent` satisfies it\n * structurally. The `content` on `ArtifactEventLike` and the whole\n * `proposal_created` variant are the runtime-side enrichments this contract\n * requires; the runtime emits them, this module consumes them.\n */\n\nimport type { Artifact } from './artifact-validator'\nimport type { ProducedProposal, ProducedState } from './completion-verifier'\n\n/** A tool the agent invoked. */\nexport interface ToolCallEventLike {\n type: 'tool_call'\n toolName: string\n}\n\n/**\n * An artifact the agent produced. `content` is the enriched field — the\n * runtime's base `artifact` event carries only metadata; the completion\n * oracle needs the body to verify the deliverable, so the runtime emits it.\n */\nexport interface ArtifactEventLike {\n type: 'artifact'\n artifactId: string\n name?: string\n mimeType?: string\n uri?: string\n content?: string\n}\n\n/** A proposal / filing the agent created. */\nexport interface ProposalEventLike {\n type: 'proposal_created'\n proposalId: string\n title: string\n status?: 'pending' | 'approved' | 'rejected'\n // body of the proposal (e.g. a submit_proposal `description`). When present,\n // the completion oracle correctness-checks it like artifact content; absent,\n // the proposal is graded presence-only.\n content?: string\n}\n\n/**\n * The subset of runtime stream events `extractProducedState` consumes.\n * agent-runtime's full `RuntimeStreamEvent` union satisfies this structurally;\n * the `{ type: string }` catch-all keeps the input permissive so callers can\n * pass the whole unfiltered telemetry stream — unrecognized events are skipped.\n */\nexport type RuntimeEventLike =\n | ToolCallEventLike\n | ArtifactEventLike\n | ProposalEventLike\n | { type: string }\n\nfunction artifactKind(mimeType: string | undefined): string {\n if (!mimeType) return 'file'\n if (mimeType.includes('json')) return 'json'\n if (mimeType.startsWith('text/')) return 'text'\n return 'file'\n}\n\n/**\n * Normalize an emission-ordered stream into its latest observed produced state.\n * Artifacts are keyed by their exact output path (name, then URI, then id);\n * proposals by id. Later observations replace earlier ones, including missing\n * content or a rejected status. An obsolete version cannot prove completion.\n * Distinct paths remain distinct; no path normalization or aliasing is inferred.\n * Results retain first-seen identity order. Tool names describe invocation,\n * not success, and are deduplicated in first-seen order.\n *\n * An artifact without observed content yields empty content, which the\n * completion oracle rejects. This projection does not verify external effects,\n * recover dropped events, or establish freshness beyond the supplied stream.\n */\nexport function extractProducedState(events: readonly RuntimeEventLike[]): ProducedState {\n const artifacts = new Map<string, Artifact>()\n const proposals = new Map<string, ProducedProposal>()\n const toolCalls: string[] = []\n const seenTools = new Set<string>()\n\n for (const ev of events) {\n if (ev.type === 'tool_call') {\n const name = (ev as ToolCallEventLike).toolName\n if (name && !seenTools.has(name)) {\n seenTools.add(name)\n toolCalls.push(name)\n }\n } else if (ev.type === 'artifact') {\n const a = ev as ArtifactEventLike\n const path = a.name ?? a.uri ?? a.artifactId\n artifacts.set(path, {\n kind: artifactKind(a.mimeType),\n path,\n content: a.content ?? '',\n })\n } else if (ev.type === 'proposal_created') {\n const p = ev as ProposalEventLike\n proposals.set(p.proposalId, {\n id: p.proposalId,\n title: p.title,\n status: p.status ?? 'pending',\n ...(p.content !== undefined ? { content: p.content } : {}),\n })\n }\n }\n\n return { artifacts: [...artifacts.values()], proposals: [...proposals.values()], toolCalls }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"produced-state-zSaGDC_Q.js","names":[],"sources":["../src/agent-profile.ts","../src/completion-verifier.ts","../src/produced-state.ts"],"sourcesContent":["import { createHash } from 'node:crypto'\nimport type { AgentProfile } from '@tangle-network/agent-interface'\nimport { type HarnessType, harnessSupportsModel } from '@tangle-network/agent-interface'\nimport { ValidationError } from './errors'\nimport { canonicalString } from './ledger-core/canonical'\n\nexport type { AgentProfile, HarnessType } from '@tangle-network/agent-interface'\n\n/**\n * The agentic coding harnesses an eval sweeps by default — the ones we care about\n * ranking. This is the SINGLE source of that list; consumers import it instead of\n * re-declaring their own (a re-declared list is how the fleet drifts). Pass an\n * explicit `harnesses` (e.g. `harnessTypeSchema.options` for literally every known\n * harness) to widen beyond these.\n */\nexport const CODING_HARNESSES: readonly HarnessType[] = [\n 'opencode',\n 'claude-code',\n 'codex',\n 'kimi-code',\n]\n\nexport interface ProfileAxisSpec {\n /** The domain profile to sweep. Its prompt/tools/skills are held fixed; only the\n * harness and model vary. `model.default` is the fallback model. */\n base: AgentProfile\n /** Harnesses to cross. Default: {@link CODING_HARNESSES}. */\n harnesses?: readonly HarnessType[]\n /** Models to cross. Default: `[base.model.default]` — one model, i.e. today's\n * single-model behaviour, so omitting this never changes an existing run. */\n models?: readonly string[]\n /** Force every (harness, model) pair verbatim, even ones the harness can't run —\n * for deliberately testing failure modes. Default (false): SNAP instead — a\n * vendor-locked harness runs only the swept models in its family, or its native\n * default when it supports none, so no harness is dropped and none gets a\n * guaranteed-failing foreign-model cell. */\n keepIncompatible?: boolean\n}\n\n/** Model sentinel for a vendor-locked harness that supports none of the swept models:\n * it carries no provider prefix, so `harnessSupportsModel` accepts it and the harness\n * resolves it to its own native default model at runtime (e.g. kimi-code → its Kimi\n * model). Lets `expandProfileAxes` snap-instead-of-drop without a per-harness flagship\n * table that would rot as router catalogs change. */\nexport const HARNESS_NATIVE_MODEL = 'default'\n\n/**\n * Expand a base profile across the harness × model matrix into the `AgentProfile[]`\n * that `runProfileMatrix` / `selfImprove` score — the ONE place \"which harnesses ×\n * which models do we evaluate\" lives, so no product hand-rolls its own harness list\n * or column→profile mapping (the pattern that let those copies drift and silently\n * break the harness pivot).\n *\n * Each cell clones `base`, sets the canonical top-level `harness` and `model.default`,\n * and stamps `metadata.harness` + `metadata.harnessModel` for matrix grouping. Both\n * metadata fields are hash-bearing, so every cell gets a distinct `agentProfileId` row\n * and results join back by harness/model via {@link harnessAxisOf} with no\n * hand-recomputed key. A vendor-locked harness snaps to its family's swept models — or\n * its native default ({@link HARNESS_NATIVE_MODEL}) when it supports none — so every\n * requested harness runs; `keepIncompatible` forces every pair verbatim.\n *\n * Omit `harnesses`/`models` to sweep the full default set — the \"turn it on for\n * everything we care about\" switch, identical in shape whether one harness or all.\n */\nexport function expandProfileAxes(spec: ProfileAxisSpec): AgentProfile[] {\n const harnesses = spec.harnesses ?? CODING_HARNESSES\n if (harnesses.length === 0) throw new ValidationError('expandProfileAxes: no harnesses to sweep')\n const baseModel = spec.base.model?.default\n const models = spec.models ?? (baseModel ? [baseModel] : [])\n if (models.length === 0) {\n throw new ValidationError(\n 'expandProfileAxes: no models to sweep — base profile has no model.default and none were supplied',\n )\n }\n const out: AgentProfile[] = []\n const seen = new Set<string>()\n for (const harness of harnesses) {\n // A universal (router-backed) harness — opencode/pi/claudish — runs every swept\n // model. A vendor-locked harness — codex/claude-code/kimi-code — runs only the\n // swept models in its own family; when it supports NONE of them it snaps to its\n // native default (the `HARNESS_NATIVE_MODEL` sentinel it resolves at runtime)\n // rather than being dropped, so every requested harness still appears in the\n // sweep on a model it can actually run — e.g. sweeping `deepseek/x` puts opencode\n // on deepseek and kimi-code on its own Kimi model, a real head-to-head.\n // `keepIncompatible` forces every (harness, model) pair verbatim (failure-mode runs).\n const supported = spec.keepIncompatible\n ? models\n : models.filter((model) => harnessSupportsModel(harness, model))\n const effective = supported.length > 0 ? supported : [HARNESS_NATIVE_MODEL]\n for (const model of effective) {\n const profile: AgentProfile = {\n ...spec.base,\n harness,\n name: `${spec.base.name ?? 'agent'}/${harness}/${model}`,\n model: { ...spec.base.model, default: model },\n metadata: { ...(spec.base.metadata ?? {}), harness, harnessModel: model },\n }\n const id = agentProfileId(profile)\n if (seen.has(id)) continue\n seen.add(id)\n out.push(profile)\n }\n }\n if (out.length === 0) {\n // Unreachable in normal use — snapping guarantees ≥1 cell per harness — but keep a\n // fail-closed guard so a future refactor can't silently produce an empty sweep.\n throw new ValidationError(\n `expandProfileAxes: produced no profiles (harnesses=[${harnesses.join(', ')}], models=[${models.join(', ')}]).`,\n )\n }\n return out\n}\n\n/**\n * Read the (harness, model) a matrix cell ran under, off a profile or a result row's\n * profile — the join-back for a `byHarness` pivot. Returns undefined when the profile\n * wasn't produced by {@link expandProfileAxes}. Callers group `result.byProfile` by\n * this instead of recomputing an id (recomputing the wrong key is what broke the pivot\n * in the hand-rolled copies).\n */\nexport function harnessAxisOf(\n profile: Pick<AgentProfile, 'metadata'>,\n): { harness: HarnessType; model: string } | undefined {\n const m = profile.metadata as Record<string, unknown> | undefined\n const harness = m?.harness\n const model = m?.harnessModel\n if (typeof harness === 'string' && typeof model === 'string') {\n return { harness: harness as HarnessType, model }\n }\n return undefined\n}\n\n/**\n * Collision-resistant, path-safe, human-readable profile id for eval artifacts.\n * Scorecard joins still use `agentProfileHash`; this id is for run ids, matrix\n * keys, and directory names where two profiles must not collapse onto one row.\n * The suffix is the first 64 bits of the behaviour hash, enough for ordinary\n * eval matrices while keeping filenames readable.\n */\nexport function agentProfileId(profile: AgentProfile): string {\n const label = pathSafeProfileLabel(agentProfileDisplayLabel(profile)) ?? 'profile'\n return `${label}-${agentProfileHash(profile).slice(0, 16)}`\n}\n\n/**\n * Model snapshot used for `RunRecord.model`. Eval surfaces require a concrete\n * model id because run records reject bare/missing model aliases.\n */\nexport function agentProfileModelId(profile: AgentProfile): string {\n const model = profile.model?.default?.trim()\n if (!model) {\n const label = agentProfileDisplayLabel(profile) ?? 'unnamed profile'\n throw new ValidationError(\n `AgentProfile \"${label}\" has no model.default — cannot record eval run`,\n )\n }\n return model\n}\n\nfunction agentProfileDisplayLabel(profile: AgentProfile): string | undefined {\n return profile.name?.trim() || profile.version?.trim() || undefined\n}\n\nfunction pathSafeProfileLabel(label: string | undefined): string | undefined {\n const safe = label\n ?.trim()\n .replace(/[^A-Za-z0-9._-]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '')\n return safe || undefined\n}\n\nfunction compact<T extends Record<string, unknown>>(input: T): Partial<T> {\n const out: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(input)) {\n if (value !== undefined) out[key] = value\n }\n return out as Partial<T>\n}\n\n/**\n * Deterministic behaviour identity for the canonical\n * `@tangle-network/agent-interface` AgentProfile.\n *\n * `name` and `description` are labels and do not affect the hash. Profile\n * `version`, prompt, model hints, tools, resources, hooks, modes, permissions,\n * and extensions do affect the hash. Resource array order is hash-bearing\n * because mount order can change agent behaviour. Undefined fields are treated\n * as absent; explicit `null` fields remain hash-bearing.\n */\nexport function agentProfileHash(profile: AgentProfile): string {\n const model = agentProfileModelId(profile)\n const behaviour = compact({\n ...profile,\n name: undefined,\n description: undefined,\n tags: profile.tags ? [...profile.tags].sort() : undefined,\n model: compact({ ...profile.model, default: model }),\n })\n return createHash('sha256').update(canonicalString(behaviour)).digest('hex')\n}\n","/**\n * Completion verifier — the task-completion oracle.\n *\n * Answers the only eval question that is not a proxy: did the agent actually\n * COMPLETE the task — produce every required deliverable, persisted and\n * correct — rather than describe what should be done. A fluent transcript\n * that never produces the artifact scores zero here.\n *\n * Per requirement, a two-stage check:\n * 1. Structural — a produced item (vault artifact / approved proposal /\n * tool call) of the right kind is matched against the requirement and\n * carries non-empty content. Deterministic; no LLM.\n * 2. Correctness — only if structurally present AND the matched item\n * carries content, one targeted check decides whether that item\n * actually fulfils the requirement. A hallucinated artifact fails here;\n * an absent one already failed stage 1.\n *\n * `completionRate` is satisfied / MEASURABLE requirements (unmeasured rows —\n * checker failures — are excluded from the denominator, never scored as\n * zeros). Quality dimensions are meaningless on an incomplete task — callers\n * gate on `fullyComplete` / `completionRate` before scoring quality.\n */\n\nimport { randomUUID } from 'node:crypto'\nimport type { ChatClient, ChatRequest } from './analyst/chat-client'\nimport type { Artifact } from './artifact-validator'\nimport { CostLedger, type CostLedgerHandle } from './cost-ledger'\nimport { assertServedModel, ModelSubstitutionError } from './integrity/served-model'\nimport { recoverTruncatedJson } from './json-recovery'\nimport { JudgeParseError } from './judges'\nimport {\n costReceiptFromLlm,\n costReceiptFromLlmError,\n maximumChargeForLlmRequest,\n} from './llm-client'\nimport { packageVersion } from './package-version'\nimport type { RawProviderEvent, RawProviderSink } from './trace/raw-provider-sink'\nimport {\n certificationEvidenceDigest,\n type DefaultVerdict,\n type VerdictCertification,\n} from './verdict'\nimport type { CheckerIdentity, VerificationStrategySource } from './verification-strategy'\n\n/** What kind of produced state can satisfy a requirement structurally. */\nexport type SatisfiedBy = 'artifact' | 'proposal' | 'tool-call' | 'any'\n\nexport interface CompletionRequirement {\n /** Stable id from the task gold (e.g. a persona's `expected_requirements[].req_id`). */\n reqId: string\n /** Human-readable description of the required deliverable. */\n title: string\n /** Optional kind/category hint, matched against a produced item's kind. */\n category?: string\n /** What produced state satisfies this requirement. Defaults to 'any'. */\n satisfiedBy?: SatisfiedBy\n}\n\nexport interface TaskGold {\n taskId: string\n requirements: CompletionRequirement[]\n}\n\nexport interface ProducedProposal {\n id: string\n title: string\n status: 'pending' | 'approved' | 'rejected'\n /** Optional persisted body — when present, enables a correctness check. */\n content?: string\n}\n\n/** Everything observable about what a run actually produced. */\nexport interface ProducedState {\n /** Persisted vault artifacts. Reuses the shared `Artifact` shape. */\n artifacts: Artifact[]\n /** Proposals / filings the agent created. */\n proposals: ProducedProposal[]\n /** Names of tools the agent invoked. */\n toolCalls: string[]\n}\n\nexport interface RequirementCheck {\n reqId: string\n title: string\n /** A produced item of the right kind matched the requirement, non-empty. */\n structurallyPresent: boolean\n /**\n * Whether the matched item actually fulfils the requirement. `null` when\n * not structurally present, when the matched item carries no content\n * to assess, or when the correctness check itself failed (`unmeasured`).\n */\n correct: boolean | null\n /** structurallyPresent && !unmeasured && correct !== false. */\n satisfied: boolean\n /**\n * Set when the correctness check itself errored (LLM call failure or an\n * unparseable response after retry). The requirement's fulfilment is\n * UNKNOWN — `correct` stays null, `satisfied` is false, and\n * `completionVerdict` excludes the row from `completionRate`'s\n * denominator. Never folded into a zero: a synthetic zero is\n * indistinguishable from a real failure (see `JudgeParseError`).\n */\n unmeasured?: true\n /** Why the correctness check could not be measured (present iff `unmeasured`). */\n unmeasuredReason?: string\n /** Human-readable evidence for the verdict. */\n evidence: string[]\n}\n\n/** Extends the substrate verdict spine: `valid` = `fullyComplete` and\n * `score` = `completionRate` — derived in `completionVerdict()`, the one\n * place those equalities hold by construction. */\nexport interface CompletionVerdict extends DefaultVerdict {\n taskId: string\n requirements: RequirementCheck[]\n /** satisfied / MEASURABLE requirements (unmeasured rows leave the denominator). */\n completionRate: number\n /** Every measurable requirement satisfied (false when anything is unmeasured). */\n fullyComplete: boolean\n /** Requirements whose correctness check errored — reported, never scored as zero. */\n unmeasuredCount: number\n}\n\n/**\n * Construct a `CompletionVerdict` from the per-requirement checks, deriving\n * `completionRate` / `fullyComplete` and the spine fields (`valid` =\n * `fullyComplete`, `score` = `completionRate`) in one place. Throws on zero\n * requirements — a verdict over nothing is a misconfiguration, mirroring\n * `verifyCompletion`'s gold-spec guard.\n */\nexport function completionVerdict(input: {\n taskId: string\n requirements: RequirementCheck[]\n /** What certified the correctness stage, when anything did. Omitted =\n * an uncertified verdict — the honest default for a bare checker. */\n certification?: VerdictCertification\n}): CompletionVerdict {\n if (input.requirements.length === 0) {\n throw new Error(\n `completionVerdict: task '${input.taskId}' has no requirement checks — nothing to derive a verdict from`,\n )\n }\n const measurable = input.requirements.filter((r) => !r.unmeasured)\n const unmeasuredCount = input.requirements.length - measurable.length\n if (measurable.length === 0) {\n // Every check errored: this is an infrastructure failure, not a scored\n // run. A 0-rate verdict here would be a fabricated measurement.\n throw new Error(\n `completionVerdict: task '${input.taskId}' has no measurable requirements — all ${input.requirements.length} correctness checks failed (${input.requirements[0]?.unmeasuredReason ?? 'unknown reason'})`,\n )\n }\n const satisfiedCount = measurable.filter((r) => r.satisfied).length\n const completionRate = satisfiedCount / measurable.length\n // A run with unmeasured rows can still report a rate over what WAS\n // measured, but must not claim full completion.\n const fullyComplete = unmeasuredCount === 0 && satisfiedCount === measurable.length\n return {\n taskId: input.taskId,\n requirements: input.requirements,\n completionRate,\n fullyComplete,\n unmeasuredCount,\n valid: fullyComplete,\n score: completionRate,\n ...(input.certification === undefined ? {} : { certification: input.certification }),\n }\n}\n\n/**\n * What a correctness checker declares about itself so the completion\n * verdict can carry a certification: which strategy member it discharges,\n * its exact identity, and the steps its answers rest on unverified.\n */\nexport interface CorrectnessCheckerAttestation {\n strategy: VerificationStrategySource\n checker: CheckerIdentity\n assumptions: string[]\n}\n\n/**\n * Decides whether a produced item's content actually fulfils a requirement.\n * Injected so the structural verifier stays pure and unit-testable; the\n * production implementation is `createLlmCorrectnessChecker`.\n *\n * `attestation` is optional metadata on the function value: a checker that\n * carries one yields certified completion verdicts; a bare function yields\n * the same verdict uncertified. A plain arrow function remains a valid\n * checker.\n */\nexport interface CorrectnessChecker {\n (\n requirement: CompletionRequirement,\n content: string,\n ): Promise<{ correct: boolean; reason: string }>\n attestation?: CorrectnessCheckerAttestation\n}\n\nconst STOPWORDS = new Set([\n 'the',\n 'a',\n 'an',\n 'of',\n 'for',\n 'and',\n 'or',\n 'to',\n 'in',\n 'on',\n 'with',\n 'by',\n])\n\n// Deliverable-FORM vocabulary — words that name the SHAPE of an output, not its\n// domain content. A correct \"swap-comparison view persisted as a ui/** artifact\"\n// is an OpenUI JSON whose body says nothing about \"artifact\" / \"persisted\" /\n// \"view\"; the discriminative tokens are the domain nouns (swap, comparison).\n// Stripped from the REQUIREMENT side of structural recall so a deliverable is\n// matched on what it IS about, not on the boilerplate describing its form. The\n// correctness checker strips the same class via TITLE_STOPWORDS. Anti-game holds:\n// the distinctive domain tokens remain, so an off-topic item still fails.\nconst REQUIREMENT_FORM_STOPWORDS = new Set([\n 'generated',\n 'generate',\n 'view',\n 'render',\n 'rendered',\n 'persisted',\n 'persist',\n 'artifact',\n 'file',\n 'document',\n 'note',\n 'proposal',\n 'deliverable',\n 'output',\n 'created',\n 'create',\n 'produce',\n 'produced',\n 'flag',\n])\n\nconst MATCH_THRESHOLD = 0.5\nconst MIN_CONTENT_CHARS = 50\n\nfunction tokens(s: string, extraStop?: Set<string>): Set<string> {\n return new Set(\n s\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length > 1 && !STOPWORDS.has(t) && !extraStop?.has(t)),\n )\n}\n\n/**\n * Recall of the requirement's tokens within a candidate's identifying text.\n * Recall, not Jaccard — a candidate's path/id legitimately carries extra\n * tokens the requirement does not name. The requirement side drops\n * deliverable-FORM vocabulary so recall keys on the distinctive domain tokens.\n */\nfunction tokenRecall(requirementText: string, candidateText: string): number {\n const req = tokens(requirementText, REQUIREMENT_FORM_STOPWORDS)\n if (req.size === 0) return 0\n const cand = tokens(candidateText)\n let hit = 0\n for (const t of req) if (cand.has(t)) hit++\n return hit / req.size\n}\n\ninterface Candidate {\n reqIndex: number\n /** Unique key for a produced item — each item satisfies at most one requirement. */\n itemKey: string\n score: number\n evidence: string\n /** Content to correctness-check, or null when the matched item has none. */\n content: string | null\n}\n\nfunction artifactCandidates(\n req: CompletionRequirement,\n reqIndex: number,\n artifacts: Artifact[],\n): Candidate[] {\n const reqText = `${req.title} ${req.category ?? ''}`\n const out: Candidate[] = []\n artifacts.forEach((a, i) => {\n if ((a.content ?? '').trim().length < MIN_CONTENT_CHARS) return\n // Match against the artifact CONTENT too, not just its path + kind — a\n // generated view / note whose path is generic still satisfies a requirement\n // when its body covers it (e.g. an OpenUI comparison grounded in the on-file\n // figures). Bounded slice keeps the recall text cheap; MATCH_THRESHOLD holds.\n let score = tokenRecall(\n reqText,\n `${a.path ?? ''} ${a.kind} ${(a.content ?? '').slice(0, 4000)}`,\n )\n if (req.category && a.kind && req.category.toLowerCase() === a.kind.toLowerCase()) {\n score = Math.max(score, 1)\n }\n if (score < MATCH_THRESHOLD) return\n out.push({\n reqIndex,\n itemKey: `artifact:${i}`,\n score,\n evidence: `artifact '${a.path ?? a.kind}' matched (token recall ${score.toFixed(2)})`,\n content: a.content ?? null,\n })\n })\n return out\n}\n\nfunction proposalCandidates(\n req: CompletionRequirement,\n reqIndex: number,\n proposals: ProducedProposal[],\n): Candidate[] {\n const reqText = `${req.title} ${req.category ?? ''}`\n const out: Candidate[] = []\n for (const p of proposals) {\n // Pending or rejected work is not a completed deliverable.\n if (p.status !== 'approved') continue\n // A proposal needs an assessable BODY to be a deliverable. A bare title is\n // not completion: correctness cannot be judged on it, so a title-only match\n // would auto-pass the oracle (structurallyPresent && correct===null →\n // satisfied) with no verifiable content. Tool calls are the only\n // legitimately content-less deliverable (`toolCallCandidates`).\n const body = (p.content ?? '').trim()\n if (body.length < MIN_CONTENT_CHARS) continue\n // Match against the body as well as the (often short) title — a refusal /\n // flag / analysis proposal whose title is a label still satisfies a\n // descriptively-worded requirement when its content covers it. MATCH_THRESHOLD\n // + the requirement's distinctive tokens keep an off-topic proposal out;\n // correctness (a SEMANTIC checker, NOT this lexical pass) then judges\n // polarity/fulfilment, so a negation that merely contains the tokens fails.\n // Structural and correctness must use different evidence or the two-stage\n // check collapses to one lexical gate.\n const score = tokenRecall(reqText, `${p.title} ${body}`)\n if (score < MATCH_THRESHOLD) continue\n out.push({\n reqIndex,\n itemKey: `proposal:${p.id}`,\n score,\n evidence: `approved proposal '${p.title}' matched (token recall ${score.toFixed(2)})`,\n content: body,\n })\n }\n return out\n}\n\nfunction toolCallCandidates(\n req: CompletionRequirement,\n reqIndex: number,\n toolCalls: string[],\n): Candidate[] {\n const out: Candidate[] = []\n toolCalls.forEach((name, i) => {\n const score = tokenRecall(req.title, name)\n if (score < MATCH_THRESHOLD) return\n out.push({\n reqIndex,\n itemKey: `tool:${i}`,\n score,\n evidence: `tool call '${name}' matched (token recall ${score.toFixed(2)})`,\n content: null,\n })\n })\n return out\n}\n\n/**\n * Verify whether a run completed the task. `checkCorrectness` is injected —\n * `createLlmCorrectnessChecker` for production, a deterministic stub in tests.\n *\n * Throws on a gold spec with no requirements: an eval task that requires\n * nothing is a misconfiguration, not a vacuously-complete task.\n */\nexport async function verifyCompletion(\n gold: TaskGold,\n state: ProducedState,\n checkCorrectness: CorrectnessChecker,\n): Promise<CompletionVerdict> {\n if (gold.requirements.length === 0) {\n throw new Error(\n `verifyCompletion: task '${gold.taskId}' has no requirements — malformed gold spec`,\n )\n }\n\n // Collect every above-threshold (requirement, produced-item) candidate, then\n // assign greedily by descending score: each requirement and each produced\n // item is used at most once. One deliverable fulfils one requirement.\n const candidates: Candidate[] = []\n gold.requirements.forEach((req, i) => {\n const by = req.satisfiedBy ?? 'any'\n if (by === 'artifact' || by === 'any') {\n candidates.push(...artifactCandidates(req, i, state.artifacts))\n }\n if (by === 'proposal' || by === 'any') {\n candidates.push(...proposalCandidates(req, i, state.proposals))\n }\n if (by === 'tool-call' || by === 'any') {\n candidates.push(...toolCallCandidates(req, i, state.toolCalls))\n }\n })\n candidates.sort((a, b) => b.score - a.score)\n\n const assigned = new Map<number, Candidate>()\n const itemTaken = new Set<string>()\n for (const c of candidates) {\n if (assigned.has(c.reqIndex) || itemTaken.has(c.itemKey)) continue\n assigned.set(c.reqIndex, c)\n itemTaken.add(c.itemKey)\n }\n\n const requirements: RequirementCheck[] = []\n for (let i = 0; i < gold.requirements.length; i++) {\n const req = gold.requirements[i]!\n const match = assigned.get(i)\n const evidence: string[] = []\n let correct: boolean | null = null\n let unmeasuredReason: string | undefined\n\n if (match) {\n evidence.push(match.evidence)\n if (match.content !== null) {\n try {\n const r = await checkCorrectness(req, match.content)\n correct = r.correct\n evidence.push(`correctness: ${r.correct ? 'pass' : 'fail'} — ${r.reason}`)\n } catch (err) {\n // The CHECKER failed, not the requirement. Recording this as a\n // zero would fabricate a model failure out of an infrastructure\n // one; the requirement is unmeasured and leaves the denominator.\n unmeasuredReason =\n err instanceof JudgeParseError\n ? `checker response unparseable after retry: ${err.raw.slice(0, 200)}`\n : `checker call failed: ${err instanceof Error ? err.message : String(err)}`\n evidence.push(`correctness: UNMEASURED — ${unmeasuredReason}`)\n }\n } else {\n evidence.push('correctness: not assessed — matched item carries no content')\n }\n } else {\n const by = req.satisfiedBy ?? 'any'\n const kind = by === 'any' ? 'artifact/proposal/tool-call' : by\n evidence.push(`no produced ${kind} matched this requirement`)\n }\n\n const structurallyPresent = match !== undefined\n const unmeasured = unmeasuredReason !== undefined\n const satisfied = structurallyPresent && !unmeasured && correct !== false\n requirements.push({\n reqId: req.reqId,\n title: req.title,\n structurallyPresent,\n correct,\n satisfied,\n ...(unmeasured ? { unmeasured: true as const, unmeasuredReason } : {}),\n evidence,\n })\n }\n\n // The verdict blends the deterministic structural stage with the injected\n // correctness stage, so the certification names the checker's own member\n // and carries the structural stage's lexical nature as an assumption. A\n // checker without an attestation yields an uncertified verdict.\n const attestation = checkCorrectness.attestation\n const certification: VerdictCertification | undefined = attestation\n ? {\n strategy: attestation.strategy,\n checker: attestation.checker,\n assumptions: [\n 'structural matching is lexical token recall over produced items — the correctness stage only sees items it matched',\n ...attestation.assumptions,\n ],\n evidenceDigest: certificationEvidenceDigest({ taskId: gold.taskId, requirements }),\n }\n : undefined\n\n return completionVerdict({\n taskId: gold.taskId,\n requirements,\n ...(certification === undefined ? {} : { certification }),\n })\n}\n\nexport interface LlmCorrectnessCheckerOpts {\n model?: string\n /** Optional ledger for direct use. */\n costLedger?: CostLedgerHandle\n costPhase?: string\n costTags?: Record<string, string>\n signal?: AbortSignal\n /** Max chars of artifact content sent to the checker. */\n maxContentChars?: number\n /**\n * Checker LLM calls per requirement before giving up (parse failures and\n * call errors both consume attempts). The failure then surfaces as an\n * `unmeasured` requirement, never a zero.\n */\n maxAttempts?: number\n /**\n * Forensic capture of every checker request/response/error — without it a\n * checker failure is unauditable (the agent-turn raws never contain the\n * checker's own calls). Same sink contract as `LlmClient`.\n */\n rawSink?: RawProviderSink\n}\n\n/**\n * Parse the correctness checker's model response. Tolerates a response\n * truncated mid-JSON (max_tokens cap) by auto-closing the prefix — the\n * verdict boolean usually lands in the first few tokens, so a recovered\n * prefix with a boolean `correct` is a real measurement, not a guess.\n * Fails loud (JudgeParseError) when no boolean verdict is recoverable.\n */\nexport function parseCorrectnessResponse(raw: string): { correct: boolean; reason: string } {\n const readVerdict = (candidate: unknown): { correct: boolean; reason: string } | null => {\n if (candidate === null || typeof candidate !== 'object') return null\n const { correct, reason } = candidate as { correct?: unknown; reason?: unknown }\n if (typeof correct !== 'boolean') return null\n return { correct, reason: typeof reason === 'string' ? reason : '' }\n }\n\n const match = raw.match(/\\{[\\s\\S]*\\}/)\n if (match) {\n try {\n const strict = readVerdict(JSON.parse(match[0]))\n if (strict) return strict\n } catch {\n // fall through to truncation recovery\n }\n }\n // The strict path needs a closing `}`; a cap-hit response has none. Take\n // everything from the first `{` and auto-close it.\n const start = raw.indexOf('{')\n if (start !== -1) {\n const recovered = readVerdict(recoverTruncatedJson(raw.slice(start)))\n if (recovered) return recovered\n }\n throw new JudgeParseError('correctness-checker', raw)\n}\n\n/**\n * Production `CorrectnessChecker` — one LLM call per matched artifact,\n * deterministic (temperature 0), structured JSON out. Judges fulfilment\n * only: a plan, a gesture, or a description of what should be done does not\n * fulfil a requirement — the artifact must BE the deliverable.\n */\nexport function createLlmCorrectnessChecker(\n chat: ChatClient,\n opts: LlmCorrectnessCheckerOpts = {},\n): CorrectnessChecker {\n const model = opts.model ?? 'claude-sonnet-4-6'\n const maxContentChars = opts.maxContentChars ?? 8000\n const maxAttempts = opts.maxAttempts ?? 2\n const costLedger = opts.costLedger ?? new CostLedger()\n const sink = opts.rawSink\n const record = async (event: RawProviderEvent): Promise<void> => {\n // Forensic capture is best-effort; the verdict is the system of record.\n try {\n await sink?.record(event)\n } catch {\n // Intentionally swallowed.\n }\n }\n const checker: CorrectnessChecker = async (requirement, content) => {\n const request = {\n model,\n messages: [\n {\n role: 'system' as const,\n content:\n 'You verify whether a produced work artifact actually fulfils a stated requirement. Judge fulfilment only — is the deliverable substantively present and on-point — not polish. A plan to do it later, a vague gesture, or a description of what should be done does NOT fulfil a requirement; the artifact must BE the deliverable. Respond with a single JSON object: {\"correct\": boolean, \"reason\": string (<= 30 words)}.',\n },\n {\n role: 'user' as const,\n content: `Requirement: ${requirement.title}\\n${\n requirement.category ? `Category: ${requirement.category}\\n` : ''\n }\\nProduced artifact:\\n${content.slice(0, maxContentChars)}`,\n },\n ],\n temperature: 0,\n maxTokens: 200,\n } satisfies ChatRequest\n let lastErr: unknown\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const started = Date.now()\n await record({\n eventId: randomUUID(),\n provider: chat.transport,\n model,\n endpoint: '/chat',\n baseUrl: '',\n attemptIndex: attempt,\n direction: 'request',\n timestamp: started,\n requestBody: request,\n redactedFields: [],\n })\n try {\n const paid = await costLedger.runPaidCall({\n channel: 'verifier',\n phase: opts.costPhase ?? 'completion.correctness',\n actor: 'correctness-checker',\n model,\n maximumCharge:\n chat.maximumAttempts === undefined\n ? undefined\n : maximumChargeForLlmRequest(request, { maximumAttempts: chat.maximumAttempts }),\n tags: {\n ...opts.costTags,\n requirementId: requirement.reqId,\n attempt: String(attempt),\n },\n signal: opts.signal,\n execute: (signal, callId) => chat.chat(request, { signal, idempotencyKey: callId }),\n receipt: costReceiptFromLlm,\n receiptFromError: costReceiptFromLlmError,\n })\n if (!paid.succeeded) throw paid.error\n const resp = paid.value\n // Hold the transport to its own word: a verdict produced by a model\n // other than the one requested is not this model's verdict. A\n // transport that echoes no id cannot be made to prove identity here —\n // callers needing that proof enable it at the client\n // (`LlmClientOptions.assertServedModel`) or gate on `assertModelsServed`.\n assertServedModel(model, resp.servedModel, {\n allowUnreported: true,\n context: `correctness checker for requirement ${requirement.reqId}`,\n })\n const raw = resp.content\n await record({\n eventId: randomUUID(),\n provider: chat.transport,\n model,\n endpoint: '/chat',\n baseUrl: '',\n attemptIndex: attempt,\n direction: 'response',\n timestamp: Date.now(),\n durationMs: Date.now() - started,\n responseBody: resp,\n redactedFields: [],\n })\n return parseCorrectnessResponse(raw)\n } catch (err) {\n lastErr = err\n await record({\n eventId: randomUUID(),\n provider: chat.transport,\n model,\n endpoint: '/chat',\n baseUrl: '',\n attemptIndex: attempt,\n direction: 'error',\n timestamp: Date.now(),\n durationMs: Date.now() - started,\n errorMessage: err instanceof Error ? err.message : String(err),\n redactedFields: [],\n })\n // Substitution is a verdict about identity, not a transient fault:\n // another attempt reaches the same wrong model and spends more tokens.\n if (err instanceof ModelSubstitutionError) throw err\n }\n }\n throw lastErr instanceof Error ? lastErr : new Error(String(lastErr))\n }\n checker.attestation = {\n strategy: 'judge',\n checker: { name: 'llm-correctness-checker', version: model },\n assumptions: [\n 'served-model identity is accepted when the transport reports none (assertServedModel allowUnreported)',\n ],\n }\n return checker\n}\n\n/** Stopwords for requirement-title tokenization — drops the imperative verbs\n * ('review', 'update', …) common to deliverable titles so recall keys on the\n * substantive nouns, not the boilerplate ask. */\nconst TITLE_STOPWORDS = new Set([\n 'the',\n 'a',\n 'an',\n 'and',\n 'or',\n 'for',\n 'to',\n 'of',\n 'in',\n 'on',\n 'with',\n 'review',\n 'update',\n 'new',\n 'proposed',\n])\n\n/**\n * Deterministic `CorrectnessChecker` — the no-LLM counterpart to\n * `createLlmCorrectnessChecker`. A produced item fulfils a requirement when its\n * content is substantive (≥ `minContentLength` chars) AND recalls ≥ `minRecall`\n * of the requirement title's significant tokens. No network.\n *\n * Polarity-blind: token recall credits a negation that contains the\n * requirement's tokens (\"I will NOT produce the comparison\" recalls every token\n * of \"produce the comparison\"). The structural match stage is ALSO lexical, so\n * pairing the two collapses to a single gameable gate. Use this only as an\n * opt-in structural pre-filter or for tasks whose requirements have no polarity\n * to invert; for produced-state grading the correctness checker MUST be semantic\n * (`createLlmCorrectnessChecker`). See the anti-game fixtures in the test suite.\n */\nexport function createTokenRecallChecker(\n opts: { minRecall?: number; minContentLength?: number } = {},\n): CorrectnessChecker {\n const minRecall = opts.minRecall ?? 0.5\n const minLen = opts.minContentLength ?? 120\n const checker: CorrectnessChecker = async (requirement, content) => {\n const body = content.trim()\n if (body.length < minLen)\n return {\n correct: false,\n reason: `content too thin (${body.length} chars) to be the deliverable`,\n }\n const titleTokens = requirement.title\n .toLowerCase()\n .split(/[^a-z0-9]+/)\n .filter((t) => t.length > 2 && !TITLE_STOPWORDS.has(t))\n if (titleTokens.length === 0)\n return {\n correct: true,\n reason: 'requirement title has no significant tokens — structural match accepted',\n }\n const lower = body.toLowerCase()\n const hits = titleTokens.filter((t) => lower.includes(t)).length\n const recall = hits / titleTokens.length\n return recall >= minRecall\n ? {\n correct: true,\n reason: `content recalls ${hits}/${titleTokens.length} requirement tokens`,\n }\n : {\n correct: false,\n reason: `content recalls only ${hits}/${titleTokens.length} requirement tokens`,\n }\n }\n // 'schema': token recall checks shape, not meaning — the member whose\n // documented failure mode (\"a well-formed wrong answer passes\") is\n // exactly this checker's polarity blindness.\n checker.attestation = {\n strategy: 'schema',\n checker: { name: 'token-recall-checker', version: packageVersion() },\n assumptions: ['polarity-blind: a negation that recalls the requirement tokens passes'],\n }\n return checker\n}\n","/**\n * Produced-state extraction — normalize a run's runtime event stream into the\n * typed `ProducedState` the completion oracle consumes.\n *\n * `ProducedState` answers \"what did the agent actually produce\" — vault\n * artifacts, proposals, tool calls. The runtime emits these as a stream of\n * events; this module is the single normalization point from that stream to\n * the shape `verifyCompletion` expects.\n *\n * Input is structurally typed (`RuntimeEventLike`) so this module does not\n * depend on agent-runtime — agent-runtime's `RuntimeStreamEvent` satisfies it\n * structurally. The `content` on `ArtifactEventLike` and the whole\n * `proposal_created` variant are the runtime-side enrichments this contract\n * requires; the runtime emits them, this module consumes them.\n */\n\nimport type { Artifact } from './artifact-validator'\nimport type { ProducedProposal, ProducedState } from './completion-verifier'\n\n/** A tool the agent invoked. */\nexport interface ToolCallEventLike {\n type: 'tool_call'\n toolName: string\n}\n\n/**\n * An artifact the agent produced. `content` is the enriched field — the\n * runtime's base `artifact` event carries only metadata; the completion\n * oracle needs the body to verify the deliverable, so the runtime emits it.\n */\nexport interface ArtifactEventLike {\n type: 'artifact'\n artifactId: string\n name?: string\n mimeType?: string\n uri?: string\n content?: string\n}\n\n/** A proposal / filing the agent created. */\nexport interface ProposalEventLike {\n type: 'proposal_created'\n proposalId: string\n title: string\n status?: 'pending' | 'approved' | 'rejected'\n // body of the proposal (e.g. a submit_proposal `description`). When present,\n // the completion oracle correctness-checks it like artifact content; absent,\n // the proposal is graded presence-only.\n content?: string\n}\n\n/**\n * The subset of runtime stream events `extractProducedState` consumes.\n * agent-runtime's full `RuntimeStreamEvent` union satisfies this structurally;\n * the `{ type: string }` catch-all keeps the input permissive so callers can\n * pass the whole unfiltered telemetry stream — unrecognized events are skipped.\n */\nexport type RuntimeEventLike =\n | ToolCallEventLike\n | ArtifactEventLike\n | ProposalEventLike\n | { type: string }\n\nfunction artifactKind(mimeType: string | undefined): string {\n if (!mimeType) return 'file'\n if (mimeType.includes('json')) return 'json'\n if (mimeType.startsWith('text/')) return 'text'\n return 'file'\n}\n\n/**\n * Normalize an emission-ordered stream into its latest observed produced state.\n * Artifacts are keyed by their exact output path (name, then URI, then id);\n * proposals by id. Later observations replace earlier ones, including missing\n * content or a rejected status. An obsolete version cannot prove completion.\n * Distinct paths remain distinct; no path normalization or aliasing is inferred.\n * Results retain first-seen identity order. Tool names describe invocation,\n * not success, and are deduplicated in first-seen order.\n *\n * An artifact without observed content yields empty content, which the\n * completion oracle rejects. This projection does not verify external effects,\n * recover dropped events, or establish freshness beyond the supplied stream.\n */\nexport function extractProducedState(events: readonly RuntimeEventLike[]): ProducedState {\n const artifacts = new Map<string, Artifact>()\n const proposals = new Map<string, ProducedProposal>()\n const toolCalls: string[] = []\n const seenTools = new Set<string>()\n\n for (const ev of events) {\n if (ev.type === 'tool_call') {\n const name = (ev as ToolCallEventLike).toolName\n if (name && !seenTools.has(name)) {\n seenTools.add(name)\n toolCalls.push(name)\n }\n } else if (ev.type === 'artifact') {\n const a = ev as ArtifactEventLike\n const path = a.name ?? a.uri ?? a.artifactId\n artifacts.set(path, {\n kind: artifactKind(a.mimeType),\n path,\n content: a.content ?? '',\n })\n } else if (ev.type === 'proposal_created') {\n const p = ev as ProposalEventLike\n proposals.set(p.proposalId, {\n id: p.proposalId,\n title: p.title,\n status: p.status ?? 'pending',\n ...(p.content !== undefined ? { content: p.content } : {}),\n })\n }\n }\n\n return { artifacts: [...artifacts.values()], proposals: [...proposals.values()], toolCalls }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { u as redact } from "./redact-DxwlUAvh.js";
1
+ import { u as redact } from "./redact-8fX8-uKr.js";
2
2
  import { promises } from "node:fs";
3
3
  import * as path from "node:path";
4
4
  //#region src/trace/raw-provider-sink.ts
@@ -161,4 +161,4 @@ function providerFromBaseUrl(baseUrl) {
161
161
  //#endregion
162
162
  export { providerFromBaseUrl as a, defaultProviderRedactor as i, InMemoryRawProviderSink as n, NoopRawProviderSink as r, FileSystemRawProviderSink as t };
163
163
 
164
- //# sourceMappingURL=raw-provider-sink-C96CcqeG.js.map
164
+ //# sourceMappingURL=raw-provider-sink-DIRCZjP_.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"raw-provider-sink-C96CcqeG.js","names":["fs"],"sources":["../src/trace/raw-provider-sink.ts"],"sourcesContent":["/**\n * RawProviderSink — first-class persistence for the actual HTTP-level\n * request/response bodies of every LLM provider call.\n *\n * Why this is a separate sink from the structured `LlmSpan`:\n *\n * - `LlmSpan` records the *intent* — model name, messages, output text,\n * usage. It's what dashboards read; it's NOT enough for forensics.\n * - When a downstream consumer reports \"the verifier used the wrong route\"\n * or \"tokens look right but reasoning was missing,\" the only way to\n * answer is the raw HTTP body. Span fields can lie (a proxy can echo\n * a different `model` value than what actually answered); the raw\n * response is ground truth.\n *\n * Default behaviour: opt-in. Pass `rawSink` to `LlmClientOptions` (or the\n * matrix runner / BuilderSession sets it up automatically) and every\n * request, response, and error is recorded — including retries, with the\n * attempt index attached so a flaky call's full event chain is recoverable.\n *\n * Redaction is enforced at sink time. The default redactor runs the redaction\n * core (`./redact`, default profile) over headers, bodies and the error\n * message: credential headers and fields are replaced by name, credential and\n * personal-data values by shape. Override via the sink constructor or the\n * per-call `redactor`. The `redactedFields` array on the persisted event lets\n * a reviewer see what was replaced without exposing the values.\n */\n\nimport { promises as fs } from 'node:fs'\nimport * as path from 'node:path'\nimport { redact } from './redact'\n\nexport type RawProviderDirection = 'request' | 'response' | 'error'\n\nexport interface RawProviderEvent {\n /** Stable id. Generated by the sink if omitted. */\n eventId: string\n /** Trace context populated by `LlmClient` when the call is wrapped in a span. */\n runId?: string\n spanId?: string\n /**\n * Logical provider name. Free-form so callers can use whatever id matches\n * their topology (`'openai'`, `'anthropic'`, `'tangle-router'`, …). When\n * omitted, derived from `baseUrl` in `LlmClientOptions`.\n */\n provider: string\n model: string\n /** Endpoint path, e.g. `'/v1/chat/completions'`. */\n endpoint: string\n /** Base URL used for the call (already-normalised — no trailing slash). */\n baseUrl: string\n /** 0-indexed retry attempt. The first attempt is 0; a retried call gets 1, 2, … */\n attemptIndex: number\n direction: RawProviderDirection\n /** Unix ms. */\n timestamp: number\n /** Wall-clock duration of the call leg. Set on `response` and `error` events; null on `request`. */\n durationMs?: number\n statusCode?: number\n requestHeaders?: Record<string, string>\n requestBody?: unknown\n responseHeaders?: Record<string, string>\n responseBody?: unknown\n /** Set on `direction: 'error'` events. */\n errorMessage?: string\n /** JSON Pointers of the fields the redactor replaced ('/requestHeaders/Authorization', '/requestBody/apiKey', …). */\n redactedFields: string[]\n}\n\nexport interface RawProviderSinkFilter {\n runId?: string\n spanId?: string\n direction?: RawProviderDirection\n attemptIndex?: number\n}\n\nexport interface RawProviderSink {\n record(event: RawProviderEvent): Promise<void>\n /** Optional listing — implementations that durably persist (file, db) should support this. */\n list?(filter?: RawProviderSinkFilter): Promise<RawProviderEvent[]>\n /** Optional teardown for backed implementations. */\n close?(): Promise<void>\n}\n\nexport type ProviderRedactor = (event: RawProviderEvent) => RawProviderEvent\n\n/**\n * The default redactor: the redaction core over the parts of an event that\n * carry provider traffic. Every replaced path is added to `redactedFields`.\n */\nexport function defaultProviderRedactor(event: RawProviderEvent): RawProviderEvent {\n const { requestHeaders, requestBody, responseHeaders, responseBody, errorMessage } = event\n const { value, report } = redact({\n requestHeaders,\n requestBody,\n responseHeaders,\n responseBody,\n errorMessage,\n })\n return {\n ...event,\n ...value,\n redactedFields: [\n ...(event.redactedFields ?? []),\n ...report.findings.map((finding) => finding.path),\n ],\n }\n}\n\n// ── In-memory ────────────────────────────────────────────────────────────\n\nexport interface InMemoryRawProviderSinkOptions {\n redactor?: ProviderRedactor\n}\n\nexport class InMemoryRawProviderSink implements RawProviderSink {\n private events: RawProviderEvent[] = []\n private redactor: ProviderRedactor\n\n constructor(opts: InMemoryRawProviderSinkOptions = {}) {\n this.redactor = opts.redactor ?? defaultProviderRedactor\n }\n\n async record(event: RawProviderEvent): Promise<void> {\n this.events.push(this.redactor({ ...event, redactedFields: event.redactedFields ?? [] }))\n }\n\n async list(filter: RawProviderSinkFilter = {}): Promise<RawProviderEvent[]> {\n return this.events.filter(\n (e) =>\n (filter.runId === undefined || e.runId === filter.runId) &&\n (filter.spanId === undefined || e.spanId === filter.spanId) &&\n (filter.direction === undefined || e.direction === filter.direction) &&\n (filter.attemptIndex === undefined || e.attemptIndex === filter.attemptIndex),\n )\n }\n\n size(): number {\n return this.events.length\n }\n}\n\nexport class NoopRawProviderSink implements RawProviderSink {\n async record(): Promise<void> {\n /* no-op */\n }\n /**\n * Returns an empty array. Implemented so `assertRunCaptured` does not\n * trip the `no_raw_sink` issue when a caller explicitly opts out of\n * capture by passing this sink — opt-out is a deliberate choice, not a\n * misconfiguration.\n */\n async list(): Promise<RawProviderEvent[]> {\n return []\n }\n}\n\n// ── Filesystem (NDJSON) ──────────────────────────────────────────────────\n\nexport interface FileSystemRawProviderSinkOptions {\n /** Directory the NDJSON file is written into. Created if missing. */\n dir: string\n /** File name; default `'raw-provider-events.ndjson'`. */\n fileName?: string\n /** Bytes after which the writer rolls over to a new file (default 32 MiB). */\n rollAtBytes?: number\n redactor?: ProviderRedactor\n}\n\nexport class FileSystemRawProviderSink implements RawProviderSink {\n private dir: string\n private fileName: string\n private rollAtBytes: number\n private redactor: ProviderRedactor\n private bytesWritten = 0\n private rollIndex = 0\n private initPromise: Promise<void> | null = null\n\n constructor(opts: FileSystemRawProviderSinkOptions) {\n this.dir = opts.dir\n this.fileName = opts.fileName ?? 'raw-provider-events.ndjson'\n this.rollAtBytes = opts.rollAtBytes ?? 32 * 1024 * 1024\n this.redactor = opts.redactor ?? defaultProviderRedactor\n }\n\n private async ensureInit(): Promise<void> {\n if (!this.initPromise) {\n this.initPromise = fs.mkdir(this.dir, { recursive: true }).then(() => undefined)\n }\n await this.initPromise\n }\n\n private currentPath(): string {\n if (this.rollIndex === 0) return path.join(this.dir, this.fileName)\n return path.join(this.dir, `${this.fileName}.${this.rollIndex}`)\n }\n\n async record(event: RawProviderEvent): Promise<void> {\n await this.ensureInit()\n const redacted = this.redactor({ ...event, redactedFields: event.redactedFields ?? [] })\n const line = `${JSON.stringify(redacted)}\\n`\n if (this.bytesWritten + line.length > this.rollAtBytes && this.bytesWritten > 0) {\n this.rollIndex += 1\n this.bytesWritten = 0\n }\n await fs.appendFile(this.currentPath(), line, 'utf8')\n this.bytesWritten += line.length\n }\n\n async list(filter: RawProviderSinkFilter = {}): Promise<RawProviderEvent[]> {\n await this.ensureInit()\n const out: RawProviderEvent[] = []\n for (let i = 0; i <= this.rollIndex; i++) {\n const file =\n i === 0 ? path.join(this.dir, this.fileName) : path.join(this.dir, `${this.fileName}.${i}`)\n let body: string\n try {\n body = await fs.readFile(file, 'utf8')\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') continue\n throw err\n }\n for (const line of body.split('\\n')) {\n if (!line) continue\n const event = JSON.parse(line) as RawProviderEvent\n if (filter.runId !== undefined && event.runId !== filter.runId) continue\n if (filter.spanId !== undefined && event.spanId !== filter.spanId) continue\n if (filter.direction !== undefined && event.direction !== filter.direction) continue\n if (filter.attemptIndex !== undefined && event.attemptIndex !== filter.attemptIndex)\n continue\n out.push(event)\n }\n }\n return out\n }\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────\n\n/**\n * Best-effort provider id from a base URL. Falls back to the URL host when\n * none of the well-known patterns match.\n */\nexport function providerFromBaseUrl(baseUrl: string): string {\n const lower = baseUrl.toLowerCase()\n if (lower.includes('api.openai.com')) return 'openai'\n if (lower.includes('api.anthropic.com')) return 'anthropic'\n if (lower.includes('generativelanguage.googleapis.com')) return 'google'\n if (lower.includes('api.together.ai') || lower.includes('api.together.xyz')) return 'together'\n if (lower.includes('api.deepseek.com')) return 'deepseek'\n if (lower.includes('router.tangle.tools')) return 'tangle-router'\n if (lower.includes('api.litellm') || lower.includes('litellm')) return 'litellm'\n try {\n return new URL(baseUrl).host\n } catch {\n return baseUrl\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"raw-provider-sink-DIRCZjP_.js","names":["fs"],"sources":["../src/trace/raw-provider-sink.ts"],"sourcesContent":["/**\n * RawProviderSink — first-class persistence for the actual HTTP-level\n * request/response bodies of every LLM provider call.\n *\n * Why this is a separate sink from the structured `LlmSpan`:\n *\n * - `LlmSpan` records the *intent* — model name, messages, output text,\n * usage. It's what dashboards read; it's NOT enough for forensics.\n * - When a downstream consumer reports \"the verifier used the wrong route\"\n * or \"tokens look right but reasoning was missing,\" the only way to\n * answer is the raw HTTP body. Span fields can lie (a proxy can echo\n * a different `model` value than what actually answered); the raw\n * response is ground truth.\n *\n * Default behaviour: opt-in. Pass `rawSink` to `LlmClientOptions` (or the\n * matrix runner / BuilderSession sets it up automatically) and every\n * request, response, and error is recorded — including retries, with the\n * attempt index attached so a flaky call's full event chain is recoverable.\n *\n * Redaction is enforced at sink time. The default redactor runs the redaction\n * core (`./redact`, default profile) over headers, bodies and the error\n * message: credential headers and fields are replaced by name, credential and\n * personal-data values by shape. Override via the sink constructor or the\n * per-call `redactor`. The `redactedFields` array on the persisted event lets\n * a reviewer see what was replaced without exposing the values.\n */\n\nimport { promises as fs } from 'node:fs'\nimport * as path from 'node:path'\nimport { redact } from './redact'\n\nexport type RawProviderDirection = 'request' | 'response' | 'error'\n\nexport interface RawProviderEvent {\n /** Stable id. Generated by the sink if omitted. */\n eventId: string\n /** Trace context populated by `LlmClient` when the call is wrapped in a span. */\n runId?: string\n spanId?: string\n /**\n * Logical provider name. Free-form so callers can use whatever id matches\n * their topology (`'openai'`, `'anthropic'`, `'tangle-router'`, …). When\n * omitted, derived from `baseUrl` in `LlmClientOptions`.\n */\n provider: string\n model: string\n /** Endpoint path, e.g. `'/v1/chat/completions'`. */\n endpoint: string\n /** Base URL used for the call (already-normalised — no trailing slash). */\n baseUrl: string\n /** 0-indexed retry attempt. The first attempt is 0; a retried call gets 1, 2, … */\n attemptIndex: number\n direction: RawProviderDirection\n /** Unix ms. */\n timestamp: number\n /** Wall-clock duration of the call leg. Set on `response` and `error` events; null on `request`. */\n durationMs?: number\n statusCode?: number\n requestHeaders?: Record<string, string>\n requestBody?: unknown\n responseHeaders?: Record<string, string>\n responseBody?: unknown\n /** Set on `direction: 'error'` events. */\n errorMessage?: string\n /** JSON Pointers of the fields the redactor replaced ('/requestHeaders/Authorization', '/requestBody/apiKey', …). */\n redactedFields: string[]\n}\n\nexport interface RawProviderSinkFilter {\n runId?: string\n spanId?: string\n direction?: RawProviderDirection\n attemptIndex?: number\n}\n\nexport interface RawProviderSink {\n record(event: RawProviderEvent): Promise<void>\n /** Optional listing — implementations that durably persist (file, db) should support this. */\n list?(filter?: RawProviderSinkFilter): Promise<RawProviderEvent[]>\n /** Optional teardown for backed implementations. */\n close?(): Promise<void>\n}\n\nexport type ProviderRedactor = (event: RawProviderEvent) => RawProviderEvent\n\n/**\n * The default redactor: the redaction core over the parts of an event that\n * carry provider traffic. Every replaced path is added to `redactedFields`.\n */\nexport function defaultProviderRedactor(event: RawProviderEvent): RawProviderEvent {\n const { requestHeaders, requestBody, responseHeaders, responseBody, errorMessage } = event\n const { value, report } = redact({\n requestHeaders,\n requestBody,\n responseHeaders,\n responseBody,\n errorMessage,\n })\n return {\n ...event,\n ...value,\n redactedFields: [\n ...(event.redactedFields ?? []),\n ...report.findings.map((finding) => finding.path),\n ],\n }\n}\n\n// ── In-memory ────────────────────────────────────────────────────────────\n\nexport interface InMemoryRawProviderSinkOptions {\n redactor?: ProviderRedactor\n}\n\nexport class InMemoryRawProviderSink implements RawProviderSink {\n private events: RawProviderEvent[] = []\n private redactor: ProviderRedactor\n\n constructor(opts: InMemoryRawProviderSinkOptions = {}) {\n this.redactor = opts.redactor ?? defaultProviderRedactor\n }\n\n async record(event: RawProviderEvent): Promise<void> {\n this.events.push(this.redactor({ ...event, redactedFields: event.redactedFields ?? [] }))\n }\n\n async list(filter: RawProviderSinkFilter = {}): Promise<RawProviderEvent[]> {\n return this.events.filter(\n (e) =>\n (filter.runId === undefined || e.runId === filter.runId) &&\n (filter.spanId === undefined || e.spanId === filter.spanId) &&\n (filter.direction === undefined || e.direction === filter.direction) &&\n (filter.attemptIndex === undefined || e.attemptIndex === filter.attemptIndex),\n )\n }\n\n size(): number {\n return this.events.length\n }\n}\n\nexport class NoopRawProviderSink implements RawProviderSink {\n async record(): Promise<void> {\n /* no-op */\n }\n /**\n * Returns an empty array. Implemented so `assertRunCaptured` does not\n * trip the `no_raw_sink` issue when a caller explicitly opts out of\n * capture by passing this sink — opt-out is a deliberate choice, not a\n * misconfiguration.\n */\n async list(): Promise<RawProviderEvent[]> {\n return []\n }\n}\n\n// ── Filesystem (NDJSON) ──────────────────────────────────────────────────\n\nexport interface FileSystemRawProviderSinkOptions {\n /** Directory the NDJSON file is written into. Created if missing. */\n dir: string\n /** File name; default `'raw-provider-events.ndjson'`. */\n fileName?: string\n /** Bytes after which the writer rolls over to a new file (default 32 MiB). */\n rollAtBytes?: number\n redactor?: ProviderRedactor\n}\n\nexport class FileSystemRawProviderSink implements RawProviderSink {\n private dir: string\n private fileName: string\n private rollAtBytes: number\n private redactor: ProviderRedactor\n private bytesWritten = 0\n private rollIndex = 0\n private initPromise: Promise<void> | null = null\n\n constructor(opts: FileSystemRawProviderSinkOptions) {\n this.dir = opts.dir\n this.fileName = opts.fileName ?? 'raw-provider-events.ndjson'\n this.rollAtBytes = opts.rollAtBytes ?? 32 * 1024 * 1024\n this.redactor = opts.redactor ?? defaultProviderRedactor\n }\n\n private async ensureInit(): Promise<void> {\n if (!this.initPromise) {\n this.initPromise = fs.mkdir(this.dir, { recursive: true }).then(() => undefined)\n }\n await this.initPromise\n }\n\n private currentPath(): string {\n if (this.rollIndex === 0) return path.join(this.dir, this.fileName)\n return path.join(this.dir, `${this.fileName}.${this.rollIndex}`)\n }\n\n async record(event: RawProviderEvent): Promise<void> {\n await this.ensureInit()\n const redacted = this.redactor({ ...event, redactedFields: event.redactedFields ?? [] })\n const line = `${JSON.stringify(redacted)}\\n`\n if (this.bytesWritten + line.length > this.rollAtBytes && this.bytesWritten > 0) {\n this.rollIndex += 1\n this.bytesWritten = 0\n }\n await fs.appendFile(this.currentPath(), line, 'utf8')\n this.bytesWritten += line.length\n }\n\n async list(filter: RawProviderSinkFilter = {}): Promise<RawProviderEvent[]> {\n await this.ensureInit()\n const out: RawProviderEvent[] = []\n for (let i = 0; i <= this.rollIndex; i++) {\n const file =\n i === 0 ? path.join(this.dir, this.fileName) : path.join(this.dir, `${this.fileName}.${i}`)\n let body: string\n try {\n body = await fs.readFile(file, 'utf8')\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') continue\n throw err\n }\n for (const line of body.split('\\n')) {\n if (!line) continue\n const event = JSON.parse(line) as RawProviderEvent\n if (filter.runId !== undefined && event.runId !== filter.runId) continue\n if (filter.spanId !== undefined && event.spanId !== filter.spanId) continue\n if (filter.direction !== undefined && event.direction !== filter.direction) continue\n if (filter.attemptIndex !== undefined && event.attemptIndex !== filter.attemptIndex)\n continue\n out.push(event)\n }\n }\n return out\n }\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────\n\n/**\n * Best-effort provider id from a base URL. Falls back to the URL host when\n * none of the well-known patterns match.\n */\nexport function providerFromBaseUrl(baseUrl: string): string {\n const lower = baseUrl.toLowerCase()\n if (lower.includes('api.openai.com')) return 'openai'\n if (lower.includes('api.anthropic.com')) return 'anthropic'\n if (lower.includes('generativelanguage.googleapis.com')) return 'google'\n if (lower.includes('api.together.ai') || lower.includes('api.together.xyz')) return 'together'\n if (lower.includes('api.deepseek.com')) return 'deepseek'\n if (lower.includes('router.tangle.tools')) return 'tangle-router'\n if (lower.includes('api.litellm') || lower.includes('litellm')) return 'litellm'\n try {\n return new URL(baseUrl).host\n } catch {\n return baseUrl\n }\n}\n"],"mappings":"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"}
@@ -45,7 +45,7 @@ const REDACTION_PROFILES = [
45
45
  * redaction behavior (saved optimizer inputs, per-span redaction stamps) keys
46
46
  * on it, so bump it whenever a detector, key list, cap or marker changes.
47
47
  */
48
- const REDACTION_VERSION = "2.0.0";
48
+ const REDACTION_VERSION = "2.1.0";
49
49
  const KIB = 1024;
50
50
  const PROFILE_RULES = {
51
51
  default: {
@@ -354,6 +354,10 @@ const CREDENTIAL_DETECTORS = [
354
354
  {
355
355
  id: "secret-assignment",
356
356
  pattern: /(?:api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key|secret|token|password|passwd|passphrase|credentials?)["']?[ \t]*[:=][ \t]*["']?(?!encrypted:)(?![$`<[{(%])(?=[^\s"'`,;&|]*[0-9])(?=[^\s"'`,;&|]*[A-Za-z])[^\s"'`,;&|]{12,}/i
357
+ },
358
+ {
359
+ id: "json-secret",
360
+ pattern: /["'][A-Za-z0-9_.-]{0,40}?(?:api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key|client[_-]?secret|secret|password|passwd|passphrase|credentials?|(?:access|refresh|id|auth|session|bearer|api|github|gh|slack|npm)[_-]?token)["'][ \t]*:[ \t]*["'](?!\[REDACTED|[$*{<])(?![^"'\r\n]* [^"'\r\n]* )[^"'\r\n]{3,}["']/i
357
361
  }
358
362
  ];
359
363
  /** Personal-data shapes, replaced in place so the surrounding text survives. */
@@ -526,17 +530,22 @@ function truncateUtf8(text, maxBytes) {
526
530
  }
527
531
  function redactStringAt(text, path, state) {
528
532
  if (isPlaceholder(text)) return text;
529
- if (DATA_URI.test(text)) {
530
- if (!state.rules.redactMedia) return text;
533
+ let output = text;
534
+ for (const form of state.knownSecrets) {
535
+ if (!output.includes(form)) continue;
536
+ output = output.split(form).join(marker("known-secret"));
537
+ record(state, path, "credential", "known-secret", "redacted");
538
+ }
539
+ if (DATA_URI.test(output)) {
540
+ if (!state.rules.redactMedia) return output;
531
541
  record(state, path, "media", "media", "redacted");
532
542
  return marker("media");
533
543
  }
534
- const credential = credentialIn(text, state.knownSecrets);
544
+ const credential = credentialIn(output, state.knownSecrets);
535
545
  if (credential) {
536
546
  record(state, path, "credential", credential, "redacted");
537
547
  return marker(credential);
538
548
  }
539
- let output = text;
540
549
  for (const detector of PERSONAL_DATA_DETECTORS) {
541
550
  if (detector.requires && !output.includes(detector.requires)) continue;
542
551
  output = output.replace(detector.pattern, (match) => {
@@ -551,6 +560,7 @@ function redactStringAt(text, path, state) {
551
560
  return truncated;
552
561
  }
553
562
  function pseudonym(value, state) {
563
+ state.pseudonymKey ??= randomBytes(32);
554
564
  return `[ID:${createHmac("sha256", state.pseudonymKey).update(String(value)).digest("hex").slice(0, 12)}]`;
555
565
  }
556
566
  function redactKeyed(key, value, path, depth, state) {
@@ -635,7 +645,6 @@ function newState(options, report) {
635
645
  maxStringBytes: typeof requested === "number" && Number.isFinite(requested) && requested >= 0 ? Math.min(Math.floor(requested), rules.maxStringBytes) : rules.maxStringBytes,
636
646
  knownSecrets: knownSecretForms(options.knownSecrets),
637
647
  report: report ?? newReport(profile),
638
- pseudonymKey: randomBytes(32),
639
648
  seen: /* @__PURE__ */ new WeakSet()
640
649
  };
641
650
  }
@@ -821,4 +830,4 @@ function redactForShare(value, options = {}) {
821
830
  //#endregion
822
831
  export { combineVerdicts as a, emptyRedactionReport as c, redactForShare as d, redactText as f, classifyKey as i, normalizeKey as l, REDACTION_VERSION as n, detectCredential as o, shareAllowed as p, assessShareSafety as r, detectPersonalData as s, REDACTION_PROFILES as t, redact as u };
823
832
 
824
- //# sourceMappingURL=redact-DxwlUAvh.js.map
833
+ //# sourceMappingURL=redact-8fX8-uKr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact-8fX8-uKr.js","names":[],"sources":["../src/trace/redact.ts"],"sourcesContent":["/**\n * The redaction core: the one place that decides what must not leave a\n * process or a machine. Every trace writer, exporter, uploader and share path\n * calls it.\n *\n * Three profiles, each a superset of the one before:\n * - `default` removes credentials, found by key name and by value shape, and\n * personal data (email addresses, card numbers, SSNs, phone numbers).\n * Content is kept. Strings are capped at 1 MiB.\n * - `share` also removes phone, IP and postal-address fields, replaces\n * account, user, tenant, session and request identifiers with pseudonyms\n * that stay consistent within one call, and caps strings at 64 KiB.\n * - `strict` also removes prompt, completion, message, tool payload and other\n * raw-content fields and embedded media, and caps strings at 4 KiB.\n *\n * A string that holds a credential is replaced whole with\n * `[REDACTED:<detector>]`. Replacing only the matched span can leave part of a\n * secret behind when a pattern's boundary is wrong. Personal data inside prose\n * is replaced in place, so the rest of the text stays readable.\n *\n * `assessShareSafety` reads a value without changing it and returns SAFE,\n * SAFE_WITH_WARNINGS, UNSAFE or UNKNOWN. UNKNOWN means the scanner could not\n * read part of the value (a cycle, binary data, nesting past the depth limit);\n * it is never reported as safe. `redactForShare` redacts and then assesses the\n * redacted output, so a detector gap shows up as UNSAFE instead of a leak.\n *\n * Reports and findings carry paths, categories and detector ids, never the\n * values they matched.\n *\n * The key classifier and the credential detector list are adapted from\n * agent-inspect (MIT, commit 3c3cbeda). `scripts/redaction-corpus.json` holds\n * the must-flag and must-not-flag cases, including the copied agent-inspect\n * safety corpus with its MIT notice; `pnpm redaction:corpus` runs this module\n * over every case.\n */\n\nimport { createHmac, randomBytes } from 'node:crypto'\n\nexport type RedactionProfile = 'default' | 'share' | 'strict'\n\nexport const REDACTION_PROFILES: readonly RedactionProfile[] = ['default', 'share', 'strict']\n\n/** What a finding is about. `media` is embedded binary content (a `data:` URI) no text detector can read. */\nexport type SafetyCategory = 'credential' | 'personal-data' | 'identifier' | 'raw-content' | 'media'\n\n/**\n * Version of the detector set and profile rules. Stored work that depends on\n * redaction behavior (saved optimizer inputs, per-span redaction stamps) keys\n * on it, so bump it whenever a detector, key list, cap or marker changes.\n */\nexport const REDACTION_VERSION = '2.1.0'\n\nexport interface RedactionFinding {\n /** JSON Pointer (RFC 6901) to the value or key that was changed. */\n path: string\n category: SafetyCategory\n detector: string\n action: 'redacted' | 'pseudonymized' | 'truncated'\n}\n\nexport interface RedactionReport {\n version: string\n profile: RedactionProfile\n /** Values and keys replaced, pseudonymized or truncated. */\n redactionCount: number\n byDetector: Record<string, number>\n truncatedCount: number\n findings: RedactionFinding[]\n}\n\nexport interface RedactOptions {\n /** Default `default`. */\n profile?: RedactionProfile\n /** Lower the profile's per-string cap. A value above the profile cap is ignored. */\n maxStringBytes?: number\n /**\n * Exact values the caller knows are secret (an API key the run was given, a\n * password from the environment), whatever shape they have. Each occurrence\n * in a string, as written or base64, base64url or URL-encoded, becomes\n * `[REDACTED:known-secret]` and the text around it is kept. A key that\n * contains one, and a whole-string base64 payload that holds one at any byte\n * offset, is replaced whole.\n */\n knownSecrets?: readonly string[]\n /** Add this call's findings to an existing report instead of a new one. */\n report?: RedactionReport\n}\n\nexport interface AssessOptions {\n /** Default `default`. */\n profile?: RedactionProfile\n /** As in `RedactOptions`: any surviving occurrence makes the verdict UNSAFE. */\n knownSecrets?: readonly string[]\n}\n\nexport type ShareSafetyStatus = 'SAFE' | 'SAFE_WITH_WARNINGS' | 'UNSAFE' | 'UNKNOWN'\n\nexport interface ShareSafetyFinding {\n category: SafetyCategory\n detector: string\n severity: 'error' | 'warning'\n count: number\n /** Up to five JSON Pointers where the detector fired. */\n paths: string[]\n}\n\nexport interface ShareSafetyVerdict {\n status: ShareSafetyStatus\n profile: RedactionProfile\n findings: ShareSafetyFinding[]\n /** Parts the scanner could not read, as `<path>: <reason>`. Any entry makes the status UNKNOWN. */\n unreadable: string[]\n}\n\n// ── Profiles ─────────────────────────────────────────────────────────────\n\ninterface ProfileRules {\n maxStringBytes: number\n /** Key categories whose values are removed or pseudonymized. */\n redactKeys: ReadonlySet<'share-personal' | 'identifier' | 'raw-content'>\n redactMedia: boolean\n severity: Record<SafetyCategory, 'error' | 'warning' | undefined>\n}\n\nconst KIB = 1024\n\nconst PROFILE_RULES: Record<RedactionProfile, ProfileRules> = {\n default: {\n maxStringBytes: 1024 * KIB,\n redactKeys: new Set(),\n redactMedia: false,\n severity: {\n credential: 'error',\n 'personal-data': 'warning',\n identifier: undefined,\n 'raw-content': 'warning',\n media: 'warning',\n },\n },\n share: {\n maxStringBytes: 64 * KIB,\n redactKeys: new Set(['share-personal', 'identifier']),\n redactMedia: false,\n severity: {\n credential: 'error',\n 'personal-data': 'error',\n identifier: 'warning',\n 'raw-content': 'warning',\n media: 'warning',\n },\n },\n strict: {\n maxStringBytes: 4 * KIB,\n redactKeys: new Set(['share-personal', 'identifier', 'raw-content']),\n redactMedia: true,\n severity: {\n credential: 'error',\n 'personal-data': 'error',\n identifier: 'warning',\n 'raw-content': 'error',\n media: 'error',\n },\n },\n}\n\n// ── Key classifier ───────────────────────────────────────────────────────\n\n/**\n * Canonical snake_case form of a field name, so camelCase, kebab-case, dotted\n * and snake_case spellings match one list: `userPassword`, `user-password` and\n * `user.password` all become `user_password`.\n */\nexport function normalizeKey(key: string): string {\n return key\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '_')\n .replace(/^_+|_+$/g, '')\n}\n\n/** Counts, limits and model vocabulary that contain the word \"token\" but are not credentials. */\nconst NON_CREDENTIAL_TOKEN_KEYS: ReadonlySet<string> = new Set([\n 'tokens',\n 'max_tokens',\n 'min_tokens',\n 'ls_max_tokens',\n 'token_count',\n 'token_limit',\n 'token_budget',\n 'input_tokens',\n 'output_tokens',\n 'total_tokens',\n 'cached_tokens',\n 'prompt_tokens',\n 'completion_tokens',\n 'reasoning_tokens',\n 'bos_token',\n 'eos_token',\n 'pad_token',\n 'unk_token',\n 'sep_token',\n 'cls_token',\n 'mask_token',\n 'stop_token',\n // Pagination cursors are opaque but not credentials.\n 'page_token',\n 'next_page_token',\n 'prev_page_token',\n 'next_token',\n 'continuation_token',\n 'cursor_token',\n 'sync_token',\n])\n\nconst CREDENTIAL_KEYS = [\n 'authorization',\n 'auth',\n 'cookie',\n 'api_key',\n 'apikey',\n 'api_token',\n 'access_key',\n 'secret_key',\n 'private_key',\n 'signing_key',\n 'encryption_key',\n 'master_key',\n 'session_key',\n 'client_secret',\n 'secret',\n 'password',\n 'passwd',\n 'passphrase',\n 'credential',\n 'credentials',\n 'bearer',\n 'dsn',\n 'database_url',\n 'connection_string',\n 'pat',\n] as const\n\n/** Email fields are personal data in every profile. */\nconst EMAIL_KEYS = ['email', 'email_address'] as const\n\n/** Personal-data fields removed by the share and strict profiles. */\nconst SHARE_PERSONAL_KEYS = [\n 'phone',\n 'phone_number',\n 'mobile',\n 'ip',\n 'ip_address',\n 'remote_addr',\n 'street_address',\n 'postal_address',\n 'home_address',\n 'billing_address',\n 'shipping_address',\n 'mailing_address',\n 'date_of_birth',\n 'dob',\n 'ssn',\n] as const\n\n/** Identifiers pseudonymized by the share and strict profiles. */\nconst IDENTIFIER_KEYS = [\n 'user_id',\n 'user_uuid',\n 'account_id',\n 'account_uuid',\n 'customer_id',\n 'tenant_id',\n 'org_id',\n 'organization_id',\n 'organisation_id',\n 'team_id',\n 'enduser_id',\n 'actor_id',\n 'principal_id',\n 'member_id',\n 'session_id',\n 'request_id',\n 'correlation_id',\n 'device_id',\n 'installation_id',\n] as const\n\n/** Fields that carry prompts, model output, tool payloads or other free text. Removed by `strict`. */\nconst RAW_CONTENT_KEYS = [\n 'prompt',\n 'prompts',\n 'system_prompt',\n 'completion',\n 'input',\n 'output',\n 'message',\n 'messages',\n 'content',\n 'text',\n 'thinking',\n 'reasoning',\n 'transcript',\n 'context',\n 'document',\n 'documents',\n 'chunk',\n 'chunks',\n 'retrieval',\n 'query',\n 'args',\n 'arguments',\n 'result',\n 'body',\n 'command',\n 'full_command',\n 'input_value',\n 'output_value',\n 'instructions',\n 'system_instructions',\n 'stdout',\n 'stderr',\n 'stacktrace',\n 'status_description',\n 'input_preview',\n 'output_preview',\n 'current_task',\n 'user_input',\n 'request_text',\n] as const\n\n/**\n * A key matches a list entry exactly or as a suffix (`openai_api_key`,\n * `db_password`, `tool_output`). Prefix compounds (`secret_name`,\n * `password_min_length`, `auth_type`, `content_type`) do not match: they name\n * or describe a value rather than hold it, and flagging them would make the\n * share verdict refuse ordinary traces. Value detectors still catch a\n * credential-shaped value under any key.\n */\nfunction matchesKeyList(normalized: string, keys: readonly string[]): boolean {\n for (const key of keys) {\n if (normalized === key || normalized.endsWith(`_${key}`)) return true\n }\n return false\n}\n\nfunction isTokenCredentialKey(normalized: string): boolean {\n if (NON_CREDENTIAL_TOKEN_KEYS.has(normalized)) return false\n if (normalized === 'token') return true\n // access_token and refreshToken are credentials; max_tokens and input_tokens are counts.\n if (normalized.endsWith('tokens')) return false\n return normalized.endsWith('token')\n}\n\ntype KeyClass = 'credential' | 'email' | 'share-personal' | 'identifier' | 'raw-content'\n\n/** Trace keys repeat on every span, so classifications are cached. */\nconst keyClassCache = new Map<string, KeyClass | null>()\n\nfunction classifyKeyName(key: string): KeyClass | undefined {\n const cached = keyClassCache.get(key)\n if (cached !== undefined) return cached ?? undefined\n const kind = classifyNormalizedKey(normalizeKey(key))\n if (keyClassCache.size >= 10_000) keyClassCache.clear()\n keyClassCache.set(key, kind ?? null)\n return kind\n}\n\nfunction classifyNormalizedKey(normalized: string): KeyClass | undefined {\n if (!normalized || NON_CREDENTIAL_TOKEN_KEYS.has(normalized)) return undefined\n if (isTokenCredentialKey(normalized)) return 'credential'\n if (matchesKeyList(normalized, CREDENTIAL_KEYS)) return 'credential'\n if (matchesKeyList(normalized, EMAIL_KEYS)) return 'email'\n if (matchesKeyList(normalized, SHARE_PERSONAL_KEYS)) return 'share-personal'\n if (matchesKeyList(normalized, IDENTIFIER_KEYS)) return 'identifier'\n if (matchesKeyList(normalized, RAW_CONTENT_KEYS)) return 'raw-content'\n return undefined\n}\n\n/**\n * The safety category a field name puts its value in, or undefined for an\n * ordinary field. Token counts and model limits (`inputTokens`, `max_tokens`,\n * `token_count`) and names such as `author` are not credentials.\n */\nexport function classifyKey(key: string): SafetyCategory | undefined {\n const kind = classifyKeyName(key)\n if (kind === undefined) return undefined\n if (kind === 'credential') return 'credential'\n if (kind === 'identifier') return 'identifier'\n if (kind === 'raw-content') return 'raw-content'\n return 'personal-data'\n}\n\n// ── Value detectors ──────────────────────────────────────────────────────\n\ninterface ValueDetector {\n id: string\n pattern: RegExp\n}\n\n/**\n * High-confidence credential shapes. A hit replaces the whole string. Each\n * pattern is written without the `g` flag so `test` stays stateless.\n */\nconst CREDENTIAL_DETECTORS: readonly ValueDetector[] = [\n { id: 'private-key', pattern: /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/ },\n { id: 'jwt', pattern: /\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}/ },\n { id: 'bearer', pattern: /\\bbearer\\s+[A-Za-z0-9._~+/=-]{12,}/i },\n {\n id: 'authorization-header',\n pattern: /^\\s*(?:basic|digest|apikey)\\s+[A-Za-z0-9._~+/=:-]{8,}\\s*$/i,\n },\n { id: 'anthropic-key', pattern: /\\bsk-ant-[A-Za-z0-9_-]{16,}/ },\n { id: 'provider-key', pattern: /\\bsk-(?:proj-|tan-)?[A-Za-z0-9_-]{16,}/ },\n { id: 'stripe-key', pattern: /\\b(?:sk|rk)_(?:live|test)_[A-Za-z0-9_]{10,}/ },\n { id: 'google-api-key', pattern: /\\bAIza[0-9A-Za-z_-]{20,}/ },\n { id: 'github-token', pattern: /\\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|github_pat_[A-Za-z0-9_]{20,})/ },\n { id: 'slack-token', pattern: /\\bxox[abeoprs]-[A-Za-z0-9-]{10,}/ },\n { id: 'aws-access-key', pattern: /\\b(?:AKIA|ASIA)[A-Z0-9]{16}\\b/ },\n { id: 'tangle-capability', pattern: /\\b(?:hubcap_|hct_)[A-Za-z0-9_=-]{8,}/ },\n { id: 'url-credentials', pattern: /\\b[a-z][a-z0-9+.-]*:\\/\\/[^\\s/:@]+:[^\\s/@]+@/i },\n {\n id: 'url-secret-param',\n pattern:\n /[?&](?:access_token|api_key|apikey|auth|token|secret|password|sig|signature|x-amz-signature|x-goog-signature)=[^&\\s\"'#)]{8,}/i,\n },\n {\n // `GH_TOKEN=…`, `--api-key=…`, `\"client_secret\": \"…\"`, `password: …` in free\n // text. The value must mix letters and digits and be at least 12\n // characters, so prose (`token: string`), variable references (`$TOKEN`)\n // and dotenvx ciphertext (`encrypted:…`) pass. The pattern starts at the\n // key word itself, with no identifier prefix, so it scans in linear time.\n id: 'secret-assignment',\n pattern:\n /(?:api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key|secret|token|password|passwd|passphrase|credentials?)[\"']?[ \\t]*[:=][ \\t]*[\"']?(?!encrypted:)(?![$`<[{(%])(?=[^\\s\"'`,;&|]*[0-9])(?=[^\\s\"'`,;&|]*[A-Za-z])[^\\s\"'`,;&|]{12,}/i,\n },\n {\n // `{\"password\": \"hunter2\"}` or `'client_secret': '…'` inside serialized\n // JSON, YAML or a log line. A quoted credential field name followed by a\n // quoted value holds a secret whatever the value's shape or length. Bare\n // `\"token\"` is left out: model logprob output uses it for text tokens.\n // Placeholders (`[REDACTED…]`, `$VAR`, `{env:VAR}`, `***`, `<value>`) and\n // prose with two or more spaces (a field description) do not match.\n id: 'json-secret',\n pattern:\n /[\"'][A-Za-z0-9_.-]{0,40}?(?:api[_-]?key|apikey|access[_-]?key|secret[_-]?key|private[_-]?key|client[_-]?secret|secret|password|passwd|passphrase|credentials?|(?:access|refresh|id|auth|session|bearer|api|github|gh|slack|npm)[_-]?token)[\"'][ \\t]*:[ \\t]*[\"'](?!\\[REDACTED|[$*{<])(?![^\"'\\r\\n]* [^\"'\\r\\n]* )[^\"'\\r\\n]{3,}[\"']/i,\n },\n]\n\n/** Personal-data shapes, replaced in place so the surrounding text survives. */\nconst PERSONAL_DATA_DETECTORS: readonly (ValueDetector & {\n /** A substring the text must contain before the pattern runs. */\n requires?: string\n accept?: (match: string) => boolean\n})[] = [\n {\n id: 'email',\n requires: '@',\n pattern: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g,\n accept: isEmailAddress,\n },\n {\n // Card numbers: a card-network prefix and a valid Luhn digit, written\n // unbroken (13-19 digits) or in printed groups (4-4-4-4, 4-4-4-4-3, Amex\n // 4-6-5). A word character or hyphen on either side rejects the match, so\n // digits inside UUIDs, dated file names and hyphenated ids never match.\n id: 'card-number',\n pattern:\n /(?<![\\w-])(?:\\d{13,19}|\\d{4}([ -])\\d{4}\\1\\d{4}\\1\\d{4}(?:\\1\\d{3})?|\\d{4}([ -])\\d{6}\\2\\d{5})(?![\\w-])/g,\n accept: (match) => {\n const digits = match.replace(/[ -]/g, '')\n return CARD_PREFIX.test(digits) && luhnValid(digits)\n },\n },\n { id: 'ssn', pattern: /\\b\\d{3}-\\d{2}-\\d{4}\\b/g },\n {\n // Separators are required: a bare ten-digit run is usually an epoch second or an id.\n id: 'phone',\n pattern: /(?:\\+1[-.\\s]?)?(?:\\(\\d{3}\\)\\s?|\\b\\d{3}[-.\\s])\\d{3}[-.\\s]\\d{4}\\b/g,\n },\n]\n\n/** Visa, Mastercard, Amex, Diners, JCB, Discover and UnionPay issuer prefixes. */\nconst CARD_PREFIX = /^(?:4|5[1-5]|2[2-7]|3[04-9]|6(?:011|4[4-9]|5|2))/\n\n/**\n * Top-level domains that are file extensions: `icon@2x.png`, `worker@3.service`\n * and `notes@AGENTS.md` are not addresses.\n */\nconst NON_EMAIL_TLDS: ReadonlySet<string> = new Set([\n 'md',\n 'mdx',\n 'txt',\n 'json',\n 'jsonl',\n 'yaml',\n 'yml',\n 'toml',\n 'lock',\n 'ts',\n 'tsx',\n 'js',\n 'jsx',\n 'mjs',\n 'cjs',\n 'py',\n 'rs',\n 'go',\n 'sh',\n 'css',\n 'html',\n 'svg',\n 'png',\n 'jpg',\n 'jpeg',\n 'gif',\n 'webp',\n 'service',\n 'socket',\n 'timer',\n 'mount',\n 'target',\n])\n\nfunction isEmailAddress(match: string): boolean {\n // `git@github.com:org/repo` is an SSH remote, not a person.\n if (match.toLowerCase().startsWith('git@')) return false\n const tld = match.slice(match.lastIndexOf('.') + 1).toLowerCase()\n return !NON_EMAIL_TLDS.has(tld)\n}\n\nfunction luhnValid(digits: string): boolean {\n if (/^(\\d)\\1+$/.test(digits)) return false\n let sum = 0\n let double = false\n for (let index = digits.length - 1; index >= 0; index -= 1) {\n let digit = digits.charCodeAt(index) - 48\n if (double) {\n digit *= 2\n if (digit > 9) digit -= 9\n }\n sum += digit\n double = !double\n }\n return sum % 10 === 0\n}\n\n/** The first credential detector that matches, or undefined. */\nexport function detectCredential(text: string): string | undefined {\n for (const detector of CREDENTIAL_DETECTORS) {\n if (detector.pattern.test(text)) return detector.id\n }\n return undefined\n}\n\n/** Personal-data detector ids that match somewhere in `text`. */\nexport function detectPersonalData(text: string): string[] {\n const hits: string[] = []\n for (const detector of PERSONAL_DATA_DETECTORS) {\n if (detector.requires && !text.includes(detector.requires)) continue\n detector.pattern.lastIndex = 0\n for (const match of text.matchAll(detector.pattern)) {\n if (!detector.accept || detector.accept(match[0])) {\n hits.push(detector.id)\n break\n }\n }\n }\n return hits\n}\n\n/**\n * Every form in which a known secret can appear in a trace: as written,\n * base64, base64url and URL-encoded. Values shorter than 4 characters are\n * ignored; they would match ordinary text.\n */\nfunction knownSecretForms(values: readonly string[] | undefined): string[] {\n const forms = new Set<string>()\n for (const value of values ?? []) {\n if (value.length < 4) continue\n const bytes = Buffer.from(value, 'utf8')\n forms.add(value)\n forms.add(bytes.toString('base64'))\n forms.add(bytes.toString('base64url'))\n forms.add(encodeURIComponent(value))\n }\n // Longest first, so a secret that contains another is reported as itself.\n return [...forms].sort((left, right) => right.length - left.length)\n}\n\nconst BASE64_TEXT = /^[A-Za-z0-9+/_-]+={0,2}$/\n\nfunction containsKnownSecret(text: string, forms: readonly string[]): boolean {\n if (forms.length === 0) return false\n if (forms.some((form) => text.includes(form))) return true\n // A whole-string base64 payload can hold a secret at any byte offset.\n if (text.length < 8 || !BASE64_TEXT.test(text)) return false\n const decoded = Buffer.from(\n text,\n text.includes('-') || text.includes('_') ? 'base64url' : 'base64',\n ).toString('utf8')\n return forms.some((form) => decoded.includes(form))\n}\n\n/** The detector that marks `text` as a credential: a known secret first, then the value shapes. */\nfunction credentialIn(text: string, knownSecrets: readonly string[]): string | undefined {\n return containsKnownSecret(text, knownSecrets) ? 'known-secret' : detectCredential(text)\n}\n\nconst MARKER_PREFIX = '[REDACTED:'\n\nfunction marker(detector: string): string {\n return `${MARKER_PREFIX}${detector}]`\n}\n\n/**\n * Placeholders other tools write for a removed value. A credential field that\n * holds one of these is already redacted and is not flagged again.\n */\nconst REDACTED_PLACEHOLDER =\n /^(?:\\[(?:REDACTED|redacted)(?::[^\\]]*)?\\]|\\[ID:[0-9a-f]{12}\\]|\\*{3,}|<redacted>|«redacted»|\\[HASH:[0-9a-f]{8,}\\])$/\n\nfunction isPlaceholder(value: string): boolean {\n return value === '' || REDACTED_PLACEHOLDER.test(value)\n}\n\nconst DATA_URI = /^data:[\\w.+-]+\\/[\\w.+-]+(?:;[\\w.+-]+=[\\w.+-]+)*;base64,/i\n\n// ── Redaction ────────────────────────────────────────────────────────────\n\nconst MAX_DEPTH = 64\n\ninterface WalkState {\n rules: ProfileRules\n maxStringBytes: number\n knownSecrets: string[]\n report: RedactionReport\n /** Created on first use: most calls pseudonymize nothing. */\n pseudonymKey?: Buffer\n seen: WeakSet<object>\n}\n\nfunction record(\n state: WalkState,\n path: string,\n category: SafetyCategory,\n detector: string,\n action: RedactionFinding['action'],\n): void {\n const report = state.report\n if (action === 'truncated') report.truncatedCount += 1\n report.redactionCount += 1\n report.byDetector[detector] = (report.byDetector[detector] ?? 0) + 1\n report.findings.push({ path, category, detector, action })\n}\n\nfunction pointer(parent: string, key: string | number): string {\n return `${parent}/${String(key).replace(/~/g, '~0').replace(/\\//g, '~1')}`\n}\n\nfunction truncateUtf8(text: string, maxBytes: number): string | undefined {\n if (text.length <= maxBytes / 4) return undefined\n const bytes = Buffer.from(text, 'utf8')\n if (bytes.length <= maxBytes) return undefined\n // Step back to the first byte of a UTF-8 sequence so no character is split.\n let cut = maxBytes\n while (cut > 0 && ((bytes[cut] ?? 0) & 0xc0) === 0x80) cut -= 1\n return `${bytes.subarray(0, cut).toString('utf8')}…[truncated ${bytes.length - cut} bytes]`\n}\n\nfunction redactStringAt(text: string, path: string, state: WalkState): string {\n if (isPlaceholder(text)) return text\n // A known secret is an exact value, so it is cut out where it stands and the\n // text around it survives: an error message keeps its diagnosis.\n let output = text\n for (const form of state.knownSecrets) {\n if (!output.includes(form)) continue\n output = output.split(form).join(marker('known-secret'))\n record(state, path, 'credential', 'known-secret', 'redacted')\n }\n if (DATA_URI.test(output)) {\n if (!state.rules.redactMedia) return output\n record(state, path, 'media', 'media', 'redacted')\n return marker('media')\n }\n // A value shape, or a whole-string base64 payload that holds a known secret\n // at any byte offset, replaces the whole string.\n const credential = credentialIn(output, state.knownSecrets)\n if (credential) {\n record(state, path, 'credential', credential, 'redacted')\n return marker(credential)\n }\n for (const detector of PERSONAL_DATA_DETECTORS) {\n if (detector.requires && !output.includes(detector.requires)) continue\n output = output.replace(detector.pattern, (match) => {\n if (detector.accept && !detector.accept(match)) return match\n record(state, path, 'personal-data', detector.id, 'redacted')\n return marker(detector.id)\n })\n }\n const truncated = truncateUtf8(output, state.maxStringBytes)\n if (truncated === undefined) return output\n record(state, path, 'raw-content', 'size-cap', 'truncated')\n return truncated\n}\n\nfunction pseudonym(value: string | number, state: WalkState): string {\n state.pseudonymKey ??= randomBytes(32)\n const digest = createHmac('sha256', state.pseudonymKey).update(String(value)).digest('hex')\n return `[ID:${digest.slice(0, 12)}]`\n}\n\nfunction redactKeyed(\n key: string,\n value: unknown,\n path: string,\n depth: number,\n state: WalkState,\n): unknown {\n const kind = classifyKeyName(key)\n if (value === null || value === undefined || typeof value === 'boolean') {\n return value\n }\n if (typeof value === 'string' && isPlaceholder(value)) return value\n // Numbers under a credential name are limits and counts (`password_max_age`), not secrets.\n if (kind === 'credential' && typeof value !== 'number') {\n record(state, path, 'credential', 'credential-key', 'redacted')\n return marker('credential-key')\n }\n if (\n kind === 'email' ||\n (kind === 'share-personal' && state.rules.redactKeys.has('share-personal'))\n ) {\n if (typeof value === 'string' || typeof value === 'number') {\n record(state, path, 'personal-data', 'personal-data-key', 'redacted')\n return marker('personal-data-key')\n }\n }\n if (kind === 'identifier' && state.rules.redactKeys.has('identifier')) {\n if (typeof value === 'string' || typeof value === 'number') {\n record(state, path, 'identifier', 'identifier-key', 'pseudonymized')\n return pseudonym(value, state)\n }\n }\n if (\n kind === 'raw-content' &&\n state.rules.redactKeys.has('raw-content') &&\n typeof value !== 'number'\n ) {\n record(state, path, 'raw-content', 'raw-content-key', 'redacted')\n return marker('raw-content-key')\n }\n return walk(value, path, depth + 1, state)\n}\n\nfunction walk(value: unknown, path: string, depth: number, state: WalkState): unknown {\n if (typeof value === 'string') return redactStringAt(value, path, state)\n if (value === null || typeof value !== 'object') return value\n if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {\n record(state, path, 'media', 'binary', 'redacted')\n return marker('binary')\n }\n if (value instanceof Date) return value\n if (depth > MAX_DEPTH) {\n record(state, path, 'raw-content', 'depth-limit', 'truncated')\n return marker('depth-limit')\n }\n if (state.seen.has(value)) {\n record(state, path, 'raw-content', 'circular', 'redacted')\n return marker('circular')\n }\n state.seen.add(value)\n try {\n if (Array.isArray(value)) {\n return value.map((item, index) => walk(item, pointer(path, index), depth + 1, state))\n }\n const out: Record<string, unknown> = {}\n let renamed = 0\n for (const [key, item] of Object.entries(value as Record<string, unknown>)) {\n let outKey = key\n const keyCredential = credentialIn(key, state.knownSecrets)\n if (keyCredential) {\n renamed += 1\n outKey = `${marker(keyCredential)}#${renamed}`\n record(state, pointer(path, outKey), 'credential', keyCredential, 'redacted')\n }\n out[outKey] = redactKeyed(key, item, pointer(path, outKey), depth, state)\n }\n return out\n } finally {\n state.seen.delete(value)\n }\n}\n\nfunction newReport(profile: RedactionProfile): RedactionReport {\n return {\n version: REDACTION_VERSION,\n profile,\n redactionCount: 0,\n byDetector: {},\n truncatedCount: 0,\n findings: [],\n }\n}\n\nfunction newState(options: RedactOptions, report?: RedactionReport): WalkState {\n const profile = options.profile ?? 'default'\n const rules = PROFILE_RULES[profile]\n if (!rules) throw new Error(`unknown redaction profile: ${String(profile)}`)\n const requested = options.maxStringBytes\n const maxStringBytes =\n typeof requested === 'number' && Number.isFinite(requested) && requested >= 0\n ? Math.min(Math.floor(requested), rules.maxStringBytes)\n : rules.maxStringBytes\n return {\n rules,\n maxStringBytes,\n knownSecrets: knownSecretForms(options.knownSecrets),\n report: report ?? newReport(profile),\n seen: new WeakSet(),\n }\n}\n\n/**\n * Return a redacted deep copy of `value` and a report of what changed. Pure:\n * the input is not mutated. Never throws on the value's shape; cycles, binary\n * data and nesting past 64 levels are replaced with markers.\n */\nexport function redact<T>(\n value: T,\n options: RedactOptions = {},\n): { value: T; report: RedactionReport } {\n const state = newState(options, options.report)\n const out = walk(value, '', 0, state) as T\n return { value: out, report: state.report }\n}\n\n/**\n * Redact one free-text string with the value detectors (no field name is\n * known). Pass `report` to accumulate counts across calls.\n */\nexport function redactText(text: string, options: RedactOptions = {}): string {\n const state = newState(options, options.report)\n return redactStringAt(text, '', state)\n}\n\n/** An empty report to accumulate `redactText` calls into. */\nexport function emptyRedactionReport(profile: RedactionProfile = 'default'): RedactionReport {\n return newReport(profile)\n}\n\n// ── Share-safety assessment ──────────────────────────────────────────────\n\ninterface ScanState {\n profile: RedactionProfile\n rules: ProfileRules\n knownSecrets: string[]\n findings: Map<string, ShareSafetyFinding>\n unreadable: string[]\n seen: WeakSet<object>\n}\n\nfunction flag(state: ScanState, path: string, category: SafetyCategory, detector: string): void {\n const severity = state.rules.severity[category]\n if (!severity) return\n const id = `${category}\\u0000${detector}`\n const existing = state.findings.get(id)\n if (existing) {\n existing.count += 1\n if (existing.paths.length < 5) existing.paths.push(path)\n return\n }\n state.findings.set(id, { category, detector, severity, count: 1, paths: [path] })\n}\n\nfunction scanString(text: string, path: string, state: ScanState): void {\n if (isPlaceholder(text)) return\n if (DATA_URI.test(text)) {\n flag(state, path, 'media', 'media')\n return\n }\n const credential = credentialIn(text, state.knownSecrets)\n if (credential) {\n flag(state, path, 'credential', credential)\n return\n }\n for (const detector of detectPersonalData(text)) flag(state, path, 'personal-data', detector)\n}\n\nfunction scanKeyed(\n key: string,\n value: unknown,\n path: string,\n depth: number,\n state: ScanState,\n): void {\n if (value === null || value === undefined || typeof value === 'boolean') return\n if (typeof value === 'string' && isPlaceholder(value)) return\n const category = classifyKey(key)\n if (category === 'credential' && typeof value !== 'number') {\n flag(state, path, 'credential', 'credential-key')\n return\n }\n const scalar = typeof value === 'string' || typeof value === 'number'\n if (category === 'personal-data' && scalar) {\n const kind = classifyKeyName(key)\n // Phone, IP and address fields are flagged from the share profile up; email always.\n if (kind === 'email' || state.profile !== 'default') {\n flag(state, path, 'personal-data', 'personal-data-key')\n return\n }\n }\n if (category === 'identifier' && scalar) flag(state, path, 'identifier', 'identifier-key')\n if (category === 'raw-content' && typeof value !== 'number')\n flag(state, path, 'raw-content', 'raw-content-key')\n scan(value, path, depth + 1, state)\n}\n\nfunction scan(value: unknown, path: string, depth: number, state: ScanState): void {\n if (typeof value === 'string') {\n scanString(value, path, state)\n return\n }\n if (value === null || typeof value !== 'object') return\n if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {\n state.unreadable.push(`${path || '/'}: binary data`)\n return\n }\n if (value instanceof Date) return\n if (depth > MAX_DEPTH) {\n state.unreadable.push(`${path || '/'}: nested deeper than ${MAX_DEPTH} levels`)\n return\n }\n if (state.seen.has(value)) {\n state.unreadable.push(`${path || '/'}: circular reference`)\n return\n }\n state.seen.add(value)\n try {\n if (Array.isArray(value)) {\n for (const [index, item] of value.entries())\n scan(item, pointer(path, index), depth + 1, state)\n return\n }\n for (const [key, item] of Object.entries(value as Record<string, unknown>)) {\n // A credential used as a key must not reach the finding paths.\n const keyCredential = credentialIn(key, state.knownSecrets)\n const segment = keyCredential ? marker(keyCredential) : key\n if (keyCredential) flag(state, pointer(path, segment), 'credential', keyCredential)\n scanKeyed(key, item, pointer(path, segment), depth, state)\n }\n } finally {\n state.seen.delete(value)\n }\n}\n\n/**\n * Read `value` without changing it and say whether it is safe to share under\n * `profile`. UNKNOWN when any part could not be read; UNSAFE when any\n * error-severity finding remains; SAFE_WITH_WARNINGS when only warnings\n * remain (raw content, embedded media, identifiers under `share`).\n */\nexport function assessShareSafety(value: unknown, options: AssessOptions = {}): ShareSafetyVerdict {\n const profile = options.profile ?? 'default'\n const rules = PROFILE_RULES[profile]\n if (!rules) throw new Error(`unknown redaction profile: ${String(profile)}`)\n const state: ScanState = {\n profile,\n rules,\n knownSecrets: knownSecretForms(options.knownSecrets),\n findings: new Map(),\n unreadable: [],\n seen: new WeakSet(),\n }\n scan(value, '', 0, state)\n return verdictFrom(profile, [...state.findings.values()], state.unreadable)\n}\n\n/**\n * One verdict over several parts (files in a directory, spans in a bundle).\n * Callers prefix each part's paths with its source before combining.\n */\nexport function combineVerdicts(\n profile: RedactionProfile,\n verdicts: readonly ShareSafetyVerdict[],\n unreadable: readonly string[] = [],\n): ShareSafetyVerdict {\n const findings = new Map<string, ShareSafetyFinding>()\n for (const verdict of verdicts) {\n for (const finding of verdict.findings) {\n const id = `${finding.category}\\u0000${finding.detector}`\n const existing = findings.get(id)\n if (!existing) {\n findings.set(id, { ...finding, paths: [...finding.paths] })\n continue\n }\n existing.count += finding.count\n for (const path of finding.paths) if (existing.paths.length < 5) existing.paths.push(path)\n }\n }\n return verdictFrom(\n profile,\n [...findings.values()],\n [...verdicts.flatMap((verdict) => verdict.unreadable), ...unreadable],\n )\n}\n\nfunction verdictFrom(\n profile: RedactionProfile,\n findings: readonly ShareSafetyFinding[],\n unreadable: readonly string[],\n): ShareSafetyVerdict {\n const status: ShareSafetyStatus =\n unreadable.length > 0\n ? 'UNKNOWN'\n : findings.some((finding) => finding.severity === 'error')\n ? 'UNSAFE'\n : findings.length > 0\n ? 'SAFE_WITH_WARNINGS'\n : 'SAFE'\n return { status, profile, findings: [...findings], unreadable: [...unreadable] }\n}\n\n/** Whether a verdict allows sharing: SAFE or SAFE_WITH_WARNINGS. UNSAFE and UNKNOWN refuse. */\nexport function shareAllowed(verdict: ShareSafetyVerdict): boolean {\n return verdict.status === 'SAFE' || verdict.status === 'SAFE_WITH_WARNINGS'\n}\n\n/**\n * Redact `value` under `profile`, then assess the redacted output. The\n * verdict describes what would actually be shared: a detector that failed to\n * remove something shows up as UNSAFE here.\n */\nexport function redactForShare<T>(\n value: T,\n options: RedactOptions = {},\n): { value: T; report: RedactionReport; verdict: ShareSafetyVerdict } {\n const redacted = redact(value, options)\n const verdict = assessShareSafety(redacted.value, {\n profile: options.profile,\n knownSecrets: options.knownSecrets,\n })\n return { value: redacted.value, report: redacted.report, verdict }\n}\n"],"mappings":"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"}
@@ -43,7 +43,7 @@ type SafetyCategory = 'credential' | 'personal-data' | 'identifier' | 'raw-conte
43
43
  * redaction behavior (saved optimizer inputs, per-span redaction stamps) keys
44
44
  * on it, so bump it whenever a detector, key list, cap or marker changes.
45
45
  */
46
- declare const REDACTION_VERSION = "2.0.0";
46
+ declare const REDACTION_VERSION = "2.1.0";
47
47
  interface RedactionFinding {
48
48
  /** JSON Pointer (RFC 6901) to the value or key that was changed. */
49
49
  path: string;
@@ -67,9 +67,11 @@ interface RedactOptions {
67
67
  maxStringBytes?: number;
68
68
  /**
69
69
  * Exact values the caller knows are secret (an API key the run was given, a
70
- * password from the environment). A string or key that contains one, or its
71
- * base64, base64url or URL-encoded form, is replaced whole with
72
- * `[REDACTED:known-secret]`, whatever shape the value has.
70
+ * password from the environment), whatever shape they have. Each occurrence
71
+ * in a string, as written or base64, base64url or URL-encoded, becomes
72
+ * `[REDACTED:known-secret]` and the text around it is kept. A key that
73
+ * contains one, and a whole-string base64 payload that holds one at any byte
74
+ * offset, is replaced whole.
73
75
  */
74
76
  knownSecrets?: readonly string[];
75
77
  /** Add this call's findings to an existing report instead of a new one. */
@@ -155,4 +157,4 @@ declare function redactForShare<T>(value: T, options?: RedactOptions): {
155
157
  };
156
158
  //#endregion
157
159
  export { shareAllowed as S, emptyRedactionReport as _, RedactionFinding as a, redactForShare as b, SafetyCategory as c, ShareSafetyVerdict as d, assessShareSafety as f, detectPersonalData as g, detectCredential as h, RedactOptions as i, ShareSafetyFinding as l, combineVerdicts as m, REDACTION_PROFILES as n, RedactionProfile as o, classifyKey as p, REDACTION_VERSION as r, RedactionReport as s, AssessOptions as t, ShareSafetyStatus as u, normalizeKey as v, redactText as x, redact as y };
158
- //# sourceMappingURL=redact-DPoH1-OW.d.ts.map
160
+ //# sourceMappingURL=redact-Du3WKUOP.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redact-Du3WKUOP.d.ts","names":[],"sources":["../src/trace/redact.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAsCY;cAEC,6BAA6B;;KAG9B;;;;;;cAOC;UAEI;;EAEf;EACA,UAAU;EACV;EACA;;UAGe;EACf;EACA,SAAS;;EAET;EACA,YAAY;EACZ;EACA,UAAU;;UAGK;;EAEf,UAAU;;EAEV;;;;;;;;;EASA;;EAEA,SAAS;;UAGM;;EAEf,UAAU;;EAEV;;KAGU;UAEK;EACf,UAAU;EACV;EACA;EACA;;EAEA;;UAGe;EACf,QAAQ;EACR,SAAS;EACT,UAAU;;EAEV;;;;;;;iBA6Dc,aAAa;;;;;;iBAmNb,YAAY,cAAc;;iBAoK1B,iBAAiB;;iBAQjB,mBAAmB;;;;;;iBAgRnB,OAAO,GACrB,OAAO,GACP,UAAS;EACN,OAAO;EAAG,QAAQ;;;;;;iBAUP,WAAW,cAAc,UAAS;;iBAMlC,qBAAqB,UAAS,mBAA+B;;;;;;;iBAmH7D,kBAAkB,gBAAgB,UAAS,gBAAqB;;;;;iBAoBhE,gBACd,SAAS,kBACT,mBAAmB,sBACnB,iCACC;;iBAsCa,aAAa,SAAS;;;;;;iBAStB,eAAe,GAC7B,OAAO,GACP,UAAS;EACN,OAAO;EAAG,QAAQ;EAAiB,SAAS"}
@@ -1,7 +1,7 @@
1
1
  import { c as CostLedgerHandle } from "./cost-ledger-DbQdN3nO.js";
2
- import { c as AnalystRunInputs, i as AnalystFinding, l as AnalystRunResult, n as AnalystContext, s as AnalystRunEvent, t as Analyst, u as AnalystRunSummary } from "./types-DDp2Mb8Y.js";
2
+ import { c as AnalystRunInputs, i as AnalystFinding, l as AnalystRunResult, n as AnalystContext, s as AnalystRunEvent, t as Analyst, u as AnalystRunSummary } from "./types-Bay1dh2o.js";
3
3
  import { x as ChatClient } from "./types-C0bWoSKC.js";
4
- import { i as ExactAnalystRunEvent, o as ExactAnalystRunResult, u as ExactExecutionComponentIdentity } from "./exact-types-CrhPoDp8.js";
4
+ import { i as ExactAnalystRunEvent, o as ExactAnalystRunResult, u as ExactExecutionComponentIdentity } from "./exact-types-CfpP2A4Z.js";
5
5
  //#region src/analyst/registry.d.ts
6
6
  interface AnalystHooks {
7
7
  /** Legacy runs may mutate ctx; exact runs provide a frozen observational context. */
@@ -173,4 +173,4 @@ declare class AnalystRegistry {
173
173
  }
174
174
  //#endregion
175
175
  export { ExactAnalystBudgetPolicy as a, RegistryRunOpts as c, BudgetPolicy as i, AnalystRegistry as n, ExactAnalystRunExecutionError as o, AnalystRegistryOptions as r, ExactRegistryRunOpts as s, AnalystHooks as t };
176
- //# sourceMappingURL=registry-BM2Kdk8T.d.ts.map
176
+ //# sourceMappingURL=registry-mAQVtuLx.d.ts.map