@tangle-network/agent-eval 0.189.0 → 0.190.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/README.md +3 -2
  3. package/dist/adapters/http.d.ts +10 -1
  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/analyst/index.d.ts +66 -3
  8. package/dist/analyst/index.d.ts.map +1 -1
  9. package/dist/analyst/index.js +212 -7
  10. package/dist/analyst/index.js.map +1 -1
  11. package/dist/{benchmark-command-BhhAYstU.js → benchmark-command-BXIr_k8D.js} +12 -11
  12. package/dist/benchmark-command-BXIr_k8D.js.map +1 -0
  13. package/dist/benchmarks/index.d.ts +1 -1
  14. package/dist/benchmarks/index.js +3 -3
  15. package/dist/campaign/index.d.ts +4 -4
  16. package/dist/campaign/index.js +7 -7
  17. package/dist/{campaign-BWoSQPSU.js → campaign-B7q74uIh.js} +256 -11
  18. package/dist/campaign-B7q74uIh.js.map +1 -0
  19. package/dist/{campaign-evidence-CBlGJ3kC.js → campaign-evidence-D40-LHIL.js} +6 -5
  20. package/dist/campaign-evidence-D40-LHIL.js.map +1 -0
  21. package/dist/{chat-client-C4dg7Ng_.js → chat-client-B_ItsSBw.js} +2 -2
  22. package/dist/{chat-client-C4dg7Ng_.js.map → chat-client-B_ItsSBw.js.map} +1 -1
  23. package/dist/{chat-json-call-JVGbfx95.js → chat-json-call-CXHBvUz8.js} +2 -2
  24. package/dist/{chat-json-call-JVGbfx95.js.map → chat-json-call-CXHBvUz8.js.map} +1 -1
  25. package/dist/claim-BGOUqoMq.d.ts +38 -0
  26. package/dist/claim-BGOUqoMq.d.ts.map +1 -0
  27. package/dist/cli.js +3 -3
  28. package/dist/contract/index.d.ts +4 -4
  29. package/dist/contract/index.js +8 -8
  30. package/dist/{default-registry-m2jhbG6Q.js → default-registry-16I-Bxlf.js} +2 -2
  31. package/dist/{default-registry-m2jhbG6Q.js.map → default-registry-16I-Bxlf.js.map} +1 -1
  32. package/dist/{define-agent-eval-98sgUaC1.js → define-agent-eval-DPizhHfy.js} +4 -4
  33. package/dist/{define-agent-eval-98sgUaC1.js.map → define-agent-eval-DPizhHfy.js.map} +1 -1
  34. package/dist/{define-agent-eval-q6890HU4.d.ts → define-agent-eval-DblGedQk.d.ts} +4 -3
  35. package/dist/define-agent-eval-DblGedQk.d.ts.map +1 -0
  36. package/dist/diagnosis/index.d.ts +14 -2
  37. package/dist/diagnosis/index.d.ts.map +1 -1
  38. package/dist/diagnosis/index.js +11 -4
  39. package/dist/diagnosis/index.js.map +1 -1
  40. package/dist/{dspy-rlm-engine-DLYttKBm.js → dspy-rlm-engine-Cs5LzJ-f.js} +3 -3
  41. package/dist/{dspy-rlm-engine-DLYttKBm.js.map → dspy-rlm-engine-Cs5LzJ-f.js.map} +1 -1
  42. package/dist/{eval-campaign-C0Q1Eeo_.js → eval-campaign-CwsBn0Gm.js} +2 -2
  43. package/dist/{eval-campaign-C0Q1Eeo_.js.map → eval-campaign-CwsBn0Gm.js.map} +1 -1
  44. package/dist/experiment/index.d.ts +2 -1
  45. package/dist/experiment/index.d.ts.map +1 -1
  46. package/dist/experiment/index.js +1 -1
  47. package/dist/exporters-XMCYJlqT.d.ts +161 -0
  48. package/dist/exporters-XMCYJlqT.d.ts.map +1 -0
  49. package/dist/{external-optimizer-process-C3quNeKV.js → external-optimizer-process-BnSmrkgL.js} +3 -3
  50. package/dist/{external-optimizer-process-C3quNeKV.js.map → external-optimizer-process-BnSmrkgL.js.map} +1 -1
  51. package/dist/{external-optimizer-subprocess-BDwgKLO1.js → external-optimizer-subprocess-kbcPu5po.js} +3 -3
  52. package/dist/{external-optimizer-subprocess-BDwgKLO1.js.map → external-optimizer-subprocess-kbcPu5po.js.map} +1 -1
  53. package/dist/{feedback-trajectory-B3ZSkfAq.d.ts → feedback-trajectory-BYjh7NEM.d.ts} +2 -2
  54. package/dist/{feedback-trajectory-B3ZSkfAq.d.ts.map → feedback-trajectory-BYjh7NEM.d.ts.map} +1 -1
  55. package/dist/{index-CnJOPR_E.d.ts → index-CJv56rhX.d.ts} +673 -244
  56. package/dist/index-CJv56rhX.d.ts.map +1 -0
  57. package/dist/{index-Dk1dOUfm.d.ts → index-CWoIJHmy.d.ts} +4 -4
  58. package/dist/{index-Dk1dOUfm.d.ts.map → index-CWoIJHmy.d.ts.map} +1 -1
  59. package/dist/{index-Bcc9PkOz.d.ts → index-D4pC7Tvd.d.ts} +3 -2
  60. package/dist/index-D4pC7Tvd.d.ts.map +1 -0
  61. package/dist/{index-BqDJ6fUK.d.ts → index-I4UJGRno.d.ts} +74 -2
  62. package/dist/index-I4UJGRno.d.ts.map +1 -0
  63. package/dist/index.d.ts +66 -17
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +375 -31
  66. package/dist/index.js.map +1 -1
  67. package/dist/{journal-CDP0Vhx_.js → journal-c3qY0SDP.js} +246 -5
  68. package/dist/journal-c3qY0SDP.js.map +1 -0
  69. package/dist/{kind-factory-BGwN0nE6.js → kind-factory-ByZeXDqF.js} +2 -2
  70. package/dist/{kind-factory-BGwN0nE6.js.map → kind-factory-ByZeXDqF.js.map} +1 -1
  71. package/dist/ledger-core/index.d.ts +2 -2
  72. package/dist/ledger-core/index.js +2 -2
  73. package/dist/{llm-client-DF9bPNYA.js → llm-client-KYQBh9C8.js} +2 -2
  74. package/dist/{llm-client-DF9bPNYA.js.map → llm-client-KYQBh9C8.js.map} +1 -1
  75. package/dist/{llm-judge-M_l_WXw2.js → llm-judge-BnxAv7ed.js} +1892 -1378
  76. package/dist/llm-judge-BnxAv7ed.js.map +1 -0
  77. package/dist/mint-C9Lojt1V.d.ts +147 -0
  78. package/dist/mint-C9Lojt1V.d.ts.map +1 -0
  79. package/dist/openapi.json +1 -1
  80. package/dist/pipelines/index.d.ts +1 -1
  81. package/dist/pipelines/index.js +1 -1
  82. package/dist/pipelines/index.js.map +1 -1
  83. package/dist/{produced-state-BOY98IJO.js → produced-state-BXjXJHJT.js} +4 -4
  84. package/dist/{produced-state-BOY98IJO.js.map → produced-state-BXjXJHJT.js.map} +1 -1
  85. package/dist/{raw-provider-sink-C96CcqeG.js → raw-provider-sink-DYh4Bszw.js} +2 -2
  86. package/dist/{raw-provider-sink-C96CcqeG.js.map → raw-provider-sink-DYh4Bszw.js.map} +1 -1
  87. package/dist/{redact-DxwlUAvh.js → redact-B7rt9Zot.js} +130 -10
  88. package/dist/redact-B7rt9Zot.js.map +1 -0
  89. package/dist/{redact-DPoH1-OW.d.ts → redact-CopuMxll.d.ts} +7 -5
  90. package/dist/redact-CopuMxll.d.ts.map +1 -0
  91. package/dist/rl.d.ts +3 -2
  92. package/dist/rl.d.ts.map +1 -1
  93. package/dist/rl.js +1 -1
  94. package/dist/rollout/index.d.ts +3 -2
  95. package/dist/search-ledger-types-C9anU2DX.d.ts +489 -0
  96. package/dist/search-ledger-types-C9anU2DX.d.ts.map +1 -0
  97. package/dist/{semantic-concept-judge-Cm2K2wAo.js → semantic-concept-judge-ClB73cei.js} +6 -6
  98. package/dist/{semantic-concept-judge-Cm2K2wAo.js.map → semantic-concept-judge-ClB73cei.js.map} +1 -1
  99. package/dist/{server-C8XvqCg8.js → server-DtetGGoA.js} +2 -2
  100. package/dist/{server-C8XvqCg8.js.map → server-DtetGGoA.js.map} +1 -1
  101. package/dist/{skillopt-optimization-method-BVSB676o.js → skillopt-optimization-method-l2vDBtKp.js} +363 -18
  102. package/dist/skillopt-optimization-method-l2vDBtKp.js.map +1 -0
  103. package/dist/{spans-W0o8pbEg.d.ts → spans-BWtjk_9c.d.ts} +2 -2
  104. package/dist/{spans-W0o8pbEg.d.ts.map → spans-BWtjk_9c.d.ts.map} +1 -1
  105. package/dist/{statistical-heldout-plAHG82H.d.ts → statistical-heldout-CLpjYffJ.d.ts} +2 -36
  106. package/dist/statistical-heldout-CLpjYffJ.d.ts.map +1 -0
  107. package/dist/{store-otlp-D0AV4cvB.js → store-otlp-84DH0OEH.js} +2 -2
  108. package/dist/{store-otlp-D0AV4cvB.js.map → store-otlp-84DH0OEH.js.map} +1 -1
  109. package/dist/{store-tool-spans-8PAyhRqb.d.ts → store-tool-spans-BBXs8hDP.d.ts} +2 -2
  110. package/dist/{store-tool-spans-8PAyhRqb.d.ts.map → store-tool-spans-BBXs8hDP.d.ts.map} +1 -1
  111. package/dist/{store-tool-spans-B_0mXvYl.js → store-tool-spans-DUiqyHvR.js} +5 -5
  112. package/dist/{store-tool-spans-B_0mXvYl.js.map → store-tool-spans-DUiqyHvR.js.map} +1 -1
  113. package/dist/traces.d.ts +3 -63
  114. package/dist/traces.d.ts.map +1 -1
  115. package/dist/traces.js +6 -195
  116. package/dist/traces.js.map +1 -1
  117. package/dist/wire/index.d.ts +1 -1
  118. package/dist/wire/index.js +1 -1
  119. package/docs/adapters-observability.md +31 -27
  120. package/docs/charter.md +1 -1
  121. package/docs/design/mlbenchmarks-book-review.md +1 -1
  122. package/docs/distributed-driver.md +1 -5
  123. package/docs/experiment.md +15 -0
  124. package/docs/public-api.md +181 -71
  125. package/docs/redaction.md +7 -1
  126. package/docs/search-ledger.md +174 -0
  127. package/docs/trace-contracts.md +48 -3
  128. package/package.json +1 -1
  129. package/dist/benchmark-command-BhhAYstU.js.map +0 -1
  130. package/dist/campaign-BWoSQPSU.js.map +0 -1
  131. package/dist/campaign-evidence-CBlGJ3kC.js.map +0 -1
  132. package/dist/define-agent-eval-q6890HU4.d.ts.map +0 -1
  133. package/dist/index-Bcc9PkOz.d.ts.map +0 -1
  134. package/dist/index-BqDJ6fUK.d.ts.map +0 -1
  135. package/dist/index-CnJOPR_E.d.ts.map +0 -1
  136. package/dist/journal-CDP0Vhx_.js.map +0 -1
  137. package/dist/llm-judge-M_l_WXw2.js.map +0 -1
  138. package/dist/mint-DsS38v5c.d.ts +0 -305
  139. package/dist/mint-DsS38v5c.d.ts.map +0 -1
  140. package/dist/redact-DPoH1-OW.d.ts.map +0 -1
  141. package/dist/redact-DxwlUAvh.js.map +0 -1
  142. package/dist/search-ledger-types-D7KDVg-o.d.ts +0 -302
  143. package/dist/search-ledger-types-D7KDVg-o.d.ts.map +0 -1
  144. package/dist/skillopt-optimization-method-BVSB676o.js.map +0 -1
  145. package/dist/statistical-heldout-plAHG82H.d.ts.map +0 -1
  146. package/docs/search-history-receipts.md +0 -233
@@ -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-BRlM99rX.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
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-CBlGJ3kC.js";
11
- import { d as mapConcurrent, n as replayLedgerText, t as FileLedgerJournal } from "./journal-CDP0Vhx_.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-D40-LHIL.js";
11
+ import { n as replayLedgerText, p as mapConcurrent, t as FileLedgerJournal } from "./journal-c3qY0SDP.js";
12
12
  import { t as deepFreezeCanonicalJson } from "./deep-freeze-CurcOQg3.js";
13
13
  import "./ledger-core/index.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-BDwgKLO1.js";
15
- 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-kbcPu5po.js";
15
+ import { f as redactText, n as REDACTION_VERSION, o as detectCredential, s as detectPersonalData } from "./redact-B7rt9Zot.js";
16
16
  import { t as detectRewardHacking } from "./reward-hacking-D0XwhVWE.js";
17
17
  import { n as assertProposalFindings, t as combineAbortSignals } from "./abort-signal-CtzAM_sJ.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-KYQBh9C8.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,339 +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
- const entries = [];
2563
- let planEvent = null;
2564
- let completion = null;
2565
- let previousOccurredAt = Number.NEGATIVE_INFINITY;
2566
- const apply = (entry, index) => {
2567
- entries.push(entry);
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) {
2568
2716
  const event = entry.event;
2569
- 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}`);
2570
2718
  const occurredAt = Date.parse(event.occurredAt);
2571
- if (occurredAt < previousOccurredAt) throw new SearchLedgerIntegrityError(`event ${event.eventId} occurred before the preceding durable event`);
2572
- previousOccurredAt = occurredAt;
2719
+ if (occurredAt < this.lastOccurredAt) throw integrity(`event ${event.eventId} occurred before the preceding event`);
2573
2720
  assertUnique(event.artifacts.map(artifactKey), "artifact receipt", event.eventId);
2574
- if (event.kind === "search-planned") {
2575
- if (index !== 0 || planEvent) throw new SearchLedgerIntegrityError("search plan must be the first and only plan event");
2576
- assertUnique(event.plan.candidateSlots.map((slot) => slot.slotId), "candidate slot", event.eventId);
2577
- assertUnique(event.plan.tasks.map((task) => task.taskId), "planned taskId", event.eventId);
2578
- assertUnique(event.plan.operations.map((operation) => operation.operationId), "planned operationId", event.eventId);
2579
- for (const operation of event.plan.operations) plannedOperations.set(operation.operationId, operation);
2580
- for (const slot of event.plan.candidateSlots) {
2581
- assertSlotGenerationOperation(slot, plannedOperations);
2582
- 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);
2583
2795
  }
2584
- planEvent = event;
2585
- return;
2586
2796
  }
2587
- if (!planEvent) throw new SearchLedgerIntegrityError(`event ${event.eventId} appears before the required search plan`);
2588
- if (event.kind === "search-plan-extended") {
2589
- assertUnique(event.extension.candidateSlots.map((slot) => slot.slotId), "candidate slot", event.eventId);
2590
- assertUnique(event.extension.operations.map((operation) => operation.operationId), "planned operationId", event.eventId);
2591
- for (const operation of event.extension.operations) {
2592
- if (plannedOperations.has(operation.operationId)) throw new SearchLedgerIntegrityError(`plan extension ${event.eventId} re-plans operation ${operation.operationId}`);
2593
- 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
2594
2861
  }
2595
- for (const slot of event.extension.candidateSlots) {
2596
- if (plannedSlots.has(slot.slotId)) throw new SearchLedgerIntegrityError(`plan extension ${event.eventId} re-plans candidate slot ${slot.slotId}`);
2597
- assertSlotGenerationOperation(slot, plannedOperations);
2598
- 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;
2599
2890
  }
2600
- planExtensions.push(event);
2601
- 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);
2602
2897
  }
2603
- if (event.kind === "candidate-registered") {
2604
- if (candidates.has(event.candidateId)) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} was registered twice`);
2605
- const plannedSlot = plannedSlots.get(event.slotId);
2606
- if (!plannedSlot) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} binds unknown slot ${event.slotId}`);
2607
- if (candidateBySlot.has(event.slotId)) throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} was bound twice`);
2608
- if (closedSlots.has(event.slotId)) throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} was already closed`);
2609
- if (event.generationOperationId !== plannedSlot.generationOperationId) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} generation operation ${event.generationOperationId} does not match slot ${event.slotId} plan ${plannedSlot.generationOperationId}`);
2610
- const generationOperation = operationsById.get(event.generationOperationId);
2611
- if (!generationOperation) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} precedes generation operation ${event.generationOperationId}`);
2612
- if (generationOperation.outcome.status === "failed") throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} cannot bind failed generation operation ${event.generationOperationId}`);
2613
- const previousCandidate = lineageNodes.get(event.lineage.lineageNodeId);
2614
- if (previousCandidate) throw new SearchLedgerIntegrityError(`lineage node ${event.lineage.lineageNodeId} is already bound to ${previousCandidate}`);
2615
- assertUnique(event.lineage.parentCandidateIds, "parentCandidateId", event.eventId);
2616
- const parents = event.lineage.parentCandidateIds.map((id) => {
2617
- const parent = candidates.get(id);
2618
- if (!parent) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} references unknown parent ${id}`);
2619
- return parent;
2620
- });
2621
- const expectedGeneration = parents.length === 0 ? 0 : Math.max(...parents.map((parent) => parent.registered.lineage.generation)) + 1;
2622
- if (event.lineage.generation !== expectedGeneration) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} generation ${event.lineage.generation} does not follow its parents (expected ${expectedGeneration})`);
2623
- assertUnique(event.surfaces.map((surface) => surface.surfaceId), "surfaceId", event.eventId);
2624
- candidates.set(event.candidateId, {
2625
- registered: event,
2626
- attempts: [],
2627
- decision: null
2628
- });
2629
- candidateBySlot.set(event.slotId, event.candidateId);
2630
- lineageNodes.set(event.lineage.lineageNodeId, event.candidateId);
2631
- candidateEvents.push(event);
2632
- 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);
2633
2910
  }
2634
- if (event.kind === "task-attempted") {
2635
- const candidate = candidates.get(event.candidateId);
2636
- if (!candidate) throw new SearchLedgerIntegrityError(`attempt ${event.eventId} references unknown candidate ${event.candidateId}`);
2637
- if (candidate.decision) throw new SearchLedgerIntegrityError(`attempt ${event.eventId} appears after candidate ${event.candidateId} was decided`);
2638
- const plannedTask = planEvent.plan.tasks.find((task) => task.taskId === event.task.taskId);
2639
- if (!plannedTask) throw new SearchLedgerIntegrityError(`attempt ${event.eventId} references unplanned task ${event.task.taskId}`);
2640
- 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`);
2641
- if (event.attemptIndex >= plannedTask.maxAttempts) throw new SearchLedgerIntegrityError(`task ${event.task.taskId} attempt ${event.attemptIndex} exceeds planned maxAttempts ${plannedTask.maxAttempts}`);
2642
- if (runIds.has(event.runId)) throw new SearchLedgerIntegrityError(`runId ${event.runId} was recorded twice`);
2643
- runIds.add(event.runId);
2644
- const attemptKey = canonicalString([
2645
- event.candidateId,
2646
- event.task.taskId,
2647
- event.attemptIndex
2648
- ]);
2649
- if (attemptKeys.has(attemptKey)) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} task ${event.task.taskId} attempt ${event.attemptIndex} was recorded twice`);
2650
- const expectedAttemptIndex = candidate.attempts.filter((attempt) => attempt.task.taskId === event.task.taskId).length;
2651
- if (event.attemptIndex !== expectedAttemptIndex) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} task ${event.task.taskId} attempt index ${event.attemptIndex} is not contiguous (expected ${expectedAttemptIndex})`);
2652
- const previousAttempt = candidate.attempts.find((attempt) => attempt.task.taskId === event.task.taskId);
2653
- if (previousAttempt?.outcome.status !== void 0 && previousAttempt.outcome.status !== "errored") throw new SearchLedgerIntegrityError(`task ${event.task.taskId} was retried after a measured outcome`);
2654
- if (previousAttempt && canonicalString({
2655
- task: previousAttempt.task,
2656
- identity: previousAttempt.identity
2657
- }) !== canonicalString({
2658
- task: event.task,
2659
- identity: event.identity
2660
- })) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} task ${event.task.taskId} changed immutable execution identity between attempts`);
2661
- attemptKeys.add(attemptKey);
2662
- const declared = candidate.registered.surfaces.map((surface) => surface.surfaceId).sort();
2663
- const observed = event.surfaceEvidence.map((surface) => surface.surfaceId).sort();
2664
- assertUnique(observed, "surface evidence", event.eventId);
2665
- for (const evidence of event.surfaceEvidence) assertUnique(evidence.evidence.map(artifactKey), "surface evidence receipt", event.eventId);
2666
- if (canonicalString(declared) !== canonicalString(observed)) throw new SearchLedgerIntegrityError(`attempt ${event.eventId} surface evidence does not exactly cover candidate ${event.candidateId}`);
2667
- candidate.attempts.push(event);
2668
- attempts.push(event);
2669
- 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;
2670
2983
  }
2671
- if (event.kind === "search-operation-recorded") {
2672
- const plannedOperation = plannedOperations.get(event.operationId);
2673
- if (!plannedOperation) throw new SearchLedgerIntegrityError(`operation ${event.operationId} was not declared in the search plan`);
2674
- if (plannedOperation.kind !== event.operationKind) throw new SearchLedgerIntegrityError(`operation ${event.operationId} kind ${event.operationKind} does not match planned ${plannedOperation.kind}`);
2675
- if (operationsById.has(event.operationId)) throw new SearchLedgerIntegrityError(`operation ${event.operationId} was recorded twice`);
2676
- operationsById.set(event.operationId, event);
2677
- operationEvents.push(event);
2678
- return;
2984
+ if (cost.unknown && cell.costKnown) {
2985
+ cell.costKnown = false;
2986
+ this.audit.spend.unknownCostCells += 1;
2987
+ node.spend.unknownCostCells += 1;
2679
2988
  }
2680
- if (event.kind === "candidate-slot-closed") {
2681
- const plannedSlot = plannedSlots.get(event.slotId);
2682
- if (!plannedSlot) throw new SearchLedgerIntegrityError(`candidate slot closure ${event.eventId} references unknown slot ${event.slotId}`);
2683
- 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}`);
2684
- if (candidateBySlot.has(event.slotId)) throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} was already bound to a candidate`);
2685
- if (closedSlots.has(event.slotId)) throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} was closed twice`);
2686
- const operation = operationsById.get(event.generationOperationId);
2687
- if (!operation) throw new SearchLedgerIntegrityError(`candidate slot closure ${event.eventId} precedes operation ${event.generationOperationId}`);
2688
- if (operation.outcome.status === "completed") throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} cannot close from completed operation ${event.generationOperationId}`);
2689
- closedSlots.set(event.slotId, event);
2690
- closedSlotEvents.push(event);
2691
- 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;
2692
3000
  }
2693
- if (event.kind === "candidate-decided") {
2694
- const candidate = candidates.get(event.candidateId);
2695
- if (!candidate) throw new SearchLedgerIntegrityError(`decision ${event.eventId} references unknown candidate ${event.candidateId}`);
2696
- if (candidate.decision) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} was decided twice`);
2697
- if (event.decision.status === "selected") {
2698
- if (!candidate.attempts.some((attempt) => attempt.outcome.status !== "errored")) throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} cannot be selected without a measured task outcome`);
2699
- if (decisions.some((decision) => decision.decision.status === "selected")) throw new SearchLedgerIntegrityError("more than one candidate was selected");
2700
- }
2701
- candidate.decision = event;
2702
- decisions.push(event);
2703
- 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;
2704
3029
  }
2705
- const missingCandidateSlots = [...plannedSlots.values()].filter((slot) => !candidateBySlot.has(slot.slotId) && !closedSlots.has(slot.slotId)).map((slot) => slot.slotId);
2706
- if (missingCandidateSlots.length > 0) throw new SearchLedgerIntegrityError(`search completed with missing candidate slots: ${missingCandidateSlots.join(", ")}`);
2707
- const missingTaskOutcomes = plannedTaskOutcomeKeys(planEvent, candidates);
2708
- if (missingTaskOutcomes.length > 0) throw new SearchLedgerIntegrityError(`search completed with missing task outcomes: ${missingTaskOutcomes.join(", ")}`);
2709
- const missingOperations = [...plannedOperations.values()].filter((operation) => !operationsById.has(operation.operationId)).map((operation) => operation.operationId);
2710
- if (missingOperations.length > 0) throw new SearchLedgerIntegrityError(`search completed with missing search operations: ${missingOperations.join(", ")}`);
2711
- for (const operation of plannedOperations.values()) {
2712
- if (operation.kind !== "candidate-generation") continue;
2713
- const generationOutcome = operationsById.get(operation.operationId).outcome.status;
2714
- const slots = [...plannedSlots.values()].filter((slot) => slot.generationOperationId === operation.operationId);
2715
- if (slots.length === 0) continue;
2716
- const registeredCount = slots.filter((slot) => candidateBySlot.has(slot.slotId)).length;
2717
- const closedCount = slots.filter((slot) => closedSlots.has(slot.slotId)).length;
2718
- if (generationOutcome === "completed" && closedCount > 0) throw new SearchLedgerIntegrityError(`completed generation operation ${operation.operationId} contains ${closedCount} closed slot(s)`);
2719
- if (generationOutcome === "failed" && registeredCount > 0) throw new SearchLedgerIntegrityError(`failed generation operation ${operation.operationId} contains ${registeredCount} registered candidate(s)`);
2720
- 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`);
2721
3040
  }
2722
- const pending = [...candidates.values()].filter((candidate) => candidate.decision === null);
2723
- if (pending.length > 0) throw new SearchLedgerIntegrityError(`search completed with ${pending.length} candidate decision(s) missing`);
2724
- const selected = decisions.filter((decision) => decision.decision.status === "selected");
2725
- if (event.result.status === "selected") {
2726
- if (selected.length !== 1 || selected[0].candidateId !== event.result.candidateId) throw new SearchLedgerIntegrityError(`search completion winner ${event.result.candidateId} does not match candidate decisions`);
2727
- } else if (selected.length !== 0) throw new SearchLedgerIntegrityError("all-rejected completion contains a selected candidate");
2728
- completion = event;
2729
- };
2730
- const snapshot = () => {
2731
- const selectedDecisions = decisions.filter((decision) => decision.decision.status === "selected");
2732
- const rejectedDecisions = decisions.filter((decision) => decision.decision.status === "rejected");
2733
- const outcomeCounts = {
2734
- passed: 0,
2735
- failed: 0,
2736
- 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"
2737
3108
  };
2738
- const operationOutcomeCounts = {
2739
- completed: 0,
2740
- partial: 0,
2741
- 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
2742
3119
  };
2743
- let inputTokens = 0;
2744
- let outputTokens = 0;
2745
- let cachedTokens = 0;
2746
- let costUsd = 0;
2747
- const unknownTokenEventIds = [];
2748
- const unknownCostEventIds = [];
2749
- for (const attempt of attempts) outcomeCounts[attempt.outcome.status] += 1;
2750
- for (const operation of operationEvents) operationOutcomeCounts[operation.outcome.status] += 1;
2751
- for (const costedEvent of [...attempts, ...operationEvents]) {
2752
- if (costedEvent.accounting.tokens.status === "known") {
2753
- inputTokens += costedEvent.accounting.tokens.inputTokens;
2754
- outputTokens += costedEvent.accounting.tokens.outputTokens;
2755
- cachedTokens += costedEvent.accounting.tokens.cachedTokens;
2756
- } else unknownTokenEventIds.push(costedEvent.eventId);
2757
- if (costedEvent.accounting.cost.status === "known") costUsd += costedEvent.accounting.cost.usd;
2758
- else {
2759
- costUsd += costedEvent.accounting.cost.knownLowerBoundUsd;
2760
- unknownCostEventIds.push(costedEvent.eventId);
2761
- }
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
+ });
2762
3211
  }
2763
- const accounting = unknownTokenEventIds.length === 0 && unknownCostEventIds.length === 0 ? {
2764
- status: "known",
2765
- inputTokens,
2766
- outputTokens,
2767
- cachedTokens,
2768
- costUsd
2769
- } : {
2770
- status: "partial",
2771
- knownInputTokens: inputTokens,
2772
- knownOutputTokens: outputTokens,
2773
- knownCachedTokens: cachedTokens,
2774
- knownCostUsd: costUsd,
2775
- unknownTokenEventIds,
2776
- 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
2777
3245
  };
2778
- const selectedCandidateId = completion?.result.status === "selected" ? completion.result.candidateId : null;
2779
- const status = completion?.result.status === "selected" ? "selected" : completion?.result.status === "all-rejected" ? "all-rejected" : "in-progress";
2780
- const missingCandidateSlots = [...plannedSlots.values()].filter((slot) => !candidateBySlot.has(slot.slotId) && !closedSlots.has(slot.slotId)).map((slot) => slot.slotId);
2781
- const missingTaskOutcomes = planEvent ? plannedTaskOutcomeKeys(planEvent, candidates) : [];
2782
- 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;
2783
3307
  return {
2784
- entries: [...entries],
2785
- plan: planEvent,
2786
- planExtensions: [...planExtensions],
2787
- candidates: [...candidateEvents],
2788
- closedCandidateSlots: [...closedSlotEvents],
2789
- attempts: [...attempts],
2790
- operations: [...operationEvents],
2791
- decisions: [...decisions],
2792
- completion,
2793
- audit: {
2794
- campaignId,
2795
- eventCount: entries.length,
2796
- candidateCount: candidates.size,
2797
- closedCandidateSlotCount: closedSlots.size,
2798
- attemptCount: attempts.length,
2799
- operationCount: operationEvents.length,
2800
- outcomes: outcomeCounts,
2801
- operationOutcomes: operationOutcomeCounts,
2802
- decisions: {
2803
- selected: selectedDecisions.length,
2804
- rejected: rejectedDecisions.length,
2805
- pending: candidates.size - decisions.length
2806
- },
2807
- expected: {
2808
- candidateSlots: plannedSlots.size,
2809
- taskOutcomes: candidates.size * (planEvent?.plan.tasks.length ?? 0),
2810
- operations: plannedOperations.size,
2811
- missingCandidateSlots,
2812
- missingTaskOutcomes,
2813
- missingOperations
2814
- },
2815
- status,
2816
- selectedCandidateId,
2817
- accounting,
2818
- headHash: entries.at(-1)?.entryHash ?? null
2819
- }
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
2820
3315
  };
2821
- };
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;
2822
3324
  return {
2823
- apply,
2824
- snapshot
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
2825
3343
  };
2826
3344
  }
2827
- /** Every candidate slot must name a planned candidate-generation operation,
2828
- * whether it arrives with the plan or with a later extension. */
2829
- function assertSlotGenerationOperation(slot, plannedOperations) {
2830
- if (plannedOperations.get(slot.generationOperationId)?.kind !== "candidate-generation") throw new SearchLedgerIntegrityError(`candidate slot ${slot.slotId} references unplanned candidate-generation operation ${slot.generationOperationId}`);
2831
- }
2832
- function plannedTaskOutcomeKeys(planEvent, candidates) {
2833
- const missing = [];
2834
- const registeredCandidates = [...candidates.values()].sort((a, b) => compareStrings(a.registered.slotId, b.registered.slotId));
2835
- for (const candidate of registeredCandidates) {
2836
- const slotId = candidate.registered.slotId;
2837
- 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}`);
2838
- }
2839
- 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`);
2840
3355
  }
2841
3356
  function assertUnique(values, label, eventId) {
2842
- 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);
2843
3368
  }
2844
- //#endregion
2845
- //#region src/campaign/search-ledger-types.ts
2846
- const SEARCH_LEDGER_SCHEMA = "tangle.search-ledger.v1";
2847
3369
  //#endregion
2848
3370
  //#region src/campaign/search-ledger.ts
2849
3371
  /**
2850
- * 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.
2851
3375
  *
2852
- * Existing campaign artifacts keep their own rich records: `RunRecord` owns a
2853
- * measured run and `CostLedger` owns per-call accounting. This ledger does not
2854
- * copy those structures. It binds their immutable ids and receipts into one replayable event stream so a
2855
- * search can answer, after a crash, exactly which candidates and task attempts
2856
- * existed, which surfaces actually fired, what they cost, and why they were
2857
- * 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.
2858
3379
  *
2859
3380
  * The file format is canonical JSONL with a SHA-256 hash chain. Every append is
2860
3381
  * serialized across processes, fsynced before acknowledgement, and idempotent
2861
3382
  * by `eventId`. A malformed, non-canonical, truncated, reordered, or conflicting
2862
- * 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.
2863
3385
  *
2864
- * The journal machinery itself (hash chain, locking, fsync, idempotent append)
2865
- * is the generic `ledger-core` journal; this module supplies the campaign
2866
- * 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.
2867
3389
  */
2868
3390
  const NON_EMPTY = z.string().min(1).refine((value) => value.trim() === value, "must not contain surrounding whitespace");
2869
3391
  const HASH = z.string().regex(/^sha256:[a-f0-9]{64}$/);
2870
- const LINEAGE_NODE_ID = z.string().regex(/^[a-f0-9]{16}$/);
2871
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+/=]+)$/);
2872
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");
2873
3394
  const NON_NEGATIVE_INT = z.number().int().nonnegative().safe();
3395
+ const POSITIVE_INT = z.number().int().positive().safe();
2874
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}$/);
2875
3400
  const ArtifactRefSchema = z.object({
2876
3401
  role: NON_EMPTY,
2877
3402
  uri: NON_EMPTY,
@@ -2882,14 +3407,47 @@ const SourceRefSchema = z.object({
2882
3407
  uri: NON_EMPTY,
2883
3408
  revision: IMMUTABLE_REVISION
2884
3409
  }).strict();
3410
+ const UnknownRefSchema = z.object({ unknown: NON_EMPTY }).strict();
2885
3411
  const FailureReasonSchema = z.object({
2886
3412
  code: NON_EMPTY,
2887
3413
  message: NON_EMPTY
2888
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();
2889
3447
  const EventBaseShape = {
2890
3448
  eventId: NON_EMPTY,
2891
3449
  occurredAt: ISO_TIMESTAMP,
2892
- artifacts: z.array(ArtifactRefSchema).min(1)
3450
+ artifacts: z.array(ArtifactRefSchema)
2893
3451
  };
2894
3452
  const OperationKindSchema = z.enum([
2895
3453
  "candidate-generation",
@@ -2898,89 +3456,19 @@ const OperationKindSchema = z.enum([
2898
3456
  "judge",
2899
3457
  "other"
2900
3458
  ]);
2901
- const CandidateSlotSchema = z.object({
2902
- slotId: NON_EMPTY,
2903
- generationOperationId: NON_EMPTY
2904
- }).strict();
2905
- const PlannedOperationSchema = z.object({
2906
- operationId: NON_EMPTY,
2907
- kind: OperationKindSchema
2908
- }).strict();
2909
- const SearchPlanExtendedSchema = z.object({
2910
- ...EventBaseShape,
2911
- kind: z.literal("search-plan-extended"),
2912
- extension: z.object({
2913
- candidateSlots: z.array(CandidateSlotSchema),
2914
- operations: z.array(PlannedOperationSchema)
2915
- }).strict().superRefine((extension, ctx) => {
2916
- if (extension.candidateSlots.length === 0 && extension.operations.length === 0) ctx.addIssue({
2917
- code: "custom",
2918
- message: "a plan extension must add slots or operations"
2919
- });
2920
- })
2921
- }).strict();
2922
- const SearchPlannedSchema = z.object({
2923
- ...EventBaseShape,
2924
- kind: z.literal("search-planned"),
2925
- plan: z.object({
2926
- candidateSlots: z.array(CandidateSlotSchema).min(1),
2927
- tasks: z.array(z.object({
2928
- taskId: NON_EMPTY,
2929
- source: SourceRefSchema,
2930
- benchmark: SourceRefSchema,
2931
- maxAttempts: z.number().int().positive().safe()
2932
- }).strict()).min(1),
2933
- operations: z.array(PlannedOperationSchema).min(1)
2934
- }).strict()
2935
- }).strict();
2936
- const CandidateRegisteredSchema = z.object({
2937
- ...EventBaseShape,
2938
- kind: z.literal("candidate-registered"),
2939
- slotId: NON_EMPTY,
2940
- generationOperationId: NON_EMPTY,
2941
- candidateId: NON_EMPTY,
2942
- lineage: z.object({
2943
- lineageNodeId: LINEAGE_NODE_ID,
2944
- parentCandidateIds: z.array(NON_EMPTY),
2945
- generation: NON_NEGATIVE_INT,
2946
- proposer: NON_EMPTY,
2947
- proposerSource: SourceRefSchema
2948
- }).strict(),
2949
- surfaces: z.array(z.object({
2950
- surfaceId: NON_EMPTY,
2951
- kind: z.enum([
2952
- "prompt",
2953
- "tool-contract",
2954
- "runtime-config",
2955
- "memory",
2956
- "knowledge",
2957
- "agent-profile",
2958
- "code",
2959
- "deployment"
2960
- ]),
2961
- artifact: ArtifactRefSchema
2962
- }).strict()).min(1)
2963
- }).strict();
2964
- const CandidateSlotClosedSchema = z.object({
2965
- ...EventBaseShape,
2966
- kind: z.literal("candidate-slot-closed"),
2967
- slotId: NON_EMPTY,
2968
- generationOperationId: NON_EMPTY,
2969
- reason: FailureReasonSchema
2970
- }).strict();
2971
3459
  const KnownTokensSchema = z.object({
2972
3460
  status: z.literal("known"),
2973
3461
  inputTokens: NON_NEGATIVE_INT,
2974
3462
  outputTokens: NON_NEGATIVE_INT,
2975
3463
  cachedTokens: NON_NEGATIVE_INT
2976
3464
  }).strict();
2977
- const UnknownSchema = z.object({
3465
+ const UnknownTokensSchema = z.object({
2978
3466
  status: z.literal("unknown"),
2979
3467
  reason: NON_EMPTY
2980
3468
  }).strict();
2981
3469
  const KnownCostSchema = z.object({
2982
3470
  status: z.literal("known"),
2983
- usd: z.number().finite().nonnegative(),
3471
+ usd: USD,
2984
3472
  source: z.enum([
2985
3473
  "provider",
2986
3474
  "pricing-table",
@@ -2994,11 +3482,11 @@ const KnownCostSchema = z.object({
2994
3482
  });
2995
3483
  const UnknownCostSchema = z.object({
2996
3484
  status: z.literal("unknown"),
2997
- knownLowerBoundUsd: z.number().finite().nonnegative(),
3485
+ knownLowerBoundUsd: USD,
2998
3486
  reason: NON_EMPTY
2999
3487
  }).strict();
3000
3488
  const AccountingSchema = z.object({
3001
- tokens: z.discriminatedUnion("status", [KnownTokensSchema, UnknownSchema]),
3489
+ tokens: z.discriminatedUnion("status", [KnownTokensSchema, UnknownTokensSchema]),
3002
3490
  cost: z.discriminatedUnion("status", [KnownCostSchema, UnknownCostSchema])
3003
3491
  }).strict();
3004
3492
  const MetricsSchema = z.record(NON_EMPTY, FINITE_NUMBER).superRefine((metrics, ctx) => {
@@ -3049,52 +3537,85 @@ const SurfaceEvidenceSchema = z.object({
3049
3537
  effect: EffectSchema,
3050
3538
  evidence: z.array(ArtifactRefSchema).min(1)
3051
3539
  }).strict().superRefine((evidence, ctx) => {
3052
- if (evidence.fired && evidence.firingCount === 0) ctx.addIssue({
3053
- code: "custom",
3054
- message: "a fired surface must have firingCount >= 1"
3055
- });
3056
- if (!evidence.fired && evidence.firingCount !== 0) ctx.addIssue({
3540
+ if (evidence.fired !== evidence.firingCount > 0) ctx.addIssue({
3057
3541
  code: "custom",
3058
- message: "a surface that did not fire must have firingCount 0"
3542
+ message: "fired must equal firingCount > 0"
3059
3543
  });
3060
3544
  if (!evidence.fired && evidence.effect.status === "measured" && evidence.effect.delta !== 0) ctx.addIssue({
3061
3545
  code: "custom",
3062
- message: "a surface that did not fire cannot claim non-zero effect"
3546
+ message: "a surface that did not fire has no effect"
3063
3547
  });
3064
3548
  });
3065
- 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({
3066
3559
  ...EventBaseShape,
3067
- kind: z.literal("task-attempted"),
3068
- candidateId: NON_EMPTY,
3069
- runId: NON_EMPTY,
3070
- attemptIndex: NON_NEGATIVE_INT,
3071
- task: z.object({
3072
- taskId: NON_EMPTY,
3073
- source: SourceRefSchema
3560
+ kind: z.literal("search-opened"),
3561
+ subject: NON_EMPTY,
3562
+ process: z.object({
3563
+ name: NON_EMPTY,
3564
+ executionRef: SourceRefSchema
3074
3565
  }).strict(),
3075
- identity: z.object({
3076
- model: z.object({
3077
- provider: NON_EMPTY,
3078
- snapshot: NON_EMPTY.refine(modelHasSnapshot, "model must include an immutable snapshot")
3079
- }).strict(),
3080
- agent: SourceRefSchema,
3081
- 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
3082
3572
  }).strict(),
3083
- outcome: OutcomeSchema,
3084
- accounting: AccountingSchema,
3085
- 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
3086
3603
  }).strict();
3087
- const SearchOperationRecordedSchema = z.object({
3604
+ const OperationStartedSchema = z.object({
3088
3605
  ...EventBaseShape,
3089
- 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"),
3090
3614
  operationId: NON_EMPTY,
3091
3615
  operationKind: OperationKindSchema,
3092
3616
  execution: z.discriminatedUnion("kind", [z.object({
3093
3617
  kind: z.literal("model"),
3094
- model: z.object({
3095
- provider: NON_EMPTY,
3096
- snapshot: NON_EMPTY.refine(modelHasSnapshot, "model must include an immutable snapshot")
3097
- }).strict(),
3618
+ model: ModelIdentitySchema,
3098
3619
  source: SourceRefSchema
3099
3620
  }).strict(), z.object({
3100
3621
  kind: z.literal("deterministic"),
@@ -3113,94 +3634,256 @@ const SearchOperationRecordedSchema = z.object({
3113
3634
  ]),
3114
3635
  accounting: AccountingSchema
3115
3636
  }).strict();
3116
- const CandidateDecidedSchema = z.object({
3637
+ const NodeRegisteredSchema = z.object({
3117
3638
  ...EventBaseShape,
3118
- kind: z.literal("candidate-decided"),
3119
- candidateId: NON_EMPTY,
3120
- decision: z.discriminatedUnion("status", [z.object({ status: z.literal("selected") }).strict(), z.object({
3121
- status: z.literal("rejected"),
3122
- reason: FailureReasonSchema
3123
- }).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
3124
3652
  }).strict();
3125
- const SearchCompletedSchema = z.object({
3653
+ const ArtifactOrUnknownSchema = z.union([ArtifactRefSchema, UnknownRefSchema]);
3654
+ const EdgeRecordedSchema = z.object({
3126
3655
  ...EventBaseShape,
3127
- kind: z.literal("search-completed"),
3128
- result: z.discriminatedUnion("status", [z.object({
3129
- status: z.literal("selected"),
3130
- candidateId: NON_EMPTY
3131
- }).strict(), z.object({
3132
- status: z.literal("all-rejected"),
3133
- reason: FailureReasonSchema
3134
- }).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()
3135
3817
  }).strict();
3136
3818
  const EventSchema = z.discriminatedUnion("kind", [
3137
- SearchPlannedSchema,
3138
- SearchPlanExtendedSchema,
3139
- CandidateRegisteredSchema,
3140
- CandidateSlotClosedSchema,
3141
- TaskAttemptedSchema,
3142
- SearchOperationRecordedSchema,
3143
- CandidateDecidedSchema,
3144
- SearchCompletedSchema
3819
+ SearchOpenedSchema,
3820
+ OperationStartedSchema,
3821
+ OperationRecordedSchema,
3822
+ NodeRegisteredSchema,
3823
+ EdgeRecordedSchema,
3824
+ CellAllocatedSchema,
3825
+ CellSettledSchema,
3826
+ CellCancelledSchema,
3827
+ NodeDecidedSchema,
3828
+ SearchClosedSchema
3145
3829
  ]);
3146
3830
  const EntrySchema = z.object({
3147
3831
  schema: z.literal(SEARCH_LEDGER_SCHEMA),
3148
- campaignId: NON_EMPTY,
3832
+ searchId: NON_EMPTY,
3149
3833
  sequence: NON_NEGATIVE_INT,
3150
3834
  previousHash: z.union([HASH, z.null()]),
3151
3835
  event: EventSchema,
3152
3836
  entryHash: HASH
3153
3837
  }).strict();
3154
- /** Validate and return a canonical copy. Arrays whose order is not semantic are
3155
- * 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. */
3156
3840
  function validateSearchLedgerEvent(input) {
3157
3841
  const parsed = EventSchema.safeParse(input);
3158
3842
  if (!parsed.success) throw new SearchLedgerError(`invalid search ledger event: ${formatZodError(parsed.error)}`);
3159
3843
  return normalizeEvent(parsed.data);
3160
3844
  }
3161
3845
  /** Open a durable filesystem search ledger. Construction performs no I/O; the
3162
- * first `append` or `replay` validates the complete existing file. */
3846
+ * first `append` or `state` verifies the whole existing file. */
3163
3847
  function openSearchLedger(options) {
3164
- if (options.path.trim().length === 0) throw new SearchLedgerError("ledger path is empty");
3165
- return new FileSearchLedger(options.path, options.campaignId, options.trustedHead);
3848
+ return new FileSearchLedger(options.path, options.searchId, options.trustedHead);
3166
3849
  }
3167
- /** Replay immutable search-ledger JSONL through the same codec as FileSearchLedger. */
3168
- function replaySearchLedgerText(text, campaignId, source) {
3169
- return replayLedgerText(text, source, searchLedgerCodec(campaignId));
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));
3170
3854
  }
3171
- function searchLedgerCodec(campaignId) {
3855
+ function searchLedgerCodec(searchId) {
3172
3856
  return {
3173
3857
  ...SEARCH_LEDGER_FILE_CONTEXT,
3174
3858
  header: {
3175
3859
  schema: SEARCH_LEDGER_SCHEMA,
3176
- campaignId
3860
+ searchId
3177
3861
  },
3178
3862
  conflictError: (message) => new SearchLedgerConflictError(message),
3179
3863
  parseEntry: parseSearchLedgerEntry,
3180
3864
  checkEntryHeader: (entry, index) => {
3181
- 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}`);
3182
3866
  },
3183
- createProjector: () => createSearchLedgerProjector(campaignId)
3867
+ createProjector: () => new SearchState(searchId)
3184
3868
  };
3185
3869
  }
3186
- /** Append-only file-backed search ledger with idempotent writes and replay. */
3870
+ /** Append-only file-backed search ledger with idempotent writes. */
3187
3871
  var FileSearchLedger = class {
3188
3872
  path;
3189
- campaignId;
3873
+ searchId;
3190
3874
  trustedHeadPath;
3191
3875
  trustedHeadMode;
3192
3876
  journal;
3193
- constructor(path, campaignId, trustedHead = "pin") {
3877
+ constructor(path, searchId, trustedHead = "pin") {
3194
3878
  if (path.trim().length === 0) throw new SearchLedgerError("ledger path is empty");
3195
- if (campaignId.length === 0) throw new SearchLedgerError("campaignId is empty");
3196
- if (campaignId.trim() !== campaignId) throw new SearchLedgerError("campaignId must not contain surrounding whitespace");
3197
- 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;
3198
3881
  this.trustedHeadMode = trustedHead;
3199
- this.journal = new FileLedgerJournal(path, searchLedgerCodec(campaignId), { requireTrustedHead: trustedHead === "require" });
3882
+ this.journal = new FileLedgerJournal(path, searchLedgerCodec(searchId), { requireTrustedHead: trustedHead === "require" });
3200
3883
  this.path = this.journal.path;
3201
3884
  this.trustedHeadPath = this.journal.trustedHeadPath;
3202
3885
  }
3203
- async replay() {
3886
+ async state() {
3204
3887
  return this.journal.replay();
3205
3888
  }
3206
3889
  async append(input) {
@@ -3209,7 +3892,7 @@ var FileSearchLedger = class {
3209
3892
  return {
3210
3893
  entry,
3211
3894
  appended,
3212
- replay: projection
3895
+ state: projection
3213
3896
  };
3214
3897
  }
3215
3898
  async trustedHead() {
@@ -3223,6 +3906,8 @@ var FileSearchLedger = class {
3223
3906
  }
3224
3907
  };
3225
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`);
3226
3911
  const parsed = EntrySchema.safeParse(raw);
3227
3912
  if (!parsed.success) throw new SearchLedgerIntegrityError(`search ledger ${context.path} has a malformed entry at line ${context.line}: ${formatZodError(parsed.error)}`);
3228
3913
  const entry = parsed.data;
@@ -3231,74 +3916,82 @@ function parseSearchLedgerEntry(raw, context) {
3231
3916
  }
3232
3917
  function normalizeEvent(event) {
3233
3918
  const artifacts = sortArtifacts(event.artifacts);
3234
- if (event.kind === "search-planned") return {
3235
- ...event,
3236
- artifacts,
3237
- plan: {
3238
- candidateSlots: [...event.plan.candidateSlots].sort((a, b) => compareStrings(a.slotId, b.slotId)),
3239
- tasks: [...event.plan.tasks].sort((a, b) => compareStrings(a.taskId, b.taskId)),
3240
- operations: [...event.plan.operations].sort((a, b) => compareStrings(a.operationId, b.operationId))
3241
- }
3242
- };
3243
- if (event.kind === "search-plan-extended") return {
3244
- ...event,
3245
- artifacts,
3246
- extension: {
3247
- candidateSlots: [...event.extension.candidateSlots].sort((a, b) => compareStrings(a.slotId, b.slotId)),
3248
- operations: [...event.extension.operations].sort((a, b) => compareStrings(a.operationId, b.operationId))
3249
- }
3250
- };
3251
- if (event.kind === "candidate-registered") return {
3252
- ...event,
3253
- artifacts,
3254
- lineage: {
3255
- ...event.lineage,
3256
- parentCandidateIds: sortedStrings(event.lineage.parentCandidateIds)
3257
- },
3258
- surfaces: [...event.surfaces].map((surface) => ({
3259
- ...surface,
3260
- artifact: { ...surface.artifact }
3261
- })).sort((a, b) => compareStrings(a.surfaceId, b.surfaceId))
3262
- };
3263
- if (event.kind === "task-attempted") return {
3264
- ...event,
3265
- artifacts,
3266
- surfaceEvidence: [...event.surfaceEvidence].map((evidence) => ({
3267
- ...evidence,
3268
- evidence: sortArtifacts(evidence.evidence)
3269
- })).sort((a, b) => compareStrings(a.surfaceId, b.surfaceId))
3270
- };
3271
- return {
3272
- ...event,
3273
- artifacts
3274
- };
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
+ }
3275
3957
  }
3276
- function sortArtifacts(artifacts) {
3277
- 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));
3278
3960
  }
3279
- function sortedStrings(values) {
3280
- return [...values].sort();
3961
+ function sortArtifacts(artifacts) {
3962
+ return [...artifacts].sort((a, b) => compareStrings(artifactKey(a), artifactKey(b)));
3281
3963
  }
3282
3964
  function formatZodError(error) {
3283
3965
  return error.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`).join("; ");
3284
3966
  }
3285
3967
  //#endregion
3286
3968
  //#region src/campaign/search-history-receipt.ts
3287
- const SEARCH_HISTORY_RECEIPT_SCHEMA_VERSION = "1.0.0";
3969
+ const SEARCH_HISTORY_RECEIPT_SCHEMA_VERSION = "2026-09";
3288
3970
  const SEARCH_HISTORY_RECEIPT_DIGEST_ALGORITHM = "rfc8785-sha256";
3289
3971
  function assertSearchHistoryAdmissionOptions(options) {
3290
3972
  if (options.searchHistoryPolicy !== void 0 && !["allow-missing", "require-complete"].includes(options.searchHistoryPolicy)) throw new Error(`unknown searchHistoryPolicy '${String(options.searchHistoryPolicy)}'`);
3291
3973
  if (options.searchHistoryVerification !== void 0 && !["receipt", "ledger"].includes(options.searchHistoryVerification)) throw new Error(`unknown searchHistoryVerification '${String(options.searchHistoryVerification)}'`);
3292
3974
  }
3293
- /** 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. */
3294
3986
  function verifySearchHistoryArtifact(receipt, storage) {
3295
3987
  verifySearchHistoryReceipt(receipt);
3296
3988
  const path = receipt.ledger.uri.startsWith("file:") ? fileURLToPath(receipt.ledger.uri) : receipt.ledger.uri;
3297
3989
  const text = storage.read(path);
3298
3990
  if (text === void 0) throw new Error(`search history ledger is missing or unreadable at '${path}'`);
3299
- if (new TextEncoder().encode(text).byteLength !== receipt.ledger.byteLength) throw new Error("search history ledger byte length mismatch");
3300
- if (hashCanonical(text) !== receipt.ledger.sha256) throw new Error("search history ledger digest mismatch");
3301
- 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));
3302
3995
  }
3303
3996
  var SearchHistoryRequiredError = class extends Error {
3304
3997
  producerId;
@@ -3311,73 +4004,25 @@ var SearchHistoryRequiredError = class extends Error {
3311
4004
  this.reasons = Object.freeze([...normalized]);
3312
4005
  }
3313
4006
  };
3314
- /** Build a bounded receipt from the projection returned by canonical ledger replay. */
3315
- function createSearchHistoryReceipt(input) {
3316
- if (!input || typeof input !== "object") throw new TypeError("search history input is required");
3317
- const producerId = nonEmpty(input.producerId, "search history producerId");
3318
- const runId = nonEmpty(input.runId, "search history runId");
3319
- const ledger = normalizeArtifactRef(input.ledger);
3320
- const replay = validateReplayProjection(input.replay);
3321
- const auditDigest = ledgerHash(hashCanonical(replay.audit), "search history auditDigest");
3322
- const summary = summarizeReplay(replay);
3323
- const incompleteReasons = Object.freeze(historyIncompleteReasons(summary));
3324
- const material = Object.freeze({
3325
- schemaVersion: SEARCH_HISTORY_RECEIPT_SCHEMA_VERSION,
3326
- kind: "search-history-receipt",
3327
- digestAlgorithm: SEARCH_HISTORY_RECEIPT_DIGEST_ALGORITHM,
3328
- producerId,
3329
- runId,
3330
- ledger,
3331
- auditDigest,
3332
- summary,
3333
- complete: incompleteReasons.length === 0,
3334
- incompleteReasons
3335
- });
3336
- return Object.freeze({
3337
- ...material,
3338
- receiptDigest: hashCanonical(material)
3339
- });
3340
- }
3341
- /** Verify the bounded receipt. Full ledger bytes are verified by SearchLedger. */
4007
+ /** Verify a receipt's own consistency and digest. */
3342
4008
  function verifySearchHistoryReceipt(receipt) {
3343
4009
  if (!receipt || typeof receipt !== "object") throw new TypeError("search history receipt is required");
3344
4010
  if (receipt.schemaVersion !== SEARCH_HISTORY_RECEIPT_SCHEMA_VERSION) throw new Error(`unsupported search history schemaVersion '${receipt.schemaVersion}'`);
3345
4011
  if (receipt.kind !== "search-history-receipt") throw new Error(`search history kind must be 'search-history-receipt'`);
3346
4012
  if (receipt.digestAlgorithm !== SEARCH_HISTORY_RECEIPT_DIGEST_ALGORITHM) throw new Error(`unsupported search history digestAlgorithm '${receipt.digestAlgorithm}'`);
3347
- const producerId = nonEmpty(receipt.producerId, "search history producerId");
3348
- const runId = nonEmpty(receipt.runId, "search history runId");
3349
- const ledger = normalizeArtifactRef(receipt.ledger);
3350
- const auditDigest = ledgerHash(receipt.auditDigest, "search history auditDigest");
3351
4013
  const summary = normalizeSummary(receipt.summary);
3352
- const incompleteReasons = normalizeReasons(receipt.incompleteReasons);
3353
- const expectedReasons = historyIncompleteReasons(summary);
3354
- if (!sameStrings(incompleteReasons, expectedReasons)) throw new Error("search history incompleteReasons disagree with its summary");
3355
- if (receipt.complete !== (expectedReasons.length === 0)) throw new Error("search history complete disagrees with its summary");
3356
- if (hashCanonical({
3357
- schemaVersion: receipt.schemaVersion,
3358
- kind: receipt.kind,
3359
- digestAlgorithm: receipt.digestAlgorithm,
3360
- producerId,
3361
- runId,
3362
- ledger,
3363
- auditDigest,
3364
- summary,
3365
- complete: receipt.complete,
3366
- incompleteReasons
3367
- }) !== 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");
3368
4018
  return receipt;
3369
4019
  }
3370
- /** Prove that a receipt still describes the exact canonical replay supplied. */
3371
- function assertSearchHistoryMatchesReplay(receipt, replay) {
4020
+ /** Prove that a receipt describes exactly the supplied replayed state. */
4021
+ function assertSearchHistoryMatchesState(receipt, state) {
3372
4022
  verifySearchHistoryReceipt(receipt);
3373
- if (createSearchHistoryReceipt({
3374
- producerId: receipt.producerId,
3375
- runId: receipt.runId,
3376
- ledger: receipt.ledger,
3377
- replay
3378
- }).receiptDigest !== receipt.receiptDigest) throw new Error("search history receipt does not match the supplied SearchLedger replay");
3379
- }
3380
- /** 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. */
3381
4026
  function assertCompleteSearchHistory(producerId, receipt) {
3382
4027
  if (receipt === void 0) throw new SearchHistoryRequiredError(producerId, []);
3383
4028
  verifySearchHistoryReceipt(receipt);
@@ -3400,590 +4045,442 @@ function searchHistoryCoverageRow(producerId, receipt) {
3400
4045
  receipt
3401
4046
  });
3402
4047
  }
3403
- function validateReplayProjection(replay) {
3404
- if (!replay || typeof replay !== "object") throw new TypeError("search history replay is required");
3405
- if (!Array.isArray(replay.entries)) throw new TypeError("search history replay.entries must be an array");
3406
- if (!replay.audit || typeof replay.audit !== "object") throw new TypeError("search history replay.audit is required");
3407
- if (replay.plan !== null && (!replay.plan || typeof replay.plan !== "object")) throw new TypeError("search history replay.plan must be an event or null");
3408
- if (replay.completion !== null && (!replay.completion || typeof replay.completion !== "object")) throw new TypeError("search history replay.completion must be an event or null");
3409
- if (replay.audit.eventCount !== replay.entries.length) throw new Error(`search history audit eventCount ${replay.audit.eventCount} does not match ${replay.entries.length} entries`);
3410
- const observedHead = replay.entries.at(-1)?.entryHash ?? null;
3411
- if (replay.audit.headHash !== observedHead) throw new Error("search history audit headHash does not match the final ledger entry");
3412
- return replay;
3413
- }
3414
- function summarizeReplay(replay) {
3415
- const audit = replay.audit;
3416
- return normalizeSummary({
3417
- campaignId: audit.campaignId,
4048
+ function receiptFor(producerId, runId, ledger, state) {
4049
+ const { audit } = state;
4050
+ const summary = normalizeSummary({
4051
+ searchId: audit.searchId,
3418
4052
  headHash: audit.headHash,
3419
4053
  status: audit.status,
3420
- selectedCandidateId: audit.selectedCandidateId,
4054
+ closeReason: audit.closeReason,
4055
+ claimDecision: state.closed?.claim?.decision ?? null,
4056
+ selectedNodeId: audit.selectedNodeId,
3421
4057
  eventCount: audit.eventCount,
3422
- candidateCount: audit.candidateCount,
3423
- closedCandidateSlotCount: audit.closedCandidateSlotCount,
3424
- attemptCount: audit.attemptCount,
3425
- operationCount: audit.operationCount,
3426
- expectedCandidateSlots: audit.expected.candidateSlots,
3427
- expectedTaskOutcomes: audit.expected.taskOutcomes,
3428
- expectedOperations: audit.expected.operations,
3429
- missingCandidateSlots: audit.expected.missingCandidateSlots.length,
3430
- missingTaskOutcomes: audit.expected.missingTaskOutcomes.length,
3431
- missingOperations: audit.expected.missingOperations.length,
3432
- pendingDecisions: audit.decisions.pending,
3433
- hasPlan: replay.plan !== null,
3434
- 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
3435
4065
  });
3436
- }
3437
- function normalizeSummary(value) {
3438
- if (!value || typeof value !== "object") throw new TypeError("search history summary is required");
3439
- const campaignId = nonEmpty(value.campaignId, "search history summary.campaignId");
3440
- const headHash = nullableLedgerHash(value.headHash, "search history summary.headHash");
3441
- const status = searchStatus(value.status);
3442
- const selectedCandidateId = optionalNullableText(value.selectedCandidateId, "search history summary.selectedCandidateId");
3443
- const eventCount = nonNegativeSafeInteger(value.eventCount, "search history summary.eventCount");
3444
- const candidateCount = nonNegativeSafeInteger(value.candidateCount, "search history summary.candidateCount");
3445
- const closedCandidateSlotCount = nonNegativeSafeInteger(value.closedCandidateSlotCount, "search history summary.closedCandidateSlotCount");
3446
- const attemptCount = nonNegativeSafeInteger(value.attemptCount, "search history summary.attemptCount");
3447
- const operationCount = nonNegativeSafeInteger(value.operationCount, "search history summary.operationCount");
3448
- const expectedCandidateSlots = nonNegativeSafeInteger(value.expectedCandidateSlots, "search history summary.expectedCandidateSlots");
3449
- const expectedTaskOutcomes = nonNegativeSafeInteger(value.expectedTaskOutcomes, "search history summary.expectedTaskOutcomes");
3450
- const expectedOperations = nonNegativeSafeInteger(value.expectedOperations, "search history summary.expectedOperations");
3451
- const missingCandidateSlots = nonNegativeSafeInteger(value.missingCandidateSlots, "search history summary.missingCandidateSlots");
3452
- const missingTaskOutcomes = nonNegativeSafeInteger(value.missingTaskOutcomes, "search history summary.missingTaskOutcomes");
3453
- const missingOperations = nonNegativeSafeInteger(value.missingOperations, "search history summary.missingOperations");
3454
- const pendingDecisions = nonNegativeSafeInteger(value.pendingDecisions, "search history summary.pendingDecisions");
3455
- const hasPlan = boolean(value.hasPlan, "search history summary.hasPlan");
3456
- const hasCompletion = boolean(value.hasCompletion, "search history summary.hasCompletion");
3457
- if (missingCandidateSlots > expectedCandidateSlots) throw new Error("search history missingCandidateSlots exceeds expectedCandidateSlots");
3458
- if (missingTaskOutcomes > expectedTaskOutcomes) throw new Error("search history missingTaskOutcomes exceeds expectedTaskOutcomes");
3459
- if (missingOperations > expectedOperations) throw new Error("search history missingOperations exceeds expectedOperations");
3460
- if (pendingDecisions > candidateCount) throw new Error("search history pendingDecisions exceeds candidateCount");
3461
- if (eventCount === 0 !== (headHash === null)) throw new Error("search history eventCount and headHash disagree");
3462
- if (status === "in-progress" === hasCompletion) throw new Error("search history status and hasCompletion disagree");
3463
- if (status === "selected" && selectedCandidateId === null) throw new Error("selected search history requires selectedCandidateId");
3464
- 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
+ };
3465
4078
  return Object.freeze({
3466
- campaignId,
3467
- headHash,
3468
- status,
3469
- selectedCandidateId,
3470
- eventCount,
3471
- candidateCount,
3472
- closedCandidateSlotCount,
3473
- attemptCount,
3474
- operationCount,
3475
- expectedCandidateSlots,
3476
- expectedTaskOutcomes,
3477
- expectedOperations,
3478
- missingCandidateSlots,
3479
- missingTaskOutcomes,
3480
- missingOperations,
3481
- pendingDecisions,
3482
- hasPlan,
3483
- hasCompletion
4079
+ ...material,
4080
+ receiptDigest: hashCanonical(material)
3484
4081
  });
3485
4082
  }
3486
- function historyIncompleteReasons(summary) {
3487
- const reasons = [];
3488
- if (!summary.hasPlan) reasons.push("search plan is missing");
3489
- if (!summary.hasCompletion) reasons.push("terminal search-completed event is missing");
3490
- if (summary.status === "in-progress") reasons.push("search status is in-progress");
3491
- if (summary.missingCandidateSlots > 0) reasons.push(countReason(summary.missingCandidateSlots, "candidate slot", "candidate slots"));
3492
- if (summary.missingTaskOutcomes > 0) reasons.push(countReason(summary.missingTaskOutcomes, "task outcome", "task outcomes"));
3493
- if (summary.missingOperations > 0) reasons.push(countReason(summary.missingOperations, "operation", "operations"));
3494
- if (summary.pendingDecisions > 0) reasons.push(countReason(summary.pendingDecisions, "candidate decision", "candidate decisions", "pending"));
3495
- return reasons;
3496
- }
3497
- function countReason(count, singular, plural, adjective = "unresolved") {
3498
- return `${count} ${count === 1 ? singular : plural} ${count === 1 ? "is" : "are"} ${adjective}`;
3499
- }
3500
- function normalizeArtifactRef(value) {
3501
- 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");
3502
4111
  return Object.freeze({
3503
- role: nonEmpty(value.role, "search history ledger role"),
3504
- uri: nonEmpty(value.uri, "search history ledger uri"),
3505
- sha256: ledgerHash(value.sha256, "search history ledger sha256"),
3506
- 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
3507
4126
  });
3508
4127
  }
3509
- function normalizeReasons(value) {
3510
- if (!Array.isArray(value)) throw new TypeError("search history incompleteReasons must be an array");
3511
- return Object.freeze(value.map((reason, index) => nonEmpty(reason, `search history incompleteReasons[${index}]`)));
3512
- }
3513
- function sameStrings(left, right) {
3514
- return left.length === right.length && left.every((value, index) => value === right[index]);
3515
- }
3516
- function searchStatus(value) {
3517
- if (value !== "in-progress" && value !== "selected" && value !== "all-rejected") throw new TypeError(`search history summary.status is invalid: ${String(value)}`);
3518
- return value;
3519
- }
3520
- function nullableLedgerHash(value, label) {
3521
- return value === null ? null : ledgerHash(value, label);
3522
- }
3523
- function optionalNullableText(value, label) {
3524
- return value === null ? null : nonEmpty(value, label);
3525
- }
3526
- function ledgerHash(value, label) {
3527
- if (typeof value !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value)) throw new TypeError(`${label} must be a lowercase sha256 digest`);
3528
- return value;
3529
- }
3530
4128
  function nonEmpty(value, label) {
3531
4129
  if (typeof value !== "string" || value.length === 0 || value.trim() !== value) throw new TypeError(`${label} must be a trimmed non-empty string`);
3532
4130
  return value;
3533
4131
  }
3534
- function nonNegativeSafeInteger(value, label) {
3535
- if (!Number.isSafeInteger(value) || value < 0) throw new TypeError(`${label} must be a non-negative safe integer`);
3536
- return value;
3537
- }
3538
- function boolean(value, label) {
3539
- if (typeof value !== "boolean") throw new TypeError(`${label} must be a boolean`);
3540
- return value;
3541
- }
3542
4132
  //#endregion
3543
4133
  //#region src/campaign/search-ledger-recording.ts
3544
- const SEARCH_LEDGER_DIR = "search-ledger";
3545
4134
  /**
3546
- * Recorder for one `runOptimization` run: `open()`, then `recordGeneration()`
3547
- * 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.
3548
4141
  *
3549
- * Every event id is derived from the run, and an id already durable is not
3550
- * appended again, so a resumed run continues one ledger instead of conflicting
3551
- * 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.
3552
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. */
3553
4175
  var SearchRecorder = class SearchRecorder {
3554
- opts;
4176
+ ledger;
4177
+ storage;
4178
+ blobDir;
4179
+ now;
3555
4180
  tasks;
3556
- registered = /* @__PURE__ */ new Map();
3557
- order = [];
3558
- coverage = /* @__PURE__ */ new Map();
3559
- openSlots = /* @__PURE__ */ new Set();
3560
- durableEventIds = /* @__PURE__ */ new Set();
3561
- lastStampMs = 0;
3562
- proposalReceiptCount = 0;
3563
- constructor(opts) {
3564
- this.opts = opts;
3565
- this.tasks = plannedTasks(opts.scenarios, opts.reps, opts.runDir, opts.splitDigest);
3566
- }
3567
- /** Open the recorder and append the plan. An existing ledger for the same
3568
- * run is re-read first, so a resumed run keeps one plan and one lineage. */
3569
- static async open(opts) {
3570
- const recorder = new SearchRecorder(opts);
3571
- await recorder.hydrate();
3572
- await recorder.plan();
3573
- return recorder;
3574
- }
3575
- /**
3576
- * Record one generation's candidate-generation call and the candidates it
3577
- * produced. A proposal larger than the planned population extends the plan
3578
- * with the extra slots; a proposal that fills fewer closes the rest.
3579
- */
3580
- async recordGeneration(input) {
3581
- const { generation, candidates } = input;
3582
- const planned = this.opts.populationSize;
3583
- if (candidates.length > planned) {
3584
- const extra = [];
3585
- for (let index = planned; index < candidates.length; index++) {
3586
- const slot = {
3587
- slotId: slotId(generation, index),
3588
- generationOperationId: generationOperationId(generation)
3589
- };
3590
- extra.push(slot);
3591
- this.openSlots.add(slot.slotId);
3592
- }
3593
- await this.append({
3594
- kind: "search-plan-extended",
3595
- eventId: `search:plan-extended:gen-${generation}`,
3596
- occurredAt: this.stamp(),
3597
- artifacts: [this.proposalArtifact(generation, candidates)],
3598
- extension: {
3599
- candidateSlots: extra,
3600
- operations: []
3601
- }
3602
- });
3603
- }
3604
- await this.recordGenerationOperation(generation, candidates, planned - candidates.length);
3605
- const parent = this.registered.get(input.parentSurfaceHash);
3606
- for (const [index, candidate] of candidates.entries()) {
3607
- const candidateId = candidate.surfaceHash;
3608
- const slot = slotId(generation, index);
3609
- const surfaceArtifact = this.writeArtifact("candidate-surface", `candidate-${candidateId}.json`, {
3610
- surfaceHash: candidateId,
3611
- surface: candidate.surface,
3612
- label: candidate.label ?? ""
3613
- });
3614
- const ledgerGeneration = parent ? parent.generation + 1 : 0;
3615
- await this.append({
3616
- kind: "candidate-registered",
3617
- eventId: `candidate:${candidateId}`,
3618
- occurredAt: this.stamp(),
3619
- artifacts: [surfaceArtifact],
3620
- slotId: slot,
3621
- generationOperationId: generationOperationId(generation),
3622
- candidateId,
3623
- lineage: {
3624
- lineageNodeId: candidateId,
3625
- parentCandidateIds: parent ? [parent.candidateId] : [],
3626
- generation: ledgerGeneration,
3627
- proposer: this.opts.proposerLabel,
3628
- proposerSource: this.opts.binding.identity.proposer.source
3629
- },
3630
- surfaces: candidateSurfaces(candidate.surface, surfaceArtifact)
3631
- });
3632
- this.remember({
3633
- candidateId,
3634
- slotId: slot,
3635
- generation: ledgerGeneration
3636
- });
3637
- }
3638
- for (let index = candidates.length; index < planned; index++) await this.closeSlot(slotId(generation, index), generationOperationId(generation), {
3639
- code: "proposal-short",
3640
- message: `generation ${generation} proposed ${candidates.length} of ${planned} planned candidates`
3641
- });
3642
- }
3643
- /** Append one task attempt per designed cell of each candidate campaign. */
3644
- async recordResults(candidates) {
3645
- for (const candidate of candidates) {
3646
- const registered = this.registered.get(candidate.surfaceHash);
3647
- if (!registered) throw new Error(`search ledger: candidate ${candidate.surfaceHash} produced results without a registration`);
3648
- this.coverage.set(registered.candidateId, candidate.coverageComplete);
3649
- const evidence = this.writeArtifact("candidate-cells", `cells-${registered.candidateId}.json`, {
3650
- surfaceHash: candidate.surfaceHash,
3651
- runDir: candidate.runDir,
3652
- cells: candidate.cells.map((cell) => ({
3653
- cellId: cell.cellId,
3654
- scenarioId: cell.scenarioId,
3655
- rep: cell.rep,
3656
- judgeScores: cell.judgeScores,
3657
- costUsd: cell.costUsd,
3658
- costProvenance: cell.costProvenance,
3659
- tokenUsage: cell.tokenUsage,
3660
- ...cell.error === void 0 ? {} : {
3661
- error: cell.error,
3662
- errorStage: cell.errorStage
3663
- }
3664
- }))
3665
- });
3666
- const surfaceIds = candidateSurfaces(candidate.surface, evidence).map((surface) => surface.surfaceId);
3667
- for (const cell of candidate.cells) {
3668
- const taskId = taskIdFor(cell.scenarioId, cell.rep);
3669
- const task = this.tasks.find((planned) => planned.taskId === taskId);
3670
- if (!task) throw new Error(`search ledger: cell ${cell.cellId} is outside the planned task set`);
3671
- await this.append({
3672
- kind: "task-attempted",
3673
- eventId: `attempt:${registered.candidateId}:${taskId}`,
3674
- occurredAt: this.stamp(),
3675
- artifacts: [evidence],
3676
- candidateId: registered.candidateId,
3677
- runId: `${registered.candidateId}:${cell.cellId}`,
3678
- attemptIndex: 0,
3679
- task: {
3680
- taskId,
3681
- source: task.source
3682
- },
3683
- identity: {
3684
- model: this.cellModel(cell),
3685
- agent: this.opts.binding.identity.agent,
3686
- benchmark: task.benchmark
3687
- },
3688
- outcome: cellOutcome(cell),
3689
- accounting: cellAccounting(cell),
3690
- surfaceEvidence: surfaceIds.map((surfaceId) => surfaceEvidenceFor(surfaceId, evidence))
3691
- });
3692
- }
3693
- }
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;
3694
4198
  }
3695
4199
  /**
3696
- * Close the search: unreached generations, the selection operation, one
3697
- * decision per candidate, then the terminal event.
3698
- *
3699
- * The terminal event is appended only when canonical replay accounts for the
3700
- * whole planned denominator. An interrupted or partly unscored search stays
3701
- * `in-progress` and its receipt reports the exact gap, instead of claiming a
3702
- * 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.
3703
4202
  */
3704
- async finish(input) {
3705
- const stopped = {
3706
- code: "generation-not-reached",
3707
- 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
+ }
3708
4226
  };
3709
- for (let generation = input.generationsRun; generation < this.opts.maxGenerations; generation++) {
3710
- await this.recordGenerationOperation(generation, [], this.opts.populationSize, stopped);
3711
- 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);
3712
4230
  }
3713
- const decisions = this.writeArtifact("search-decisions", "decisions.json", {
3714
- winnerSurfaceHash: input.winnerSurfaceHash,
3715
- 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()
3716
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);
3717
4270
  await this.append({
3718
- kind: "search-operation-recorded",
3719
- eventId: "operation:selection",
4271
+ kind: "node-registered",
4272
+ eventId: `node:${nodeId}`,
3720
4273
  occurredAt: this.stamp(),
3721
- artifacts: [decisions],
3722
- operationId: "selection",
3723
- operationKind: "selection",
3724
- execution: {
3725
- kind: "deterministic",
3726
- source: this.opts.binding.identity.search
3727
- },
3728
- outcome: { status: "completed" },
3729
- accounting: {
3730
- tokens: {
3731
- status: "known",
3732
- inputTokens: 0,
3733
- outputTokens: 0,
3734
- cachedTokens: 0
3735
- },
3736
- cost: {
3737
- status: "known",
3738
- usd: 0,
3739
- source: "free"
3740
- }
3741
- }
4274
+ artifacts: [],
4275
+ nodeId,
4276
+ artifactDigest: input.artifactDigest,
4277
+ artifact: input.artifact,
4278
+ surfaces: input.surfaces
3742
4279
  });
3743
- const winner = this.registered.get(input.winnerSurfaceHash);
3744
- for (const entry of this.order) await this.append({
3745
- kind: "candidate-decided",
3746
- eventId: `decision:${entry.candidateId}`,
3747
- occurredAt: this.stamp(),
3748
- artifacts: [decisions],
3749
- candidateId: entry.candidateId,
3750
- decision: winner?.candidateId === entry.candidateId ? { status: "selected" } : {
3751
- status: "rejected",
3752
- reason: this.coverage.get(entry.candidateId) === false ? {
3753
- code: "coverage-incomplete",
3754
- message: "the candidate missed a designed cell and could not be ranked"
3755
- } : {
3756
- code: "not-promoted",
3757
- message: "the candidate did not beat the incumbent"
3758
- }
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
3759
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 ?? "")
3760
4315
  });
3761
- const replay = await this.opts.binding.ledger.replay();
3762
- const { missingCandidateSlots, missingTaskOutcomes, missingOperations } = replay.audit.expected;
3763
- if (missingCandidateSlots.length === 0 && missingTaskOutcomes.length === 0 && missingOperations.length === 0 && replay.audit.decisions.pending === 0) await this.append({
3764
- kind: "search-completed",
3765
- 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}`,
3766
4323
  occurredAt: this.stamp(),
3767
- artifacts: [decisions],
3768
- result: winner ? {
3769
- status: "selected",
3770
- candidateId: winner.candidateId
3771
- } : {
3772
- status: "all-rejected",
3773
- reason: {
3774
- code: "no-promoted-candidate",
3775
- message: "no candidate beat the incumbent on the designed denominator"
3776
- }
3777
- }
4324
+ artifacts: [],
4325
+ operationId: input.operationId,
4326
+ operationKind: input.operationKind,
4327
+ reservation: input.reservation ?? null
3778
4328
  });
3779
- return this.receipt(input.runId);
3780
4329
  }
3781
- /** Bounded receipt over the exact ledger bytes this run produced. */
3782
- async receipt(runId) {
3783
- const { ledger } = this.opts.binding;
3784
- const replay = await ledger.replay();
3785
- const bytes = replay.entries.map((entry) => `${canonicalString(entry)}\n`).join("");
3786
- return createSearchHistoryReceipt({
3787
- producerId: this.opts.proposerLabel,
3788
- runId,
3789
- ledger: {
3790
- role: "search-ledger",
3791
- uri: `file://${ledger.path}`,
3792
- sha256: hashCanonical(bytes),
3793
- byteLength: new TextEncoder().encode(bytes).byteLength
3794
- },
3795
- 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 ?? []
3796
4338
  });
3797
4339
  }
3798
- /** Read an existing ledger for this run so a resume continues it. */
3799
- async hydrate() {
3800
- const replay = await this.opts.binding.ledger.replay();
3801
- for (const entry of replay.entries) {
3802
- this.durableEventIds.add(entry.event.eventId);
3803
- const stamped = Date.parse(entry.event.occurredAt);
3804
- if (stamped > this.lastStampMs) this.lastStampMs = stamped;
3805
- }
3806
- for (const slot of replay.plan?.plan.candidateSlots ?? []) this.openSlots.add(slot.slotId);
3807
- for (const extension of replay.planExtensions) for (const slot of extension.extension.candidateSlots) this.openSlots.add(slot.slotId);
3808
- for (const closed of replay.closedCandidateSlots) this.openSlots.delete(closed.slotId);
3809
- for (const candidate of replay.candidates) {
3810
- this.remember({
3811
- candidateId: candidate.candidateId,
3812
- slotId: candidate.slotId,
3813
- generation: candidate.lineage.generation
3814
- });
3815
- this.openSlots.delete(candidate.slotId);
3816
- }
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;
3817
4362
  }
3818
- async plan() {
3819
- const candidateSlots = [];
3820
- const operations = [{
3821
- operationId: "selection",
3822
- kind: "selection"
3823
- }];
3824
- for (let generation = 0; generation < this.opts.maxGenerations; generation++) {
3825
- operations.push({
3826
- operationId: generationOperationId(generation),
3827
- kind: "candidate-generation"
3828
- });
3829
- for (let index = 0; index < this.opts.populationSize; index++) {
3830
- const slot = {
3831
- slotId: slotId(generation, index),
3832
- generationOperationId: generationOperationId(generation)
3833
- };
3834
- candidateSlots.push(slot);
3835
- if (!this.registeredSlot(slot.slotId)) this.openSlots.add(slot.slotId);
3836
- }
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));
3837
4378
  }
3838
4379
  await this.append({
3839
- kind: "search-planned",
3840
- eventId: "search:plan",
4380
+ kind: "cell-settled",
4381
+ eventId: `settled:${input.cellId}:${attempt}`,
3841
4382
  occurredAt: this.stamp(),
3842
- artifacts: [this.writeArtifact("search-plan", "plan.json", {
3843
- runDir: this.opts.runDir,
3844
- splitDigest: this.opts.splitDigest,
3845
- maxGenerations: this.opts.maxGenerations,
3846
- populationSize: this.opts.populationSize,
3847
- tasks: this.tasks
3848
- })],
3849
- plan: {
3850
- candidateSlots,
3851
- tasks: this.tasks,
3852
- operations
3853
- }
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" }
3854
4396
  });
3855
4397
  }
3856
- registeredSlot(slot) {
3857
- return this.order.some((entry) => entry.slotId === slot);
3858
- }
3859
- remember(entry) {
3860
- if (this.registered.has(entry.candidateId)) return;
3861
- this.registered.set(entry.candidateId, entry);
3862
- this.order.push(entry);
3863
- }
3864
- async recordGenerationOperation(generation, candidates, unfilled, failure) {
3865
- const shortfall = {
3866
- code: "proposal-short",
3867
- message: `generation ${generation} left ${unfilled} planned candidate slot(s) unfilled`
3868
- };
3869
- const outcome = failure ? {
3870
- status: "failed",
3871
- failure
3872
- } : unfilled <= 0 ? { status: "completed" } : candidates.length === 0 ? {
3873
- status: "failed",
3874
- failure: shortfall
3875
- } : {
3876
- status: "partial",
3877
- failure: shortfall
3878
- };
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;
3879
4402
  await this.append({
3880
- kind: "search-operation-recorded",
3881
- eventId: `operation:${generationOperationId(generation)}`,
4403
+ kind: "cell-cancelled",
4404
+ eventId: `cancelled:${input.cellId}`,
3882
4405
  occurredAt: this.stamp(),
3883
- artifacts: [this.proposalArtifact(generation, candidates)],
3884
- operationId: generationOperationId(generation),
3885
- operationKind: "candidate-generation",
3886
- execution: this.opts.binding.identity.proposer,
3887
- outcome,
3888
- accounting: this.proposalAccounting()
4406
+ artifacts: [],
4407
+ cellId: input.cellId,
4408
+ reason: input.reason
3889
4409
  });
3890
4410
  }
3891
- async closeSlot(slot, generationOperation, reason) {
3892
- if (!this.openSlots.has(slot)) return;
3893
- 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;
3894
4417
  await this.append({
3895
- kind: "candidate-slot-closed",
3896
- eventId: `slot-closed:${slot}`,
4418
+ kind: "node-decided",
4419
+ eventId: `decided:${input.nodeId}:${node.decisions.length}`,
3897
4420
  occurredAt: this.stamp(),
3898
- artifacts: [this.writeArtifact("closed-slot", `slot-${slot}.json`, {
3899
- slot,
3900
- reason
3901
- })],
3902
- slotId: slot,
3903
- generationOperationId: generationOperation,
3904
- reason
4421
+ artifacts: [],
4422
+ nodeId: input.nodeId,
4423
+ decision: input.decision,
4424
+ basis: input.basis ?? null,
4425
+ rule: input.rule,
4426
+ reason: input.reason
3905
4427
  });
3906
4428
  }
3907
- /** Spend booked to candidate generation since the previous generation. */
3908
- proposalAccounting() {
3909
- const receipts = this.opts.costLedger.list({ phase: "search.proposal" });
3910
- const fresh = receipts.slice(this.proposalReceiptCount);
3911
- this.proposalReceiptCount = receipts.length;
3912
- return receiptAccounting(fresh);
3913
- }
3914
- cellModel(cell) {
3915
- const resolved = cell.resolvedModel;
3916
- if (resolved === void 0) return this.opts.binding.identity.model;
3917
- 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`);
3918
- return {
3919
- provider: this.opts.binding.identity.model.provider,
3920
- snapshot: resolved
3921
- };
3922
- }
3923
- proposalArtifact(generation, candidates) {
3924
- return this.writeArtifact("candidate-proposal", `proposal-gen-${generation}.json`, {
3925
- generation,
3926
- candidates: candidates.map((candidate) => ({
3927
- surfaceHash: candidate.surfaceHash,
3928
- label: candidate.label ?? ""
3929
- }))
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
3930
4440
  });
3931
4441
  }
3932
- /** Write one canonical evidence document and return its content address. */
3933
- writeArtifact(role, name, body) {
3934
- const directory = `${this.opts.runDir}/${SEARCH_LEDGER_DIR}`;
3935
- const path = `${directory}/${name}`;
3936
- const contents = canonicalString(body);
3937
- this.opts.storage.ensureDir(directory);
3938
- this.opts.storage.write(path, contents);
3939
- return {
3940
- role,
3941
- uri: `file://${path}`,
3942
- sha256: hashCanonical(contents),
3943
- byteLength: new TextEncoder().encode(contents).byteLength
3944
- };
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
+ });
3945
4449
  }
3946
4450
  async append(event) {
3947
- if (this.durableEventIds.has(event.eventId)) return;
3948
- await this.opts.binding.ledger.append(event);
3949
- this.durableEventIds.add(event.eventId);
4451
+ await this.ledger.append(event);
3950
4452
  }
3951
4453
  /** Non-decreasing ISO stamps; the ledger refuses an event that moves back. */
3952
4454
  stamp() {
3953
- const now = Date.now();
4455
+ const now = this.now();
3954
4456
  this.lastStampMs = now > this.lastStampMs ? now : this.lastStampMs + 1;
3955
4457
  return new Date(this.lastStampMs).toISOString();
3956
4458
  }
3957
4459
  };
3958
- /** One task per designed (scenario, replicate) cell. */
3959
- function plannedTasks(scenarios, reps, runDir, splitDigest) {
3960
- const tasks = [];
3961
- for (const scenario of scenarios) for (let rep = 0; rep < reps; rep++) tasks.push({
3962
- taskId: taskIdFor(scenario.id, rep),
3963
- source: {
3964
- uri: `scenario://${scenario.id}`,
3965
- revision: hashCanonical(scenario)
3966
- },
3967
- benchmark: {
3968
- uri: `campaign://${runDir}`,
3969
- revision: splitDigest
3970
- },
3971
- 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
3972
4465
  });
3973
- return tasks;
3974
- }
3975
- function taskIdFor(scenarioId, rep) {
3976
- return `${scenarioId}#rep-${rep}`;
3977
- }
3978
- function slotId(generation, index) {
3979
- return `gen-${generation}-slot-${index}`;
4466
+ return {
4467
+ artifactDigest: surfaceContentHash(surface),
4468
+ artifact,
4469
+ surfaces: declaredSurfaces(surface, artifact)
4470
+ };
3980
4471
  }
3981
- function generationOperationId(generation) {
3982
- 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
+ });
3983
4480
  }
3984
- /** Declared surfaces of one candidate. A component surface declares one
3985
- * surface per named component, so per-component evidence stays addressable. */
3986
- 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) {
3987
4484
  if (typeof surface === "string") return [{
3988
4485
  surfaceId: "prompt",
3989
4486
  kind: "prompt",
@@ -4000,19 +4497,269 @@ function candidateSurfaces(surface, artifact) {
4000
4497
  artifact
4001
4498
  }));
4002
4499
  }
4003
- /** The campaign measures a candidate surface as a whole, so per-surface
4004
- * attribution stays unmeasured instead of inventing a per-component delta. */
4005
- function surfaceEvidenceFor(surfaceId, evidence) {
4006
- return {
4007
- surfaceId,
4008
- fired: true,
4009
- firingCount: 1,
4010
- effect: {
4011
- status: "not-measured",
4012
- reason: "the campaign measures the candidate surface as a whole, not per surface"
4013
- },
4014
- evidence: [evidence]
4015
- };
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}`;
4016
4763
  }
4017
4764
  function cellOutcome(cell) {
4018
4765
  const scores = Object.entries(cell.judgeScores).filter(([, score]) => score.failed !== true && Number.isFinite(score.composite));
@@ -4093,264 +4840,6 @@ function receiptAccounting(receipts) {
4093
4840
  }
4094
4841
  };
4095
4842
  }
4096
- /**
4097
- * Record an optimizer's own candidate graph into the same ledger.
4098
- *
4099
- * A complete optimization method searches inside its own process and reports
4100
- * one artifact when it finishes: the candidate population, with each
4101
- * candidate's parents and its score per selection scenario. This turns that
4102
- * artifact into the canonical event stream, so a first-party method returns
4103
- * the same `SearchHistoryReceipt` the in-process loop returns, and
4104
- * `compareOptimizationMethods({ searchHistoryPolicy: 'require-complete' })`
4105
- * accepts it.
4106
- *
4107
- * A candidate the optimizer left unscored on a planned scenario leaves the
4108
- * planned denominator open, so the receipt reports the gap instead of closing
4109
- * the search.
4110
- */
4111
- async function recordCandidatePopulationSearch(input) {
4112
- const { population, ledger, identity } = input;
4113
- const stamps = monotonicStamps();
4114
- const writeArtifact = artifactWriter(input.storage, input.runDir);
4115
- const populationArtifact = {
4116
- role: "candidate-population",
4117
- uri: `file://${population.summary.path}`,
4118
- sha256: population.summary.sha256,
4119
- byteLength: population.summary.bytes
4120
- };
4121
- const tasks = input.scenarios.filter((scenario) => population.summary.scenarioIds.includes(scenario.id)).map((scenario) => ({
4122
- taskId: taskIdFor(scenario.id, 0),
4123
- source: {
4124
- uri: `scenario://${scenario.id}`,
4125
- revision: hashCanonical(scenario)
4126
- },
4127
- benchmark: {
4128
- uri: `optimizer://${input.producerId}`,
4129
- revision: population.summary.sha256
4130
- },
4131
- maxAttempts: 1
4132
- }));
4133
- 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`);
4134
- const generationOperation = "candidate-generation:population";
4135
- await ledger.append({
4136
- kind: "search-planned",
4137
- eventId: "search:plan",
4138
- occurredAt: stamps(),
4139
- artifacts: [populationArtifact],
4140
- plan: {
4141
- candidateSlots: population.candidates.map((candidate) => ({
4142
- slotId: `candidate-${candidate.index}`,
4143
- generationOperationId: generationOperation
4144
- })),
4145
- tasks,
4146
- operations: [{
4147
- operationId: generationOperation,
4148
- kind: "candidate-generation"
4149
- }, {
4150
- operationId: "selection",
4151
- kind: "selection"
4152
- }]
4153
- }
4154
- });
4155
- await ledger.append({
4156
- kind: "search-operation-recorded",
4157
- eventId: `operation:${generationOperation}`,
4158
- occurredAt: stamps(),
4159
- artifacts: [populationArtifact],
4160
- operationId: generationOperation,
4161
- operationKind: "candidate-generation",
4162
- execution: identity.proposer,
4163
- outcome: { status: "completed" },
4164
- accounting: input.generationAccounting
4165
- });
4166
- const candidateIds = /* @__PURE__ */ new Map();
4167
- const depths = /* @__PURE__ */ new Map();
4168
- for (const candidate of population.candidates) {
4169
- const parents = candidate.parentIndices.filter((index) => index !== null);
4170
- const candidateId = candidate.candidateHash;
4171
- candidateIds.set(candidate.index, candidateId);
4172
- const depth = parents.length === 0 ? 0 : Math.max(...parents.map((index) => depths.get(index) ?? 0)) + 1;
4173
- depths.set(candidate.index, depth);
4174
- const surfaceArtifact = writeArtifact("candidate-surface", `candidate-${candidate.index}.json`, {
4175
- index: candidate.index,
4176
- candidateDigest: candidate.candidateDigest,
4177
- candidate: candidate.candidate
4178
- });
4179
- await ledger.append({
4180
- kind: "candidate-registered",
4181
- eventId: `candidate:${candidate.index}`,
4182
- occurredAt: stamps(),
4183
- artifacts: [surfaceArtifact],
4184
- slotId: `candidate-${candidate.index}`,
4185
- generationOperationId: generationOperation,
4186
- candidateId,
4187
- lineage: {
4188
- lineageNodeId: candidateId.slice(0, 16),
4189
- parentCandidateIds: parents.map((index) => {
4190
- const parentId = candidateIds.get(index);
4191
- if (!parentId) throw new Error(`search ledger: candidate ${candidate.index} names parent ${index}, which precedes no registered candidate`);
4192
- return parentId;
4193
- }),
4194
- generation: depth,
4195
- proposer: input.producerId,
4196
- proposerSource: identity.proposer.source
4197
- },
4198
- surfaces: candidateSurfaces(externalSurface(candidate.candidate), surfaceArtifact)
4199
- });
4200
- const surfaceIds = candidateSurfaces(externalSurface(candidate.candidate), surfaceArtifact).map((surface) => surface.surfaceId);
4201
- for (const score of candidate.selectionScores) {
4202
- const task = tasks.find((planned) => planned.taskId === taskIdFor(score.scenarioId, 0));
4203
- if (!task) throw new Error(`search ledger: candidate ${candidate.index} scored unplanned scenario ${score.scenarioId}`);
4204
- await ledger.append({
4205
- kind: "task-attempted",
4206
- eventId: `attempt:${candidate.index}:${task.taskId}`,
4207
- occurredAt: stamps(),
4208
- artifacts: [populationArtifact],
4209
- candidateId,
4210
- runId: `${input.runId}:${candidate.index}:${task.taskId}`,
4211
- attemptIndex: 0,
4212
- task: {
4213
- taskId: task.taskId,
4214
- source: task.source
4215
- },
4216
- identity: {
4217
- model: identity.model,
4218
- agent: identity.agent,
4219
- benchmark: task.benchmark
4220
- },
4221
- outcome: {
4222
- status: "passed",
4223
- score: score.score,
4224
- metrics: { composite: score.score }
4225
- },
4226
- accounting: {
4227
- tokens: {
4228
- status: "unknown",
4229
- reason: "the optimizer reports no token usage per candidate evaluation"
4230
- },
4231
- cost: {
4232
- status: "unknown",
4233
- knownLowerBoundUsd: 0,
4234
- reason: "the optimizer reports no cost per candidate evaluation"
4235
- }
4236
- },
4237
- surfaceEvidence: surfaceIds.map((surfaceId) => surfaceEvidenceFor(surfaceId, populationArtifact))
4238
- });
4239
- }
4240
- }
4241
- await ledger.append({
4242
- kind: "search-operation-recorded",
4243
- eventId: "operation:selection",
4244
- occurredAt: stamps(),
4245
- artifacts: [populationArtifact],
4246
- operationId: "selection",
4247
- operationKind: "selection",
4248
- execution: {
4249
- kind: "deterministic",
4250
- source: identity.search
4251
- },
4252
- outcome: { status: "completed" },
4253
- accounting: {
4254
- tokens: {
4255
- status: "known",
4256
- inputTokens: 0,
4257
- outputTokens: 0,
4258
- cachedTokens: 0
4259
- },
4260
- cost: {
4261
- status: "known",
4262
- usd: 0,
4263
- source: "free"
4264
- }
4265
- }
4266
- });
4267
- const winnerId = candidateIds.get(population.bestIndex);
4268
- for (const candidate of population.candidates) await ledger.append({
4269
- kind: "candidate-decided",
4270
- eventId: `decision:${candidate.index}`,
4271
- occurredAt: stamps(),
4272
- artifacts: [populationArtifact],
4273
- candidateId: candidateIds.get(candidate.index),
4274
- decision: candidate.index === population.bestIndex ? { status: "selected" } : {
4275
- status: "rejected",
4276
- reason: {
4277
- code: "not-selected",
4278
- message: "the optimizer selected another candidate"
4279
- }
4280
- }
4281
- });
4282
- const replay = await ledger.replay();
4283
- const { missingCandidateSlots, missingTaskOutcomes, missingOperations } = replay.audit.expected;
4284
- if (winnerId && missingCandidateSlots.length === 0 && missingTaskOutcomes.length === 0 && missingOperations.length === 0 && replay.audit.decisions.pending === 0) await ledger.append({
4285
- kind: "search-completed",
4286
- eventId: "search:completed",
4287
- occurredAt: stamps(),
4288
- artifacts: [populationArtifact],
4289
- result: {
4290
- status: "selected",
4291
- candidateId: winnerId
4292
- }
4293
- });
4294
- const final = await ledger.replay();
4295
- const bytes = final.entries.map((entry) => `${canonicalString(entry)}\n`).join("");
4296
- return createSearchHistoryReceipt({
4297
- producerId: input.producerId,
4298
- runId: input.runId,
4299
- ledger: {
4300
- role: "search-ledger",
4301
- uri: `file://${ledger.path}`,
4302
- sha256: hashCanonical(bytes),
4303
- byteLength: new TextEncoder().encode(bytes).byteLength
4304
- },
4305
- replay: final
4306
- });
4307
- }
4308
- /** An optimizer candidate as the canonical mutable surface it represents. */
4309
- function externalSurface(candidate) {
4310
- return typeof candidate === "string" ? candidate : {
4311
- kind: "components",
4312
- components: candidate
4313
- };
4314
- }
4315
- function artifactWriter(storage, runDir) {
4316
- const directory = `${runDir}/${SEARCH_LEDGER_DIR}`;
4317
- return (role, name, body) => {
4318
- const contents = canonicalString(body);
4319
- storage.ensureDir(directory);
4320
- storage.write(`${directory}/${name}`, contents);
4321
- return {
4322
- role,
4323
- uri: `file://${directory}/${name}`,
4324
- sha256: hashCanonical(contents),
4325
- byteLength: new TextEncoder().encode(contents).byteLength
4326
- };
4327
- };
4328
- }
4329
- /** Non-decreasing ISO stamps; the ledger refuses an event that moves back. */
4330
- function monotonicStamps() {
4331
- let last = 0;
4332
- return () => {
4333
- const now = Date.now();
4334
- last = now > last ? now : last + 1;
4335
- return new Date(last).toISOString();
4336
- };
4337
- }
4338
- //#endregion
4339
- //#region src/campaign/types.ts
4340
- /** Type guard: a proposal carrying its rationale vs a bare
4341
- * surface. The loop branches on this to populate `GenerationCandidate`. */
4342
- function isProposedCandidate(value) {
4343
- return typeof value === "object" && value !== null && "surface" in value && "label" in value && "rationale" in value;
4344
- }
4345
- const LABEL_TRUST_RANK = {
4346
- unverified: 0,
4347
- "verified-signal": 1,
4348
- "human-rated": 2
4349
- };
4350
- /** Ordinal rank for a label-trust tier; absent ⇒ `unverified` (rank 0). */
4351
- function labelTrustRank(trust) {
4352
- return LABEL_TRUST_RANK[trust ?? "unverified"];
4353
- }
4354
4843
  //#endregion
4355
4844
  //#region src/campaign/presets/run-optimization.ts
4356
4845
  /**
@@ -4406,18 +4895,26 @@ async function runOptimization(opts) {
4406
4895
  const label = opts.premeasuredBaseline ? "premeasured baseline" : "baseline";
4407
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.`);
4408
4897
  }
4409
- const recorder = opts.searchLedger ? await SearchRecorder.open({
4898
+ const opened = opts.searchLedger ? await OptimizationSearch.open({
4410
4899
  binding: opts.searchLedger,
4411
4900
  storage,
4412
- runDir: opts.runDir,
4901
+ costLedger,
4413
4902
  scenarios: opts.scenarios,
4414
4903
  reps,
4415
- maxGenerations: opts.maxGenerations,
4416
- populationSize: opts.populationSize,
4904
+ seed: opts.seed ?? 42,
4417
4905
  splitDigest: baselineCampaign.splitDigest,
4418
- proposerLabel: proposer.kind,
4419
- costLedger
4906
+ proposerName: proposer.kind,
4907
+ expansion: opts.selectParent ? "select-parent" : "incumbent",
4908
+ maxUsd: opts.costCeiling ?? null,
4909
+ baselineSurface,
4910
+ baselineCells: baselineCampaign.cells
4420
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";
4421
4918
  const generations = [];
4422
4919
  const history = [];
4423
4920
  let currentFindings = initialFindings;
@@ -4455,7 +4952,10 @@ async function runOptimization(opts) {
4455
4952
  }
4456
4953
  for (let gen = 0; gen < opts.maxGenerations; gen++) {
4457
4954
  const proposalHistory = immutableProposalSnapshot(history, "history");
4458
- if (proposer.decide?.({ history: proposalHistory }).stop) break;
4955
+ if (proposer.decide?.({ history: proposalHistory }).stop) {
4956
+ stopReason = "converged";
4957
+ break;
4958
+ }
4459
4959
  const paretoParents = immutableProposalSnapshot(computeParetoFrontier(scored), "Pareto parents");
4460
4960
  const incumbentOutcome = immutableProposalSnapshot(winnerOutcome, "incumbent outcome");
4461
4961
  const selected = opts.selectParent ? resolveSelectedParent(opts.selectParent(Object.freeze({
@@ -4483,10 +4983,15 @@ async function runOptimization(opts) {
4483
4983
  costLedger,
4484
4984
  costPhase: "search.proposal"
4485
4985
  });
4986
+ await search?.startGeneration(gen);
4486
4987
  const proposed = await proposer.propose(proposalContext);
4487
4988
  if (!Array.isArray(proposed)) throw new TypeError("runOptimization: proposer must return an array");
4488
4989
  const proposalSnapshot = immutableProposalSnapshot(proposed, "candidate outputs");
4489
- 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
+ }
4490
4995
  const candidates = proposalSnapshot.map((p) => isProposedCandidate(p) ? p : {
4491
4996
  surface: p,
4492
4997
  label: "",
@@ -4499,15 +5004,22 @@ async function runOptimization(opts) {
4499
5004
  generationHashes.add(hash);
4500
5005
  }
4501
5006
  for (const hash of generationHashes) admittedCandidateHashes.add(hash);
4502
- await recorder?.recordGeneration({
4503
- generation: gen,
4504
- parentSurfaceHash,
4505
- candidates: candidates.map(({ surface, label }) => ({
4506
- surface,
4507
- surfaceHash: surfaceHash(surface),
4508
- ...label ? { label } : {}
4509
- }))
4510
- });
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
+ }
4511
5023
  const surfaceResults = await mapConcurrent(candidates, candidateConcurrency, async ({ surface, label, rationale, attribution }, i, signal) => {
4512
5024
  const hash = surfaceHash(surface);
4513
5025
  const campaign = await runCampaign({
@@ -4546,13 +5058,11 @@ async function runOptimization(opts) {
4546
5058
  });
4547
5059
  }
4548
5060
  }
4549
- await recorder?.recordResults(surfaceResults.map((result) => ({
4550
- surface: result.surface,
4551
- surfaceHash: result.surfaceHash,
4552
- cells: result.campaign.cells,
4553
- runDir: result.campaign.runDir,
4554
- coverageComplete: result.coverage.complete
4555
- })));
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
+ }
4556
5066
  surfaceResults.sort((a, b) => {
4557
5067
  if (a.coverage.complete !== b.coverage.complete) return a.coverage.complete ? -1 : 1;
4558
5068
  if (a.rankKey && b.rankKey) return compareRankKeys(b.rankKey, a.rankKey);
@@ -4562,6 +5072,8 @@ async function runOptimization(opts) {
4562
5072
  const promoted = top && compareRankKeys(top.rankKey, winnerRankKey) > 0 ? [top] : [];
4563
5073
  if (promoted[0]) {
4564
5074
  const top = promoted[0];
5075
+ const promotedNodeId = nodeIdByHash.get(top.surfaceHash);
5076
+ if (promotedNodeId) promotedNodeIds.add(promotedNodeId);
4565
5077
  winnerSurface = top.surface;
4566
5078
  winnerSurfaceHash = top.surfaceHash;
4567
5079
  winnerComposite = top.composite;
@@ -4623,9 +5135,11 @@ async function runOptimization(opts) {
4623
5135
  currentFindings = immutableProposalSnapshot(assertProposalFindings(fresh, "runOptimization generation analysis findings"), "generation analysis findings");
4624
5136
  }
4625
5137
  }
4626
- const searchHistory = await recorder?.finish({
4627
- winnerSurfaceHash,
4628
- generationsRun: generations.length,
5138
+ const searchHistory = await search?.finish({
5139
+ winnerNodeId: nodeIdByHash.get(winnerSurfaceHash),
5140
+ promotedNodeIds,
5141
+ incompleteNodeIds,
5142
+ reason: stopReason,
4629
5143
  runId: opts.runDir
4630
5144
  });
4631
5145
  return {
@@ -6156,6 +6670,6 @@ function firstString(value) {
6156
6670
  return typeof value === "string" && value.trim() ? value : void 0;
6157
6671
  }
6158
6672
  //#endregion
6159
- 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 };
6160
6674
 
6161
- //# sourceMappingURL=llm-judge-M_l_WXw2.js.map
6675
+ //# sourceMappingURL=llm-judge-BnxAv7ed.js.map