@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
@@ -3,26 +3,26 @@ import { a as hashCanonical, i as compareCodeUnits, r as canonicalString, t as L
3
3
  import { o as summarizeNumberSeries, s as weightedComposite, t as confidenceInterval } from "./descriptive-1V17A-qa.js";
4
4
  import { t as decidePairedPromotion } from "./paired-promotion-decision-6cn3bLc3.js";
5
5
  import { a as heldoutSignificance, i as dimensionRegressions, o as pairHoldout } from "./statistical-heldout-GcLIPxk4.js";
6
- import { i as modelHasSnapshot } from "./run-record-tg_5H2IK.js";
6
+ import { i as modelHasSnapshot, l as searchCellRunId } from "./run-record-BRlM99rX.js";
7
7
  import { n as contentHash } from "./verdict-cache-BQEsVy9n.js";
8
- import { o as projectCampaignCellQuality, t as campaignCellCostProvenance } from "./run-record-CNNzWGpB.js";
8
+ import { o as projectCampaignCellQuality, t as campaignCellCostProvenance } from "./run-record-DnMFyg_g.js";
9
9
  import { i as CostLedger, t as CostAccountingIncompleteError } from "./cost-ledger-_IbkejCF.js";
10
- import { $ as formatCoverageFailures, A as FinalEvidenceConflictError, B as surfaceDispatchRef, C as campaignMeanCompositeOrNull, H as surfaceHashMatches, J as assertCompleteCampaign, K as assertCampaignDesign, N as defineEvaluationClaim, P as summarizeEvaluationUnits, R as renderSurfaceDiff, S as campaignMeanComposite, U as BackendIntegrityError, V as surfaceHash, X as campaignScenarioIdentity, Y as campaignCoverage, Z as campaignSplitDigest, b as assertFiniteRankKey, j as FinalEvidenceError, q as assertCampaignSplitIdentity, t as createCampaignEvidenceReceipt, w as compareRankKeys, x as campaignBreakdown, z as surfaceContentHash } from "./campaign-evidence-IF1p0oDa.js";
11
- import { d as mapConcurrent, n as replayLedgerText, t as FileLedgerJournal } from "./journal-BuRLm6ho.js";
10
+ import { A as FinalEvidenceConflictError, B as surfaceContentHash, C as campaignMeanCompositeOrNull, F as summarizeEvaluationUnits, H as surfaceHash, J as assertCampaignSplitIdentity, N as defineEvaluationClaim, P as evaluationClaimSchema, Q as campaignSplitDigest, S as campaignMeanComposite, U as surfaceHashMatches, V as surfaceDispatchRef, W as BackendIntegrityError, X as campaignCoverage, Y as assertCompleteCampaign, Z as campaignScenarioIdentity, b as assertFiniteRankKey, et as formatCoverageFailures, j as FinalEvidenceError, q as assertCampaignDesign, t as createCampaignEvidenceReceipt, w as compareRankKeys, x as campaignBreakdown, z as renderSurfaceDiff } from "./campaign-evidence-CWF8pEIk.js";
11
+ import { d as mapConcurrent, n as replayLedgerText, t as FileLedgerJournal } from "./journal-CDP0Vhx_.js";
12
+ import { t as deepFreezeCanonicalJson } from "./deep-freeze-CurcOQg3.js";
12
13
  import "./ledger-core/index.js";
13
- import { D as SearchLedgerIntegrityError, E as SearchLedgerError, S as fsCampaignStorage, T as SearchLedgerConflictError, g as isRecord, h as isExternalTextCandidate, w as SEARCH_LEDGER_FILE_CONTEXT, x as createRunCostLedger } from "./external-optimizer-subprocess-BrfsgL92.js";
14
- import { f as redactText, o as detectCredential, s as detectPersonalData } from "./redact-DxwlUAvh.js";
14
+ import { D as SearchLedgerIntegrityError, E as SearchLedgerError, S as fsCampaignStorage, T as SearchLedgerConflictError, g as isRecord, h as isExternalTextCandidate, w as SEARCH_LEDGER_FILE_CONTEXT, x as createRunCostLedger } from "./external-optimizer-subprocess-C0dHXH8e.js";
15
+ import { f as redactText, n as REDACTION_VERSION, o as detectCredential, s as detectPersonalData } from "./redact-8fX8-uKr.js";
15
16
  import { t as detectRewardHacking } from "./reward-hacking-D0XwhVWE.js";
16
17
  import { n as assertProposalFindings, t as combineAbortSignals } from "./abort-signal-CtzAM_sJ.js";
17
- import { a as deepFreezeCanonicalJson } from "./types-C8VBbiw6.js";
18
- import { d as stripFencedJson, o as costReceiptFromLlm, s as costReceiptFromLlmError, u as maximumChargeForLlmRequest } from "./llm-client-DF9bPNYA.js";
18
+ import { d as stripFencedJson, o as costReceiptFromLlm, s as costReceiptFromLlmError, u as maximumChargeForLlmRequest } from "./llm-client-BqmRnibA.js";
19
19
  import { createHash } from "node:crypto";
20
20
  import { writeFileSync } from "node:fs";
21
21
  import { z } from "zod";
22
- import { basename, isAbsolute, join } from "node:path";
22
+ import { basename, dirname, isAbsolute, join } from "node:path";
23
23
  import { homedir, tmpdir } from "node:os";
24
24
  import { execSync } from "node:child_process";
25
- import { fileURLToPath } from "node:url";
25
+ import { fileURLToPath, pathToFileURL } from "node:url";
26
26
  //#region src/campaign/campaign-manifest.ts
27
27
  /**
28
28
  * Campaign identity: the manifest hash over (scenarios, judges, dispatch,
@@ -2531,6 +2531,22 @@ function paretoFrontierWithCrowding(candidates, objectives) {
2531
2531
  return crowdingDistance(frontier, objectives).sort((a, b) => b.distance - a.distance);
2532
2532
  }
2533
2533
  //#endregion
2534
+ //#region src/campaign/types.ts
2535
+ /** Type guard: a proposal carrying its rationale vs a bare
2536
+ * surface. The loop branches on this to populate `GenerationCandidate`. */
2537
+ function isProposedCandidate(value) {
2538
+ return typeof value === "object" && value !== null && "surface" in value && "label" in value && "rationale" in value;
2539
+ }
2540
+ const LABEL_TRUST_RANK = {
2541
+ unverified: 0,
2542
+ "verified-signal": 1,
2543
+ "human-rated": 2
2544
+ };
2545
+ /** Ordinal rank for a label-trust tier; absent ⇒ `unverified` (rank 0). */
2546
+ function labelTrustRank(trust) {
2547
+ return LABEL_TRUST_RANK[trust ?? "unverified"];
2548
+ }
2549
+ //#endregion
2534
2550
  //#region src/campaign/search-ledger-ordering.ts
2535
2551
  function artifactKey(artifact) {
2536
2552
  return canonicalString(artifact);
@@ -2539,337 +2555,848 @@ function compareStrings(a, b) {
2539
2555
  return a < b ? -1 : a > b ? 1 : 0;
2540
2556
  }
2541
2557
  //#endregion
2542
- //#region src/campaign/search-ledger-projector.ts
2543
- /** Replay the campaign search state machine over chain-verified entries. The
2544
- * generic journal owns sequence, hash, and eventId-uniqueness checks; this
2545
- * projector owns every campaign invariant and builds the replay projection. */
2546
- function createSearchLedgerProjector(campaignId) {
2547
- const candidates = /* @__PURE__ */ new Map();
2548
- const plannedSlots = /* @__PURE__ */ new Map();
2549
- const plannedOperations = /* @__PURE__ */ new Map();
2550
- const planExtensions = [];
2551
- const candidateBySlot = /* @__PURE__ */ new Map();
2552
- const closedSlots = /* @__PURE__ */ new Map();
2553
- const lineageNodes = /* @__PURE__ */ new Map();
2554
- const runIds = /* @__PURE__ */ new Set();
2555
- const attemptKeys = /* @__PURE__ */ new Set();
2556
- const candidateEvents = [];
2557
- const closedSlotEvents = [];
2558
- const attempts = [];
2559
- const operationEvents = [];
2560
- const operationsById = /* @__PURE__ */ new Map();
2561
- const decisions = [];
2562
- let planEvent = null;
2563
- let completion = null;
2564
- let previousOccurredAt = Number.NEGATIVE_INFINITY;
2565
- const apply = (entry, index) => {
2558
+ //#region src/campaign/search-ledger-types.ts
2559
+ /**
2560
+ * Schema tag every search-ledger entry carries. A ledger written under another
2561
+ * tag, such as the retired `tangle.search-ledger.v1` candidate-slot format, is
2562
+ * refused with its tag named; it is never translated.
2563
+ */
2564
+ const SEARCH_LEDGER_SCHEMA = "tangle.search-ledger.2026-09";
2565
+ //#endregion
2566
+ //#region src/campaign/search-state.ts
2567
+ /**
2568
+ * `SearchState`: the one projection of a search ledger.
2569
+ *
2570
+ * The same code checks every invariant for the producer's journal, the kernel,
2571
+ * and the Intelligence verifier, and builds the read model they share. It keeps
2572
+ * compact indexes (nodes, edges, cells, per-unit score sums, running audit
2573
+ * totals), not the raw event list, and applies each entry in time proportional
2574
+ * to that entry, so a search of any length appends at a constant cost.
2575
+ *
2576
+ * The journal asks for a snapshot after every append. A snapshot is a
2577
+ * `SearchStateView` that reads the live indexes, so taking one costs nothing.
2578
+ * The view is valid until the state applies its next entry; a read after that
2579
+ * throws rather than returning numbers from a later ledger position. Read the
2580
+ * ledger's state again to see newer entries. The audit, header, head and claim
2581
+ * on a view are plain values and stay readable.
2582
+ */
2583
+ /** Deterministic node id: identical content in one search is one node. */
2584
+ function searchNodeId(searchId, artifactDigest) {
2585
+ return `node_${shortDigest([searchId, artifactDigest])}`;
2586
+ }
2587
+ /** Deterministic cell id: one node on one task in one split at one repeat. */
2588
+ function searchCellId(searchId, nodeId, taskId, split, rep) {
2589
+ return `cell_${shortDigest([
2590
+ searchId,
2591
+ nodeId,
2592
+ taskId,
2593
+ split,
2594
+ rep
2595
+ ])}`;
2596
+ }
2597
+ /** Digest a split's task list in taskId order. */
2598
+ function searchTaskSetDigest(tasks) {
2599
+ return hashCanonical([...tasks].sort((left, right) => compareCodeUnits(left.taskId, right.taskId)));
2600
+ }
2601
+ /** 128 bits of SHA-256 over the canonical encoding: collision-safe and short
2602
+ * enough for directory names. */
2603
+ function shortDigest(value) {
2604
+ return createHash("sha256").update(canonicalString(value)).digest("hex").slice(0, 32);
2605
+ }
2606
+ const MAX_LABEL_CHARS$1 = 200;
2607
+ const USD_TOLERANCE = 1e-9;
2608
+ const SPLITS = [
2609
+ "train",
2610
+ "selection",
2611
+ "test"
2612
+ ];
2613
+ /**
2614
+ * Per-unit means of scored cells, in unitId order. Each unit sums its cells in
2615
+ * cellId order, so the result depends on the set of cells alone, never on the
2616
+ * order they settled in: a verifier that reads the same cells from a database
2617
+ * gets the same bits.
2618
+ */
2619
+ function searchUnitScores(cells) {
2620
+ const byUnit = /* @__PURE__ */ new Map();
2621
+ for (const cell of cells) {
2622
+ const unit = byUnit.get(cell.unitId);
2623
+ if (unit) unit.push(cell);
2624
+ else byUnit.set(cell.unitId, [cell]);
2625
+ }
2626
+ return [...byUnit.entries()].sort(([left], [right]) => compareCodeUnits(left, right)).map(([unitId, unitCells]) => {
2627
+ let sum = 0;
2628
+ for (const cell of unitCells.sort((left, right) => compareCodeUnits(left.cellId, right.cellId))) sum += cell.score;
2629
+ return {
2630
+ unitId,
2631
+ sum,
2632
+ count: unitCells.length,
2633
+ mean: sum / unitCells.length
2634
+ };
2635
+ });
2636
+ }
2637
+ /**
2638
+ * The pure, incremental projection of one search ledger. `apply` refuses any
2639
+ * entry that breaks an invariant; the journal then discards this instance and
2640
+ * replays into a new one, so a refused entry leaves no partial state behind.
2641
+ */
2642
+ var SearchState = class {
2643
+ searchId;
2644
+ version = 0;
2645
+ header = null;
2646
+ closed = null;
2647
+ head = null;
2648
+ lastOccurredAt = Number.NEGATIVE_INFINITY;
2649
+ lastOccurredAtText = null;
2650
+ tasks = /* @__PURE__ */ new Map();
2651
+ nodes = /* @__PURE__ */ new Map();
2652
+ nodeOrder = [];
2653
+ nodeByDigest = /* @__PURE__ */ new Map();
2654
+ edges = /* @__PURE__ */ new Map();
2655
+ edgeOrder = [];
2656
+ cells = /* @__PURE__ */ new Map();
2657
+ operations = /* @__PURE__ */ new Map();
2658
+ selectedNodeId = null;
2659
+ claimUsedUsd = 0;
2660
+ audit;
2661
+ constructor(searchId) {
2662
+ this.searchId = searchId;
2663
+ this.audit = {
2664
+ searchId,
2665
+ eventCount: 0,
2666
+ headHash: null,
2667
+ status: "open",
2668
+ closeReason: null,
2669
+ nodes: 0,
2670
+ nodesWithoutEdge: 0,
2671
+ undecidedNodes: 0,
2672
+ selectedNodeId: null,
2673
+ edges: {
2674
+ explicit: 0,
2675
+ correlated: 0,
2676
+ unknown: 0,
2677
+ reproposals: 0
2678
+ },
2679
+ cells: {
2680
+ allocated: 0,
2681
+ settled: 0,
2682
+ cancelled: 0,
2683
+ open: 0
2684
+ },
2685
+ attempts: 0,
2686
+ outcomes: {
2687
+ passed: 0,
2688
+ failed: 0,
2689
+ errored: 0
2690
+ },
2691
+ operations: {
2692
+ started: 0,
2693
+ recorded: 0,
2694
+ open: 0
2695
+ },
2696
+ spend: {
2697
+ knownUsd: 0,
2698
+ floorUsd: 0,
2699
+ unknownCostCells: 0,
2700
+ unknownCostOperations: 0,
2701
+ committedUsd: 0,
2702
+ openReservationUsd: 0,
2703
+ overspendUsd: 0,
2704
+ unreservedCells: 0,
2705
+ boxMinutes: 0
2706
+ },
2707
+ tokens: {
2708
+ inputTokens: 0,
2709
+ outputTokens: 0,
2710
+ cachedTokens: 0,
2711
+ unknownTokenAttempts: 0
2712
+ }
2713
+ };
2714
+ }
2715
+ apply(entry, index) {
2566
2716
  const event = entry.event;
2567
- if (completion) throw new SearchLedgerIntegrityError(`event ${event.eventId} appears after terminal event ${completion.eventId}`);
2717
+ if (this.closed) throw integrity(`event ${event.eventId} follows search-closed ${this.closed.eventId}`);
2568
2718
  const occurredAt = Date.parse(event.occurredAt);
2569
- if (occurredAt < previousOccurredAt) throw new SearchLedgerIntegrityError(`event ${event.eventId} occurred before the preceding durable event`);
2570
- previousOccurredAt = occurredAt;
2719
+ if (occurredAt < this.lastOccurredAt) throw integrity(`event ${event.eventId} occurred before the preceding event`);
2571
2720
  assertUnique(event.artifacts.map(artifactKey), "artifact receipt", event.eventId);
2572
- if (event.kind === "search-planned") {
2573
- if (index !== 0 || planEvent) throw new SearchLedgerIntegrityError("search plan must be the first and only plan event");
2574
- assertUnique(event.plan.candidateSlots.map((slot) => slot.slotId), "candidate slot", event.eventId);
2575
- assertUnique(event.plan.tasks.map((task) => task.taskId), "planned taskId", event.eventId);
2576
- assertUnique(event.plan.operations.map((operation) => operation.operationId), "planned operationId", event.eventId);
2577
- for (const operation of event.plan.operations) plannedOperations.set(operation.operationId, operation);
2578
- for (const slot of event.plan.candidateSlots) {
2579
- assertSlotGenerationOperation(slot, plannedOperations);
2580
- plannedSlots.set(slot.slotId, slot);
2721
+ if (event.kind === "search-opened") {
2722
+ if (index !== 0 || this.header) throw integrity("search-opened must be the first and only opening event");
2723
+ this.open(event);
2724
+ } else {
2725
+ if (!this.header) throw integrity(`event ${event.eventId} appears before search-opened`);
2726
+ this.applyEvent(event, index);
2727
+ }
2728
+ this.lastOccurredAt = occurredAt;
2729
+ this.lastOccurredAtText = event.occurredAt;
2730
+ this.head = {
2731
+ sequence: index,
2732
+ entryHash: entry.entryHash
2733
+ };
2734
+ this.audit.eventCount = index + 1;
2735
+ this.audit.headHash = entry.entryHash;
2736
+ this.version += 1;
2737
+ }
2738
+ snapshot() {
2739
+ return new SearchStateView(this, this.version);
2740
+ }
2741
+ /** @internal Views check this before every read of the live indexes. */
2742
+ isAt(version) {
2743
+ return this.version === version;
2744
+ }
2745
+ applyEvent(event, index) {
2746
+ switch (event.kind) {
2747
+ case "operation-started":
2748
+ this.startOperation(event);
2749
+ return;
2750
+ case "operation-recorded":
2751
+ this.recordOperation(event);
2752
+ return;
2753
+ case "node-registered":
2754
+ this.registerNode(event, index);
2755
+ return;
2756
+ case "edge-recorded":
2757
+ this.recordEdge(event, index);
2758
+ return;
2759
+ case "cell-allocated":
2760
+ this.allocateCell(event, index);
2761
+ return;
2762
+ case "cell-settled":
2763
+ this.settleCell(event, index);
2764
+ return;
2765
+ case "cell-cancelled":
2766
+ this.cancelCell(event, index);
2767
+ return;
2768
+ case "node-decided":
2769
+ this.decideNode(event, index);
2770
+ return;
2771
+ case "search-closed":
2772
+ this.close(event);
2773
+ return;
2774
+ }
2775
+ }
2776
+ open(event) {
2777
+ if (event.containment?.searchId === this.searchId) throw integrity("a search cannot contain itself");
2778
+ if (event.derivedFrom?.searchId === this.searchId) throw integrity("a search cannot derive from itself");
2779
+ const units = {
2780
+ train: /* @__PURE__ */ new Set(),
2781
+ selection: /* @__PURE__ */ new Set(),
2782
+ test: /* @__PURE__ */ new Set()
2783
+ };
2784
+ for (const split of SPLITS) {
2785
+ const { tasks, taskSetDigest } = event.splits[split];
2786
+ if (searchTaskSetDigest(tasks) !== taskSetDigest) throw integrity(`the ${split} split's taskSetDigest does not match its tasks`);
2787
+ for (const task of tasks) {
2788
+ const owner = this.tasks.get(task.taskId);
2789
+ if (owner) throw integrity(`task ${task.taskId} is in both the ${owner.split} and ${split} splits`);
2790
+ this.tasks.set(task.taskId, {
2791
+ split,
2792
+ unitId: task.unitId
2793
+ });
2794
+ units[split].add(task.unitId);
2581
2795
  }
2582
- planEvent = event;
2583
- return;
2584
2796
  }
2585
- if (!planEvent) throw new SearchLedgerIntegrityError(`event ${event.eventId} appears before the required search plan`);
2586
- if (event.kind === "search-plan-extended") {
2587
- assertUnique(event.extension.candidateSlots.map((slot) => slot.slotId), "candidate slot", event.eventId);
2588
- assertUnique(event.extension.operations.map((operation) => operation.operationId), "planned operationId", event.eventId);
2589
- for (const operation of event.extension.operations) {
2590
- if (plannedOperations.has(operation.operationId)) throw new SearchLedgerIntegrityError(`plan extension ${event.eventId} re-plans operation ${operation.operationId}`);
2591
- plannedOperations.set(operation.operationId, operation);
2797
+ if (event.splits.heldOutUnits) {
2798
+ const seen = [...units.test].filter((unit) => units.train.has(unit) || units.selection.has(unit));
2799
+ if (seen.length > 0) throw integrity(`test units ${seen.join(", ")} also appear in train or selection, but the splits declare heldOutUnits`);
2800
+ }
2801
+ const { budget } = event;
2802
+ if (budget.maxUsd !== null && budget.reservedClaimUsd > budget.maxUsd + USD_TOLERANCE) throw integrity(`the claim reserve $${budget.reservedClaimUsd} exceeds the search cap $${budget.maxUsd}`);
2803
+ this.header = event;
2804
+ }
2805
+ startOperation(event) {
2806
+ if (this.operations.has(event.operationId)) throw integrity(`operation ${event.operationId} was started twice`);
2807
+ this.admit(event.reservation, false, `operation ${event.operationId}`);
2808
+ this.operations.set(event.operationId, {
2809
+ operationId: event.operationId,
2810
+ operationKind: event.operationKind,
2811
+ reservation: event.reservation,
2812
+ recorded: false,
2813
+ spentUsd: 0
2814
+ });
2815
+ this.audit.operations.started += 1;
2816
+ this.audit.operations.open += 1;
2817
+ this.audit.spend.openReservationUsd += event.reservation?.usd ?? 0;
2818
+ }
2819
+ recordOperation(event) {
2820
+ const operation = this.operations.get(event.operationId);
2821
+ if (!operation) throw integrity(`operation ${event.operationId} was recorded before it started`);
2822
+ if (operation.recorded) throw integrity(`operation ${event.operationId} was recorded twice`);
2823
+ if (operation.operationKind !== event.operationKind) throw integrity(`operation ${event.operationId} started as ${operation.operationKind} but recorded as ${event.operationKind}`);
2824
+ const cost = this.book(event.accounting);
2825
+ if (cost.unknown) this.audit.spend.unknownCostOperations += 1;
2826
+ const reserved = operation.reservation?.usd ?? 0;
2827
+ this.audit.spend.openReservationUsd -= reserved;
2828
+ if (operation.reservation) this.audit.spend.overspendUsd += Math.max(0, cost.usd - reserved);
2829
+ operation.recorded = true;
2830
+ operation.spentUsd = cost.usd;
2831
+ this.audit.operations.recorded += 1;
2832
+ this.audit.operations.open -= 1;
2833
+ }
2834
+ registerNode(event, index) {
2835
+ const expected = searchNodeId(this.searchId, event.artifactDigest);
2836
+ if (event.nodeId !== expected) throw integrity(`node ${event.nodeId} does not match its artifact digest (expected ${expected})`);
2837
+ if (this.nodes.has(event.nodeId)) throw integrity(`node ${event.nodeId} was registered twice; a re-proposal is a second edge into it`);
2838
+ assertUnique(event.surfaces.map((surface) => surface.surfaceId), "surfaceId", event.eventId);
2839
+ const maxNodes = this.header.budget.maxNodes;
2840
+ if (maxNodes !== null && this.nodes.size + 1 > maxNodes) throw integrity(`node ${event.nodeId} exceeds the search's maxNodes ${maxNodes}`);
2841
+ this.nodes.set(event.nodeId, {
2842
+ event,
2843
+ ordinal: this.nodeOrder.length,
2844
+ registeredSequence: index,
2845
+ updatedSequence: index,
2846
+ edgeIds: [],
2847
+ parents: [],
2848
+ parentKeys: /* @__PURE__ */ new Set(),
2849
+ primaryParentId: null,
2850
+ depth: null,
2851
+ children: [],
2852
+ decisions: [],
2853
+ finalist: false,
2854
+ cellIds: [],
2855
+ scoredCells: 0,
2856
+ spend: {
2857
+ knownUsd: 0,
2858
+ floorUsd: 0,
2859
+ unknownCostCells: 0,
2860
+ boxMinutes: 0
2592
2861
  }
2593
- for (const slot of event.extension.candidateSlots) {
2594
- if (plannedSlots.has(slot.slotId)) throw new SearchLedgerIntegrityError(`plan extension ${event.eventId} re-plans candidate slot ${slot.slotId}`);
2595
- assertSlotGenerationOperation(slot, plannedOperations);
2596
- plannedSlots.set(slot.slotId, slot);
2862
+ });
2863
+ this.nodeOrder.push(event.nodeId);
2864
+ this.nodeByDigest.set(event.artifactDigest, event.nodeId);
2865
+ this.audit.nodes += 1;
2866
+ this.audit.nodesWithoutEdge += 1;
2867
+ this.audit.undecidedNodes += 1;
2868
+ }
2869
+ recordEdge(event, index) {
2870
+ if (this.edges.has(event.edgeId)) throw integrity(`edge ${event.edgeId} was recorded twice`);
2871
+ const child = this.nodes.get(event.childNodeId);
2872
+ if (!child) throw integrity(`edge ${event.edgeId} names unregistered child ${event.childNodeId}`);
2873
+ if (event.label.length > MAX_LABEL_CHARS$1) throw integrity(`edge ${event.edgeId} label exceeds ${MAX_LABEL_CHARS$1} characters`);
2874
+ if (event.diffs.length !== event.parents.length) throw integrity(`edge ${event.edgeId} needs one diff per parent`);
2875
+ assertUnique(event.parents.map((parent) => canonicalString(parent)), "parent", event.eventId);
2876
+ const { operator, attribution, parents } = event;
2877
+ if (operator === "seed" !== (event.proposer === null)) throw integrity(`edge ${event.edgeId}: exactly the seed edge has no proposer`);
2878
+ if (operator === "seed" && (parents.length > 0 || attribution === "unknown")) throw integrity(`seed edge ${event.edgeId} must have no parents and a known attribution`);
2879
+ if (attribution === "unknown" && parents.length > 0) throw integrity(`edge ${event.edgeId} has unknown attribution but names parents`);
2880
+ if (operator !== "seed" && attribution !== "unknown" && parents.length === 0) throw integrity(`${operator} edge ${event.edgeId} names no parent`);
2881
+ if (operator === "merge" && attribution !== "unknown" && parents.length < 2) throw integrity(`merge edge ${event.edgeId} needs at least two parents`);
2882
+ const operationId = event.proposer?.operationId ?? null;
2883
+ if (operationId !== null && !this.operations.has(operationId)) throw integrity(`edge ${event.edgeId} names operation ${operationId}, which never started`);
2884
+ const inSearch = [];
2885
+ for (const parent of parents) {
2886
+ if (parent.searchId !== this.searchId) {
2887
+ const from = this.header.derivedFrom;
2888
+ if (operator !== "derive" || parents.length !== 1 || !from || from.searchId !== parent.searchId || from.nodeId !== parent.nodeId) throw integrity(`edge ${event.edgeId} names a parent in search ${parent.searchId}; only a derive edge matching derivedFrom may`);
2889
+ continue;
2597
2890
  }
2598
- planExtensions.push(event);
2599
- return;
2891
+ if (operator === "derive") throw integrity(`derive edge ${event.edgeId} must name the parent in derivedFrom`);
2892
+ const record = this.nodes.get(parent.nodeId);
2893
+ if (!record) throw integrity(`edge ${event.edgeId} names unregistered parent ${parent.nodeId}`);
2894
+ if (record.ordinal >= child.ordinal) throw integrity(`edge ${event.edgeId}: parent ${parent.nodeId} registered after child ${child.event.nodeId}`);
2895
+ if (record.edgeIds.length === 0) throw integrity(`edge ${event.edgeId}: parent ${parent.nodeId} has no edge of its own yet`);
2896
+ inSearch.push(record);
2600
2897
  }
2601
- if (event.kind === "candidate-registered") {
2602
- if (candidates.has(event.candidateId)) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} was registered twice`);
2603
- const plannedSlot = plannedSlots.get(event.slotId);
2604
- if (!plannedSlot) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} binds unknown slot ${event.slotId}`);
2605
- if (candidateBySlot.has(event.slotId)) throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} was bound twice`);
2606
- if (closedSlots.has(event.slotId)) throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} was already closed`);
2607
- if (event.generationOperationId !== plannedSlot.generationOperationId) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} generation operation ${event.generationOperationId} does not match slot ${event.slotId} plan ${plannedSlot.generationOperationId}`);
2608
- const generationOperation = operationsById.get(event.generationOperationId);
2609
- if (!generationOperation) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} precedes generation operation ${event.generationOperationId}`);
2610
- if (generationOperation.outcome.status === "failed") throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} cannot bind failed generation operation ${event.generationOperationId}`);
2611
- const previousCandidate = lineageNodes.get(event.lineage.lineageNodeId);
2612
- if (previousCandidate) throw new SearchLedgerIntegrityError(`lineage node ${event.lineage.lineageNodeId} is already bound to ${previousCandidate}`);
2613
- assertUnique(event.lineage.parentCandidateIds, "parentCandidateId", event.eventId);
2614
- const parents = event.lineage.parentCandidateIds.map((id) => {
2615
- const parent = candidates.get(id);
2616
- if (!parent) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} references unknown parent ${id}`);
2617
- return parent;
2618
- });
2619
- const expectedGeneration = parents.length === 0 ? 0 : Math.max(...parents.map((parent) => parent.registered.lineage.generation)) + 1;
2620
- if (event.lineage.generation !== expectedGeneration) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} generation ${event.lineage.generation} does not follow its parents (expected ${expectedGeneration})`);
2621
- assertUnique(event.surfaces.map((surface) => surface.surfaceId), "surfaceId", event.eventId);
2622
- candidates.set(event.candidateId, {
2623
- registered: event,
2624
- attempts: [],
2625
- decision: null
2626
- });
2627
- candidateBySlot.set(event.slotId, event.candidateId);
2628
- lineageNodes.set(event.lineage.lineageNodeId, event.candidateId);
2629
- candidateEvents.push(event);
2630
- return;
2898
+ if (child.edgeIds.length === 0) {
2899
+ const primary = inSearch[0];
2900
+ child.primaryParentId = primary?.event.nodeId ?? null;
2901
+ child.depth = operator === "seed" || operator === "derive" ? 0 : primary && primary.depth !== null ? primary.depth + 1 : null;
2902
+ this.audit.nodesWithoutEdge -= 1;
2903
+ } else this.audit.edges.reproposals += 1;
2904
+ child.edgeIds.push(event.edgeId);
2905
+ for (const parent of parents) {
2906
+ const key = canonicalString(parent);
2907
+ if (child.parentKeys.has(key)) continue;
2908
+ child.parentKeys.add(key);
2909
+ child.parents.push(parent);
2631
2910
  }
2632
- if (event.kind === "task-attempted") {
2633
- const candidate = candidates.get(event.candidateId);
2634
- if (!candidate) throw new SearchLedgerIntegrityError(`attempt ${event.eventId} references unknown candidate ${event.candidateId}`);
2635
- if (candidate.decision) throw new SearchLedgerIntegrityError(`attempt ${event.eventId} appears after candidate ${event.candidateId} was decided`);
2636
- const plannedTask = planEvent.plan.tasks.find((task) => task.taskId === event.task.taskId);
2637
- if (!plannedTask) throw new SearchLedgerIntegrityError(`attempt ${event.eventId} references unplanned task ${event.task.taskId}`);
2638
- if (canonicalString(plannedTask.source) !== canonicalString(event.task.source) || canonicalString(plannedTask.benchmark) !== canonicalString(event.identity.benchmark)) throw new SearchLedgerIntegrityError(`task ${event.task.taskId} does not match its planned source identity`);
2639
- if (event.attemptIndex >= plannedTask.maxAttempts) throw new SearchLedgerIntegrityError(`task ${event.task.taskId} attempt ${event.attemptIndex} exceeds planned maxAttempts ${plannedTask.maxAttempts}`);
2640
- if (runIds.has(event.runId)) throw new SearchLedgerIntegrityError(`runId ${event.runId} was recorded twice`);
2641
- runIds.add(event.runId);
2642
- const attemptKey = canonicalString([
2643
- event.candidateId,
2644
- event.task.taskId,
2645
- event.attemptIndex
2646
- ]);
2647
- if (attemptKeys.has(attemptKey)) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} task ${event.task.taskId} attempt ${event.attemptIndex} was recorded twice`);
2648
- const expectedAttemptIndex = candidate.attempts.filter((attempt) => attempt.task.taskId === event.task.taskId).length;
2649
- if (event.attemptIndex !== expectedAttemptIndex) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} task ${event.task.taskId} attempt index ${event.attemptIndex} is not contiguous (expected ${expectedAttemptIndex})`);
2650
- const previousAttempt = candidate.attempts.find((attempt) => attempt.task.taskId === event.task.taskId);
2651
- if (previousAttempt?.outcome.status !== void 0 && previousAttempt.outcome.status !== "errored") throw new SearchLedgerIntegrityError(`task ${event.task.taskId} was retried after a measured outcome`);
2652
- if (previousAttempt && canonicalString({
2653
- task: previousAttempt.task,
2654
- identity: previousAttempt.identity
2655
- }) !== canonicalString({
2656
- task: event.task,
2657
- identity: event.identity
2658
- })) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} task ${event.task.taskId} changed immutable execution identity between attempts`);
2659
- attemptKeys.add(attemptKey);
2660
- const declared = candidate.registered.surfaces.map((surface) => surface.surfaceId).sort();
2661
- const observed = event.surfaceEvidence.map((surface) => surface.surfaceId).sort();
2662
- assertUnique(observed, "surface evidence", event.eventId);
2663
- for (const evidence of event.surfaceEvidence) assertUnique(evidence.evidence.map(artifactKey), "surface evidence receipt", event.eventId);
2664
- if (canonicalString(declared) !== canonicalString(observed)) throw new SearchLedgerIntegrityError(`attempt ${event.eventId} surface evidence does not exactly cover candidate ${event.candidateId}`);
2665
- candidate.attempts.push(event);
2666
- attempts.push(event);
2667
- return;
2911
+ for (const parent of inSearch) if (!parent.children.includes(child.event.nodeId)) parent.children.push(child.event.nodeId);
2912
+ child.updatedSequence = index;
2913
+ this.edges.set(event.edgeId, event);
2914
+ this.edgeOrder.push(event.edgeId);
2915
+ this.audit.edges[attribution] += 1;
2916
+ }
2917
+ allocateCell(event, index) {
2918
+ const expected = searchCellId(this.searchId, event.nodeId, event.taskId, event.split, event.rep);
2919
+ if (event.cellId !== expected) throw integrity(`cell ${event.cellId} does not match its coordinates (expected ${expected})`);
2920
+ if (this.cells.has(event.cellId)) throw integrity(`cell ${event.cellId} was allocated twice`);
2921
+ const node = this.nodes.get(event.nodeId);
2922
+ if (!node) throw integrity(`cell ${event.cellId} names unregistered node ${event.nodeId}`);
2923
+ if (node.edgeIds.length === 0) throw integrity(`cell ${event.cellId}: node ${event.nodeId} has no edge yet`);
2924
+ const task = this.tasks.get(event.taskId);
2925
+ if (!task || task.split !== event.split) throw integrity(`cell ${event.cellId}: task ${event.taskId} is not in the ${event.split} split`);
2926
+ if (task.unitId !== event.unitId) throw integrity(`cell ${event.cellId}: task ${event.taskId} samples unit ${task.unitId}`);
2927
+ assertStageSplit(event.cellId, event.stage, event.split, node.ordinal === 0);
2928
+ if (event.split === "test" && node.ordinal !== 0 && !node.finalist) throw integrity(`cell ${event.cellId}: the test split is sealed; only the root and finalists run on it`);
2929
+ const maxCells = this.header.budget.maxCells;
2930
+ if (maxCells !== null && this.audit.cells.allocated + 1 > maxCells) throw integrity(`cell ${event.cellId} exceeds the search's maxCells ${maxCells}`);
2931
+ this.admit(event.reservation, event.stage === "claim", `cell ${event.cellId}`);
2932
+ this.cells.set(event.cellId, {
2933
+ cellId: event.cellId,
2934
+ nodeId: event.nodeId,
2935
+ taskId: event.taskId,
2936
+ unitId: event.unitId,
2937
+ split: event.split,
2938
+ rep: event.rep,
2939
+ stage: event.stage,
2940
+ lane: event.lane,
2941
+ reservation: event.reservation,
2942
+ attempts: 0,
2943
+ lastRunId: null,
2944
+ outcome: null,
2945
+ score: null,
2946
+ final: false,
2947
+ cancelled: null,
2948
+ spentUsd: 0,
2949
+ costKnown: true,
2950
+ traceId: null,
2951
+ allocatedSequence: index,
2952
+ updatedSequence: index
2953
+ });
2954
+ node.cellIds.push(event.cellId);
2955
+ node.updatedSequence = index;
2956
+ this.audit.cells.allocated += 1;
2957
+ this.audit.cells.open += 1;
2958
+ this.audit.spend.openReservationUsd += event.reservation?.usd ?? 0;
2959
+ if (event.stage === "claim") this.claimUsedUsd += event.reservation?.usd ?? 0;
2960
+ }
2961
+ settleCell(event, index) {
2962
+ const cell = this.cells.get(event.cellId);
2963
+ if (!cell) throw integrity(`attempt ${event.eventId} names unallocated cell ${event.cellId}`);
2964
+ if (cell.cancelled) throw integrity(`cell ${event.cellId} was settled after it was cancelled`);
2965
+ if (cell.final) throw integrity(`cell ${event.cellId} was attempted again after a final ${cell.outcome} outcome; only a retryable error admits another attempt`);
2966
+ if (event.attempt !== cell.attempts + 1) throw integrity(`cell ${event.cellId} attempt ${event.attempt} is not contiguous (expected ${cell.attempts + 1})`);
2967
+ const runId = searchCellRunId({
2968
+ cellId: event.cellId,
2969
+ attempt: event.attempt
2970
+ });
2971
+ if (event.runId !== runId) throw integrity(`cell ${event.cellId} attempt ${event.attempt} must have runId ${runId}`);
2972
+ const node = this.nodes.get(cell.nodeId);
2973
+ const declared = new Set(node.event.surfaces.map((surface) => surface.surfaceId));
2974
+ assertUnique(event.surfaceEvidence.map((evidence) => evidence.surfaceId), "surface evidence", event.eventId);
2975
+ for (const evidence of event.surfaceEvidence) if (!declared.has(evidence.surfaceId)) throw integrity(`attempt ${event.eventId} reports surface ${evidence.surfaceId}, which node ${cell.nodeId} does not declare`);
2976
+ if (event.artifacts.filter((artifact) => artifact.role === "run-record").length > 1) throw integrity(`attempt ${event.eventId} binds more than one RunRecord`);
2977
+ const before = this.reservationParts(cell);
2978
+ const cost = this.book(event.accounting);
2979
+ if (cell.attempts === 0) {
2980
+ this.audit.cells.settled += 1;
2981
+ this.audit.cells.open -= 1;
2982
+ if (cell.reservation === null) this.audit.spend.unreservedCells += 1;
2668
2983
  }
2669
- if (event.kind === "search-operation-recorded") {
2670
- const plannedOperation = plannedOperations.get(event.operationId);
2671
- if (!plannedOperation) throw new SearchLedgerIntegrityError(`operation ${event.operationId} was not declared in the search plan`);
2672
- if (plannedOperation.kind !== event.operationKind) throw new SearchLedgerIntegrityError(`operation ${event.operationId} kind ${event.operationKind} does not match planned ${plannedOperation.kind}`);
2673
- if (operationsById.has(event.operationId)) throw new SearchLedgerIntegrityError(`operation ${event.operationId} was recorded twice`);
2674
- operationsById.set(event.operationId, event);
2675
- operationEvents.push(event);
2676
- return;
2984
+ if (cost.unknown && cell.costKnown) {
2985
+ cell.costKnown = false;
2986
+ this.audit.spend.unknownCostCells += 1;
2987
+ node.spend.unknownCostCells += 1;
2677
2988
  }
2678
- if (event.kind === "candidate-slot-closed") {
2679
- const plannedSlot = plannedSlots.get(event.slotId);
2680
- if (!plannedSlot) throw new SearchLedgerIntegrityError(`candidate slot closure ${event.eventId} references unknown slot ${event.slotId}`);
2681
- if (event.generationOperationId !== plannedSlot.generationOperationId) throw new SearchLedgerIntegrityError(`candidate slot closure ${event.eventId} generation operation ${event.generationOperationId} does not match slot ${event.slotId} plan ${plannedSlot.generationOperationId}`);
2682
- if (candidateBySlot.has(event.slotId)) throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} was already bound to a candidate`);
2683
- if (closedSlots.has(event.slotId)) throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} was closed twice`);
2684
- const operation = operationsById.get(event.generationOperationId);
2685
- if (!operation) throw new SearchLedgerIntegrityError(`candidate slot closure ${event.eventId} precedes operation ${event.generationOperationId}`);
2686
- if (operation.outcome.status === "completed") throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} cannot close from completed operation ${event.generationOperationId}`);
2687
- closedSlots.set(event.slotId, event);
2688
- closedSlotEvents.push(event);
2689
- return;
2989
+ cell.attempts = event.attempt;
2990
+ cell.lastRunId = runId;
2991
+ cell.outcome = event.outcome.status;
2992
+ cell.spentUsd += cost.usd;
2993
+ cell.updatedSequence = index;
2994
+ if ("traceId" in event.traceRef) cell.traceId = event.traceRef.traceId;
2995
+ if (event.outcome.status === "errored") cell.final = !event.outcome.error.retryable;
2996
+ else {
2997
+ cell.final = true;
2998
+ cell.score = event.outcome.score;
2999
+ node.scoredCells += 1;
2690
3000
  }
2691
- if (event.kind === "candidate-decided") {
2692
- const candidate = candidates.get(event.candidateId);
2693
- if (!candidate) throw new SearchLedgerIntegrityError(`decision ${event.eventId} references unknown candidate ${event.candidateId}`);
2694
- if (candidate.decision) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} was decided twice`);
2695
- if (event.decision.status === "selected") {
2696
- if (!candidate.attempts.some((attempt) => attempt.outcome.status !== "errored")) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} cannot be selected without a measured task outcome`);
2697
- if (decisions.some((decision) => decision.decision.status === "selected")) throw new SearchLedgerIntegrityError("more than one candidate was selected");
2698
- }
2699
- candidate.decision = event;
2700
- decisions.push(event);
2701
- return;
3001
+ const after = this.reservationParts(cell);
3002
+ this.audit.spend.openReservationUsd += after.open - before.open;
3003
+ this.audit.spend.overspendUsd += after.over - before.over;
3004
+ if (cell.stage === "claim") this.claimUsedUsd += after.used - before.used;
3005
+ if (cost.unknown) node.spend.floorUsd += cost.usd;
3006
+ else node.spend.knownUsd += cost.usd;
3007
+ const boxMinutes = event.boxMinutes ?? 0;
3008
+ node.spend.boxMinutes += boxMinutes;
3009
+ this.audit.spend.boxMinutes += boxMinutes;
3010
+ node.updatedSequence = index;
3011
+ this.audit.attempts += 1;
3012
+ this.audit.outcomes[event.outcome.status] += 1;
3013
+ }
3014
+ cancelCell(event, index) {
3015
+ const cell = this.cells.get(event.cellId);
3016
+ if (!cell) throw integrity(`cancellation ${event.eventId} names unallocated cell ${event.cellId}`);
3017
+ if (cell.cancelled) throw integrity(`cell ${event.cellId} was cancelled twice`);
3018
+ if (cell.final) throw integrity(`cell ${event.cellId} was cancelled after a final outcome`);
3019
+ const before = this.reservationParts(cell);
3020
+ cell.cancelled = event.reason;
3021
+ cell.updatedSequence = index;
3022
+ const after = this.reservationParts(cell);
3023
+ this.audit.spend.openReservationUsd += after.open - before.open;
3024
+ this.audit.spend.overspendUsd += after.over - before.over;
3025
+ if (cell.stage === "claim") this.claimUsedUsd += after.used - before.used;
3026
+ if (cell.attempts === 0) {
3027
+ this.audit.cells.cancelled += 1;
3028
+ this.audit.cells.open -= 1;
2702
3029
  }
2703
- const missingCandidateSlots = [...plannedSlots.values()].filter((slot) => !candidateBySlot.has(slot.slotId) && !closedSlots.has(slot.slotId)).map((slot) => slot.slotId);
2704
- if (missingCandidateSlots.length > 0) throw new SearchLedgerIntegrityError(`search completed with missing candidate slots: ${missingCandidateSlots.join(", ")}`);
2705
- const missingTaskOutcomes = plannedTaskOutcomeKeys(planEvent, candidates);
2706
- if (missingTaskOutcomes.length > 0) throw new SearchLedgerIntegrityError(`search completed with missing task outcomes: ${missingTaskOutcomes.join(", ")}`);
2707
- const missingOperations = [...plannedOperations.values()].filter((operation) => !operationsById.has(operation.operationId)).map((operation) => operation.operationId);
2708
- if (missingOperations.length > 0) throw new SearchLedgerIntegrityError(`search completed with missing search operations: ${missingOperations.join(", ")}`);
2709
- for (const operation of plannedOperations.values()) {
2710
- if (operation.kind !== "candidate-generation") continue;
2711
- const generationOutcome = operationsById.get(operation.operationId).outcome.status;
2712
- const slots = [...plannedSlots.values()].filter((slot) => slot.generationOperationId === operation.operationId);
2713
- if (slots.length === 0) continue;
2714
- const registeredCount = slots.filter((slot) => candidateBySlot.has(slot.slotId)).length;
2715
- const closedCount = slots.filter((slot) => closedSlots.has(slot.slotId)).length;
2716
- if (generationOutcome === "completed" && closedCount > 0) throw new SearchLedgerIntegrityError(`completed generation operation ${operation.operationId} contains ${closedCount} closed slot(s)`);
2717
- if (generationOutcome === "failed" && registeredCount > 0) throw new SearchLedgerIntegrityError(`failed generation operation ${operation.operationId} contains ${registeredCount} registered candidate(s)`);
2718
- if (generationOutcome === "partial" && (registeredCount === 0 || closedCount === 0)) throw new SearchLedgerIntegrityError(`partial generation operation ${operation.operationId} must contain both a registered candidate and a closed slot`);
3030
+ this.nodes.get(cell.nodeId).updatedSequence = index;
3031
+ }
3032
+ decideNode(event, index) {
3033
+ const node = this.nodes.get(event.nodeId);
3034
+ if (!node) throw integrity(`decision ${event.eventId} names unregistered node ${event.nodeId}`);
3035
+ if (node.edgeIds.length === 0) throw integrity(`decision ${event.eventId}: node ${event.nodeId} has no edge yet`);
3036
+ const status = event.decision.status;
3037
+ if (status === "selected") {
3038
+ if (this.selectedNodeId !== null && this.selectedNodeId !== event.nodeId) throw integrity(`node ${event.nodeId} was selected while ${this.selectedNodeId} is still selected`);
3039
+ if (node.scoredCells === 0) throw integrity(`node ${event.nodeId} cannot be selected without a scored cell`);
2719
3040
  }
2720
- const pending = [...candidates.values()].filter((candidate) => candidate.decision === null);
2721
- if (pending.length > 0) throw new SearchLedgerIntegrityError(`search completed with ${pending.length} candidate decision(s) missing`);
2722
- const selected = decisions.filter((decision) => decision.decision.status === "selected");
2723
- if (event.result.status === "selected") {
2724
- if (selected.length !== 1 || selected[0].candidateId !== event.result.candidateId) throw new SearchLedgerIntegrityError(`search completion winner ${event.result.candidateId} does not match candidate decisions`);
2725
- } else if (selected.length !== 0) throw new SearchLedgerIntegrityError("all-rejected completion contains a selected candidate");
2726
- completion = event;
2727
- };
2728
- const finish = (entries) => {
2729
- const selectedDecisions = decisions.filter((decision) => decision.decision.status === "selected");
2730
- const rejectedDecisions = decisions.filter((decision) => decision.decision.status === "rejected");
2731
- const outcomeCounts = {
2732
- passed: 0,
2733
- failed: 0,
2734
- errored: 0
3041
+ const previous = node.decisions.at(-1)?.decision.status ?? null;
3042
+ const wasUndecided = previous === null || previous === "advanced";
3043
+ const isUndecided = status === "advanced";
3044
+ this.audit.undecidedNodes += (isUndecided ? 1 : 0) - (wasUndecided ? 1 : 0);
3045
+ if (previous === "selected" && status !== "selected") this.selectedNodeId = null;
3046
+ if (status === "selected") this.selectedNodeId = event.nodeId;
3047
+ if (status === "finalist") node.finalist = true;
3048
+ node.decisions.push({
3049
+ sequence: index,
3050
+ decision: event.decision,
3051
+ basis: event.basis,
3052
+ rule: event.rule,
3053
+ reason: event.reason
3054
+ });
3055
+ node.updatedSequence = index;
3056
+ this.audit.selectedNodeId = this.selectedNodeId;
3057
+ }
3058
+ close(event) {
3059
+ const reasons = this.incompleteReasons();
3060
+ if (reasons.length > 0) throw integrity(`search-closed while incomplete: ${reasons.join("; ")}`);
3061
+ if (event.claim) this.checkClaim(event.claim);
3062
+ this.closed = event;
3063
+ this.audit.status = "closed";
3064
+ this.audit.closeReason = event.reason;
3065
+ this.audit.spend.openReservationUsd = 0;
3066
+ }
3067
+ checkClaim(claim) {
3068
+ if (claim.finalists.length > 3) throw integrity(`the claim names ${claim.finalists.length} finalists; at most 3 are allowed`);
3069
+ assertUnique(claim.finalists.map((finalist) => finalist.nodeId), "claim finalist", "search-closed");
3070
+ for (const finalist of claim.finalists) if (!this.nodes.get(finalist.nodeId)?.finalist) throw integrity(`claim finalist ${finalist.nodeId} was never decided finalist`);
3071
+ if (claim.selected !== this.selectedNodeId) throw integrity(`the claim selects ${claim.selected ?? "nothing"} but the selected node is ${this.selectedNodeId ?? "none"}`);
3072
+ const root = this.nodeOrder[0] ?? null;
3073
+ if (claim.decision === "ship") {
3074
+ const shipped = claim.finalists.find((finalist) => finalist.nodeId === claim.selected);
3075
+ if (claim.selected === null || claim.selected === root || !shipped?.promote) throw integrity("a ship claim must select a finalist that the paired test promoted");
3076
+ if (!this.header.splits.heldOutUnits) throw integrity("a ship claim needs test units held out from train and selection");
3077
+ } else if (claim.selected !== null && claim.selected !== root) throw integrity(`a ${claim.decision} claim keeps the root; it cannot select another node`);
3078
+ if (claim.decision === "test-cannot-resolve") {
3079
+ for (const cell of this.cells.values()) if (cell.split === "test" && cell.attempts > 0) throw integrity("a test-cannot-resolve claim spends nothing on test cells");
3080
+ }
3081
+ }
3082
+ /** @internal */
3083
+ incompleteReasons() {
3084
+ const reasons = [];
3085
+ if (!this.header) reasons.push("search-opened is missing");
3086
+ if (this.audit.cells.open > 0) reasons.push(count(this.audit.cells.open, "cell", "has no outcome"));
3087
+ if (this.audit.operations.open > 0) reasons.push(count(this.audit.operations.open, "operation", "is unrecorded"));
3088
+ if (this.audit.nodesWithoutEdge > 0) reasons.push(count(this.audit.nodesWithoutEdge, "node", "has no edge"));
3089
+ if (this.audit.undecidedNodes > 0) reasons.push(count(this.audit.undecidedNodes, "node", "has no terminal decision"));
3090
+ return reasons;
3091
+ }
3092
+ /** Book an attempt's or operation's accounting into the running totals. An
3093
+ * unknown cost counts as its proven floor. */
3094
+ book(accounting) {
3095
+ const { tokens, cost } = accounting;
3096
+ if (tokens.status === "known") {
3097
+ this.audit.tokens.inputTokens += tokens.inputTokens;
3098
+ this.audit.tokens.outputTokens += tokens.outputTokens;
3099
+ this.audit.tokens.cachedTokens += tokens.cachedTokens;
3100
+ } else this.audit.tokens.unknownTokenAttempts += 1;
3101
+ const usd = cost.status === "known" ? cost.usd : cost.knownLowerBoundUsd;
3102
+ if (cost.status === "known") this.audit.spend.knownUsd += usd;
3103
+ else this.audit.spend.floorUsd += usd;
3104
+ this.audit.spend.committedUsd += usd;
3105
+ return {
3106
+ usd,
3107
+ unknown: cost.status === "unknown"
2735
3108
  };
2736
- const operationOutcomeCounts = {
2737
- completed: 0,
2738
- partial: 0,
2739
- failed: 0
3109
+ }
3110
+ /** A cell's share of the open reservations, its overspend, and what it draws
3111
+ * from the claim reserve: the larger of its hold and its spend. */
3112
+ reservationParts(cell) {
3113
+ const reserved = cell.reservation?.usd ?? 0;
3114
+ const holding = !cell.final && cell.cancelled === null;
3115
+ return {
3116
+ open: holding ? Math.max(0, reserved - cell.spentUsd) : 0,
3117
+ over: cell.reservation ? Math.max(0, cell.spentUsd - reserved) : 0,
3118
+ used: holding ? Math.max(reserved, cell.spentUsd) : cell.spentUsd
2740
3119
  };
2741
- let inputTokens = 0;
2742
- let outputTokens = 0;
2743
- let cachedTokens = 0;
2744
- let costUsd = 0;
2745
- const unknownTokenEventIds = [];
2746
- const unknownCostEventIds = [];
2747
- for (const attempt of attempts) outcomeCounts[attempt.outcome.status] += 1;
2748
- for (const operation of operationEvents) operationOutcomeCounts[operation.outcome.status] += 1;
2749
- for (const costedEvent of [...attempts, ...operationEvents]) {
2750
- if (costedEvent.accounting.tokens.status === "known") {
2751
- inputTokens += costedEvent.accounting.tokens.inputTokens;
2752
- outputTokens += costedEvent.accounting.tokens.outputTokens;
2753
- cachedTokens += costedEvent.accounting.tokens.cachedTokens;
2754
- } else unknownTokenEventIds.push(costedEvent.eventId);
2755
- if (costedEvent.accounting.cost.status === "known") costUsd += costedEvent.accounting.cost.usd;
2756
- else {
2757
- costUsd += costedEvent.accounting.cost.knownLowerBoundUsd;
2758
- unknownCostEventIds.push(costedEvent.eventId);
2759
- }
3120
+ }
3121
+ /** The admission rule: committed spend, open holds, the unspent claim
3122
+ * reserve and the new hold stay within the cap. A claim cell draws on the
3123
+ * claim reserve instead of adding to it. */
3124
+ admit(reservation, claim, subject) {
3125
+ const { maxUsd, reservedClaimUsd } = this.header.budget;
3126
+ if (maxUsd === null) return;
3127
+ const hold = reservation?.usd ?? 0;
3128
+ const remainingClaim = Math.max(0, reservedClaimUsd - this.claimUsedUsd);
3129
+ if (this.audit.spend.committedUsd + this.audit.spend.openReservationUsd + (claim ? Math.max(0, remainingClaim - hold) : remainingClaim) + hold > maxUsd + USD_TOLERANCE) throw integrity(`${subject} is not admissible: committed $${round(this.audit.spend.committedUsd)} + open $${round(this.audit.spend.openReservationUsd)} + claim reserve $${round(remainingClaim)} + hold $${round(hold)} exceeds the cap $${maxUsd}`);
3130
+ }
3131
+ /** @internal */
3132
+ readHeader() {
3133
+ return this.header;
3134
+ }
3135
+ /** @internal */
3136
+ readClosed() {
3137
+ return this.closed;
3138
+ }
3139
+ /** @internal */
3140
+ readHead() {
3141
+ return this.head;
3142
+ }
3143
+ /** @internal */
3144
+ readLastOccurredAt() {
3145
+ return this.lastOccurredAtText;
3146
+ }
3147
+ /** @internal */
3148
+ readAudit() {
3149
+ const audit = this.audit;
3150
+ return {
3151
+ ...audit,
3152
+ edges: { ...audit.edges },
3153
+ cells: { ...audit.cells },
3154
+ outcomes: { ...audit.outcomes },
3155
+ operations: { ...audit.operations },
3156
+ spend: { ...audit.spend },
3157
+ tokens: { ...audit.tokens }
3158
+ };
3159
+ }
3160
+ /** @internal */
3161
+ readNode(nodeId) {
3162
+ const record = this.nodes.get(nodeId);
3163
+ return record ? nodeView(record) : void 0;
3164
+ }
3165
+ /** @internal */
3166
+ readNodeIds() {
3167
+ return this.nodeOrder;
3168
+ }
3169
+ /** @internal */
3170
+ readHasNode(nodeId) {
3171
+ return this.nodes.has(nodeId);
3172
+ }
3173
+ /** @internal */
3174
+ readNodeIdForDigest(artifactDigest) {
3175
+ return this.nodeByDigest.get(artifactDigest);
3176
+ }
3177
+ /** @internal */
3178
+ readEdge(edgeId) {
3179
+ return this.edges.get(edgeId);
3180
+ }
3181
+ /** @internal */
3182
+ readEdgeIds() {
3183
+ return this.edgeOrder;
3184
+ }
3185
+ /** @internal */
3186
+ readCell(cellId) {
3187
+ const cell = this.cells.get(cellId);
3188
+ return cell ? { ...cell } : void 0;
3189
+ }
3190
+ /** @internal */
3191
+ readCellIds(nodeId) {
3192
+ if (nodeId === void 0) return [...this.cells.keys()];
3193
+ return this.nodes.get(nodeId)?.cellIds ?? [];
3194
+ }
3195
+ /** @internal */
3196
+ readOperation(operationId) {
3197
+ const operation = this.operations.get(operationId);
3198
+ return operation ? { ...operation } : void 0;
3199
+ }
3200
+ /** @internal */
3201
+ readScoredCells(nodeId, split) {
3202
+ const scored = [];
3203
+ for (const cellId of this.nodes.get(nodeId)?.cellIds ?? []) {
3204
+ const cell = this.cells.get(cellId);
3205
+ if (cell.split === split && cell.score !== null) scored.push({
3206
+ cellId,
3207
+ unitId: cell.unitId,
3208
+ attempt: cell.attempts,
3209
+ score: cell.score
3210
+ });
2760
3211
  }
2761
- const accounting = unknownTokenEventIds.length === 0 && unknownCostEventIds.length === 0 ? {
2762
- status: "known",
2763
- inputTokens,
2764
- outputTokens,
2765
- cachedTokens,
2766
- costUsd
2767
- } : {
2768
- status: "partial",
2769
- knownInputTokens: inputTokens,
2770
- knownOutputTokens: outputTokens,
2771
- knownCachedTokens: cachedTokens,
2772
- knownCostUsd: costUsd,
2773
- unknownTokenEventIds,
2774
- unknownCostEventIds
3212
+ return scored;
3213
+ }
3214
+ };
3215
+ /**
3216
+ * A read of `SearchState` at one ledger position. Header, head, audit and
3217
+ * claim are values. Node, edge, cell and score reads go to the live indexes and
3218
+ * throw once the state has applied a later entry.
3219
+ */
3220
+ var SearchStateView = class {
3221
+ searchId;
3222
+ header;
3223
+ closed;
3224
+ head;
3225
+ /** `occurredAt` of the head entry; a writer stamps later events at or after it. */
3226
+ lastOccurredAt;
3227
+ audit;
3228
+ completion;
3229
+ state;
3230
+ version;
3231
+ constructor(state, version) {
3232
+ this.state = state;
3233
+ this.version = version;
3234
+ this.searchId = state.searchId;
3235
+ this.header = state.readHeader();
3236
+ this.closed = state.readClosed();
3237
+ this.head = state.readHead();
3238
+ this.lastOccurredAt = state.readLastOccurredAt();
3239
+ this.audit = state.readAudit();
3240
+ const reasons = state.incompleteReasons();
3241
+ if (!this.closed) reasons.unshift("search is open");
3242
+ this.completion = {
3243
+ complete: this.closed !== null,
3244
+ reasons
2775
3245
  };
2776
- const selectedCandidateId = completion?.result.status === "selected" ? completion.result.candidateId : null;
2777
- const status = completion?.result.status === "selected" ? "selected" : completion?.result.status === "all-rejected" ? "all-rejected" : "in-progress";
2778
- const missingCandidateSlots = [...plannedSlots.values()].filter((slot) => !candidateBySlot.has(slot.slotId) && !closedSlots.has(slot.slotId)).map((slot) => slot.slotId);
2779
- const missingTaskOutcomes = planEvent ? plannedTaskOutcomeKeys(planEvent, candidates) : [];
2780
- const missingOperations = [...plannedOperations.values()].filter((operation) => !operationsById.has(operation.operationId)).map((operation) => operation.operationId);
3246
+ }
3247
+ /** The node the search started from: the first registered. */
3248
+ get rootNodeId() {
3249
+ return this.live().readNodeIds()[0] ?? null;
3250
+ }
3251
+ node(nodeId) {
3252
+ return this.live().readNode(nodeId);
3253
+ }
3254
+ hasNode(nodeId) {
3255
+ return this.live().readHasNode(nodeId);
3256
+ }
3257
+ /** Node ids in registration order. */
3258
+ nodeIds() {
3259
+ return [...this.live().readNodeIds()];
3260
+ }
3261
+ /** Nodes in registration order. */
3262
+ nodes() {
3263
+ const state = this.live();
3264
+ return state.readNodeIds().map((nodeId) => state.readNode(nodeId));
3265
+ }
3266
+ nodeIdForDigest(artifactDigest) {
3267
+ return this.live().readNodeIdForDigest(artifactDigest);
3268
+ }
3269
+ edge(edgeId) {
3270
+ return this.live().readEdge(edgeId);
3271
+ }
3272
+ /** Edges in ledger order. */
3273
+ edges() {
3274
+ const state = this.live();
3275
+ return state.readEdgeIds().map((edgeId) => state.readEdge(edgeId));
3276
+ }
3277
+ cell(cellId) {
3278
+ return this.live().readCell(cellId);
3279
+ }
3280
+ /** Cells in allocation order, optionally of one node. */
3281
+ cells(filter = {}) {
3282
+ const state = this.live();
3283
+ return state.readCellIds(filter.nodeId).map((cellId) => state.readCell(cellId));
3284
+ }
3285
+ operation(operationId) {
3286
+ return this.live().readOperation(operationId);
3287
+ }
3288
+ /** A node's scored cells on one split, in allocation order. Unscored cells
3289
+ * (errored, cancelled or in flight) are absent, never zero. */
3290
+ scoredCells(nodeId, split) {
3291
+ return this.live().readScoredCells(nodeId, split);
3292
+ }
3293
+ /** Per-unit score means of a node on one split, in unitId order. */
3294
+ unitScores(nodeId, split) {
3295
+ return searchUnitScores(this.scoredCells(nodeId, split));
3296
+ }
3297
+ /** The lineage a RunRecord mint needs for one cell attempt. */
3298
+ lineage(coordinates) {
3299
+ const state = this.live();
3300
+ if (coordinates.searchId !== this.searchId) throw new SearchLedgerError(`coordinates name search ${coordinates.searchId}, not ${this.searchId}`);
3301
+ const cell = state.readCell(coordinates.cellId);
3302
+ if (!cell || cell.nodeId !== coordinates.nodeId) throw new SearchLedgerError(`cell ${coordinates.cellId} of node ${coordinates.nodeId} is not in search ${this.searchId}`);
3303
+ if (coordinates.attempt < 1 || coordinates.attempt > cell.attempts) throw new SearchLedgerError(`cell ${coordinates.cellId} has ${cell.attempts} settled attempt(s), not attempt ${coordinates.attempt}`);
3304
+ const node = state.readNode(cell.nodeId);
3305
+ if (node.depth === null) throw new SearchLedgerError(`node ${node.nodeId} has unknown lineage, so its depth is unknown`);
3306
+ const containment = this.header?.containment ?? null;
2781
3307
  return {
2782
- entries: [...entries],
2783
- plan: planEvent,
2784
- planExtensions,
2785
- candidates: candidateEvents,
2786
- closedCandidateSlots: closedSlotEvents,
2787
- attempts,
2788
- operations: operationEvents,
2789
- decisions,
2790
- completion,
2791
- audit: {
2792
- campaignId,
2793
- eventCount: entries.length,
2794
- candidateCount: candidates.size,
2795
- closedCandidateSlotCount: closedSlots.size,
2796
- attemptCount: attempts.length,
2797
- operationCount: operationEvents.length,
2798
- outcomes: outcomeCounts,
2799
- operationOutcomes: operationOutcomeCounts,
2800
- decisions: {
2801
- selected: selectedDecisions.length,
2802
- rejected: rejectedDecisions.length,
2803
- pending: candidates.size - decisions.length
2804
- },
2805
- expected: {
2806
- candidateSlots: plannedSlots.size,
2807
- taskOutcomes: candidates.size * (planEvent?.plan.tasks.length ?? 0),
2808
- operations: plannedOperations.size,
2809
- missingCandidateSlots,
2810
- missingTaskOutcomes,
2811
- missingOperations
2812
- },
2813
- status,
2814
- selectedCandidateId,
2815
- accounting,
2816
- headHash: entries.at(-1)?.entryHash ?? null
2817
- }
3308
+ depth: node.depth,
3309
+ ordinal: node.ordinal,
3310
+ rep: cell.rep,
3311
+ containingRunId: containment ? searchCellRunId({
3312
+ cellId: containment.cellId,
3313
+ attempt: containment.attempt
3314
+ }) : null
2818
3315
  };
2819
- };
3316
+ }
3317
+ live() {
3318
+ if (!this.state.isAt(this.version)) throw new SearchLedgerError(`this SearchStateView is from sequence ${this.head?.sequence ?? -1}, and the ledger has moved on; read the ledger's state again`);
3319
+ return this.state;
3320
+ }
3321
+ };
3322
+ function nodeView(record) {
3323
+ const latest = record.decisions.at(-1)?.decision ?? null;
2820
3324
  return {
2821
- apply,
2822
- finish
3325
+ nodeId: record.event.nodeId,
3326
+ ordinal: record.ordinal,
3327
+ artifactDigest: record.event.artifactDigest,
3328
+ artifact: record.event.artifact,
3329
+ surfaces: record.event.surfaces,
3330
+ primaryParentId: record.primaryParentId,
3331
+ depth: record.depth,
3332
+ parents: [...record.parents],
3333
+ children: [...record.children],
3334
+ edgeIds: [...record.edgeIds],
3335
+ status: latest?.status ?? null,
3336
+ rung: latest?.status === "advanced" ? latest.rung : null,
3337
+ finalist: record.finalist,
3338
+ decisions: [...record.decisions],
3339
+ cellCount: record.cellIds.length,
3340
+ spend: { ...record.spend },
3341
+ registeredSequence: record.registeredSequence,
3342
+ updatedSequence: record.updatedSequence
2823
3343
  };
2824
3344
  }
2825
- /** Every candidate slot must name a planned candidate-generation operation,
2826
- * whether it arrives with the plan or with a later extension. */
2827
- function assertSlotGenerationOperation(slot, plannedOperations) {
2828
- if (plannedOperations.get(slot.generationOperationId)?.kind !== "candidate-generation") throw new SearchLedgerIntegrityError(`candidate slot ${slot.slotId} references unplanned candidate-generation operation ${slot.generationOperationId}`);
2829
- }
2830
- function plannedTaskOutcomeKeys(planEvent, candidates) {
2831
- const missing = [];
2832
- const registeredCandidates = [...candidates.values()].sort((a, b) => compareStrings(a.registered.slotId, b.registered.slotId));
2833
- for (const candidate of registeredCandidates) {
2834
- const slotId = candidate.registered.slotId;
2835
- for (const task of planEvent.plan.tasks) if (!candidate.attempts.some((attempt) => attempt.task.taskId === task.taskId && attempt.outcome.status !== "errored")) missing.push(`${slotId}/${task.taskId}`);
2836
- }
2837
- return missing;
3345
+ function assertStageSplit(cellId, stage, split, isRoot) {
3346
+ if (!{
3347
+ root: ["train", "selection"],
3348
+ train: ["train"],
3349
+ screen: ["selection"],
3350
+ rung: ["selection"],
3351
+ claim: ["test"],
3352
+ external: ["train", "selection"]
3353
+ }[stage].includes(split)) throw integrity(`cell ${cellId}: a ${stage} cell cannot run on the ${split} split`);
3354
+ if (stage === "root" && !isRoot) throw integrity(`cell ${cellId}: only the root node runs root cells`);
2838
3355
  }
2839
3356
  function assertUnique(values, label, eventId) {
2840
- if (new Set(values).size !== values.length) throw new SearchLedgerIntegrityError(`event ${eventId} contains duplicate ${label} values`);
3357
+ if (new Set(values).size !== values.length) throw integrity(`event ${eventId} contains duplicate ${label} values`);
3358
+ }
3359
+ function count(value, noun, predicate) {
3360
+ const verb = predicate.replace(/^(has|is) /, (word) => value === 1 ? word : word === "has " ? "have " : "are ");
3361
+ return `${value} ${value === 1 ? noun : `${noun}s`} ${verb}`;
3362
+ }
3363
+ function round(usd) {
3364
+ return Math.round(usd * 1e6) / 1e6;
3365
+ }
3366
+ function integrity(message) {
3367
+ return new SearchLedgerIntegrityError(message);
2841
3368
  }
2842
- //#endregion
2843
- //#region src/campaign/search-ledger-types.ts
2844
- const SEARCH_LEDGER_SCHEMA = "tangle.search-ledger.v1";
2845
3369
  //#endregion
2846
3370
  //#region src/campaign/search-ledger.ts
2847
3371
  /**
2848
- * Durable append-only audit log for improvement searches.
3372
+ * Durable, append-only event log of one search: nodes (content-addressed
3373
+ * artifacts), edges (the proposals that derived them), cells (one run of one
3374
+ * node on one task), operations, decisions, and the claim.
2849
3375
  *
2850
- * Existing campaign artifacts keep their own rich records: `RunRecord` owns a
2851
- * measured run and `CostLedger` owns per-call accounting. This ledger does not
2852
- * copy those structures. It binds their immutable ids and receipts into one replayable event stream so a
2853
- * search can answer, after a crash, exactly which candidates and task attempts
2854
- * existed, which surfaces actually fired, what they cost, and why they were
2855
- * selected or rejected.
3376
+ * The ledger is the search's only lineage record and its only resume
3377
+ * checkpoint. A RunRecord owns one measured run and `CostLedger` owns per-call
3378
+ * accounting; this ledger binds them by digest instead of copying them.
2856
3379
  *
2857
3380
  * The file format is canonical JSONL with a SHA-256 hash chain. Every append is
2858
3381
  * serialized across processes, fsynced before acknowledgement, and idempotent
2859
3382
  * by `eventId`. A malformed, non-canonical, truncated, reordered, or conflicting
2860
- * log fails loudly; the implementation never skips a bad row.
3383
+ * log fails loudly; a bad row is never skipped. A ledger written under another
3384
+ * schema tag is refused with that tag named, never translated.
2861
3385
  *
2862
- * The journal machinery itself (hash chain, locking, fsync, idempotent append)
2863
- * is the generic `ledger-core` journal; this module supplies the campaign
2864
- * codec: event schemas, canonical event ordering, and the search state machine.
3386
+ * The journal machinery is the generic `ledger-core` journal. This module is
3387
+ * the search codec: event schemas and canonical ordering. `SearchState` is the
3388
+ * state machine and read model.
2865
3389
  */
2866
3390
  const NON_EMPTY = z.string().min(1).refine((value) => value.trim() === value, "must not contain surrounding whitespace");
2867
3391
  const HASH = z.string().regex(/^sha256:[a-f0-9]{64}$/);
2868
- const LINEAGE_NODE_ID = z.string().regex(/^[a-f0-9]{16}$/);
2869
3392
  const IMMUTABLE_REVISION = z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64}|sha256:[a-f0-9]{64}|sha512:[A-Za-z0-9+/=]+)$/);
2870
3393
  const ISO_TIMESTAMP = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/).refine((value) => Number.isFinite(Date.parse(value)), "invalid timestamp");
2871
3394
  const NON_NEGATIVE_INT = z.number().int().nonnegative().safe();
3395
+ const POSITIVE_INT = z.number().int().positive().safe();
2872
3396
  const FINITE_NUMBER = z.number().finite();
3397
+ const USD = z.number().finite().nonnegative();
3398
+ const NODE_ID = z.string().regex(/^node_[a-f0-9]{32}$/);
3399
+ const CELL_ID = z.string().regex(/^cell_[a-f0-9]{32}$/);
2873
3400
  const ArtifactRefSchema = z.object({
2874
3401
  role: NON_EMPTY,
2875
3402
  uri: NON_EMPTY,
@@ -2880,14 +3407,47 @@ const SourceRefSchema = z.object({
2880
3407
  uri: NON_EMPTY,
2881
3408
  revision: IMMUTABLE_REVISION
2882
3409
  }).strict();
3410
+ const UnknownRefSchema = z.object({ unknown: NON_EMPTY }).strict();
2883
3411
  const FailureReasonSchema = z.object({
2884
3412
  code: NON_EMPTY,
2885
3413
  message: NON_EMPTY
2886
3414
  }).strict();
3415
+ const ModelIdentitySchema = z.union([z.object({
3416
+ provider: NON_EMPTY,
3417
+ snapshot: NON_EMPTY.refine(modelHasSnapshot, "model must include an immutable snapshot")
3418
+ }).strict(), z.object({
3419
+ provider: NON_EMPTY,
3420
+ alias: NON_EMPTY,
3421
+ unknown: NON_EMPTY
3422
+ }).strict()]);
3423
+ const ExecutionIdentitySchema = z.object({
3424
+ model: ModelIdentitySchema,
3425
+ agent: SourceRefSchema,
3426
+ benchmark: SourceRefSchema
3427
+ }).strict();
3428
+ const SURFACE_KINDS = [
3429
+ "prompt",
3430
+ "tool-contract",
3431
+ "runtime-config",
3432
+ "memory",
3433
+ "knowledge",
3434
+ "agent-profile",
3435
+ "code",
3436
+ "deployment"
3437
+ ];
3438
+ const SPLIT = z.enum([
3439
+ "train",
3440
+ "selection",
3441
+ "test"
3442
+ ]);
3443
+ const ReservationSchema = z.object({
3444
+ kind: z.enum(["hard", "estimate"]),
3445
+ usd: USD
3446
+ }).strict();
2887
3447
  const EventBaseShape = {
2888
3448
  eventId: NON_EMPTY,
2889
3449
  occurredAt: ISO_TIMESTAMP,
2890
- artifacts: z.array(ArtifactRefSchema).min(1)
3450
+ artifacts: z.array(ArtifactRefSchema)
2891
3451
  };
2892
3452
  const OperationKindSchema = z.enum([
2893
3453
  "candidate-generation",
@@ -2896,89 +3456,19 @@ const OperationKindSchema = z.enum([
2896
3456
  "judge",
2897
3457
  "other"
2898
3458
  ]);
2899
- const CandidateSlotSchema = z.object({
2900
- slotId: NON_EMPTY,
2901
- generationOperationId: NON_EMPTY
2902
- }).strict();
2903
- const PlannedOperationSchema = z.object({
2904
- operationId: NON_EMPTY,
2905
- kind: OperationKindSchema
2906
- }).strict();
2907
- const SearchPlanExtendedSchema = z.object({
2908
- ...EventBaseShape,
2909
- kind: z.literal("search-plan-extended"),
2910
- extension: z.object({
2911
- candidateSlots: z.array(CandidateSlotSchema),
2912
- operations: z.array(PlannedOperationSchema)
2913
- }).strict().superRefine((extension, ctx) => {
2914
- if (extension.candidateSlots.length === 0 && extension.operations.length === 0) ctx.addIssue({
2915
- code: "custom",
2916
- message: "a plan extension must add slots or operations"
2917
- });
2918
- })
2919
- }).strict();
2920
- const SearchPlannedSchema = z.object({
2921
- ...EventBaseShape,
2922
- kind: z.literal("search-planned"),
2923
- plan: z.object({
2924
- candidateSlots: z.array(CandidateSlotSchema).min(1),
2925
- tasks: z.array(z.object({
2926
- taskId: NON_EMPTY,
2927
- source: SourceRefSchema,
2928
- benchmark: SourceRefSchema,
2929
- maxAttempts: z.number().int().positive().safe()
2930
- }).strict()).min(1),
2931
- operations: z.array(PlannedOperationSchema).min(1)
2932
- }).strict()
2933
- }).strict();
2934
- const CandidateRegisteredSchema = z.object({
2935
- ...EventBaseShape,
2936
- kind: z.literal("candidate-registered"),
2937
- slotId: NON_EMPTY,
2938
- generationOperationId: NON_EMPTY,
2939
- candidateId: NON_EMPTY,
2940
- lineage: z.object({
2941
- lineageNodeId: LINEAGE_NODE_ID,
2942
- parentCandidateIds: z.array(NON_EMPTY),
2943
- generation: NON_NEGATIVE_INT,
2944
- proposer: NON_EMPTY,
2945
- proposerSource: SourceRefSchema
2946
- }).strict(),
2947
- surfaces: z.array(z.object({
2948
- surfaceId: NON_EMPTY,
2949
- kind: z.enum([
2950
- "prompt",
2951
- "tool-contract",
2952
- "runtime-config",
2953
- "memory",
2954
- "knowledge",
2955
- "agent-profile",
2956
- "code",
2957
- "deployment"
2958
- ]),
2959
- artifact: ArtifactRefSchema
2960
- }).strict()).min(1)
2961
- }).strict();
2962
- const CandidateSlotClosedSchema = z.object({
2963
- ...EventBaseShape,
2964
- kind: z.literal("candidate-slot-closed"),
2965
- slotId: NON_EMPTY,
2966
- generationOperationId: NON_EMPTY,
2967
- reason: FailureReasonSchema
2968
- }).strict();
2969
3459
  const KnownTokensSchema = z.object({
2970
3460
  status: z.literal("known"),
2971
3461
  inputTokens: NON_NEGATIVE_INT,
2972
3462
  outputTokens: NON_NEGATIVE_INT,
2973
3463
  cachedTokens: NON_NEGATIVE_INT
2974
3464
  }).strict();
2975
- const UnknownSchema = z.object({
3465
+ const UnknownTokensSchema = z.object({
2976
3466
  status: z.literal("unknown"),
2977
3467
  reason: NON_EMPTY
2978
3468
  }).strict();
2979
3469
  const KnownCostSchema = z.object({
2980
3470
  status: z.literal("known"),
2981
- usd: z.number().finite().nonnegative(),
3471
+ usd: USD,
2982
3472
  source: z.enum([
2983
3473
  "provider",
2984
3474
  "pricing-table",
@@ -2992,11 +3482,11 @@ const KnownCostSchema = z.object({
2992
3482
  });
2993
3483
  const UnknownCostSchema = z.object({
2994
3484
  status: z.literal("unknown"),
2995
- knownLowerBoundUsd: z.number().finite().nonnegative(),
3485
+ knownLowerBoundUsd: USD,
2996
3486
  reason: NON_EMPTY
2997
3487
  }).strict();
2998
3488
  const AccountingSchema = z.object({
2999
- tokens: z.discriminatedUnion("status", [KnownTokensSchema, UnknownSchema]),
3489
+ tokens: z.discriminatedUnion("status", [KnownTokensSchema, UnknownTokensSchema]),
3000
3490
  cost: z.discriminatedUnion("status", [KnownCostSchema, UnknownCostSchema])
3001
3491
  }).strict();
3002
3492
  const MetricsSchema = z.record(NON_EMPTY, FINITE_NUMBER).superRefine((metrics, ctx) => {
@@ -3047,52 +3537,85 @@ const SurfaceEvidenceSchema = z.object({
3047
3537
  effect: EffectSchema,
3048
3538
  evidence: z.array(ArtifactRefSchema).min(1)
3049
3539
  }).strict().superRefine((evidence, ctx) => {
3050
- if (evidence.fired && evidence.firingCount === 0) ctx.addIssue({
3051
- code: "custom",
3052
- message: "a fired surface must have firingCount >= 1"
3053
- });
3054
- if (!evidence.fired && evidence.firingCount !== 0) ctx.addIssue({
3540
+ if (evidence.fired !== evidence.firingCount > 0) ctx.addIssue({
3055
3541
  code: "custom",
3056
- message: "a surface that did not fire must have firingCount 0"
3542
+ message: "fired must equal firingCount > 0"
3057
3543
  });
3058
3544
  if (!evidence.fired && evidence.effect.status === "measured" && evidence.effect.delta !== 0) ctx.addIssue({
3059
3545
  code: "custom",
3060
- message: "a surface that did not fire cannot claim non-zero effect"
3546
+ message: "a surface that did not fire has no effect"
3061
3547
  });
3062
3548
  });
3063
- const TaskAttemptedSchema = z.object({
3549
+ const TaskSchema = z.object({
3550
+ taskId: NON_EMPTY,
3551
+ unitId: NON_EMPTY,
3552
+ source: SourceRefSchema
3553
+ }).strict();
3554
+ const SplitTasksSchema = z.object({
3555
+ taskSetDigest: HASH,
3556
+ tasks: z.array(TaskSchema)
3557
+ }).strict();
3558
+ const SearchOpenedSchema = z.object({
3064
3559
  ...EventBaseShape,
3065
- kind: z.literal("task-attempted"),
3066
- candidateId: NON_EMPTY,
3067
- runId: NON_EMPTY,
3068
- attemptIndex: NON_NEGATIVE_INT,
3069
- task: z.object({
3070
- taskId: NON_EMPTY,
3071
- source: SourceRefSchema
3560
+ kind: z.literal("search-opened"),
3561
+ subject: NON_EMPTY,
3562
+ process: z.object({
3563
+ name: NON_EMPTY,
3564
+ executionRef: SourceRefSchema
3072
3565
  }).strict(),
3073
- identity: z.object({
3074
- model: z.object({
3075
- provider: NON_EMPTY,
3076
- snapshot: NON_EMPTY.refine(modelHasSnapshot, "model must include an immutable snapshot")
3077
- }).strict(),
3078
- agent: SourceRefSchema,
3079
- benchmark: SourceRefSchema
3566
+ artifactKind: z.enum([...SURFACE_KINDS, "output"]),
3567
+ objective: z.object({
3568
+ metric: NON_EMPTY,
3569
+ direction: z.enum(["maximize", "minimize"]),
3570
+ judge: z.union([SourceRefSchema, UnknownRefSchema]),
3571
+ claim: evaluationClaimSchema
3080
3572
  }).strict(),
3081
- outcome: OutcomeSchema,
3082
- accounting: AccountingSchema,
3083
- surfaceEvidence: z.array(SurfaceEvidenceSchema).min(1)
3573
+ splits: z.object({
3574
+ train: SplitTasksSchema,
3575
+ selection: SplitTasksSchema,
3576
+ test: SplitTasksSchema,
3577
+ heldOutUnits: z.boolean()
3578
+ }).strict(),
3579
+ policy: z.object({
3580
+ expansion: NON_EMPTY,
3581
+ allocation: NON_EMPTY,
3582
+ seed: z.number().int().safe()
3583
+ }).strict(),
3584
+ budget: z.object({
3585
+ maxUsd: USD.nullable(),
3586
+ maxCells: POSITIVE_INT.nullable(),
3587
+ maxNodes: POSITIVE_INT.nullable(),
3588
+ deadline: ISO_TIMESTAMP.nullable(),
3589
+ maxConcurrency: POSITIVE_INT.nullable(),
3590
+ reservedClaimUsd: USD
3591
+ }).strict(),
3592
+ containment: z.object({
3593
+ searchId: NON_EMPTY,
3594
+ cellId: CELL_ID,
3595
+ attempt: POSITIVE_INT
3596
+ }).strict().nullable(),
3597
+ derivedFrom: z.object({
3598
+ searchId: NON_EMPTY,
3599
+ nodeId: NODE_ID,
3600
+ headHash: HASH
3601
+ }).strict().nullable(),
3602
+ identity: ExecutionIdentitySchema
3084
3603
  }).strict();
3085
- const SearchOperationRecordedSchema = z.object({
3604
+ const OperationStartedSchema = z.object({
3086
3605
  ...EventBaseShape,
3087
- kind: z.literal("search-operation-recorded"),
3606
+ kind: z.literal("operation-started"),
3607
+ operationId: NON_EMPTY,
3608
+ operationKind: OperationKindSchema,
3609
+ reservation: ReservationSchema.nullable()
3610
+ }).strict();
3611
+ const OperationRecordedSchema = z.object({
3612
+ ...EventBaseShape,
3613
+ kind: z.literal("operation-recorded"),
3088
3614
  operationId: NON_EMPTY,
3089
3615
  operationKind: OperationKindSchema,
3090
3616
  execution: z.discriminatedUnion("kind", [z.object({
3091
3617
  kind: z.literal("model"),
3092
- model: z.object({
3093
- provider: NON_EMPTY,
3094
- snapshot: NON_EMPTY.refine(modelHasSnapshot, "model must include an immutable snapshot")
3095
- }).strict(),
3618
+ model: ModelIdentitySchema,
3096
3619
  source: SourceRefSchema
3097
3620
  }).strict(), z.object({
3098
3621
  kind: z.literal("deterministic"),
@@ -3111,95 +3634,257 @@ const SearchOperationRecordedSchema = z.object({
3111
3634
  ]),
3112
3635
  accounting: AccountingSchema
3113
3636
  }).strict();
3114
- const CandidateDecidedSchema = z.object({
3637
+ const NodeRegisteredSchema = z.object({
3115
3638
  ...EventBaseShape,
3116
- kind: z.literal("candidate-decided"),
3117
- candidateId: NON_EMPTY,
3118
- decision: z.discriminatedUnion("status", [z.object({ status: z.literal("selected") }).strict(), z.object({
3119
- status: z.literal("rejected"),
3120
- reason: FailureReasonSchema
3121
- }).strict()])
3639
+ kind: z.literal("node-registered"),
3640
+ nodeId: NODE_ID,
3641
+ artifactDigest: HASH,
3642
+ artifact: ArtifactRefSchema,
3643
+ surfaces: z.array(z.object({
3644
+ surfaceId: NON_EMPTY,
3645
+ kind: z.enum(SURFACE_KINDS),
3646
+ artifact: ArtifactRefSchema
3647
+ }).strict())
3648
+ }).strict();
3649
+ const NodeRefSchema = z.object({
3650
+ searchId: NON_EMPTY,
3651
+ nodeId: NODE_ID
3122
3652
  }).strict();
3123
- const SearchCompletedSchema = z.object({
3653
+ const ArtifactOrUnknownSchema = z.union([ArtifactRefSchema, UnknownRefSchema]);
3654
+ const EdgeRecordedSchema = z.object({
3124
3655
  ...EventBaseShape,
3125
- kind: z.literal("search-completed"),
3126
- result: z.discriminatedUnion("status", [z.object({
3127
- status: z.literal("selected"),
3128
- candidateId: NON_EMPTY
3129
- }).strict(), z.object({
3130
- status: z.literal("all-rejected"),
3131
- reason: FailureReasonSchema
3132
- }).strict()])
3656
+ kind: z.literal("edge-recorded"),
3657
+ edgeId: z.string().regex(/^edge_[a-f0-9]{32}$/),
3658
+ childNodeId: NODE_ID,
3659
+ parents: z.array(NodeRefSchema),
3660
+ operator: z.enum([
3661
+ "seed",
3662
+ "draft",
3663
+ "improve",
3664
+ "debug",
3665
+ "merge",
3666
+ "derive"
3667
+ ]),
3668
+ attribution: z.enum([
3669
+ "explicit",
3670
+ "correlated",
3671
+ "unknown"
3672
+ ]),
3673
+ proposer: z.object({
3674
+ kind: z.enum([
3675
+ "trace",
3676
+ "frontier-author",
3677
+ "human",
3678
+ "optimizer",
3679
+ "compound"
3680
+ ]),
3681
+ name: NON_EMPTY,
3682
+ operationId: NON_EMPTY.nullable(),
3683
+ source: SourceRefSchema
3684
+ }).strict().nullable(),
3685
+ selection: z.object({
3686
+ rule: NON_EMPTY,
3687
+ evidence: z.record(NON_EMPTY, FINITE_NUMBER)
3688
+ }).strict().nullable(),
3689
+ rationale: ArtifactOrUnknownSchema,
3690
+ diffs: z.array(ArtifactOrUnknownSchema),
3691
+ label: z.string()
3692
+ }).strict();
3693
+ const CellAllocatedSchema = z.object({
3694
+ ...EventBaseShape,
3695
+ kind: z.literal("cell-allocated"),
3696
+ cellId: CELL_ID,
3697
+ nodeId: NODE_ID,
3698
+ taskId: NON_EMPTY,
3699
+ unitId: NON_EMPTY,
3700
+ split: SPLIT,
3701
+ rep: NON_NEGATIVE_INT,
3702
+ stage: z.enum([
3703
+ "root",
3704
+ "train",
3705
+ "screen",
3706
+ "rung",
3707
+ "claim",
3708
+ "external"
3709
+ ]),
3710
+ lane: NON_EMPTY.nullable(),
3711
+ reservation: ReservationSchema.nullable()
3712
+ }).strict();
3713
+ const TraceRefSchema = z.union([z.object({
3714
+ traceId: NON_EMPTY,
3715
+ execRunId: NON_EMPTY.nullable(),
3716
+ spansWritten: NON_NEGATIVE_INT.nullable(),
3717
+ spansDropped: NON_NEGATIVE_INT.nullable()
3718
+ }).strict(), UnknownRefSchema]);
3719
+ const NON_NEGATIVE = z.number().finite().nonnegative();
3720
+ const CellSettledSchema = z.object({
3721
+ ...EventBaseShape,
3722
+ kind: z.literal("cell-settled"),
3723
+ cellId: CELL_ID,
3724
+ attempt: POSITIVE_INT,
3725
+ runId: NON_EMPTY,
3726
+ outcome: OutcomeSchema,
3727
+ accounting: AccountingSchema,
3728
+ boxMinutes: NON_NEGATIVE.nullable(),
3729
+ wallMs: NON_NEGATIVE.nullable(),
3730
+ queueMs: NON_NEGATIVE.nullable(),
3731
+ placement: z.object({
3732
+ lane: NON_EMPTY,
3733
+ boxId: NON_EMPTY.nullable()
3734
+ }).strict().nullable(),
3735
+ identity: ExecutionIdentitySchema,
3736
+ surfaceEvidence: z.array(SurfaceEvidenceSchema),
3737
+ traceRef: TraceRefSchema
3738
+ }).strict();
3739
+ const CellCancelledSchema = z.object({
3740
+ ...EventBaseShape,
3741
+ kind: z.literal("cell-cancelled"),
3742
+ cellId: CELL_ID,
3743
+ reason: z.enum([
3744
+ "pruned",
3745
+ "budget",
3746
+ "deadline",
3747
+ "aborted"
3748
+ ])
3749
+ }).strict();
3750
+ const NodeEstimateSchema = z.object({
3751
+ against: NODE_ID,
3752
+ split: SPLIT,
3753
+ units: NON_NEGATIVE_INT,
3754
+ pairs: NON_NEGATIVE_INT,
3755
+ delta: FINITE_NUMBER.nullable(),
3756
+ interval: z.tuple([FINITE_NUMBER, FINITE_NUMBER]).nullable(),
3757
+ method: z.enum([
3758
+ "none",
3759
+ "insufficient",
3760
+ "descriptive",
3761
+ "bootstrap"
3762
+ ]),
3763
+ exactSignP: z.number().min(0).max(1).nullable(),
3764
+ indeterminate: z.boolean(),
3765
+ cellSetDigest: HASH,
3766
+ estimator: SourceRefSchema
3767
+ }).strict();
3768
+ const NodeDecidedSchema = z.object({
3769
+ ...EventBaseShape,
3770
+ kind: z.literal("node-decided"),
3771
+ nodeId: NODE_ID,
3772
+ decision: z.discriminatedUnion("status", [
3773
+ z.object({
3774
+ status: z.literal("advanced"),
3775
+ rung: NON_NEGATIVE_INT
3776
+ }).strict(),
3777
+ z.object({ status: z.literal("pruned") }).strict(),
3778
+ z.object({ status: z.literal("invalid") }).strict(),
3779
+ z.object({ status: z.literal("finalist") }).strict(),
3780
+ z.object({ status: z.literal("selected") }).strict(),
3781
+ z.object({ status: z.literal("rejected") }).strict()
3782
+ ]),
3783
+ basis: NodeEstimateSchema.nullable(),
3784
+ rule: NON_EMPTY,
3785
+ reason: NON_EMPTY
3786
+ }).strict();
3787
+ const SearchClosedSchema = z.object({
3788
+ ...EventBaseShape,
3789
+ kind: z.literal("search-closed"),
3790
+ reason: z.enum([
3791
+ "budget",
3792
+ "deadline",
3793
+ "max-nodes",
3794
+ "patience",
3795
+ "converged",
3796
+ "aborted"
3797
+ ]),
3798
+ claim: z.object({
3799
+ power: z.union([z.object({
3800
+ adequate: z.boolean(),
3801
+ minimumEffect: z.number().finite().positive(),
3802
+ powerAtMinimumEffect: z.number().min(0).max(1),
3803
+ units: NON_NEGATIVE_INT
3804
+ }).strict(), UnknownRefSchema]),
3805
+ finalists: z.array(z.object({
3806
+ nodeId: NODE_ID,
3807
+ estimate: NodeEstimateSchema.nullable(),
3808
+ promote: z.boolean()
3809
+ }).strict()),
3810
+ selected: NODE_ID.nullable(),
3811
+ decision: z.enum([
3812
+ "ship",
3813
+ "hold",
3814
+ "test-cannot-resolve"
3815
+ ])
3816
+ }).strict().nullable()
3133
3817
  }).strict();
3134
3818
  const EventSchema = z.discriminatedUnion("kind", [
3135
- SearchPlannedSchema,
3136
- SearchPlanExtendedSchema,
3137
- CandidateRegisteredSchema,
3138
- CandidateSlotClosedSchema,
3139
- TaskAttemptedSchema,
3140
- SearchOperationRecordedSchema,
3141
- CandidateDecidedSchema,
3142
- SearchCompletedSchema
3819
+ SearchOpenedSchema,
3820
+ OperationStartedSchema,
3821
+ OperationRecordedSchema,
3822
+ NodeRegisteredSchema,
3823
+ EdgeRecordedSchema,
3824
+ CellAllocatedSchema,
3825
+ CellSettledSchema,
3826
+ CellCancelledSchema,
3827
+ NodeDecidedSchema,
3828
+ SearchClosedSchema
3143
3829
  ]);
3144
3830
  const EntrySchema = z.object({
3145
3831
  schema: z.literal(SEARCH_LEDGER_SCHEMA),
3146
- campaignId: NON_EMPTY,
3832
+ searchId: NON_EMPTY,
3147
3833
  sequence: NON_NEGATIVE_INT,
3148
3834
  previousHash: z.union([HASH, z.null()]),
3149
3835
  event: EventSchema,
3150
3836
  entryHash: HASH
3151
3837
  }).strict();
3152
- /** Validate and return a canonical copy. Arrays whose order is not semantic are
3153
- * sorted so retries from different processes produce byte-identical events. */
3838
+ /** Validate and return a canonical copy. Arrays whose order carries no meaning
3839
+ * are sorted, so retries from different processes produce identical bytes. */
3154
3840
  function validateSearchLedgerEvent(input) {
3155
3841
  const parsed = EventSchema.safeParse(input);
3156
3842
  if (!parsed.success) throw new SearchLedgerError(`invalid search ledger event: ${formatZodError(parsed.error)}`);
3157
3843
  return normalizeEvent(parsed.data);
3158
3844
  }
3159
3845
  /** Open a durable filesystem search ledger. Construction performs no I/O; the
3160
- * first `append` or `replay` validates the complete existing file. */
3846
+ * first `append` or `state` verifies the whole existing file. */
3161
3847
  function openSearchLedger(options) {
3162
- if (options.path.trim().length === 0) throw new SearchLedgerError("ledger path is empty");
3163
- return new FileSearchLedger(options.path, options.campaignId, options.trustedHead);
3848
+ return new FileSearchLedger(options.path, options.searchId, options.trustedHead);
3164
3849
  }
3165
- /** Replay immutable search-ledger JSONL through the same codec as FileSearchLedger. */
3166
- function replaySearchLedgerText(text, campaignId, source) {
3167
- return replayLedgerText(text, source, searchLedgerCodec(campaignId)).projection;
3850
+ /** Verify and project immutable search-ledger JSONL through the same codec as
3851
+ * `FileSearchLedger`. */
3852
+ function replaySearchLedgerText(text, searchId, source) {
3853
+ return replayLedgerText(text, source, searchLedgerCodec(searchId));
3168
3854
  }
3169
- function searchLedgerCodec(campaignId) {
3855
+ function searchLedgerCodec(searchId) {
3170
3856
  return {
3171
3857
  ...SEARCH_LEDGER_FILE_CONTEXT,
3172
3858
  header: {
3173
3859
  schema: SEARCH_LEDGER_SCHEMA,
3174
- campaignId
3860
+ searchId
3175
3861
  },
3176
3862
  conflictError: (message) => new SearchLedgerConflictError(message),
3177
3863
  parseEntry: parseSearchLedgerEntry,
3178
3864
  checkEntryHeader: (entry, index) => {
3179
- if (entry.campaignId !== campaignId) throw new SearchLedgerIntegrityError(`entry ${index} belongs to campaign ${entry.campaignId}, expected ${campaignId}`);
3865
+ if (entry.searchId !== searchId) throw new SearchLedgerIntegrityError(`entry ${index} belongs to search ${entry.searchId}, expected ${searchId}`);
3180
3866
  },
3181
- createProjector: () => createSearchLedgerProjector(campaignId)
3867
+ createProjector: () => new SearchState(searchId)
3182
3868
  };
3183
3869
  }
3184
- /** Append-only file-backed search ledger with idempotent writes and replay. */
3870
+ /** Append-only file-backed search ledger with idempotent writes. */
3185
3871
  var FileSearchLedger = class {
3186
3872
  path;
3187
- campaignId;
3873
+ searchId;
3188
3874
  trustedHeadPath;
3189
3875
  trustedHeadMode;
3190
3876
  journal;
3191
- constructor(path, campaignId, trustedHead = "pin") {
3877
+ constructor(path, searchId, trustedHead = "pin") {
3192
3878
  if (path.trim().length === 0) throw new SearchLedgerError("ledger path is empty");
3193
- if (campaignId.length === 0) throw new SearchLedgerError("campaignId is empty");
3194
- if (campaignId.trim() !== campaignId) throw new SearchLedgerError("campaignId must not contain surrounding whitespace");
3195
- this.campaignId = campaignId;
3879
+ if (searchId.length === 0 || searchId.trim() !== searchId) throw new SearchLedgerError("searchId must be non-empty without surrounding whitespace");
3880
+ this.searchId = searchId;
3196
3881
  this.trustedHeadMode = trustedHead;
3197
- this.journal = new FileLedgerJournal(path, searchLedgerCodec(campaignId), { requireTrustedHead: trustedHead === "require" });
3882
+ this.journal = new FileLedgerJournal(path, searchLedgerCodec(searchId), { requireTrustedHead: trustedHead === "require" });
3198
3883
  this.path = this.journal.path;
3199
3884
  this.trustedHeadPath = this.journal.trustedHeadPath;
3200
3885
  }
3201
- async replay() {
3202
- return (await this.journal.replay()).projection;
3886
+ async state() {
3887
+ return this.journal.replay();
3203
3888
  }
3204
3889
  async append(input) {
3205
3890
  const event = validateSearchLedgerEvent(input);
@@ -3207,7 +3892,7 @@ var FileSearchLedger = class {
3207
3892
  return {
3208
3893
  entry,
3209
3894
  appended,
3210
- replay: projection
3895
+ state: projection
3211
3896
  };
3212
3897
  }
3213
3898
  async trustedHead() {
@@ -3221,6 +3906,8 @@ var FileSearchLedger = class {
3221
3906
  }
3222
3907
  };
3223
3908
  function parseSearchLedgerEntry(raw, context) {
3909
+ const schema = raw?.schema;
3910
+ if (schema !== "tangle.search-ledger.2026-09") throw new SearchLedgerIntegrityError(`search ledger ${context.path} line ${context.line} was written under schema ${JSON.stringify(schema)}; this version reads only ${SEARCH_LEDGER_SCHEMA} and does not translate other formats`);
3224
3911
  const parsed = EntrySchema.safeParse(raw);
3225
3912
  if (!parsed.success) throw new SearchLedgerIntegrityError(`search ledger ${context.path} has a malformed entry at line ${context.line}: ${formatZodError(parsed.error)}`);
3226
3913
  const entry = parsed.data;
@@ -3229,74 +3916,82 @@ function parseSearchLedgerEntry(raw, context) {
3229
3916
  }
3230
3917
  function normalizeEvent(event) {
3231
3918
  const artifacts = sortArtifacts(event.artifacts);
3232
- if (event.kind === "search-planned") return {
3233
- ...event,
3234
- artifacts,
3235
- plan: {
3236
- candidateSlots: [...event.plan.candidateSlots].sort((a, b) => compareStrings(a.slotId, b.slotId)),
3237
- tasks: [...event.plan.tasks].sort((a, b) => compareStrings(a.taskId, b.taskId)),
3238
- operations: [...event.plan.operations].sort((a, b) => compareStrings(a.operationId, b.operationId))
3239
- }
3240
- };
3241
- if (event.kind === "search-plan-extended") return {
3242
- ...event,
3243
- artifacts,
3244
- extension: {
3245
- candidateSlots: [...event.extension.candidateSlots].sort((a, b) => compareStrings(a.slotId, b.slotId)),
3246
- operations: [...event.extension.operations].sort((a, b) => compareStrings(a.operationId, b.operationId))
3247
- }
3248
- };
3249
- if (event.kind === "candidate-registered") return {
3250
- ...event,
3251
- artifacts,
3252
- lineage: {
3253
- ...event.lineage,
3254
- parentCandidateIds: sortedStrings(event.lineage.parentCandidateIds)
3255
- },
3256
- surfaces: [...event.surfaces].map((surface) => ({
3257
- ...surface,
3258
- artifact: { ...surface.artifact }
3259
- })).sort((a, b) => compareStrings(a.surfaceId, b.surfaceId))
3260
- };
3261
- if (event.kind === "task-attempted") return {
3262
- ...event,
3263
- artifacts,
3264
- surfaceEvidence: [...event.surfaceEvidence].map((evidence) => ({
3265
- ...evidence,
3266
- evidence: sortArtifacts(evidence.evidence)
3267
- })).sort((a, b) => compareStrings(a.surfaceId, b.surfaceId))
3268
- };
3269
- return {
3270
- ...event,
3271
- artifacts
3272
- };
3919
+ switch (event.kind) {
3920
+ case "search-opened": return {
3921
+ ...event,
3922
+ artifacts,
3923
+ splits: {
3924
+ ...event.splits,
3925
+ train: {
3926
+ ...event.splits.train,
3927
+ tasks: sortTasks(event.splits.train.tasks)
3928
+ },
3929
+ selection: {
3930
+ ...event.splits.selection,
3931
+ tasks: sortTasks(event.splits.selection.tasks)
3932
+ },
3933
+ test: {
3934
+ ...event.splits.test,
3935
+ tasks: sortTasks(event.splits.test.tasks)
3936
+ }
3937
+ }
3938
+ };
3939
+ case "node-registered": return {
3940
+ ...event,
3941
+ artifacts,
3942
+ surfaces: [...event.surfaces].sort((a, b) => compareStrings(a.surfaceId, b.surfaceId))
3943
+ };
3944
+ case "cell-settled": return {
3945
+ ...event,
3946
+ artifacts,
3947
+ surfaceEvidence: [...event.surfaceEvidence].map((evidence) => ({
3948
+ ...evidence,
3949
+ evidence: sortArtifacts(evidence.evidence)
3950
+ })).sort((a, b) => compareStrings(a.surfaceId, b.surfaceId))
3951
+ };
3952
+ default: return {
3953
+ ...event,
3954
+ artifacts
3955
+ };
3956
+ }
3273
3957
  }
3274
- function sortArtifacts(artifacts) {
3275
- return [...artifacts].map((artifact) => ({ ...artifact })).sort((a, b) => compareStrings(artifactKey(a), artifactKey(b)));
3958
+ function sortTasks(tasks) {
3959
+ return [...tasks].sort((a, b) => compareStrings(a.taskId, b.taskId));
3276
3960
  }
3277
- function sortedStrings(values) {
3278
- return [...values].sort();
3961
+ function sortArtifacts(artifacts) {
3962
+ return [...artifacts].sort((a, b) => compareStrings(artifactKey(a), artifactKey(b)));
3279
3963
  }
3280
3964
  function formatZodError(error) {
3281
3965
  return error.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`).join("; ");
3282
3966
  }
3283
3967
  //#endregion
3284
3968
  //#region src/campaign/search-history-receipt.ts
3285
- const SEARCH_HISTORY_RECEIPT_SCHEMA_VERSION = "1.0.0";
3969
+ const SEARCH_HISTORY_RECEIPT_SCHEMA_VERSION = "2026-09";
3286
3970
  const SEARCH_HISTORY_RECEIPT_DIGEST_ALGORITHM = "rfc8785-sha256";
3287
3971
  function assertSearchHistoryAdmissionOptions(options) {
3288
3972
  if (options.searchHistoryPolicy !== void 0 && !["allow-missing", "require-complete"].includes(options.searchHistoryPolicy)) throw new Error(`unknown searchHistoryPolicy '${String(options.searchHistoryPolicy)}'`);
3289
3973
  if (options.searchHistoryVerification !== void 0 && !["receipt", "ledger"].includes(options.searchHistoryVerification)) throw new Error(`unknown searchHistoryVerification '${String(options.searchHistoryVerification)}'`);
3290
3974
  }
3291
- /** Verify resolved bytes with the canonical codec, never a caller-supplied replay projection. */
3975
+ /** Read the ledger's bytes, replay them through the search codec, and build
3976
+ * the receipt from that replay. */
3977
+ function createSearchHistoryReceipt(input) {
3978
+ const producerId = nonEmpty(input.producerId, "search history producerId");
3979
+ const runId = nonEmpty(input.runId, "search history runId");
3980
+ const text = (input.storage ?? fsCampaignStorage()).read(input.ledger.path) ?? "";
3981
+ const state = replaySearchLedgerText(text, input.ledger.searchId, input.ledger.path);
3982
+ return receiptFor(producerId, runId, ledgerArtifact(input.ledger.path, text), state);
3983
+ }
3984
+ /** Verify the receipt, then resolve its ledger bytes, check them, and replay
3985
+ * them with the canonical codec. Never trusts a caller-supplied projection. */
3292
3986
  function verifySearchHistoryArtifact(receipt, storage) {
3293
3987
  verifySearchHistoryReceipt(receipt);
3294
3988
  const path = receipt.ledger.uri.startsWith("file:") ? fileURLToPath(receipt.ledger.uri) : receipt.ledger.uri;
3295
3989
  const text = storage.read(path);
3296
3990
  if (text === void 0) throw new Error(`search history ledger is missing or unreadable at '${path}'`);
3297
- if (new TextEncoder().encode(text).byteLength !== receipt.ledger.byteLength) throw new Error("search history ledger byte length mismatch");
3298
- if (hashCanonical(text) !== receipt.ledger.sha256) throw new Error("search history ledger digest mismatch");
3299
- assertSearchHistoryMatchesReplay(receipt, replaySearchLedgerText(text, receipt.summary.campaignId, path));
3991
+ const artifact = ledgerArtifact(path, text);
3992
+ if (artifact.byteLength !== receipt.ledger.byteLength) throw new Error("search history ledger byte length mismatch");
3993
+ if (artifact.sha256 !== receipt.ledger.sha256) throw new Error("search history ledger digest mismatch");
3994
+ assertSearchHistoryMatchesState(receipt, replaySearchLedgerText(text, receipt.summary.searchId, path));
3300
3995
  }
3301
3996
  var SearchHistoryRequiredError = class extends Error {
3302
3997
  producerId;
@@ -3309,73 +4004,25 @@ var SearchHistoryRequiredError = class extends Error {
3309
4004
  this.reasons = Object.freeze([...normalized]);
3310
4005
  }
3311
4006
  };
3312
- /** Build a bounded receipt from the projection returned by canonical ledger replay. */
3313
- function createSearchHistoryReceipt(input) {
3314
- if (!input || typeof input !== "object") throw new TypeError("search history input is required");
3315
- const producerId = nonEmpty(input.producerId, "search history producerId");
3316
- const runId = nonEmpty(input.runId, "search history runId");
3317
- const ledger = normalizeArtifactRef(input.ledger);
3318
- const replay = validateReplayProjection(input.replay);
3319
- const auditDigest = ledgerHash(hashCanonical(replay.audit), "search history auditDigest");
3320
- const summary = summarizeReplay(replay);
3321
- const incompleteReasons = Object.freeze(historyIncompleteReasons(summary));
3322
- const material = Object.freeze({
3323
- schemaVersion: SEARCH_HISTORY_RECEIPT_SCHEMA_VERSION,
3324
- kind: "search-history-receipt",
3325
- digestAlgorithm: SEARCH_HISTORY_RECEIPT_DIGEST_ALGORITHM,
3326
- producerId,
3327
- runId,
3328
- ledger,
3329
- auditDigest,
3330
- summary,
3331
- complete: incompleteReasons.length === 0,
3332
- incompleteReasons
3333
- });
3334
- return Object.freeze({
3335
- ...material,
3336
- receiptDigest: hashCanonical(material)
3337
- });
3338
- }
3339
- /** Verify the bounded receipt. Full ledger bytes are verified by SearchLedger. */
4007
+ /** Verify a receipt's own consistency and digest. */
3340
4008
  function verifySearchHistoryReceipt(receipt) {
3341
4009
  if (!receipt || typeof receipt !== "object") throw new TypeError("search history receipt is required");
3342
4010
  if (receipt.schemaVersion !== SEARCH_HISTORY_RECEIPT_SCHEMA_VERSION) throw new Error(`unsupported search history schemaVersion '${receipt.schemaVersion}'`);
3343
4011
  if (receipt.kind !== "search-history-receipt") throw new Error(`search history kind must be 'search-history-receipt'`);
3344
4012
  if (receipt.digestAlgorithm !== SEARCH_HISTORY_RECEIPT_DIGEST_ALGORITHM) throw new Error(`unsupported search history digestAlgorithm '${receipt.digestAlgorithm}'`);
3345
- const producerId = nonEmpty(receipt.producerId, "search history producerId");
3346
- const runId = nonEmpty(receipt.runId, "search history runId");
3347
- const ledger = normalizeArtifactRef(receipt.ledger);
3348
- const auditDigest = ledgerHash(receipt.auditDigest, "search history auditDigest");
3349
4013
  const summary = normalizeSummary(receipt.summary);
3350
- const incompleteReasons = normalizeReasons(receipt.incompleteReasons);
3351
- const expectedReasons = historyIncompleteReasons(summary);
3352
- if (!sameStrings(incompleteReasons, expectedReasons)) throw new Error("search history incompleteReasons disagree with its summary");
3353
- if (receipt.complete !== (expectedReasons.length === 0)) throw new Error("search history complete disagrees with its summary");
3354
- if (hashCanonical({
3355
- schemaVersion: receipt.schemaVersion,
3356
- kind: receipt.kind,
3357
- digestAlgorithm: receipt.digestAlgorithm,
3358
- producerId,
3359
- runId,
3360
- ledger,
3361
- auditDigest,
3362
- summary,
3363
- complete: receipt.complete,
3364
- incompleteReasons
3365
- }) !== receipt.receiptDigest) throw new Error("search history receipt digest mismatch");
4014
+ if (receipt.complete !== (summary.status === "closed")) throw new Error("search history complete disagrees with its summary");
4015
+ if (!Array.isArray(receipt.incompleteReasons) || receipt.complete !== (receipt.incompleteReasons.length === 0)) throw new Error("search history incompleteReasons disagree with complete");
4016
+ const { receiptDigest, ...material } = receipt;
4017
+ if (hashCanonical(material) !== receiptDigest) throw new Error("search history receipt digest mismatch");
3366
4018
  return receipt;
3367
4019
  }
3368
- /** Prove that a receipt still describes the exact canonical replay supplied. */
3369
- function assertSearchHistoryMatchesReplay(receipt, replay) {
4020
+ /** Prove that a receipt describes exactly the supplied replayed state. */
4021
+ function assertSearchHistoryMatchesState(receipt, state) {
3370
4022
  verifySearchHistoryReceipt(receipt);
3371
- if (createSearchHistoryReceipt({
3372
- producerId: receipt.producerId,
3373
- runId: receipt.runId,
3374
- ledger: receipt.ledger,
3375
- replay
3376
- }).receiptDigest !== receipt.receiptDigest) throw new Error("search history receipt does not match the supplied SearchLedger replay");
3377
- }
3378
- /** Require a receipt owned by this producer and a terminal, denominator-complete history. */
4023
+ if (receiptFor(receipt.producerId, receipt.runId, receipt.ledger, state).receiptDigest !== receipt.receiptDigest) throw new Error("search history receipt does not match the supplied search ledger state");
4024
+ }
4025
+ /** Require a receipt owned by this producer over a closed search. */
3379
4026
  function assertCompleteSearchHistory(producerId, receipt) {
3380
4027
  if (receipt === void 0) throw new SearchHistoryRequiredError(producerId, []);
3381
4028
  verifySearchHistoryReceipt(receipt);
@@ -3398,590 +4045,442 @@ function searchHistoryCoverageRow(producerId, receipt) {
3398
4045
  receipt
3399
4046
  });
3400
4047
  }
3401
- function validateReplayProjection(replay) {
3402
- if (!replay || typeof replay !== "object") throw new TypeError("search history replay is required");
3403
- if (!Array.isArray(replay.entries)) throw new TypeError("search history replay.entries must be an array");
3404
- if (!replay.audit || typeof replay.audit !== "object") throw new TypeError("search history replay.audit is required");
3405
- if (replay.plan !== null && (!replay.plan || typeof replay.plan !== "object")) throw new TypeError("search history replay.plan must be an event or null");
3406
- if (replay.completion !== null && (!replay.completion || typeof replay.completion !== "object")) throw new TypeError("search history replay.completion must be an event or null");
3407
- if (replay.audit.eventCount !== replay.entries.length) throw new Error(`search history audit eventCount ${replay.audit.eventCount} does not match ${replay.entries.length} entries`);
3408
- const observedHead = replay.entries.at(-1)?.entryHash ?? null;
3409
- if (replay.audit.headHash !== observedHead) throw new Error("search history audit headHash does not match the final ledger entry");
3410
- return replay;
3411
- }
3412
- function summarizeReplay(replay) {
3413
- const audit = replay.audit;
3414
- return normalizeSummary({
3415
- campaignId: audit.campaignId,
4048
+ function receiptFor(producerId, runId, ledger, state) {
4049
+ const { audit } = state;
4050
+ const summary = normalizeSummary({
4051
+ searchId: audit.searchId,
3416
4052
  headHash: audit.headHash,
3417
4053
  status: audit.status,
3418
- selectedCandidateId: audit.selectedCandidateId,
4054
+ closeReason: audit.closeReason,
4055
+ claimDecision: state.closed?.claim?.decision ?? null,
4056
+ selectedNodeId: audit.selectedNodeId,
3419
4057
  eventCount: audit.eventCount,
3420
- candidateCount: audit.candidateCount,
3421
- closedCandidateSlotCount: audit.closedCandidateSlotCount,
3422
- attemptCount: audit.attemptCount,
3423
- operationCount: audit.operationCount,
3424
- expectedCandidateSlots: audit.expected.candidateSlots,
3425
- expectedTaskOutcomes: audit.expected.taskOutcomes,
3426
- expectedOperations: audit.expected.operations,
3427
- missingCandidateSlots: audit.expected.missingCandidateSlots.length,
3428
- missingTaskOutcomes: audit.expected.missingTaskOutcomes.length,
3429
- missingOperations: audit.expected.missingOperations.length,
3430
- pendingDecisions: audit.decisions.pending,
3431
- hasPlan: replay.plan !== null,
3432
- hasCompletion: replay.completion !== null
4058
+ nodes: audit.nodes,
4059
+ edges: audit.edges.explicit + audit.edges.correlated + audit.edges.unknown,
4060
+ cells: audit.cells.allocated,
4061
+ openCells: audit.cells.open,
4062
+ openOperations: audit.operations.open,
4063
+ nodesWithoutEdge: audit.nodesWithoutEdge,
4064
+ undecidedNodes: audit.undecidedNodes
3433
4065
  });
3434
- }
3435
- function normalizeSummary(value) {
3436
- if (!value || typeof value !== "object") throw new TypeError("search history summary is required");
3437
- const campaignId = nonEmpty(value.campaignId, "search history summary.campaignId");
3438
- const headHash = nullableLedgerHash(value.headHash, "search history summary.headHash");
3439
- const status = searchStatus(value.status);
3440
- const selectedCandidateId = optionalNullableText(value.selectedCandidateId, "search history summary.selectedCandidateId");
3441
- const eventCount = nonNegativeSafeInteger(value.eventCount, "search history summary.eventCount");
3442
- const candidateCount = nonNegativeSafeInteger(value.candidateCount, "search history summary.candidateCount");
3443
- const closedCandidateSlotCount = nonNegativeSafeInteger(value.closedCandidateSlotCount, "search history summary.closedCandidateSlotCount");
3444
- const attemptCount = nonNegativeSafeInteger(value.attemptCount, "search history summary.attemptCount");
3445
- const operationCount = nonNegativeSafeInteger(value.operationCount, "search history summary.operationCount");
3446
- const expectedCandidateSlots = nonNegativeSafeInteger(value.expectedCandidateSlots, "search history summary.expectedCandidateSlots");
3447
- const expectedTaskOutcomes = nonNegativeSafeInteger(value.expectedTaskOutcomes, "search history summary.expectedTaskOutcomes");
3448
- const expectedOperations = nonNegativeSafeInteger(value.expectedOperations, "search history summary.expectedOperations");
3449
- const missingCandidateSlots = nonNegativeSafeInteger(value.missingCandidateSlots, "search history summary.missingCandidateSlots");
3450
- const missingTaskOutcomes = nonNegativeSafeInteger(value.missingTaskOutcomes, "search history summary.missingTaskOutcomes");
3451
- const missingOperations = nonNegativeSafeInteger(value.missingOperations, "search history summary.missingOperations");
3452
- const pendingDecisions = nonNegativeSafeInteger(value.pendingDecisions, "search history summary.pendingDecisions");
3453
- const hasPlan = boolean(value.hasPlan, "search history summary.hasPlan");
3454
- const hasCompletion = boolean(value.hasCompletion, "search history summary.hasCompletion");
3455
- if (missingCandidateSlots > expectedCandidateSlots) throw new Error("search history missingCandidateSlots exceeds expectedCandidateSlots");
3456
- if (missingTaskOutcomes > expectedTaskOutcomes) throw new Error("search history missingTaskOutcomes exceeds expectedTaskOutcomes");
3457
- if (missingOperations > expectedOperations) throw new Error("search history missingOperations exceeds expectedOperations");
3458
- if (pendingDecisions > candidateCount) throw new Error("search history pendingDecisions exceeds candidateCount");
3459
- if (eventCount === 0 !== (headHash === null)) throw new Error("search history eventCount and headHash disagree");
3460
- if (status === "in-progress" === hasCompletion) throw new Error("search history status and hasCompletion disagree");
3461
- if (status === "selected" && selectedCandidateId === null) throw new Error("selected search history requires selectedCandidateId");
3462
- if (status !== "selected" && selectedCandidateId !== null) throw new Error("non-selected search history cannot name selectedCandidateId");
4066
+ const material = {
4067
+ schemaVersion: SEARCH_HISTORY_RECEIPT_SCHEMA_VERSION,
4068
+ kind: "search-history-receipt",
4069
+ digestAlgorithm: SEARCH_HISTORY_RECEIPT_DIGEST_ALGORITHM,
4070
+ producerId,
4071
+ runId,
4072
+ ledger: Object.freeze({ ...ledger }),
4073
+ auditDigest: hashCanonical(audit),
4074
+ summary,
4075
+ complete: state.completion.complete,
4076
+ incompleteReasons: Object.freeze([...state.completion.reasons])
4077
+ };
3463
4078
  return Object.freeze({
3464
- campaignId,
3465
- headHash,
3466
- status,
3467
- selectedCandidateId,
3468
- eventCount,
3469
- candidateCount,
3470
- closedCandidateSlotCount,
3471
- attemptCount,
3472
- operationCount,
3473
- expectedCandidateSlots,
3474
- expectedTaskOutcomes,
3475
- expectedOperations,
3476
- missingCandidateSlots,
3477
- missingTaskOutcomes,
3478
- missingOperations,
3479
- pendingDecisions,
3480
- hasPlan,
3481
- hasCompletion
4079
+ ...material,
4080
+ receiptDigest: hashCanonical(material)
3482
4081
  });
3483
4082
  }
3484
- function historyIncompleteReasons(summary) {
3485
- const reasons = [];
3486
- if (!summary.hasPlan) reasons.push("search plan is missing");
3487
- if (!summary.hasCompletion) reasons.push("terminal search-completed event is missing");
3488
- if (summary.status === "in-progress") reasons.push("search status is in-progress");
3489
- if (summary.missingCandidateSlots > 0) reasons.push(countReason(summary.missingCandidateSlots, "candidate slot", "candidate slots"));
3490
- if (summary.missingTaskOutcomes > 0) reasons.push(countReason(summary.missingTaskOutcomes, "task outcome", "task outcomes"));
3491
- if (summary.missingOperations > 0) reasons.push(countReason(summary.missingOperations, "operation", "operations"));
3492
- if (summary.pendingDecisions > 0) reasons.push(countReason(summary.pendingDecisions, "candidate decision", "candidate decisions", "pending"));
3493
- return reasons;
3494
- }
3495
- function countReason(count, singular, plural, adjective = "unresolved") {
3496
- return `${count} ${count === 1 ? singular : plural} ${count === 1 ? "is" : "are"} ${adjective}`;
3497
- }
3498
- function normalizeArtifactRef(value) {
3499
- if (!value || typeof value !== "object") throw new TypeError("search history ledger artifact is required");
4083
+ function ledgerArtifact(path, text) {
4084
+ const bytes = Buffer.from(text, "utf8");
4085
+ return {
4086
+ role: "search-ledger",
4087
+ uri: pathToFileURL(path).href,
4088
+ sha256: `sha256:${createHash("sha256").update(bytes).digest("hex")}`,
4089
+ byteLength: bytes.byteLength
4090
+ };
4091
+ }
4092
+ function normalizeSummary(value) {
4093
+ if (!value || typeof value !== "object") throw new TypeError("search history summary is required");
4094
+ for (const key of [
4095
+ "eventCount",
4096
+ "nodes",
4097
+ "edges",
4098
+ "cells",
4099
+ "openCells",
4100
+ "openOperations",
4101
+ "nodesWithoutEdge",
4102
+ "undecidedNodes"
4103
+ ]) {
4104
+ const count = value[key];
4105
+ if (!Number.isSafeInteger(count) || count < 0) throw new TypeError(`search history summary.${key} must be a non-negative safe integer`);
4106
+ }
4107
+ if (value.status !== "open" && value.status !== "closed") throw new TypeError(`search history summary.status is invalid: ${String(value.status)}`);
4108
+ if (value.eventCount === 0 !== (value.headHash === null)) throw new Error("search history eventCount and headHash disagree");
4109
+ if (value.status === "closed" !== (value.closeReason !== null)) throw new Error("search history status and closeReason disagree");
4110
+ if (value.status === "closed" && value.openCells + value.openOperations + value.nodesWithoutEdge + value.undecidedNodes > 0) throw new Error("a closed search history cannot have open work");
3500
4111
  return Object.freeze({
3501
- role: nonEmpty(value.role, "search history ledger role"),
3502
- uri: nonEmpty(value.uri, "search history ledger uri"),
3503
- sha256: ledgerHash(value.sha256, "search history ledger sha256"),
3504
- byteLength: nonNegativeSafeInteger(value.byteLength, "search history ledger byteLength")
4112
+ searchId: nonEmpty(value.searchId, "search history summary.searchId"),
4113
+ headHash: value.headHash,
4114
+ status: value.status,
4115
+ closeReason: value.closeReason,
4116
+ claimDecision: value.claimDecision,
4117
+ selectedNodeId: value.selectedNodeId,
4118
+ eventCount: value.eventCount,
4119
+ nodes: value.nodes,
4120
+ edges: value.edges,
4121
+ cells: value.cells,
4122
+ openCells: value.openCells,
4123
+ openOperations: value.openOperations,
4124
+ nodesWithoutEdge: value.nodesWithoutEdge,
4125
+ undecidedNodes: value.undecidedNodes
3505
4126
  });
3506
4127
  }
3507
- function normalizeReasons(value) {
3508
- if (!Array.isArray(value)) throw new TypeError("search history incompleteReasons must be an array");
3509
- return Object.freeze(value.map((reason, index) => nonEmpty(reason, `search history incompleteReasons[${index}]`)));
3510
- }
3511
- function sameStrings(left, right) {
3512
- return left.length === right.length && left.every((value, index) => value === right[index]);
3513
- }
3514
- function searchStatus(value) {
3515
- if (value !== "in-progress" && value !== "selected" && value !== "all-rejected") throw new TypeError(`search history summary.status is invalid: ${String(value)}`);
3516
- return value;
3517
- }
3518
- function nullableLedgerHash(value, label) {
3519
- return value === null ? null : ledgerHash(value, label);
3520
- }
3521
- function optionalNullableText(value, label) {
3522
- return value === null ? null : nonEmpty(value, label);
3523
- }
3524
- function ledgerHash(value, label) {
3525
- if (typeof value !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value)) throw new TypeError(`${label} must be a lowercase sha256 digest`);
3526
- return value;
3527
- }
3528
4128
  function nonEmpty(value, label) {
3529
4129
  if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new TypeError(`${label} must be a trimmed non-empty string`);
3530
4130
  return value;
3531
4131
  }
3532
- function nonNegativeSafeInteger(value, label) {
3533
- if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${label} must be a non-negative safe integer`);
3534
- return value;
3535
- }
3536
- function boolean(value, label) {
3537
- if (typeof value !== "boolean") throw new TypeError(`${label} must be a boolean`);
3538
- return value;
3539
- }
3540
4132
  //#endregion
3541
4133
  //#region src/campaign/search-ledger-recording.ts
3542
- const SEARCH_LEDGER_DIR = "search-ledger";
3543
4134
  /**
3544
- * Recorder for one `runOptimization` run: `open()`, then `recordGeneration()`
3545
- * and `recordResults()` per generation, then `finish()`.
4135
+ * `SearchRecorder`: writes a search into its ledger as it happens.
4136
+ *
4137
+ * Every loop that scores artifacts against an objective and keeps the better
4138
+ * ones records through this one writer: the kernel, `runOptimization`, and the
4139
+ * GEPA importer. Each call appends one event the moment the fact exists, so an
4140
+ * interrupted search leaves an exact, replayable account of what ran.
3546
4141
  *
3547
- * Every event id is derived from the run, and an id already durable is not
3548
- * appended again, so a resumed run continues one ledger instead of conflicting
3549
- * with its own history.
4142
+ * Content (surfaces, rationales, diffs, RunRecords) goes to content-addressed
4143
+ * blobs beside the ledger; the ledger holds their digests. Free text a proposer
4144
+ * wrote (rationales, labels) is redacted with the `share` profile before it is
4145
+ * hashed, so the chain never covers a secret. Surfaces are stored as they ran:
4146
+ * a redacted surface is not what ran.
4147
+ *
4148
+ * Every write is idempotent against the ledger's state. Ids are deterministic,
4149
+ * and a fact already in the ledger is not appended again, so a resumed search
4150
+ * continues its ledger instead of conflicting with its own history.
3550
4151
  */
4152
+ const MAX_LABEL_CHARS = 200;
4153
+ /** The default claim: development numbers over exactly the supplied tasks. */
4154
+ function developmentClaim(populationId) {
4155
+ return {
4156
+ use: "development",
4157
+ population: {
4158
+ id: populationId,
4159
+ description: "the tasks this search was given"
4160
+ },
4161
+ samplingFrame: "the caller-supplied task list",
4162
+ independentUnit: "taskId",
4163
+ generalization: "fixed-roster"
4164
+ };
4165
+ }
4166
+ /** Deterministic edge id: one proposal of one child within one search. */
4167
+ function searchEdgeId(searchId, childNodeId, proposalKey) {
4168
+ return `edge_${createHash("sha256").update(canonicalString([
4169
+ searchId,
4170
+ childNodeId,
4171
+ proposalKey
4172
+ ])).digest("hex").slice(0, 32)}`;
4173
+ }
4174
+ /** Writes one search's events into its ledger as they happen. */
3551
4175
  var SearchRecorder = class SearchRecorder {
3552
- opts;
4176
+ ledger;
4177
+ storage;
4178
+ blobDir;
4179
+ now;
3553
4180
  tasks;
3554
- registered = /* @__PURE__ */ new Map();
3555
- order = [];
3556
- coverage = /* @__PURE__ */ new Map();
3557
- openSlots = /* @__PURE__ */ new Set();
3558
- durableEventIds = /* @__PURE__ */ new Set();
3559
- lastStampMs = 0;
3560
- proposalReceiptCount = 0;
3561
- constructor(opts) {
3562
- this.opts = opts;
3563
- this.tasks = plannedTasks(opts.scenarios, opts.reps, opts.runDir, opts.splitDigest);
3564
- }
3565
- /** Open the recorder and append the plan. An existing ledger for the same
3566
- * run is re-read first, so a resumed run keeps one plan and one lineage. */
3567
- static async open(opts) {
3568
- const recorder = new SearchRecorder(opts);
3569
- await recorder.hydrate();
3570
- await recorder.plan();
3571
- return recorder;
3572
- }
3573
- /**
3574
- * Record one generation's candidate-generation call and the candidates it
3575
- * produced. A proposal larger than the planned population extends the plan
3576
- * with the extra slots; a proposal that fills fewer closes the rest.
3577
- */
3578
- async recordGeneration(input) {
3579
- const { generation, candidates } = input;
3580
- const planned = this.opts.populationSize;
3581
- if (candidates.length > planned) {
3582
- const extra = [];
3583
- for (let index = planned; index < candidates.length; index++) {
3584
- const slot = {
3585
- slotId: slotId(generation, index),
3586
- generationOperationId: generationOperationId(generation)
3587
- };
3588
- extra.push(slot);
3589
- this.openSlots.add(slot.slotId);
3590
- }
3591
- await this.append({
3592
- kind: "search-plan-extended",
3593
- eventId: `search:plan-extended:gen-${generation}`,
3594
- occurredAt: this.stamp(),
3595
- artifacts: [this.proposalArtifact(generation, candidates)],
3596
- extension: {
3597
- candidateSlots: extra,
3598
- operations: []
3599
- }
3600
- });
3601
- }
3602
- await this.recordGenerationOperation(generation, candidates, planned - candidates.length);
3603
- const parent = this.registered.get(input.parentSurfaceHash);
3604
- for (const [index, candidate] of candidates.entries()) {
3605
- const candidateId = candidate.surfaceHash;
3606
- const slot = slotId(generation, index);
3607
- const surfaceArtifact = this.writeArtifact("candidate-surface", `candidate-${candidateId}.json`, {
3608
- surfaceHash: candidateId,
3609
- surface: candidate.surface,
3610
- label: candidate.label ?? ""
3611
- });
3612
- const ledgerGeneration = parent ? parent.generation + 1 : 0;
3613
- await this.append({
3614
- kind: "candidate-registered",
3615
- eventId: `candidate:${candidateId}`,
3616
- occurredAt: this.stamp(),
3617
- artifacts: [surfaceArtifact],
3618
- slotId: slot,
3619
- generationOperationId: generationOperationId(generation),
3620
- candidateId,
3621
- lineage: {
3622
- lineageNodeId: candidateId,
3623
- parentCandidateIds: parent ? [parent.candidateId] : [],
3624
- generation: ledgerGeneration,
3625
- proposer: this.opts.proposerLabel,
3626
- proposerSource: this.opts.binding.identity.proposer.source
3627
- },
3628
- surfaces: candidateSurfaces(candidate.surface, surfaceArtifact)
3629
- });
3630
- this.remember({
3631
- candidateId,
3632
- slotId: slot,
3633
- generation: ledgerGeneration
3634
- });
3635
- }
3636
- for (let index = candidates.length; index < planned; index++) await this.closeSlot(slotId(generation, index), generationOperationId(generation), {
3637
- code: "proposal-short",
3638
- message: `generation ${generation} proposed ${candidates.length} of ${planned} planned candidates`
3639
- });
3640
- }
3641
- /** Append one task attempt per designed cell of each candidate campaign. */
3642
- async recordResults(candidates) {
3643
- for (const candidate of candidates) {
3644
- const registered = this.registered.get(candidate.surfaceHash);
3645
- if (!registered) throw new Error(`search ledger: candidate ${candidate.surfaceHash} produced results without a registration`);
3646
- this.coverage.set(registered.candidateId, candidate.coverageComplete);
3647
- const evidence = this.writeArtifact("candidate-cells", `cells-${registered.candidateId}.json`, {
3648
- surfaceHash: candidate.surfaceHash,
3649
- runDir: candidate.runDir,
3650
- cells: candidate.cells.map((cell) => ({
3651
- cellId: cell.cellId,
3652
- scenarioId: cell.scenarioId,
3653
- rep: cell.rep,
3654
- judgeScores: cell.judgeScores,
3655
- costUsd: cell.costUsd,
3656
- costProvenance: cell.costProvenance,
3657
- tokenUsage: cell.tokenUsage,
3658
- ...cell.error === void 0 ? {} : {
3659
- error: cell.error,
3660
- errorStage: cell.errorStage
3661
- }
3662
- }))
3663
- });
3664
- const surfaceIds = candidateSurfaces(candidate.surface, evidence).map((surface) => surface.surfaceId);
3665
- for (const cell of candidate.cells) {
3666
- const taskId = taskIdFor(cell.scenarioId, cell.rep);
3667
- const task = this.tasks.find((planned) => planned.taskId === taskId);
3668
- if (!task) throw new Error(`search ledger: cell ${cell.cellId} is outside the planned task set`);
3669
- await this.append({
3670
- kind: "task-attempted",
3671
- eventId: `attempt:${registered.candidateId}:${taskId}`,
3672
- occurredAt: this.stamp(),
3673
- artifacts: [evidence],
3674
- candidateId: registered.candidateId,
3675
- runId: `${registered.candidateId}:${cell.cellId}`,
3676
- attemptIndex: 0,
3677
- task: {
3678
- taskId,
3679
- source: task.source
3680
- },
3681
- identity: {
3682
- model: this.cellModel(cell),
3683
- agent: this.opts.binding.identity.agent,
3684
- benchmark: task.benchmark
3685
- },
3686
- outcome: cellOutcome(cell),
3687
- accounting: cellAccounting(cell),
3688
- surfaceEvidence: surfaceIds.map((surfaceId) => surfaceEvidenceFor(surfaceId, evidence))
3689
- });
3690
- }
3691
- }
4181
+ lastStampMs;
4182
+ constructor(options, header, lastOccurredAt) {
4183
+ this.ledger = options.ledger;
4184
+ this.storage = options.storage ?? fsCampaignStorage();
4185
+ this.blobDir = options.blobDir ?? join(dirname(options.ledger.path), "blobs");
4186
+ this.now = options.now ?? Date.now;
4187
+ this.lastStampMs = lastOccurredAt === null ? 0 : Date.parse(lastOccurredAt);
4188
+ const tasks = /* @__PURE__ */ new Map();
4189
+ for (const split of [
4190
+ "train",
4191
+ "selection",
4192
+ "test"
4193
+ ]) for (const task of header.splits[split].tasks) tasks.set(task.taskId, {
4194
+ split,
4195
+ unitId: task.unitId
4196
+ });
4197
+ this.tasks = tasks;
3692
4198
  }
3693
4199
  /**
3694
- * Close the search: unreached generations, the selection operation, one
3695
- * decision per candidate, then the terminal event.
3696
- *
3697
- * The terminal event is appended only when canonical replay accounts for the
3698
- * whole planned denominator. An interrupted or partly unscored search stays
3699
- * `in-progress` and its receipt reports the exact gap, instead of claiming a
3700
- * closed search.
4200
+ * Open the search: append `search-opened`, or, when the ledger already holds
4201
+ * one, check that it is this search and continue it.
3701
4202
  */
3702
- async finish(input) {
3703
- const stopped = {
3704
- code: "generation-not-reached",
3705
- message: `the search stopped after ${input.generationsRun} generation(s)`
4203
+ static async open(options, opening, artifacts = []) {
4204
+ const state = await options.ledger.state();
4205
+ const event = {
4206
+ ...opening,
4207
+ kind: "search-opened",
4208
+ eventId: "search-opened",
4209
+ occurredAt: "",
4210
+ artifacts,
4211
+ splits: {
4212
+ train: {
4213
+ taskSetDigest: searchTaskSetDigest(opening.splits.train),
4214
+ tasks: opening.splits.train
4215
+ },
4216
+ selection: {
4217
+ taskSetDigest: searchTaskSetDigest(opening.splits.selection),
4218
+ tasks: opening.splits.selection
4219
+ },
4220
+ test: {
4221
+ taskSetDigest: searchTaskSetDigest(opening.splits.test),
4222
+ tasks: opening.splits.test
4223
+ },
4224
+ heldOutUnits: opening.splits.heldOutUnits
4225
+ }
3706
4226
  };
3707
- for (let generation = input.generationsRun; generation < this.opts.maxGenerations; generation++) {
3708
- await this.recordGenerationOperation(generation, [], this.opts.populationSize, stopped);
3709
- for (let index = 0; index < this.opts.populationSize; index++) await this.closeSlot(slotId(generation, index), generationOperationId(generation), stopped);
4227
+ if (state.header) {
4228
+ if (headerMaterial(state.header) !== headerMaterial(canonicalHeader(event))) throw new Error(`search ledger ${options.ledger.path} already holds a different search-opened header for ${state.searchId}; a changed search is a derived search`);
4229
+ return new SearchRecorder(options, state.header, state.lastOccurredAt);
3710
4230
  }
3711
- const decisions = this.writeArtifact("search-decisions", "decisions.json", {
3712
- winnerSurfaceHash: input.winnerSurfaceHash,
3713
- candidates: this.order.map((entry) => entry.candidateId)
4231
+ const recorder = new SearchRecorder(options, event, state.lastOccurredAt);
4232
+ await recorder.append({
4233
+ ...event,
4234
+ occurredAt: recorder.stamp()
3714
4235
  });
4236
+ return recorder;
4237
+ }
4238
+ get searchId() {
4239
+ return this.ledger.searchId;
4240
+ }
4241
+ /** The ledger's verified state now. */
4242
+ state() {
4243
+ return this.ledger.state();
4244
+ }
4245
+ /** Store `body` as a canonical JSON blob and return its content address. */
4246
+ blob(role, body) {
4247
+ const text = canonicalString(body);
4248
+ const bytes = Buffer.from(text, "utf8");
4249
+ const hex = createHash("sha256").update(bytes).digest("hex");
4250
+ const path = join(this.blobDir, `${hex}.json`);
4251
+ if (!this.storage.exists(path)) {
4252
+ this.storage.ensureDir(this.blobDir);
4253
+ this.storage.write(path, text);
4254
+ }
4255
+ return {
4256
+ role,
4257
+ uri: `file://${path}`,
4258
+ sha256: `sha256:${hex}`,
4259
+ byteLength: bytes.byteLength
4260
+ };
4261
+ }
4262
+ /** Register a node, or return the existing node with the same artifact digest. */
4263
+ async registerNode(input) {
4264
+ const existing = (await this.ledger.state()).nodeIdForDigest(input.artifactDigest);
4265
+ if (existing !== void 0) return {
4266
+ nodeId: existing,
4267
+ existed: true
4268
+ };
4269
+ const nodeId = searchNodeId(this.searchId, input.artifactDigest);
3715
4270
  await this.append({
3716
- kind: "search-operation-recorded",
3717
- eventId: "operation:selection",
4271
+ kind: "node-registered",
4272
+ eventId: `node:${nodeId}`,
3718
4273
  occurredAt: this.stamp(),
3719
- artifacts: [decisions],
3720
- operationId: "selection",
3721
- operationKind: "selection",
3722
- execution: {
3723
- kind: "deterministic",
3724
- source: this.opts.binding.identity.search
3725
- },
3726
- outcome: { status: "completed" },
3727
- accounting: {
3728
- tokens: {
3729
- status: "known",
3730
- inputTokens: 0,
3731
- outputTokens: 0,
3732
- cachedTokens: 0
3733
- },
3734
- cost: {
3735
- status: "known",
3736
- usd: 0,
3737
- source: "free"
3738
- }
3739
- }
4274
+ artifacts: [],
4275
+ nodeId,
4276
+ artifactDigest: input.artifactDigest,
4277
+ artifact: input.artifact,
4278
+ surfaces: input.surfaces
3740
4279
  });
3741
- const winner = this.registered.get(input.winnerSurfaceHash);
3742
- for (const entry of this.order) await this.append({
3743
- kind: "candidate-decided",
3744
- eventId: `decision:${entry.candidateId}`,
3745
- occurredAt: this.stamp(),
3746
- artifacts: [decisions],
3747
- candidateId: entry.candidateId,
3748
- decision: winner?.candidateId === entry.candidateId ? { status: "selected" } : {
3749
- status: "rejected",
3750
- reason: this.coverage.get(entry.candidateId) === false ? {
3751
- code: "coverage-incomplete",
3752
- message: "the candidate missed a designed cell and could not be ranked"
3753
- } : {
3754
- code: "not-promoted",
3755
- message: "the candidate did not beat the incumbent"
3756
- }
4280
+ return {
4281
+ nodeId,
4282
+ existed: false
4283
+ };
4284
+ }
4285
+ /** Record the proposal that derived a node. Returns the edge id. */
4286
+ async recordEdge(input) {
4287
+ const edgeId = searchEdgeId(this.searchId, input.childNodeId, input.proposalKey);
4288
+ if ((await this.ledger.state()).edge(edgeId)) return edgeId;
4289
+ const rationale = typeof input.rationale === "string" ? input.rationale.trim().length === 0 ? { unknown: "the proposer returned no rationale" } : this.blob("rationale", {
4290
+ kind: "rationale",
4291
+ text: redactText(input.rationale, { profile: "share" }),
4292
+ redaction: {
4293
+ profile: "share",
4294
+ version: REDACTION_VERSION
3757
4295
  }
4296
+ }) : input.rationale;
4297
+ await this.append({
4298
+ kind: "edge-recorded",
4299
+ eventId: `edge:${edgeId}`,
4300
+ occurredAt: this.stamp(),
4301
+ artifacts: [],
4302
+ edgeId,
4303
+ childNodeId: input.childNodeId,
4304
+ parents: input.parents.map((parent) => typeof parent === "string" ? {
4305
+ searchId: this.searchId,
4306
+ nodeId: parent
4307
+ } : parent),
4308
+ operator: input.operator,
4309
+ attribution: input.attribution,
4310
+ proposer: input.proposer,
4311
+ selection: input.selection ?? null,
4312
+ rationale,
4313
+ diffs: input.diffs,
4314
+ label: boundedLabel(input.label ?? "")
3758
4315
  });
3759
- const replay = await this.opts.binding.ledger.replay();
3760
- const { missingCandidateSlots, missingTaskOutcomes, missingOperations } = replay.audit.expected;
3761
- if (missingCandidateSlots.length === 0 && missingTaskOutcomes.length === 0 && missingOperations.length === 0 && replay.audit.decisions.pending === 0) await this.append({
3762
- kind: "search-completed",
3763
- eventId: "search:completed",
4316
+ return edgeId;
4317
+ }
4318
+ async startOperation(input) {
4319
+ if ((await this.ledger.state()).operation(input.operationId)) return;
4320
+ await this.append({
4321
+ kind: "operation-started",
4322
+ eventId: `operation-started:${input.operationId}`,
3764
4323
  occurredAt: this.stamp(),
3765
- artifacts: [decisions],
3766
- result: winner ? {
3767
- status: "selected",
3768
- candidateId: winner.candidateId
3769
- } : {
3770
- status: "all-rejected",
3771
- reason: {
3772
- code: "no-promoted-candidate",
3773
- message: "no candidate beat the incumbent on the designed denominator"
3774
- }
3775
- }
4324
+ artifacts: [],
4325
+ operationId: input.operationId,
4326
+ operationKind: input.operationKind,
4327
+ reservation: input.reservation ?? null
3776
4328
  });
3777
- return this.receipt(input.runId);
3778
4329
  }
3779
- /** Bounded receipt over the exact ledger bytes this run produced. */
3780
- async receipt(runId) {
3781
- const { ledger } = this.opts.binding;
3782
- const replay = await ledger.replay();
3783
- const bytes = replay.entries.map((entry) => `${canonicalString(entry)}\n`).join("");
3784
- return createSearchHistoryReceipt({
3785
- producerId: this.opts.proposerLabel,
3786
- runId,
3787
- ledger: {
3788
- role: "search-ledger",
3789
- uri: `file://${ledger.path}`,
3790
- sha256: hashCanonical(bytes),
3791
- byteLength: new TextEncoder().encode(bytes).byteLength
3792
- },
3793
- replay
4330
+ async recordOperation(input) {
4331
+ if ((await this.ledger.state()).operation(input.operationId)?.recorded) return;
4332
+ await this.append({
4333
+ ...input,
4334
+ kind: "operation-recorded",
4335
+ eventId: `operation-recorded:${input.operationId}`,
4336
+ occurredAt: this.stamp(),
4337
+ artifacts: input.artifacts ?? []
3794
4338
  });
3795
4339
  }
3796
- /** Read an existing ledger for this run so a resume continues it. */
3797
- async hydrate() {
3798
- const replay = await this.opts.binding.ledger.replay();
3799
- for (const entry of replay.entries) {
3800
- this.durableEventIds.add(entry.event.eventId);
3801
- const stamped = Date.parse(entry.event.occurredAt);
3802
- if (stamped > this.lastStampMs) this.lastStampMs = stamped;
3803
- }
3804
- for (const slot of replay.plan?.plan.candidateSlots ?? []) this.openSlots.add(slot.slotId);
3805
- for (const extension of replay.planExtensions) for (const slot of extension.extension.candidateSlots) this.openSlots.add(slot.slotId);
3806
- for (const closed of replay.closedCandidateSlots) this.openSlots.delete(closed.slotId);
3807
- for (const candidate of replay.candidates) {
3808
- this.remember({
3809
- candidateId: candidate.candidateId,
3810
- slotId: candidate.slotId,
3811
- generation: candidate.lineage.generation
3812
- });
3813
- this.openSlots.delete(candidate.slotId);
3814
- }
4340
+ /** Plan one cell. Returns the cell id; an allocated cell is not allocated again. */
4341
+ async allocateCell(input) {
4342
+ const task = this.tasks.get(input.taskId);
4343
+ if (!task) throw new Error(`search ${this.searchId} declares no task ${input.taskId}`);
4344
+ const cellId = searchCellId(this.searchId, input.nodeId, input.taskId, input.split, input.rep);
4345
+ if ((await this.ledger.state()).cell(cellId)) return cellId;
4346
+ await this.append({
4347
+ kind: "cell-allocated",
4348
+ eventId: `cell:${cellId}`,
4349
+ occurredAt: this.stamp(),
4350
+ artifacts: [],
4351
+ cellId,
4352
+ nodeId: input.nodeId,
4353
+ taskId: input.taskId,
4354
+ unitId: task.unitId,
4355
+ split: input.split,
4356
+ rep: input.rep,
4357
+ stage: input.stage,
4358
+ lane: input.lane ?? null,
4359
+ reservation: input.reservation ?? null
4360
+ });
4361
+ return cellId;
3815
4362
  }
3816
- async plan() {
3817
- const candidateSlots = [];
3818
- const operations = [{
3819
- operationId: "selection",
3820
- kind: "selection"
3821
- }];
3822
- for (let generation = 0; generation < this.opts.maxGenerations; generation++) {
3823
- operations.push({
3824
- operationId: generationOperationId(generation),
3825
- kind: "candidate-generation"
3826
- });
3827
- for (let index = 0; index < this.opts.populationSize; index++) {
3828
- const slot = {
3829
- slotId: slotId(generation, index),
3830
- generationOperationId: generationOperationId(generation)
3831
- };
3832
- candidateSlots.push(slot);
3833
- if (!this.registeredSlot(slot.slotId)) this.openSlots.add(slot.slotId);
3834
- }
4363
+ /** Record one finished attempt at a cell. */
4364
+ async settleCell(input) {
4365
+ const cell = (await this.ledger.state()).cell(input.cellId);
4366
+ if (!cell) throw new Error(`cell ${input.cellId} was never allocated in ${this.searchId}`);
4367
+ const attempt = input.attempt ?? cell.attempts + 1;
4368
+ if (attempt <= cell.attempts) return;
4369
+ const runId = searchCellRunId({
4370
+ cellId: input.cellId,
4371
+ attempt
4372
+ });
4373
+ const artifacts = [...input.artifacts ?? []];
4374
+ if (input.runRecord) {
4375
+ const { search } = input.runRecord;
4376
+ if (input.runRecord.runId !== runId || search?.searchId !== this.searchId || search.nodeId !== cell.nodeId || search.cellId !== input.cellId || search.attempt !== attempt) throw new Error(`RunRecord ${input.runRecord.runId} does not carry the coordinates of ${runId}`);
4377
+ artifacts.push(this.blob("run-record", input.runRecord));
3835
4378
  }
3836
4379
  await this.append({
3837
- kind: "search-planned",
3838
- eventId: "search:plan",
4380
+ kind: "cell-settled",
4381
+ eventId: `settled:${input.cellId}:${attempt}`,
3839
4382
  occurredAt: this.stamp(),
3840
- artifacts: [this.writeArtifact("search-plan", "plan.json", {
3841
- runDir: this.opts.runDir,
3842
- splitDigest: this.opts.splitDigest,
3843
- maxGenerations: this.opts.maxGenerations,
3844
- populationSize: this.opts.populationSize,
3845
- tasks: this.tasks
3846
- })],
3847
- plan: {
3848
- candidateSlots,
3849
- tasks: this.tasks,
3850
- operations
3851
- }
4383
+ artifacts,
4384
+ cellId: input.cellId,
4385
+ attempt,
4386
+ runId,
4387
+ outcome: input.outcome,
4388
+ accounting: input.accounting,
4389
+ boxMinutes: input.boxMinutes ?? null,
4390
+ wallMs: input.wallMs ?? null,
4391
+ queueMs: input.queueMs ?? null,
4392
+ placement: input.placement ?? null,
4393
+ identity: input.identity,
4394
+ surfaceEvidence: input.surfaceEvidence ?? [],
4395
+ traceRef: input.traceRef ?? { unknown: "the producer recorded no trace for this attempt" }
3852
4396
  });
3853
4397
  }
3854
- registeredSlot(slot) {
3855
- return this.order.some((entry) => entry.slotId === slot);
3856
- }
3857
- remember(entry) {
3858
- if (this.registered.has(entry.candidateId)) return;
3859
- this.registered.set(entry.candidateId, entry);
3860
- this.order.push(entry);
3861
- }
3862
- async recordGenerationOperation(generation, candidates, unfilled, failure) {
3863
- const shortfall = {
3864
- code: "proposal-short",
3865
- message: `generation ${generation} left ${unfilled} planned candidate slot(s) unfilled`
3866
- };
3867
- const outcome = failure ? {
3868
- status: "failed",
3869
- failure
3870
- } : unfilled <= 0 ? { status: "completed" } : candidates.length === 0 ? {
3871
- status: "failed",
3872
- failure: shortfall
3873
- } : {
3874
- status: "partial",
3875
- failure: shortfall
3876
- };
4398
+ async cancelCell(input) {
4399
+ const cell = (await this.ledger.state()).cell(input.cellId);
4400
+ if (!cell) throw new Error(`cell ${input.cellId} was never allocated in ${this.searchId}`);
4401
+ if (cell.cancelled || cell.final) return;
3877
4402
  await this.append({
3878
- kind: "search-operation-recorded",
3879
- eventId: `operation:${generationOperationId(generation)}`,
4403
+ kind: "cell-cancelled",
4404
+ eventId: `cancelled:${input.cellId}`,
3880
4405
  occurredAt: this.stamp(),
3881
- artifacts: [this.proposalArtifact(generation, candidates)],
3882
- operationId: generationOperationId(generation),
3883
- operationKind: "candidate-generation",
3884
- execution: this.opts.binding.identity.proposer,
3885
- outcome,
3886
- accounting: this.proposalAccounting()
4406
+ artifacts: [],
4407
+ cellId: input.cellId,
4408
+ reason: input.reason
3887
4409
  });
3888
4410
  }
3889
- async closeSlot(slot, generationOperation, reason) {
3890
- if (!this.openSlots.has(slot)) return;
3891
- this.openSlots.delete(slot);
4411
+ /** Record a decision about a node. Repeating the node's latest decision appends nothing. */
4412
+ async decideNode(input) {
4413
+ const node = (await this.ledger.state()).node(input.nodeId);
4414
+ if (!node) throw new Error(`node ${input.nodeId} is not in ${this.searchId}`);
4415
+ const latest = node.decisions.at(-1);
4416
+ if (latest && canonicalString(latest.decision) === canonicalString(input.decision)) return;
3892
4417
  await this.append({
3893
- kind: "candidate-slot-closed",
3894
- eventId: `slot-closed:${slot}`,
4418
+ kind: "node-decided",
4419
+ eventId: `decided:${input.nodeId}:${node.decisions.length}`,
3895
4420
  occurredAt: this.stamp(),
3896
- artifacts: [this.writeArtifact("closed-slot", `slot-${slot}.json`, {
3897
- slot,
3898
- reason
3899
- })],
3900
- slotId: slot,
3901
- generationOperationId: generationOperation,
3902
- reason
4421
+ artifacts: [],
4422
+ nodeId: input.nodeId,
4423
+ decision: input.decision,
4424
+ basis: input.basis ?? null,
4425
+ rule: input.rule,
4426
+ reason: input.reason
3903
4427
  });
3904
4428
  }
3905
- /** Spend booked to candidate generation since the previous generation. */
3906
- proposalAccounting() {
3907
- const receipts = this.opts.costLedger.list({ phase: "search.proposal" });
3908
- const fresh = receipts.slice(this.proposalReceiptCount);
3909
- this.proposalReceiptCount = receipts.length;
3910
- return receiptAccounting(fresh);
3911
- }
3912
- cellModel(cell) {
3913
- const resolved = cell.resolvedModel;
3914
- if (resolved === void 0) return this.opts.binding.identity.model;
3915
- if (!modelHasSnapshot(resolved)) throw new Error(`search ledger: cell ${cell.cellId} ran model '${resolved}', which carries no immutable snapshot; the ledger cannot record a moving alias as execution identity`);
3916
- return {
3917
- provider: this.opts.binding.identity.model.provider,
3918
- snapshot: resolved
3919
- };
3920
- }
3921
- proposalArtifact(generation, candidates) {
3922
- return this.writeArtifact("candidate-proposal", `proposal-gen-${generation}.json`, {
3923
- generation,
3924
- candidates: candidates.map((candidate) => ({
3925
- surfaceHash: candidate.surfaceHash,
3926
- label: candidate.label ?? ""
3927
- }))
4429
+ /** Close the search. The ledger refuses a close while any cell, operation or
4430
+ * node decision is outstanding. */
4431
+ async close(input) {
4432
+ if ((await this.ledger.state()).closed) return;
4433
+ await this.append({
4434
+ kind: "search-closed",
4435
+ eventId: "search-closed",
4436
+ occurredAt: this.stamp(),
4437
+ artifacts: [],
4438
+ reason: input.reason,
4439
+ claim: input.claim
3928
4440
  });
3929
4441
  }
3930
- /** Write one canonical evidence document and return its content address. */
3931
- writeArtifact(role, name, body) {
3932
- const directory = `${this.opts.runDir}/${SEARCH_LEDGER_DIR}`;
3933
- const path = `${directory}/${name}`;
3934
- const contents = canonicalString(body);
3935
- this.opts.storage.ensureDir(directory);
3936
- this.opts.storage.write(path, contents);
3937
- return {
3938
- role,
3939
- uri: `file://${path}`,
3940
- sha256: hashCanonical(contents),
3941
- byteLength: new TextEncoder().encode(contents).byteLength
3942
- };
4442
+ /** A bounded receipt over the exact ledger bytes. */
4443
+ receipt(input) {
4444
+ return createSearchHistoryReceipt({
4445
+ ...input,
4446
+ ledger: this.ledger,
4447
+ storage: this.storage
4448
+ });
3943
4449
  }
3944
4450
  async append(event) {
3945
- if (this.durableEventIds.has(event.eventId)) return;
3946
- await this.opts.binding.ledger.append(event);
3947
- this.durableEventIds.add(event.eventId);
4451
+ await this.ledger.append(event);
3948
4452
  }
3949
4453
  /** Non-decreasing ISO stamps; the ledger refuses an event that moves back. */
3950
4454
  stamp() {
3951
- const now = Date.now();
4455
+ const now = this.now();
3952
4456
  this.lastStampMs = now > this.lastStampMs ? now : this.lastStampMs + 1;
3953
4457
  return new Date(this.lastStampMs).toISOString();
3954
4458
  }
3955
4459
  };
3956
- /** One task per designed (scenario, replicate) cell. */
3957
- function plannedTasks(scenarios, reps, runDir, splitDigest) {
3958
- const tasks = [];
3959
- for (const scenario of scenarios) for (let rep = 0; rep < reps; rep++) tasks.push({
3960
- taskId: taskIdFor(scenario.id, rep),
3961
- source: {
3962
- uri: `scenario://${scenario.id}`,
3963
- revision: hashCanonical(scenario)
3964
- },
3965
- benchmark: {
3966
- uri: `campaign://${runDir}`,
3967
- revision: splitDigest
3968
- },
3969
- maxAttempts: 1
4460
+ /** A mutable surface as a node: its content digest, stored bytes and declared surfaces. */
4461
+ function surfaceNode(recorder, surface) {
4462
+ const artifact = recorder.blob("surface", {
4463
+ kind: "mutable-surface",
4464
+ surface
3970
4465
  });
3971
- return tasks;
3972
- }
3973
- function taskIdFor(scenarioId, rep) {
3974
- return `${scenarioId}#rep-${rep}`;
3975
- }
3976
- function slotId(generation, index) {
3977
- return `gen-${generation}-slot-${index}`;
4466
+ return {
4467
+ artifactDigest: surfaceContentHash(surface),
4468
+ artifact,
4469
+ surfaces: declaredSurfaces(surface, artifact)
4470
+ };
3978
4471
  }
3979
- function generationOperationId(generation) {
3980
- return `candidate-generation:gen-${generation}`;
4472
+ /** A parent-to-child diff of two mutable surfaces, stored as a blob. */
4473
+ function surfaceDiff(recorder, parent, child) {
4474
+ return recorder.blob("diff", {
4475
+ kind: "surface-diff",
4476
+ from: surfaceContentHash(parent),
4477
+ to: surfaceContentHash(child),
4478
+ text: renderSurfaceDiff(child, parent)
4479
+ });
3981
4480
  }
3982
- /** Declared surfaces of one candidate. A component surface declares one
3983
- * surface per named component, so per-component evidence stays addressable. */
3984
- function candidateSurfaces(surface, artifact) {
4481
+ /** Declared surfaces of one node. A component surface declares one surface per
4482
+ * named component, so per-component evidence stays addressable. */
4483
+ function declaredSurfaces(surface, artifact) {
3985
4484
  if (typeof surface === "string") return [{
3986
4485
  surfaceId: "prompt",
3987
4486
  kind: "prompt",
@@ -3998,19 +4497,269 @@ function candidateSurfaces(surface, artifact) {
3998
4497
  artifact
3999
4498
  }));
4000
4499
  }
4001
- /** The campaign measures a candidate surface as a whole, so per-surface
4002
- * attribution stays unmeasured instead of inventing a per-component delta. */
4003
- function surfaceEvidenceFor(surfaceId, evidence) {
4004
- return {
4005
- surfaceId,
4006
- fired: true,
4007
- firingCount: 1,
4008
- effect: {
4009
- status: "not-measured",
4010
- reason: "the campaign measures the candidate surface as a whole, not per surface"
4011
- },
4012
- evidence: [evidence]
4013
- };
4500
+ function boundedLabel(label) {
4501
+ const redacted = redactText(label.trim(), { profile: "share" });
4502
+ return redacted.length <= MAX_LABEL_CHARS ? redacted : `${redacted.slice(0, MAX_LABEL_CHARS - 1)}…`;
4503
+ }
4504
+ /** The header without its stamp, for comparing a resumed opening to the stored one. */
4505
+ function headerMaterial(header) {
4506
+ const { occurredAt: _occurredAt, ...material } = header;
4507
+ return canonicalString(material);
4508
+ }
4509
+ /** The header as the ledger would store it, so the comparison ignores ordering. */
4510
+ function canonicalHeader(header) {
4511
+ return validateSearchLedgerEvent({
4512
+ ...header,
4513
+ occurredAt: "2000-01-01T00:00:00.000Z"
4514
+ });
4515
+ }
4516
+ //#endregion
4517
+ //#region src/campaign/presets/run-optimization-recording.ts
4518
+ var OptimizationSearch = class OptimizationSearch {
4519
+ recorder;
4520
+ binding;
4521
+ costLedger;
4522
+ scenarioIds;
4523
+ reps;
4524
+ identity;
4525
+ proposerName;
4526
+ proposalReceiptCount = 0;
4527
+ constructor(args) {
4528
+ this.recorder = args.recorder;
4529
+ this.binding = args.binding;
4530
+ this.costLedger = args.costLedger;
4531
+ this.scenarioIds = args.scenarioIds;
4532
+ this.reps = args.reps;
4533
+ this.identity = args.identity;
4534
+ this.proposerName = args.proposerName;
4535
+ }
4536
+ /** Open the search and record the measured baseline as its seeded root. */
4537
+ static async open(args) {
4538
+ const { binding } = args;
4539
+ const identity = {
4540
+ model: binding.identity.model,
4541
+ agent: binding.identity.agent,
4542
+ benchmark: {
4543
+ uri: "campaign://scenarios",
4544
+ revision: args.splitDigest
4545
+ }
4546
+ };
4547
+ const train = args.scenarios.map((scenario) => ({
4548
+ taskId: scenario.id,
4549
+ unitId: scenario.id,
4550
+ source: {
4551
+ uri: `scenario://${scenario.id}`,
4552
+ revision: hashCanonical(scenario)
4553
+ }
4554
+ }));
4555
+ const recorder = await SearchRecorder.open({
4556
+ ledger: binding.ledger,
4557
+ storage: args.storage
4558
+ }, {
4559
+ subject: binding.identity.subject ?? args.proposerName,
4560
+ process: {
4561
+ name: "runOptimization",
4562
+ executionRef: binding.identity.search
4563
+ },
4564
+ artifactKind: "prompt",
4565
+ objective: {
4566
+ metric: "composite",
4567
+ direction: "maximize",
4568
+ judge: binding.identity.judge ?? { unknown: "runOptimization receives its judges as functions without a pinned source" },
4569
+ claim: binding.identity.claim ?? developmentClaim(args.splitDigest)
4570
+ },
4571
+ splits: {
4572
+ train,
4573
+ selection: [],
4574
+ test: [],
4575
+ heldOutUnits: true
4576
+ },
4577
+ policy: {
4578
+ expansion: args.expansion,
4579
+ allocation: "uniform",
4580
+ seed: args.seed
4581
+ },
4582
+ budget: {
4583
+ maxUsd: args.maxUsd,
4584
+ maxCells: null,
4585
+ maxNodes: null,
4586
+ deadline: null,
4587
+ maxConcurrency: null,
4588
+ reservedClaimUsd: 0
4589
+ },
4590
+ containment: null,
4591
+ derivedFrom: null,
4592
+ identity
4593
+ });
4594
+ const search = new OptimizationSearch({
4595
+ recorder,
4596
+ binding,
4597
+ costLedger: args.costLedger,
4598
+ scenarioIds: args.scenarios.map((scenario) => scenario.id),
4599
+ reps: args.reps,
4600
+ identity,
4601
+ proposerName: args.proposerName
4602
+ });
4603
+ const root = await recorder.registerNode(surfaceNode(recorder, args.baselineSurface));
4604
+ await recorder.recordEdge({
4605
+ childNodeId: root.nodeId,
4606
+ parents: [],
4607
+ operator: "seed",
4608
+ attribution: "explicit",
4609
+ proposer: null,
4610
+ proposalKey: "baseline",
4611
+ rationale: { unknown: "the baseline is the caller-supplied starting surface" },
4612
+ diffs: [],
4613
+ label: "baseline"
4614
+ });
4615
+ await search.recordCells(root.nodeId, "root", args.baselineCells);
4616
+ return {
4617
+ search,
4618
+ rootNodeId: root.nodeId
4619
+ };
4620
+ }
4621
+ /** Start one generation's candidate-generation operation, before `propose()`. */
4622
+ async startGeneration(generation) {
4623
+ await this.recorder.startOperation({
4624
+ operationId: generationOperationId(generation),
4625
+ operationKind: "candidate-generation"
4626
+ });
4627
+ }
4628
+ /** Record the generation's proposal: its operation, then one node and edge per candidate. */
4629
+ async recordProposal(input) {
4630
+ const operationId = generationOperationId(input.generation);
4631
+ await this.recorder.recordOperation({
4632
+ operationId,
4633
+ operationKind: "candidate-generation",
4634
+ execution: this.binding.identity.proposer,
4635
+ outcome: { status: "completed" },
4636
+ accounting: this.proposalAccounting()
4637
+ });
4638
+ const nodeIds = [];
4639
+ for (const [index, candidate] of input.candidates.entries()) {
4640
+ const { nodeId } = await this.recorder.registerNode(surfaceNode(this.recorder, candidate.surface));
4641
+ await this.recorder.recordEdge({
4642
+ childNodeId: nodeId,
4643
+ parents: [input.parent.nodeId],
4644
+ operator: "improve",
4645
+ attribution: "explicit",
4646
+ proposer: {
4647
+ kind: "optimizer",
4648
+ name: this.proposerName,
4649
+ operationId,
4650
+ source: this.binding.identity.proposer.source
4651
+ },
4652
+ proposalKey: `gen-${input.generation}:candidate-${index}`,
4653
+ selection: {
4654
+ rule: input.selectionRule,
4655
+ evidence: input.parent.composite === null ? {} : { parentComposite: input.parent.composite }
4656
+ },
4657
+ rationale: candidate.rationale,
4658
+ diffs: [surfaceDiff(this.recorder, input.parent.surface, candidate.surface)],
4659
+ label: candidate.label
4660
+ });
4661
+ nodeIds.push(nodeId);
4662
+ }
4663
+ return nodeIds;
4664
+ }
4665
+ /** Close a generation whose proposer produced nothing or failed. */
4666
+ async failGeneration(generation, message) {
4667
+ await this.recorder.recordOperation({
4668
+ operationId: generationOperationId(generation),
4669
+ operationKind: "candidate-generation",
4670
+ execution: this.binding.identity.proposer,
4671
+ outcome: {
4672
+ status: "failed",
4673
+ failure: {
4674
+ code: "no-candidates",
4675
+ message
4676
+ }
4677
+ },
4678
+ accounting: this.proposalAccounting()
4679
+ });
4680
+ }
4681
+ /** Allocate every designed cell of a node before its campaign runs. */
4682
+ async allocate(nodeId) {
4683
+ for (const taskId of this.scenarioIds) for (let rep = 0; rep < this.reps; rep++) await this.recorder.allocateCell({
4684
+ nodeId,
4685
+ taskId,
4686
+ split: "train",
4687
+ rep,
4688
+ stage: "train"
4689
+ });
4690
+ }
4691
+ /** Settle a node's campaign cells; allocates any the campaign ran beyond the design. */
4692
+ async recordCells(nodeId, stage, cells) {
4693
+ for (const cell of cells) {
4694
+ const cellId = await this.recorder.allocateCell({
4695
+ nodeId,
4696
+ taskId: cell.scenarioId,
4697
+ split: "train",
4698
+ rep: cell.rep,
4699
+ stage
4700
+ });
4701
+ await this.recorder.settleCell({
4702
+ cellId,
4703
+ attempt: 1,
4704
+ outcome: cellOutcome(cell),
4705
+ accounting: cellAccounting(cell),
4706
+ identity: {
4707
+ ...this.identity,
4708
+ model: this.cellModel(cell)
4709
+ },
4710
+ wallMs: cell.durationMs,
4711
+ traceRef: { unknown: "runCampaign reports no trace id per cell" }
4712
+ });
4713
+ }
4714
+ }
4715
+ /**
4716
+ * Decide every node and close. The winner is selected; every other node is
4717
+ * rejected. Allocated cells a campaign never returned are cancelled.
4718
+ */
4719
+ async finish(input) {
4720
+ const openCells = (await this.recorder.state()).cells().filter((cell) => cell.attempts === 0 && !cell.cancelled);
4721
+ for (const cell of openCells) await this.recorder.cancelCell({
4722
+ cellId: cell.cellId,
4723
+ reason: "aborted"
4724
+ });
4725
+ for (const node of (await this.recorder.state()).nodes()) await this.recorder.decideNode({
4726
+ nodeId: node.nodeId,
4727
+ decision: node.nodeId === input.winnerNodeId ? { status: "selected" } : { status: "rejected" },
4728
+ rule: "incumbent",
4729
+ reason: node.nodeId === input.winnerNodeId ? "the incumbent when the loop stopped" : node.ordinal === 0 || input.promotedNodeIds.has(node.nodeId) ? "a later candidate replaced it as the incumbent" : input.incompleteNodeIds.has(node.nodeId) ? "the candidate missed a designed cell and could not be ranked" : "the candidate did not beat the incumbent"
4730
+ });
4731
+ await this.recorder.close({
4732
+ reason: input.reason,
4733
+ claim: null
4734
+ });
4735
+ return this.recorder.receipt({
4736
+ producerId: this.proposerName,
4737
+ runId: input.runId
4738
+ });
4739
+ }
4740
+ /** Spend booked to candidate generation since the previous generation. */
4741
+ proposalAccounting() {
4742
+ const receipts = this.costLedger.list({ phase: "search.proposal" });
4743
+ const fresh = receipts.slice(this.proposalReceiptCount);
4744
+ this.proposalReceiptCount = receipts.length;
4745
+ return receiptAccounting(fresh);
4746
+ }
4747
+ cellModel(cell) {
4748
+ const resolved = cell.resolvedModel;
4749
+ if (resolved === void 0) return this.identity.model;
4750
+ const provider = this.identity.model.provider;
4751
+ return modelHasSnapshot(resolved) ? {
4752
+ provider,
4753
+ snapshot: resolved
4754
+ } : {
4755
+ provider,
4756
+ alias: resolved,
4757
+ unknown: "the provider reported a moving alias, not a snapshot"
4758
+ };
4759
+ }
4760
+ };
4761
+ function generationOperationId(generation) {
4762
+ return `candidate-generation:gen-${generation}`;
4014
4763
  }
4015
4764
  function cellOutcome(cell) {
4016
4765
  const scores = Object.entries(cell.judgeScores).filter(([, score]) => score.failed !== true && Number.isFinite(score.composite));
@@ -4091,264 +4840,6 @@ function receiptAccounting(receipts) {
4091
4840
  }
4092
4841
  };
4093
4842
  }
4094
- /**
4095
- * Record an optimizer's own candidate graph into the same ledger.
4096
- *
4097
- * A complete optimization method searches inside its own process and reports
4098
- * one artifact when it finishes: the candidate population, with each
4099
- * candidate's parents and its score per selection scenario. This turns that
4100
- * artifact into the canonical event stream, so a first-party method returns
4101
- * the same `SearchHistoryReceipt` the in-process loop returns, and
4102
- * `compareOptimizationMethods({ searchHistoryPolicy: 'require-complete' })`
4103
- * accepts it.
4104
- *
4105
- * A candidate the optimizer left unscored on a planned scenario leaves the
4106
- * planned denominator open, so the receipt reports the gap instead of closing
4107
- * the search.
4108
- */
4109
- async function recordCandidatePopulationSearch(input) {
4110
- const { population, ledger, identity } = input;
4111
- const stamps = monotonicStamps();
4112
- const writeArtifact = artifactWriter(input.storage, input.runDir);
4113
- const populationArtifact = {
4114
- role: "candidate-population",
4115
- uri: `file://${population.summary.path}`,
4116
- sha256: population.summary.sha256,
4117
- byteLength: population.summary.bytes
4118
- };
4119
- const tasks = input.scenarios.filter((scenario) => population.summary.scenarioIds.includes(scenario.id)).map((scenario) => ({
4120
- taskId: taskIdFor(scenario.id, 0),
4121
- source: {
4122
- uri: `scenario://${scenario.id}`,
4123
- revision: hashCanonical(scenario)
4124
- },
4125
- benchmark: {
4126
- uri: `optimizer://${input.producerId}`,
4127
- revision: population.summary.sha256
4128
- },
4129
- maxAttempts: 1
4130
- }));
4131
- if (tasks.length !== population.summary.scenarioIds.length) throw new Error(`search ledger: the candidate population scored ${population.summary.scenarioIds.length} scenarios, but ${tasks.length} were supplied`);
4132
- const generationOperation = "candidate-generation:population";
4133
- await ledger.append({
4134
- kind: "search-planned",
4135
- eventId: "search:plan",
4136
- occurredAt: stamps(),
4137
- artifacts: [populationArtifact],
4138
- plan: {
4139
- candidateSlots: population.candidates.map((candidate) => ({
4140
- slotId: `candidate-${candidate.index}`,
4141
- generationOperationId: generationOperation
4142
- })),
4143
- tasks,
4144
- operations: [{
4145
- operationId: generationOperation,
4146
- kind: "candidate-generation"
4147
- }, {
4148
- operationId: "selection",
4149
- kind: "selection"
4150
- }]
4151
- }
4152
- });
4153
- await ledger.append({
4154
- kind: "search-operation-recorded",
4155
- eventId: `operation:${generationOperation}`,
4156
- occurredAt: stamps(),
4157
- artifacts: [populationArtifact],
4158
- operationId: generationOperation,
4159
- operationKind: "candidate-generation",
4160
- execution: identity.proposer,
4161
- outcome: { status: "completed" },
4162
- accounting: input.generationAccounting
4163
- });
4164
- const candidateIds = /* @__PURE__ */ new Map();
4165
- const depths = /* @__PURE__ */ new Map();
4166
- for (const candidate of population.candidates) {
4167
- const parents = candidate.parentIndices.filter((index) => index !== null);
4168
- const candidateId = candidate.candidateHash;
4169
- candidateIds.set(candidate.index, candidateId);
4170
- const depth = parents.length === 0 ? 0 : Math.max(...parents.map((index) => depths.get(index) ?? 0)) + 1;
4171
- depths.set(candidate.index, depth);
4172
- const surfaceArtifact = writeArtifact("candidate-surface", `candidate-${candidate.index}.json`, {
4173
- index: candidate.index,
4174
- candidateDigest: candidate.candidateDigest,
4175
- candidate: candidate.candidate
4176
- });
4177
- await ledger.append({
4178
- kind: "candidate-registered",
4179
- eventId: `candidate:${candidate.index}`,
4180
- occurredAt: stamps(),
4181
- artifacts: [surfaceArtifact],
4182
- slotId: `candidate-${candidate.index}`,
4183
- generationOperationId: generationOperation,
4184
- candidateId,
4185
- lineage: {
4186
- lineageNodeId: candidateId.slice(0, 16),
4187
- parentCandidateIds: parents.map((index) => {
4188
- const parentId = candidateIds.get(index);
4189
- if (!parentId) throw new Error(`search ledger: candidate ${candidate.index} names parent ${index}, which precedes no registered candidate`);
4190
- return parentId;
4191
- }),
4192
- generation: depth,
4193
- proposer: input.producerId,
4194
- proposerSource: identity.proposer.source
4195
- },
4196
- surfaces: candidateSurfaces(externalSurface(candidate.candidate), surfaceArtifact)
4197
- });
4198
- const surfaceIds = candidateSurfaces(externalSurface(candidate.candidate), surfaceArtifact).map((surface) => surface.surfaceId);
4199
- for (const score of candidate.selectionScores) {
4200
- const task = tasks.find((planned) => planned.taskId === taskIdFor(score.scenarioId, 0));
4201
- if (!task) throw new Error(`search ledger: candidate ${candidate.index} scored unplanned scenario ${score.scenarioId}`);
4202
- await ledger.append({
4203
- kind: "task-attempted",
4204
- eventId: `attempt:${candidate.index}:${task.taskId}`,
4205
- occurredAt: stamps(),
4206
- artifacts: [populationArtifact],
4207
- candidateId,
4208
- runId: `${input.runId}:${candidate.index}:${task.taskId}`,
4209
- attemptIndex: 0,
4210
- task: {
4211
- taskId: task.taskId,
4212
- source: task.source
4213
- },
4214
- identity: {
4215
- model: identity.model,
4216
- agent: identity.agent,
4217
- benchmark: task.benchmark
4218
- },
4219
- outcome: {
4220
- status: "passed",
4221
- score: score.score,
4222
- metrics: { composite: score.score }
4223
- },
4224
- accounting: {
4225
- tokens: {
4226
- status: "unknown",
4227
- reason: "the optimizer reports no token usage per candidate evaluation"
4228
- },
4229
- cost: {
4230
- status: "unknown",
4231
- knownLowerBoundUsd: 0,
4232
- reason: "the optimizer reports no cost per candidate evaluation"
4233
- }
4234
- },
4235
- surfaceEvidence: surfaceIds.map((surfaceId) => surfaceEvidenceFor(surfaceId, populationArtifact))
4236
- });
4237
- }
4238
- }
4239
- await ledger.append({
4240
- kind: "search-operation-recorded",
4241
- eventId: "operation:selection",
4242
- occurredAt: stamps(),
4243
- artifacts: [populationArtifact],
4244
- operationId: "selection",
4245
- operationKind: "selection",
4246
- execution: {
4247
- kind: "deterministic",
4248
- source: identity.search
4249
- },
4250
- outcome: { status: "completed" },
4251
- accounting: {
4252
- tokens: {
4253
- status: "known",
4254
- inputTokens: 0,
4255
- outputTokens: 0,
4256
- cachedTokens: 0
4257
- },
4258
- cost: {
4259
- status: "known",
4260
- usd: 0,
4261
- source: "free"
4262
- }
4263
- }
4264
- });
4265
- const winnerId = candidateIds.get(population.bestIndex);
4266
- for (const candidate of population.candidates) await ledger.append({
4267
- kind: "candidate-decided",
4268
- eventId: `decision:${candidate.index}`,
4269
- occurredAt: stamps(),
4270
- artifacts: [populationArtifact],
4271
- candidateId: candidateIds.get(candidate.index),
4272
- decision: candidate.index === population.bestIndex ? { status: "selected" } : {
4273
- status: "rejected",
4274
- reason: {
4275
- code: "not-selected",
4276
- message: "the optimizer selected another candidate"
4277
- }
4278
- }
4279
- });
4280
- const replay = await ledger.replay();
4281
- const { missingCandidateSlots, missingTaskOutcomes, missingOperations } = replay.audit.expected;
4282
- if (winnerId && missingCandidateSlots.length === 0 && missingTaskOutcomes.length === 0 && missingOperations.length === 0 && replay.audit.decisions.pending === 0) await ledger.append({
4283
- kind: "search-completed",
4284
- eventId: "search:completed",
4285
- occurredAt: stamps(),
4286
- artifacts: [populationArtifact],
4287
- result: {
4288
- status: "selected",
4289
- candidateId: winnerId
4290
- }
4291
- });
4292
- const final = await ledger.replay();
4293
- const bytes = final.entries.map((entry) => `${canonicalString(entry)}\n`).join("");
4294
- return createSearchHistoryReceipt({
4295
- producerId: input.producerId,
4296
- runId: input.runId,
4297
- ledger: {
4298
- role: "search-ledger",
4299
- uri: `file://${ledger.path}`,
4300
- sha256: hashCanonical(bytes),
4301
- byteLength: new TextEncoder().encode(bytes).byteLength
4302
- },
4303
- replay: final
4304
- });
4305
- }
4306
- /** An optimizer candidate as the canonical mutable surface it represents. */
4307
- function externalSurface(candidate) {
4308
- return typeof candidate === "string" ? candidate : {
4309
- kind: "components",
4310
- components: candidate
4311
- };
4312
- }
4313
- function artifactWriter(storage, runDir) {
4314
- const directory = `${runDir}/${SEARCH_LEDGER_DIR}`;
4315
- return (role, name, body) => {
4316
- const contents = canonicalString(body);
4317
- storage.ensureDir(directory);
4318
- storage.write(`${directory}/${name}`, contents);
4319
- return {
4320
- role,
4321
- uri: `file://${directory}/${name}`,
4322
- sha256: hashCanonical(contents),
4323
- byteLength: new TextEncoder().encode(contents).byteLength
4324
- };
4325
- };
4326
- }
4327
- /** Non-decreasing ISO stamps; the ledger refuses an event that moves back. */
4328
- function monotonicStamps() {
4329
- let last = 0;
4330
- return () => {
4331
- const now = Date.now();
4332
- last = now > last ? now : last + 1;
4333
- return new Date(last).toISOString();
4334
- };
4335
- }
4336
- //#endregion
4337
- //#region src/campaign/types.ts
4338
- /** Type guard: a proposal carrying its rationale vs a bare
4339
- * surface. The loop branches on this to populate `GenerationCandidate`. */
4340
- function isProposedCandidate(value) {
4341
- return typeof value === "object" && value !== null && "surface" in value && "label" in value && "rationale" in value;
4342
- }
4343
- const LABEL_TRUST_RANK = {
4344
- unverified: 0,
4345
- "verified-signal": 1,
4346
- "human-rated": 2
4347
- };
4348
- /** Ordinal rank for a label-trust tier; absent ⇒ `unverified` (rank 0). */
4349
- function labelTrustRank(trust) {
4350
- return LABEL_TRUST_RANK[trust ?? "unverified"];
4351
- }
4352
4843
  //#endregion
4353
4844
  //#region src/campaign/presets/run-optimization.ts
4354
4845
  /**
@@ -4404,18 +4895,26 @@ async function runOptimization(opts) {
4404
4895
  const label = opts.premeasuredBaseline ? "premeasured baseline" : "baseline";
4405
4896
  throw new Error(`runOptimization: ${label} is incomplete (${baselineCoverage.scorableCellIds.length}/${baselineCoverage.expectedCellIds.length} designed cells scorable) — ${formatCoverageFailures(baselineCoverage)}. Refusing to optimize against an incomplete incumbent.`);
4406
4897
  }
4407
- const recorder = opts.searchLedger ? await SearchRecorder.open({
4898
+ const opened = opts.searchLedger ? await OptimizationSearch.open({
4408
4899
  binding: opts.searchLedger,
4409
4900
  storage,
4410
- runDir: opts.runDir,
4901
+ costLedger,
4411
4902
  scenarios: opts.scenarios,
4412
4903
  reps,
4413
- maxGenerations: opts.maxGenerations,
4414
- populationSize: opts.populationSize,
4904
+ seed: opts.seed ?? 42,
4415
4905
  splitDigest: baselineCampaign.splitDigest,
4416
- proposerLabel: proposer.kind,
4417
- costLedger
4906
+ proposerName: proposer.kind,
4907
+ expansion: opts.selectParent ? "select-parent" : "incumbent",
4908
+ maxUsd: opts.costCeiling ?? null,
4909
+ baselineSurface,
4910
+ baselineCells: baselineCampaign.cells
4418
4911
  }) : void 0;
4912
+ const search = opened?.search;
4913
+ const nodeIdByHash = /* @__PURE__ */ new Map();
4914
+ if (opened) nodeIdByHash.set(surfaceHash(baselineSurface), opened.rootNodeId);
4915
+ const incompleteNodeIds = /* @__PURE__ */ new Set();
4916
+ const promotedNodeIds = /* @__PURE__ */ new Set();
4917
+ let stopReason = "max-nodes";
4419
4918
  const generations = [];
4420
4919
  const history = [];
4421
4920
  let currentFindings = initialFindings;
@@ -4453,7 +4952,10 @@ async function runOptimization(opts) {
4453
4952
  }
4454
4953
  for (let gen = 0; gen < opts.maxGenerations; gen++) {
4455
4954
  const proposalHistory = immutableProposalSnapshot(history, "history");
4456
- if (proposer.decide?.({ history: proposalHistory }).stop) break;
4955
+ if (proposer.decide?.({ history: proposalHistory }).stop) {
4956
+ stopReason = "converged";
4957
+ break;
4958
+ }
4457
4959
  const paretoParents = immutableProposalSnapshot(computeParetoFrontier(scored), "Pareto parents");
4458
4960
  const incumbentOutcome = immutableProposalSnapshot(winnerOutcome, "incumbent outcome");
4459
4961
  const selected = opts.selectParent ? resolveSelectedParent(opts.selectParent(Object.freeze({
@@ -4481,10 +4983,15 @@ async function runOptimization(opts) {
4481
4983
  costLedger,
4482
4984
  costPhase: "search.proposal"
4483
4985
  });
4986
+ await search?.startGeneration(gen);
4484
4987
  const proposed = await proposer.propose(proposalContext);
4485
4988
  if (!Array.isArray(proposed)) throw new TypeError("runOptimization: proposer must return an array");
4486
4989
  const proposalSnapshot = immutableProposalSnapshot(proposed, "candidate outputs");
4487
- if (proposalSnapshot.length === 0) break;
4990
+ if (proposalSnapshot.length === 0) {
4991
+ await search?.failGeneration(gen, "the proposer returned no candidates");
4992
+ stopReason = "converged";
4993
+ break;
4994
+ }
4488
4995
  const candidates = proposalSnapshot.map((p) => isProposedCandidate(p) ? p : {
4489
4996
  surface: p,
4490
4997
  label: "",
@@ -4497,15 +5004,22 @@ async function runOptimization(opts) {
4497
5004
  generationHashes.add(hash);
4498
5005
  }
4499
5006
  for (const hash of generationHashes) admittedCandidateHashes.add(hash);
4500
- await recorder?.recordGeneration({
4501
- generation: gen,
4502
- parentSurfaceHash,
4503
- candidates: candidates.map(({ surface, label }) => ({
4504
- surface,
4505
- surfaceHash: surfaceHash(surface),
4506
- ...label ? { label } : {}
4507
- }))
4508
- });
5007
+ if (search) {
5008
+ const nodeIds = await search.recordProposal({
5009
+ generation: gen,
5010
+ parent: {
5011
+ nodeId: nodeIdByHash.get(parentSurfaceHash),
5012
+ surface: parentSurface,
5013
+ composite: parentComposite
5014
+ },
5015
+ selectionRule: opts.selectParent ? "select-parent" : "incumbent",
5016
+ candidates
5017
+ });
5018
+ for (const [index, nodeId] of nodeIds.entries()) {
5019
+ nodeIdByHash.set(surfaceHash(candidates[index].surface), nodeId);
5020
+ await search.allocate(nodeId);
5021
+ }
5022
+ }
4509
5023
  const surfaceResults = await mapConcurrent(candidates, candidateConcurrency, async ({ surface, label, rationale, attribution }, i, signal) => {
4510
5024
  const hash = surfaceHash(surface);
4511
5025
  const campaign = await runCampaign({
@@ -4544,13 +5058,11 @@ async function runOptimization(opts) {
4544
5058
  });
4545
5059
  }
4546
5060
  }
4547
- await recorder?.recordResults(surfaceResults.map((result) => ({
4548
- surface: result.surface,
4549
- surfaceHash: result.surfaceHash,
4550
- cells: result.campaign.cells,
4551
- runDir: result.campaign.runDir,
4552
- coverageComplete: result.coverage.complete
4553
- })));
5061
+ if (search) for (const result of surfaceResults) {
5062
+ const nodeId = nodeIdByHash.get(result.surfaceHash);
5063
+ await search.recordCells(nodeId, "train", result.campaign.cells);
5064
+ if (!result.coverage.complete) incompleteNodeIds.add(nodeId);
5065
+ }
4554
5066
  surfaceResults.sort((a, b) => {
4555
5067
  if (a.coverage.complete !== b.coverage.complete) return a.coverage.complete ? -1 : 1;
4556
5068
  if (a.rankKey && b.rankKey) return compareRankKeys(b.rankKey, a.rankKey);
@@ -4560,6 +5072,8 @@ async function runOptimization(opts) {
4560
5072
  const promoted = top && compareRankKeys(top.rankKey, winnerRankKey) > 0 ? [top] : [];
4561
5073
  if (promoted[0]) {
4562
5074
  const top = promoted[0];
5075
+ const promotedNodeId = nodeIdByHash.get(top.surfaceHash);
5076
+ if (promotedNodeId) promotedNodeIds.add(promotedNodeId);
4563
5077
  winnerSurface = top.surface;
4564
5078
  winnerSurfaceHash = top.surfaceHash;
4565
5079
  winnerComposite = top.composite;
@@ -4621,9 +5135,11 @@ async function runOptimization(opts) {
4621
5135
  currentFindings = immutableProposalSnapshot(assertProposalFindings(fresh, "runOptimization generation analysis findings"), "generation analysis findings");
4622
5136
  }
4623
5137
  }
4624
- const searchHistory = await recorder?.finish({
4625
- winnerSurfaceHash,
4626
- generationsRun: generations.length,
5138
+ const searchHistory = await search?.finish({
5139
+ winnerNodeId: nodeIdByHash.get(winnerSurfaceHash),
5140
+ promotedNodeIds,
5141
+ incompleteNodeIds,
5142
+ reason: stopReason,
4627
5143
  runId: opts.runDir
4628
5144
  });
4629
5145
  return {
@@ -6154,6 +6670,6 @@ function firstString(value) {
6154
6670
  return typeof value === "string" && value.trim() ? value : void 0;
6155
6671
  }
6156
6672
  //#endregion
6157
- export { tangleTracesRoot as $, openSearchLedger as A, DEFAULT_RED_TEAM_CORPUS as B, assertSearchHistoryAdmissionOptions as C, verifySearchHistoryArtifact as D, searchHistoryCoverageRow as E, paretoFrontierWithCrowding as F, assertIndependentEvaluationSplit as G, redTeamReport as H, runFinalComparison as I, reserveFinalEvidence as J, captureFinalEvidencePolicy as K, openAutoPr as L, validateSearchLedgerEvent as M, dominates as N, verifySearchHistoryReceipt as O, paretoFrontier as P, resolveRunDir as Q, captureJudge as R, assertCompleteSearchHistory as S, createSearchHistoryReceipt as T, scoreRedTeamOutput as U, redTeamDataset as V, runCanaries as W, runCampaign as X, runEval as Y, planCampaignRun as Z, isProposedCandidate as _, transientDispatchFailure as a, recordCandidatePopulationSearch as b, compareOptimizationMethods as c, combineComparisonCosts as d, readCachedCell as et, costFromLedgerSummary as f, runOptimization as g, runImprovementLoop as h, quotaExhaustedUntil as i, replaySearchLedgerText as j, FileSearchLedger as k, optimizationTokenUsageFromSummary as l, readGepaCandidatePopulationArtifact as m, JudgeParseError as n, cellCachePath as nt, aggregateRunScore as o, assertGepaCandidatePopulationSummary as p, evaluationUnitMap as q, isTransientTransportFailure as r, computeManifestHash as rt, clamp01 as s, llmJudge as t, buildCellSchedule as tt, executeOptimizationMethod as u, labelTrustRank as v, assertSearchHistoryMatchesReplay as w, SearchHistoryRequiredError as x, SearchRecorder as y, defaultProductionGate as z };
6673
+ export { scoreRedTeamOutput as $, FileSearchLedger as A, SEARCH_LEDGER_SCHEMA as B, assertCompleteSearchHistory as C, searchHistoryCoverageRow as D, createSearchHistoryReceipt as E, SearchStateView as F, paretoFrontierWithCrowding as G, labelTrustRank as H, searchCellId as I, captureJudge as J, runFinalComparison as K, searchNodeId as L, replaySearchLedgerText as M, validateSearchLedgerEvent as N, verifySearchHistoryArtifact as O, SearchState as P, redTeamReport as Q, searchTaskSetDigest as R, SearchHistoryRequiredError as S, assertSearchHistoryMatchesState as T, dominates as U, isProposedCandidate as V, paretoFrontier as W, DEFAULT_RED_TEAM_CORPUS as X, defaultProductionGate as Y, redTeamDataset as Z, SearchRecorder as _, transientDispatchFailure as a, runEval as at, surfaceDiff as b, compareOptimizationMethods as c, resolveRunDir as ct, combineComparisonCosts as d, buildCellSchedule as dt, runCanaries as et, costFromLedgerSummary as f, cellCachePath as ft, runOptimization as g, runImprovementLoop as h, quotaExhaustedUntil as i, reserveFinalEvidence as it, openSearchLedger as j, verifySearchHistoryReceipt as k, optimizationTokenUsageFromSummary as l, tangleTracesRoot as lt, readGepaCandidatePopulationArtifact as m, JudgeParseError as n, captureFinalEvidencePolicy as nt, aggregateRunScore as o, runCampaign as ot, assertGepaCandidatePopulationSummary as p, computeManifestHash as pt, openAutoPr as q, isTransientTransportFailure as r, evaluationUnitMap as rt, clamp01 as s, planCampaignRun as st, llmJudge as t, assertIndependentEvaluationSplit as tt, executeOptimizationMethod as u, readCachedCell as ut, developmentClaim as v, assertSearchHistoryAdmissionOptions as w, surfaceNode as x, searchEdgeId as y, searchUnitScores as z };
6158
6674
 
6159
- //# sourceMappingURL=llm-judge-DF18J3CR.js.map
6675
+ //# sourceMappingURL=llm-judge-bwbgDMP0.js.map