@tangle-network/agent-eval 0.191.0 → 0.193.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/adapters/http.d.ts +2 -2
  3. package/dist/analyst/index.d.ts +6 -6
  4. package/dist/analyst/index.js +3 -3
  5. package/dist/{analyze-runs-SCSWYpy0.js → analyze-runs-B6gv7vAT.js} +2 -2
  6. package/dist/{analyze-runs-SCSWYpy0.js.map → analyze-runs-B6gv7vAT.js.map} +1 -1
  7. package/dist/{benchmark-command-3tZJnD82.js → benchmark-command-DVhUPU7H.js} +7 -7
  8. package/dist/{benchmark-command-3tZJnD82.js.map → benchmark-command-DVhUPU7H.js.map} +1 -1
  9. package/dist/benchmarks/index.d.ts +3 -3
  10. package/dist/benchmarks/index.js +3 -3
  11. package/dist/campaign/index.d.ts +5 -5
  12. package/dist/campaign/index.js +12 -12
  13. package/dist/{campaign-C8iWEF0M.js → campaign-DdE6S80e.js} +15 -328
  14. package/dist/campaign-DdE6S80e.js.map +1 -0
  15. package/dist/{campaign-evidence-BqjlTM3P.js → campaign-evidence-DJc60F9C.js} +11 -46
  16. package/dist/campaign-evidence-DJc60F9C.js.map +1 -0
  17. package/dist/cli.js +47 -10
  18. package/dist/cli.js.map +1 -1
  19. package/dist/{client-wFyDisxi.d.ts → client-dMMKoQNS.d.ts} +2 -2
  20. package/dist/{client-wFyDisxi.d.ts.map → client-dMMKoQNS.d.ts.map} +1 -1
  21. package/dist/contract/index.d.ts +7 -7
  22. package/dist/contract/index.js +10 -10
  23. package/dist/{default-registry-DK_9aVJf.d.ts → default-registry-C3HTE08O.d.ts} +3 -3
  24. package/dist/{default-registry-DK_9aVJf.d.ts.map → default-registry-C3HTE08O.d.ts.map} +1 -1
  25. package/dist/{define-agent-eval-P28y7mI1.d.ts → define-agent-eval-DQJvVa1P.d.ts} +15 -24
  26. package/dist/{define-agent-eval-P28y7mI1.d.ts.map → define-agent-eval-DQJvVa1P.d.ts.map} +1 -1
  27. package/dist/{define-agent-eval-BE43YTm4.js → define-agent-eval-DkPbnJEY.js} +9 -10
  28. package/dist/define-agent-eval-DkPbnJEY.js.map +1 -0
  29. package/dist/diagnosis/index.js +1 -1
  30. package/dist/{dspy-rlm-engine-MfpwoEaj.js → dspy-rlm-engine-BX34vPVY.js} +2 -2
  31. package/dist/{dspy-rlm-engine-MfpwoEaj.js.map → dspy-rlm-engine-BX34vPVY.js.map} +1 -1
  32. package/dist/{engine-vNiM02Ub.d.ts → engine-DjRV55F9.d.ts} +2 -2
  33. package/dist/{engine-vNiM02Ub.d.ts.map → engine-DjRV55F9.d.ts.map} +1 -1
  34. package/dist/evaluation.d.ts +1 -1
  35. package/dist/experiment/index.d.ts +2 -2
  36. package/dist/experiment/index.js +4 -4
  37. package/dist/{external-optimizer-process-xFTliuiK.js → external-optimizer-process-D-Sab4v3.js} +3 -62
  38. package/dist/external-optimizer-process-D-Sab4v3.js.map +1 -0
  39. package/dist/{external-optimizer-subprocess-B234V9V0.js → external-optimizer-subprocess-kbqC-iWf.js} +64 -3
  40. package/dist/external-optimizer-subprocess-kbqC-iWf.js.map +1 -0
  41. package/dist/hosted/index.d.ts +2 -2
  42. package/dist/hosted/index.js +1 -1
  43. package/dist/{index-D-TN7ZRM.d.ts → index-0jEtpSM7.d.ts} +3 -3
  44. package/dist/{index-D-TN7ZRM.d.ts.map → index-0jEtpSM7.d.ts.map} +1 -1
  45. package/dist/{index-I4UJGRno.d.ts → index-Bj96W0jZ.d.ts} +28 -2
  46. package/dist/{index-I4UJGRno.d.ts.map → index-Bj96W0jZ.d.ts.map} +1 -1
  47. package/dist/{index-CzfnASj_.d.ts → index-DMPLG226.d.ts} +400 -374
  48. package/dist/index-DMPLG226.d.ts.map +1 -0
  49. package/dist/{index-DH-MWH5f.d.ts → index-DWQNDqPd.d.ts} +7 -7
  50. package/dist/{index-DH-MWH5f.d.ts.map → index-DWQNDqPd.d.ts.map} +1 -1
  51. package/dist/index.d.ts +9 -9
  52. package/dist/index.js +9 -292
  53. package/dist/index.js.map +1 -1
  54. package/dist/jev.d.ts +1 -1
  55. package/dist/{journal-c3qY0SDP.js → journal-DvKCvjty.js} +94 -2
  56. package/dist/{journal-c3qY0SDP.js.map → journal-DvKCvjty.js.map} +1 -1
  57. package/dist/ledger-core/index.d.ts +2 -2
  58. package/dist/ledger-core/index.js +2 -2
  59. package/dist/{llm-judge-NJOHM0iA.js → llm-judge-ChbWfeWl.js} +2653 -656
  60. package/dist/llm-judge-ChbWfeWl.js.map +1 -0
  61. package/dist/{matrix-CgqdFm5N.d.ts → matrix-CNu13gg8.d.ts} +2 -2
  62. package/dist/{matrix-CgqdFm5N.d.ts.map → matrix-CNu13gg8.d.ts.map} +1 -1
  63. package/dist/multishot/golden/index.d.ts +1 -1
  64. package/dist/multishot/index.d.ts +2 -2
  65. package/dist/openapi.json +1 -1
  66. package/dist/paired-delta-test-BF-FEP8V.js +78 -0
  67. package/dist/paired-delta-test-BF-FEP8V.js.map +1 -0
  68. package/dist/{paired-promotion-decision-6cn3bLc3.js → paired-promotion-decision-DTKl6-G8.js} +4 -77
  69. package/dist/paired-promotion-decision-DTKl6-G8.js.map +1 -0
  70. package/dist/{produced-state-CY-L7CqS.js → produced-state-BqeIaIp2.js} +3 -3
  71. package/dist/{produced-state-CY-L7CqS.js.map → produced-state-BqeIaIp2.js.map} +1 -1
  72. package/dist/{promotion-policy-CfMNUro2.js → promotion-policy-Cy5OV8BV.js} +3 -3
  73. package/dist/{promotion-policy-CfMNUro2.js.map → promotion-policy-Cy5OV8BV.js.map} +1 -1
  74. package/dist/rl.d.ts +1 -1
  75. package/dist/rl.js +1 -1
  76. package/dist/{search-ledger-CQ8Uy4FK.js → search-ledger-CJiAEU3j.js} +164 -36
  77. package/dist/search-ledger-CJiAEU3j.js.map +1 -0
  78. package/dist/{search-ledger-file-EvdDZRhv.js → search-ledger-file-DY0AXX3W.js} +2 -2
  79. package/dist/{search-ledger-file-EvdDZRhv.js.map → search-ledger-file-DY0AXX3W.js.map} +1 -1
  80. package/dist/{search-ledger-types-C9anU2DX.d.ts → search-ledger-types-B3El5xgS.d.ts} +95 -11
  81. package/dist/search-ledger-types-B3El5xgS.d.ts.map +1 -0
  82. package/dist/{search-shipper-C55V7HE3.js → search-shipper-DUlwlCsK.js} +3 -3
  83. package/dist/{search-shipper-C55V7HE3.js.map → search-shipper-DUlwlCsK.js.map} +1 -1
  84. package/dist/search-summary-BhY8p0Av.js +378 -0
  85. package/dist/search-summary-BhY8p0Av.js.map +1 -0
  86. package/dist/{semantic-concept-judge-Bt7bXAAo.js → semantic-concept-judge-Bg-560H0.js} +3 -3
  87. package/dist/{semantic-concept-judge-Bt7bXAAo.js.map → semantic-concept-judge-Bg-560H0.js.map} +1 -1
  88. package/dist/{sequential-lwJOTRON.js → sequential-C21_PP7F.js} +2 -2
  89. package/dist/{sequential-lwJOTRON.js.map → sequential-C21_PP7F.js.map} +1 -1
  90. package/dist/{skillopt-optimization-method-DwO3TOAV.js → skillopt-optimization-method-DXQdaBjx.js} +7 -7
  91. package/dist/{skillopt-optimization-method-DwO3TOAV.js.map → skillopt-optimization-method-DXQdaBjx.js.map} +1 -1
  92. package/dist/{statistical-heldout-BED3wBtX.d.ts → statistical-heldout-D-xBpbrd.d.ts} +2 -2
  93. package/dist/{statistical-heldout-BED3wBtX.d.ts.map → statistical-heldout-D-xBpbrd.d.ts.map} +1 -1
  94. package/dist/{statistical-heldout-GcLIPxk4.js → statistical-heldout-DANVmyug.js} +2 -2
  95. package/dist/{statistical-heldout-GcLIPxk4.js.map → statistical-heldout-DANVmyug.js.map} +1 -1
  96. package/dist/{store-tool-spans-Cfh9XUCm.d.ts → store-tool-spans-BxrfuU2t.d.ts} +4 -4
  97. package/dist/{store-tool-spans-Cfh9XUCm.d.ts.map → store-tool-spans-BxrfuU2t.d.ts.map} +1 -1
  98. package/dist/{tool-groups-D-mkeN0N.d.ts → tool-groups-BC9O7Epn.d.ts} +2 -2
  99. package/dist/tool-groups-BC9O7Epn.d.ts.map +1 -0
  100. package/dist/trace-repair/index.d.ts +1 -1
  101. package/dist/traces.d.ts +2 -2
  102. package/dist/{types-BmQRs661.d.ts → types-BWwRXofp.d.ts} +340 -17
  103. package/dist/types-BWwRXofp.d.ts.map +1 -0
  104. package/dist/{types-ClK-sHs8.d.ts → types-qoxUfKQg.d.ts} +2 -2
  105. package/dist/{types-ClK-sHs8.d.ts.map → types-qoxUfKQg.d.ts.map} +1 -1
  106. package/docs/campaign-proposers.md +14 -16
  107. package/docs/search-ledger.md +129 -3
  108. package/package.json +1 -1
  109. package/dist/campaign-C8iWEF0M.js.map +0 -1
  110. package/dist/campaign-evidence-BqjlTM3P.js.map +0 -1
  111. package/dist/claim-BGOUqoMq.d.ts +0 -38
  112. package/dist/claim-BGOUqoMq.d.ts.map +0 -1
  113. package/dist/define-agent-eval-BE43YTm4.js.map +0 -1
  114. package/dist/external-optimizer-process-xFTliuiK.js.map +0 -1
  115. package/dist/external-optimizer-subprocess-B234V9V0.js.map +0 -1
  116. package/dist/index-CzfnASj_.d.ts.map +0 -1
  117. package/dist/llm-judge-NJOHM0iA.js.map +0 -1
  118. package/dist/paired-promotion-decision-6cn3bLc3.js.map +0 -1
  119. package/dist/search-ledger-CQ8Uy4FK.js.map +0 -1
  120. package/dist/search-ledger-types-C9anU2DX.d.ts.map +0 -1
  121. package/dist/tool-groups-D-mkeN0N.d.ts.map +0 -1
  122. package/dist/types-BmQRs661.d.ts.map +0 -1
@@ -1,21 +1,24 @@
1
1
  import { i as JudgeError, s as ValidationError } from "./errors-Dngq5h35.js";
2
2
  import { a as hashCanonical, i as compareCodeUnits, r as canonicalString, t as LEDGER_HASH_PATTERN } from "./canonical-BsZVgBhW.js";
3
+ import { t as mulberry32 } from "./random-Dn5fPWkt.js";
3
4
  import { o as summarizeNumberSeries, s as weightedComposite, t as confidenceInterval } from "./descriptive-1V17A-qa.js";
4
- import { t as decidePairedPromotion } from "./paired-promotion-decision-6cn3bLc3.js";
5
- import { a as heldoutSignificance, i as dimensionRegressions, o as pairHoldout } from "./statistical-heldout-GcLIPxk4.js";
5
+ import { v as wilson } from "./paired-arms-JSDcs2zx.js";
6
+ import { t as decidePairedPromotion } from "./paired-promotion-decision-DTKl6-G8.js";
7
+ import { a as heldoutSignificance, i as dimensionRegressions, o as pairHoldout } from "./statistical-heldout-DANVmyug.js";
6
8
  import { i as modelHasSnapshot, l as searchCellRunId } from "./run-record-BRlM99rX.js";
7
9
  import { n as contentHash } from "./verdict-cache-BQEsVy9n.js";
8
10
  import { r as projectCampaignCellQuality, t as campaignCellCostProvenance } from "./run-record-tNnlQIrD.js";
9
11
  import { i as CostLedger, t as CostAccountingIncompleteError } from "./cost-ledger-_IbkejCF.js";
10
- import { A as FinalEvidenceConflictError, B as surfaceHashMatches, C as campaignMeanCompositeOrNull, G as assertCampaignSplitIdentity, I as renderSurfaceDiff, J as campaignScenarioIdentity, K as assertCompleteCampaign, L as surfaceContentHash, R as surfaceDispatchRef, S as campaignMeanComposite, V as BackendIntegrityError, W as assertCampaignDesign, Y as campaignSplitDigest, Z as formatCoverageFailures, b as assertFiniteRankKey, j as FinalEvidenceError, q as campaignCoverage, t as createCampaignEvidenceReceipt, w as compareRankKeys, x as campaignBreakdown, z as surfaceHash } from "./campaign-evidence-BqjlTM3P.js";
11
- import { p as mapConcurrent } from "./journal-c3qY0SDP.js";
12
+ import { F as surfaceContentHash, G as campaignCoverage, H as assertCampaignDesign, I as surfaceDispatchRef, K as campaignScenarioIdentity, L as surfaceHash, O as FinalEvidenceConflictError, P as renderSurfaceDiff, R as surfaceHashMatches, S as campaignMeanCompositeOrNull, U as assertCampaignSplitIdentity, W as assertCompleteCampaign, Y as formatCoverageFailures, b as campaignBreakdown, k as FinalEvidenceError, q as campaignSplitDigest, t as createCampaignEvidenceReceipt, x as campaignMeanComposite, z as BackendIntegrityError } from "./campaign-evidence-DJc60F9C.js";
13
+ import { m as mapConcurrent } from "./journal-DvKCvjty.js";
12
14
  import { t as deepFreezeCanonicalJson } from "./deep-freeze-CurcOQg3.js";
13
- import { S as fsCampaignStorage, g as isRecord, h as isExternalTextCandidate, x as createRunCostLedger } from "./external-optimizer-subprocess-B234V9V0.js";
15
+ import { C as fsCampaignStorage, S as createRunCostLedger, g as isRecord, h as isExternalTextCandidate, x as acquireSingleRunLock } from "./external-optimizer-subprocess-kbqC-iWf.js";
14
16
  import { r as summarizeEvaluationUnits, t as defineEvaluationClaim } from "./claim-V9Zqbq0J.js";
15
17
  import { f as redactText, n as REDACTION_VERSION, o as detectCredential, s as detectPersonalData } from "./redact-B7rt9Zot.js";
16
18
  import { t as detectRewardHacking } from "./reward-hacking-D0XwhVWE.js";
17
19
  import { n as assertProposalFindings, t as combineAbortSignals } from "./abort-signal-CtzAM_sJ.js";
18
- import { a as validateSearchLedgerEvent, c as searchCellId, i as replaySearchLedgerText, l as searchNodeId, u as searchTaskSetDigest } from "./search-ledger-CQ8Uy4FK.js";
20
+ import { a as validateSearchLedgerEvent, d as searchTaskSetDigest, i as replaySearchLedgerText, l as searchCellId, n as openSearchLedger, t as FileSearchLedger, u as searchNodeId } from "./search-ledger-CJiAEU3j.js";
21
+ import { c as seedFromDigest, i as estimateNode, n as searchProposerView, s as searchPosterior, t as renderSearchSummary } from "./search-summary-BhY8p0Av.js";
19
22
  import { d as stripFencedJson, o as costReceiptFromLlm, s as costReceiptFromLlmError, u as maximumChargeForLlmRequest } from "./llm-client-KYQBh9C8.js";
20
23
  import { createHash } from "node:crypto";
21
24
  import { writeFileSync } from "node:fs";
@@ -1161,8 +1164,8 @@ function detectCalibrationDrift(runs, opts) {
1161
1164
  alpha,
1162
1165
  recentN: recent.length,
1163
1166
  historyN: historical.length,
1164
- recentMean: mean$1(recent),
1165
- historyMean: mean$1(historical)
1167
+ recentMean: mean$3(recent),
1168
+ historyMean: mean$3(historical)
1166
1169
  }
1167
1170
  }],
1168
1171
  evaluation: {
@@ -1399,7 +1402,7 @@ function chiSquareCritical(df, alpha) {
1399
1402
  }
1400
1403
  return TABLE[10][idx];
1401
1404
  }
1402
- function mean$1(xs) {
1405
+ function mean$3(xs) {
1403
1406
  if (xs.length === 0) return 0;
1404
1407
  return xs.reduce((s, x) => s + x, 0) / xs.length;
1405
1408
  }
@@ -2532,20 +2535,1775 @@ function paretoFrontierWithCrowding(candidates, objectives) {
2532
2535
  return crowdingDistance(frontier, objectives).sort((a, b) => b.distance - a.distance);
2533
2536
  }
2534
2537
  //#endregion
2535
- //#region src/campaign/types.ts
2536
- /** Type guard: a proposal carrying its rationale vs a bare
2537
- * surface. The loop branches on this to populate `GenerationCandidate`. */
2538
- function isProposedCandidate(value) {
2539
- return typeof value === "object" && value !== null && "surface" in value && "label" in value && "rationale" in value;
2538
+ //#region src/campaign/allocation.ts
2539
+ /**
2540
+ * Every node gets every train and selection task at every repeat: the root as
2541
+ * `root` cells, every other node as `train` cells (the proposer's feedback) and
2542
+ * `screen` cells (the policy's private evidence). This is the fixed plan the
2543
+ * generation loop ran and GEPA runs.
2544
+ */
2545
+ function uniform(options = {}) {
2546
+ const reps = options.reps ?? 1;
2547
+ if (!Number.isSafeInteger(reps) || reps < 1) throw new TypeError(`uniform: reps must be a positive integer, got ${String(reps)}`);
2548
+ const cells = (state, root) => {
2549
+ const header = state.header;
2550
+ if (!header) return [];
2551
+ const plan = [];
2552
+ for (const split of ["train", "selection"]) for (const task of header.splits[split].tasks) for (let rep = 0; rep < reps; rep++) plan.push({
2553
+ taskId: task.taskId,
2554
+ split,
2555
+ rep,
2556
+ stage: root ? "root" : split === "train" ? "train" : "screen"
2557
+ });
2558
+ return plan;
2559
+ };
2560
+ return {
2561
+ name: reps === 1 ? "uniform" : `uniform(reps=${reps})`,
2562
+ reps,
2563
+ plan: (state, nodeId) => cells(state, nodeId === state.rootNodeId),
2564
+ screenSize: (state) => cells(state, false).length
2565
+ };
2540
2566
  }
2541
- const LABEL_TRUST_RANK = {
2542
- unverified: 0,
2543
- "verified-signal": 1,
2544
- "human-rated": 2
2567
+ //#endregion
2568
+ //#region src/paired-promotion-power.ts
2569
+ /**
2570
+ * @module
2571
+ * Joint power of a configured {@link decidePairedPromotion} call, computed by
2572
+ * running that call on data drawn from a registered alternative.
2573
+ *
2574
+ * Why a simulation and not a formula: `decidePairedPromotion` is not one test.
2575
+ * It routes by sample size (exact sign test below `BOOTSTRAP_GATE_MIN_N`,
2576
+ * paired bootstrap at or above it) and by shape (Tango's score interval on a
2577
+ * two-point outcome), and on top of the deciding interval it applies a
2578
+ * sufficiency floor, a zero-width refusal and McNemar's exact veto. A
2579
+ * preregistration usually seals two such calls, a continuous primary and a
2580
+ * pass/fail veto drawn from the same episodes, and promotes only when both
2581
+ * promote. The sample size at which THAT procedure reaches a target power is
2582
+ * not the power of any one channel: the sign-test power at n can be 0.8 while
2583
+ * the promotion probability is well below it because the veto binds
2584
+ * (agent-eval#785). Closed forms exist for single channels only, and each one
2585
+ * shipped here under-sized the call it stood in for (`mcnemarRequiredN` is
2586
+ * Lachin's normal approximation, 0.65 exact power where it promised 0.8 at 14
2587
+ * pairs; `requiredPairedSampleSize` sizes a paired t the procedure never runs).
2588
+ * So the only faithful power is the procedure's own promotion rate under the
2589
+ * alternative, and that is what this module computes: draw n pairs from the
2590
+ * registered law, run every configured call on them exactly as the caller
2591
+ * will, repeat, count.
2592
+ *
2593
+ * The alternative is DECLARATIVE and serializable, never a sampler function,
2594
+ * so a preregistration can seal it as bytes: a mixture of cells, each with a
2595
+ * probability, an optional pass/fail outcome per arm for the binary calls,
2596
+ * and a law for the candidate-minus-baseline delta on the continuous scale.
2597
+ * Drawing the cell first and the delta given the cell is what makes the two
2598
+ * channels one joint law rather than two marginals.
2599
+ *
2600
+ * Everything is seeded (`mulberry32`), so a sizing is reproducible from its
2601
+ * inputs, and every estimate carries its Monte Carlo standard error and a
2602
+ * Wilson interval: a power read at 0.80 from 1,000 simulations is 0.80 ± 0.013.
2603
+ */
2604
+ /**
2605
+ * Joint power of the configured calls at `n` under the registered alternative:
2606
+ * the fraction of simulated data sets on which every call promotes.
2607
+ */
2608
+ function pairedPromotionPower(options) {
2609
+ const n = options.n;
2610
+ if (!Number.isInteger(n) || n < 1) throw new Error(`pairedPromotionPower: n must be a positive integer, got ${n}`);
2611
+ const simulations = options.simulations ?? 1e3;
2612
+ if (!Number.isInteger(simulations) || simulations < 1) throw new Error(`pairedPromotionPower: simulations must be a positive integer, got ${simulations}`);
2613
+ const seed = options.seed ?? 1;
2614
+ if (!Number.isFinite(seed)) throw new Error(`pairedPromotionPower: seed must be finite, got ${seed}`);
2615
+ const calls = validateCalls(options.calls);
2616
+ const alternative = validateAlternative(options.alternative, calls);
2617
+ const rng = mulberry32(seed);
2618
+ let promotions = 0;
2619
+ let refusals = 0;
2620
+ const perCall = calls.map(() => ({
2621
+ promote: 0,
2622
+ insufficient: 0,
2623
+ indeterminate: 0,
2624
+ exactTestVetoes: 0,
2625
+ hold: 0,
2626
+ methods: /* @__PURE__ */ new Map()
2627
+ }));
2628
+ for (let s = 0; s < simulations; s++) {
2629
+ const pairs = drawPairs(alternative, n, rng);
2630
+ let all = true;
2631
+ let anyRefusal = false;
2632
+ for (let c = 0; c < calls.length; c++) {
2633
+ const call = calls[c];
2634
+ const decision = runCall(call, pairs, n);
2635
+ const tally = perCall[c];
2636
+ tally.methods.set(decision.method, (tally.methods.get(decision.method) ?? 0) + 1);
2637
+ if (decision.promote) tally.promote++;
2638
+ else {
2639
+ if (!decision.sufficient) tally.insufficient++;
2640
+ if (decision.indeterminate) tally.indeterminate++;
2641
+ if (decision.exactTestVetoes) tally.exactTestVetoes++;
2642
+ if (decision.sufficient && !decision.indeterminate && !decision.exactTestVetoes) tally.hold++;
2643
+ }
2644
+ if (!decision.promote) all = false;
2645
+ if (!decision.sufficient || decision.indeterminate || decision.exactTestVetoes) anyRefusal = true;
2646
+ }
2647
+ if (all) promotions++;
2648
+ else if (anyRefusal) refusals++;
2649
+ }
2650
+ const power = promotions / simulations;
2651
+ const interval = wilson(promotions, simulations, .95);
2652
+ return {
2653
+ n,
2654
+ simulations,
2655
+ seed,
2656
+ power,
2657
+ standardError: Math.sqrt(power * (1 - power) / simulations),
2658
+ low: interval.lower,
2659
+ high: interval.upper,
2660
+ refuse: refusals / simulations,
2661
+ hold: (simulations - promotions - refusals) / simulations,
2662
+ calls: perCall.map((tally, c) => ({
2663
+ outcome: calls[c].outcome,
2664
+ promote: tally.promote / simulations,
2665
+ insufficient: tally.insufficient / simulations,
2666
+ indeterminate: tally.indeterminate / simulations,
2667
+ exactTestVetoes: tally.exactTestVetoes / simulations,
2668
+ hold: tally.hold / simulations,
2669
+ methods: Object.fromEntries([...tally.methods.entries()].map(([method, count]) => [method, count / simulations]))
2670
+ }))
2671
+ };
2672
+ }
2673
+ /**
2674
+ * First n at which {@link pairedPromotionPower} reaches the target, scanning
2675
+ * every n from `minPairs` upward. Both the point-estimate answer and the
2676
+ * confirmed lower-bound answer are returned; the scan runs until the latter is
2677
+ * reached (so the curve past `n` is retained) or `maxPairs` is exhausted. A
2678
+ * candidate selected by the scan's bound is certified only by an independent
2679
+ * simulation at that n on a different seed; a candidate that fails it is
2680
+ * recorded and the scan continues.
2681
+ */
2682
+ function requiredPairsForPairedPromotion(options) {
2683
+ const target = options.target ?? .8;
2684
+ if (!Number.isFinite(target) || target <= 0 || target >= 1) throw new Error(`requiredPairsForPairedPromotion: target must be in (0,1), got ${target}`);
2685
+ const minPairs = options.minPairs ?? 1;
2686
+ const maxPairs = options.maxPairs ?? 200;
2687
+ if (!Number.isInteger(minPairs) || minPairs < 1) throw new Error(`requiredPairsForPairedPromotion: minPairs must be a positive integer, got ${minPairs}`);
2688
+ if (!Number.isInteger(maxPairs) || maxPairs < minPairs) throw new Error(`requiredPairsForPairedPromotion: maxPairs must be an integer >= minPairs, got ${maxPairs}`);
2689
+ const seed = options.seed ?? 1;
2690
+ const curve = [];
2691
+ const rejected = [];
2692
+ let n = null;
2693
+ let nAtLowerBound = null;
2694
+ let confirmation = null;
2695
+ for (let candidate = minPairs; candidate <= maxPairs; candidate++) {
2696
+ const result = pairedPromotionPower({
2697
+ n: candidate,
2698
+ alternative: options.alternative,
2699
+ calls: options.calls,
2700
+ simulations: options.simulations,
2701
+ seed
2702
+ });
2703
+ curve.push(result);
2704
+ if (n === null && result.power >= target) n = candidate;
2705
+ if (result.low >= target) {
2706
+ const check = pairedPromotionPower({
2707
+ n: candidate,
2708
+ alternative: options.alternative,
2709
+ calls: options.calls,
2710
+ simulations: options.simulations,
2711
+ seed: confirmationSeed(seed, candidate)
2712
+ });
2713
+ if (check.low >= target) {
2714
+ nAtLowerBound = candidate;
2715
+ confirmation = check;
2716
+ break;
2717
+ }
2718
+ rejected.push(check);
2719
+ }
2720
+ }
2721
+ return {
2722
+ target,
2723
+ n,
2724
+ nAtLowerBound,
2725
+ confirmation,
2726
+ curve,
2727
+ rejected
2728
+ };
2729
+ }
2730
+ /** A seed for the confirmation at `n` that never equals the scan seed. */
2731
+ function confirmationSeed(seed, n) {
2732
+ return (Math.imul((seed | 0) ^ 2654435769, 2246822507) + Math.imul(n, 3266489909) | 1) >>> 0;
2733
+ }
2734
+ function drawPairs(alternative, n, rng) {
2735
+ const baseline = alternative.baseline ?? {
2736
+ kind: "point",
2737
+ value: 0
2738
+ };
2739
+ const control = new Array(n);
2740
+ const candidate = new Array(n);
2741
+ const controlPass = new Array(n);
2742
+ const candidatePass = new Array(n);
2743
+ for (let i = 0; i < n; i++) {
2744
+ const cell = drawCell(alternative.cells, rng);
2745
+ const base = drawValue(baseline, rng);
2746
+ control[i] = base;
2747
+ candidate[i] = base + drawValue(cell.delta, rng);
2748
+ controlPass[i] = cell.pass?.control ?? false;
2749
+ candidatePass[i] = cell.pass?.treatment ?? false;
2750
+ }
2751
+ return {
2752
+ control,
2753
+ candidate,
2754
+ controlPass,
2755
+ candidatePass
2756
+ };
2757
+ }
2758
+ function drawCell(cells, rng) {
2759
+ const u = rng();
2760
+ let cumulative = 0;
2761
+ for (const cell of cells) {
2762
+ cumulative += cell.probability;
2763
+ if (u < cumulative) return cell;
2764
+ }
2765
+ return cells[cells.length - 1];
2766
+ }
2767
+ function drawValue(law, rng) {
2768
+ switch (law.kind) {
2769
+ case "point": return law.value;
2770
+ case "atoms": {
2771
+ const u = rng();
2772
+ let cumulative = 0;
2773
+ for (const atom of law.atoms) {
2774
+ cumulative += atom.probability;
2775
+ if (u < cumulative) return atom.value;
2776
+ }
2777
+ return law.atoms[law.atoms.length - 1].value;
2778
+ }
2779
+ case "normal": {
2780
+ const u1 = 1 - rng();
2781
+ const u2 = rng();
2782
+ return law.mean + law.sd * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
2783
+ }
2784
+ }
2785
+ }
2786
+ function runCall(call, pairs, n) {
2787
+ const options = {
2788
+ minPairs: n,
2789
+ ...call.options
2790
+ };
2791
+ if (call.outcome === "pass") {
2792
+ const scale = call.options.binaryScale;
2793
+ return decidePairedPromotion(pairs.controlPass.map((p) => p ? scale : 0), pairs.candidatePass.map((p) => p ? scale : 0), options);
2794
+ }
2795
+ return decidePairedPromotion(pairs.control, pairs.candidate, options);
2796
+ }
2797
+ function validateCalls(calls) {
2798
+ if (!Array.isArray(calls) || calls.length === 0) throw new Error("pairedPromotionPower: at least one call is required");
2799
+ calls.forEach((call, i) => {
2800
+ if (call.outcome !== "delta" && call.outcome !== "pass") throw new Error(`pairedPromotionPower: calls[${i}].outcome must be 'delta' or 'pass'`);
2801
+ if (call.options === null || typeof call.options !== "object") throw new Error(`pairedPromotionPower: calls[${i}].options must be an object`);
2802
+ if (call.options.minPairs !== void 0 && (!Number.isInteger(call.options.minPairs) || call.options.minPairs < 1)) throw new Error(`pairedPromotionPower: calls[${i}].options.minPairs must be a positive integer`);
2803
+ if (call.outcome === "pass") {
2804
+ const scale = call.options.binaryScale;
2805
+ if (scale === void 0 || !Number.isFinite(scale) || scale <= 0) throw new Error(`pairedPromotionPower: calls[${i}] is a 'pass' call and must declare a finite positive options.binaryScale`);
2806
+ if (call.options.continuous) throw new Error(`pairedPromotionPower: calls[${i}] is a 'pass' call; continuous cannot accompany it`);
2807
+ if (call.options.statistic === "median") throw new Error(`pairedPromotionPower: calls[${i}] is a 'pass' call; binaryScale requires the mean statistic, not median`);
2808
+ } else if (call.options.binaryScale !== void 0) throw new Error(`pairedPromotionPower: calls[${i}] is a 'delta' call and cannot declare binaryScale; use a 'pass' call for a two-point outcome`);
2809
+ });
2810
+ return calls;
2811
+ }
2812
+ function validateAlternative(alternative, calls) {
2813
+ if (!alternative || !Array.isArray(alternative.cells) || alternative.cells.length === 0) throw new Error("pairedPromotionPower: alternative.cells must be a non-empty array");
2814
+ const needsPass = calls.some((call) => call.outcome === "pass");
2815
+ let total = 0;
2816
+ alternative.cells.forEach((cell, i) => {
2817
+ if (!Number.isFinite(cell.probability) || cell.probability < 0) throw new Error(`pairedPromotionPower: cells[${i}].probability must be a finite number >= 0`);
2818
+ total += cell.probability;
2819
+ if (needsPass) {
2820
+ if (!cell.pass || typeof cell.pass.control !== "boolean" || typeof cell.pass.treatment !== "boolean") throw new Error(`pairedPromotionPower: cells[${i}].pass must give control and treatment booleans when a 'pass' call is configured`);
2821
+ }
2822
+ validateLaw(cell.delta, `cells[${i}].delta`);
2823
+ });
2824
+ if (Math.abs(total - 1) > 1e-9) throw new Error(`pairedPromotionPower: cell probabilities must sum to 1, got ${total}`);
2825
+ if (alternative.baseline !== void 0) validateLaw(alternative.baseline, "baseline");
2826
+ return alternative;
2827
+ }
2828
+ function validateLaw(law, where) {
2829
+ if (!law || typeof law !== "object") throw new Error(`pairedPromotionPower: ${where} must be a law object`);
2830
+ switch (law.kind) {
2831
+ case "point":
2832
+ if (!Number.isFinite(law.value)) throw new Error(`pairedPromotionPower: ${where}.value must be finite`);
2833
+ return;
2834
+ case "atoms": {
2835
+ if (!Array.isArray(law.atoms) || law.atoms.length === 0) throw new Error(`pairedPromotionPower: ${where}.atoms must be a non-empty array`);
2836
+ let total = 0;
2837
+ law.atoms.forEach((atom, i) => {
2838
+ if (!Number.isFinite(atom.value)) throw new Error(`pairedPromotionPower: ${where}.atoms[${i}].value must be finite`);
2839
+ if (!Number.isFinite(atom.probability) || atom.probability < 0) throw new Error(`pairedPromotionPower: ${where}.atoms[${i}].probability must be a finite number >= 0`);
2840
+ total += atom.probability;
2841
+ });
2842
+ if (Math.abs(total - 1) > 1e-9) throw new Error(`pairedPromotionPower: ${where}.atoms probabilities must sum to 1, got ${total}`);
2843
+ return;
2844
+ }
2845
+ case "normal":
2846
+ if (!Number.isFinite(law.mean) || !Number.isFinite(law.sd) || law.sd < 0) throw new Error(`pairedPromotionPower: ${where} needs a finite mean and a finite sd >= 0`);
2847
+ return;
2848
+ default: throw new Error(`pairedPromotionPower: ${where}.kind must be 'point', 'atoms' or 'normal'`);
2849
+ }
2850
+ }
2851
+ //#endregion
2852
+ //#region src/campaign/search-claim.ts
2853
+ /**
2854
+ * The claim: the one statement a search makes about its nodes, made once, on
2855
+ * its sealed test split, after the search stops expanding.
2856
+ *
2857
+ * `planSearchClaim` fixes the claim's design from the ledger before any test
2858
+ * cell runs: up to 3 finalists ranked by selection mean, the estimator the
2859
+ * paired decision will use, the number of finalists the family-wise confidence
2860
+ * is divided among, and a power check on selection data. The kernel stores the
2861
+ * plan on the ledger's `claim` operation, so the ledger proves the design was
2862
+ * fixed before the test data existed. `decideSearchClaim` then reads the test
2863
+ * cells of the root and the finalists, which ran together, and decides each
2864
+ * finalist against the root with `decidePairedPromotion` at the Bonferroni
2865
+ * confidence `1 - 0.05 / k`.
2866
+ *
2867
+ * Every number is a pure function of the ledger: the power simulation is
2868
+ * seeded from the search's seed and the decisions from the digest of the test
2869
+ * cells they read, so a verifier with the same ledger gets the same claim.
2870
+ */
2871
+ const FAMILY_CONFIDENCE = .95;
2872
+ const TARGET_POWER = .8;
2873
+ const POWER_SIMULATIONS = 500;
2874
+ const RESAMPLES = 2e3;
2875
+ /**
2876
+ * Every rule and parameter the claim depends on. Its digest is the claim's
2877
+ * revision; change a value here whenever the procedure changes, so the
2878
+ * revision moves with it.
2879
+ */
2880
+ const CLAIM_DEFINITION = {
2881
+ name: "tangle.search-claim.2026-09",
2882
+ finalists: `up to 3 non-root nodes not decided invalid that scored every selection unit and beat the root's mean on the selection units they share, ranked by selection mean in the objective's direction, then registration order`,
2883
+ familyConfidence: FAMILY_CONFIDENCE,
2884
+ correction: "Bonferroni: each of k finalists against the root at 1 - (1 - familyConfidence) / k",
2885
+ finalistCount: "the largest k whose test cells the unspent claim reserve and headroom cover and whose power reaches the target",
2886
+ estimator: "binary with scale s when every train, selection and test unit has one task at one repeat and every pre-test score is 0 or s; otherwise continuous",
2887
+ power: {
2888
+ method: "pairedPromotionPower of the claim decision, simulated at the claim minimumEffect",
2889
+ continuous: "per-unit improvement normal with the pooled selection variance",
2890
+ binary: "discordant units at the pooled selection variance over 2 s^2 per side, shifted by minimumEffect / s; concordant units pass at the root selection pass rate",
2891
+ targetPower: TARGET_POWER,
2892
+ simulations: POWER_SIMULATIONS,
2893
+ seed: "the first 32 bits of hashCanonical([search seed, k])"
2894
+ },
2895
+ decision: {
2896
+ call: "decidePairedPromotion(root, finalist) on per-unit test means in the objective direction",
2897
+ threshold: 0,
2898
+ minPairs: "every test unit",
2899
+ resamples: RESAMPLES,
2900
+ seed: "the first 32 bits of the finalist's test cellSetDigest"
2901
+ },
2902
+ select: "the promoted finalist with the largest improvement; ties to the better selection rank",
2903
+ ship: "a promoted finalist that scored every test unit, with the root, on held-out units, under a pinned judge"
2545
2904
  };
2546
- /** Ordinal rank for a label-trust tier; absent ⇒ `unverified` (rank 0). */
2547
- function labelTrustRank(trust) {
2548
- return LABEL_TRUST_RANK[trust ?? "unverified"];
2905
+ /** The claim procedure every `SearchClaim` and claim plan names. */
2906
+ const SEARCH_CLAIM_RULE = {
2907
+ uri: "npm:@tangle-network/agent-eval#searchClaim",
2908
+ revision: hashCanonical(CLAIM_DEFINITION)
2909
+ };
2910
+ /** The rule name decisions made by the claim record. */
2911
+ const SEARCH_CLAIM_RULE_NAME = CLAIM_DEFINITION.name;
2912
+ /**
2913
+ * The claim reserve a search needs: the root and 3 finalists on every test
2914
+ * task at `reps` repeats, each held at `cellUsd`. Declare at least this as
2915
+ * `budget.reservedClaimUsd`; `runSearch` refuses to start a capped search
2916
+ * with less.
2917
+ */
2918
+ function searchClaimReserveUsd(input) {
2919
+ return 4 * input.testTasks * input.reps * input.cellUsd;
2920
+ }
2921
+ /** Fix the claim's design from a search that stopped expanding. */
2922
+ function planSearchClaim(state, input) {
2923
+ const header = requireHeader(state);
2924
+ const root = state.rootNodeId;
2925
+ const plan = (finalists, power, test, reason) => ({
2926
+ kind: "search-claim-plan",
2927
+ rule: SEARCH_CLAIM_RULE,
2928
+ stopReason: input.stopReason,
2929
+ finalists,
2930
+ power,
2931
+ test,
2932
+ reason
2933
+ });
2934
+ const selectionUnits = unitIds(header.splits.selection.tasks);
2935
+ const testUnits = unitIds(header.splits.test.tasks);
2936
+ if (testUnits.length === 0) return plan([], { unknown: "the search has no test split" }, "hold", "the search has no test split, so it makes no claim");
2937
+ if (selectionUnits.length === 0) return plan([], { unknown: "the search has no selection split to choose finalists on" }, "hold", "the search has no selection split, so no finalist can be chosen without the test data");
2938
+ const candidates = rankFinalists(state, selectionUnits);
2939
+ if (candidates.length === 0) return plan([], { unknown: "no node went to test" }, "hold", "no node scored every selection unit and beat the root's selection mean, so the search keeps the root");
2940
+ const minimumEffect = header.objective.claim.minimumEffect;
2941
+ if (minimumEffect === void 0) return plan(candidates.slice(0, 1), { unknown: "the claim declares no minimumEffect" }, "test-cannot-resolve", "the claim declares no minimumEffect, so no test split can be shown to resolve it");
2942
+ const { pooledVariance } = searchPosterior(state, { split: "selection" });
2943
+ if (pooledVariance === null) return plan(candidates.slice(0, 1), { unknown: "no node shares 2 selection units with the root, so the variance is unknown" }, "test-cannot-resolve", "the selection split gives no variance for the power check, so the test cannot be shown to resolve the minimum effect");
2944
+ const estimator = claimEstimator(state, input.reps);
2945
+ const rootPassRate = passRate(state.unitScores(root, "selection"), estimator);
2946
+ let best = null;
2947
+ let budgetBound = false;
2948
+ for (let k = candidates.length; k >= 1; k--) {
2949
+ const power = claimPower({
2950
+ units: testUnits.length,
2951
+ finalists: k,
2952
+ minimumEffect,
2953
+ pooledVariance,
2954
+ estimator,
2955
+ rootPassRate,
2956
+ seed: header.policy.seed
2957
+ });
2958
+ best = power;
2959
+ if (!power.adequate) continue;
2960
+ if (!input.affordable(k)) {
2961
+ budgetBound = true;
2962
+ continue;
2963
+ }
2964
+ return plan(candidates.slice(0, k), power, "run", `${k} finalist${k === 1 ? "" : "s"} go to test: power ${round(power.powerAtMinimumEffect)} at an improvement of ${minimumEffect} on ${testUnits.length} test units reaches ${TARGET_POWER} at confidence ${round(bonferroni(k))} each`);
2965
+ }
2966
+ const power = best;
2967
+ return plan(candidates.slice(0, 1), power, "test-cannot-resolve", budgetBound ? "the unspent claim reserve and headroom do not cover the test cells of the root and one finalist" : `even one finalist reaches power ${round("adequate" in power ? power.powerAtMinimumEffect : 0)} at an improvement of ${minimumEffect} on ${testUnits.length} test units, below ${TARGET_POWER}`);
2968
+ }
2969
+ /**
2970
+ * Decide the claim from the test cells of the root and the finalists. Call it
2971
+ * once every claim cell is settled or cancelled. It returns the claim and the
2972
+ * terminal decisions of the root and every finalist.
2973
+ */
2974
+ function decideSearchClaim(state, plan) {
2975
+ const header = requireHeader(state);
2976
+ const root = state.rootNodeId;
2977
+ const rootScored = [
2978
+ "train",
2979
+ "selection",
2980
+ "test"
2981
+ ].some((split) => state.scoredCells(root, split).length > 0);
2982
+ const keepRoot = rootScored ? root : null;
2983
+ const rule = SEARCH_CLAIM_RULE_NAME;
2984
+ const decisions = [];
2985
+ const claimOf = (finalists, selected, decision, reason) => ({
2986
+ rule: SEARCH_CLAIM_RULE,
2987
+ confidence: FAMILY_CONFIDENCE,
2988
+ power: plan.power,
2989
+ finalists,
2990
+ selected,
2991
+ decision,
2992
+ reason
2993
+ });
2994
+ if (plan.test !== "run") {
2995
+ for (const finalist of plan.finalists) decisions.push({
2996
+ nodeId: finalist.nodeId,
2997
+ decision: { status: "rejected" },
2998
+ basis: null,
2999
+ rule,
3000
+ reason: `no test cell ran: ${plan.reason}`
3001
+ });
3002
+ decisions.push(rootDecision(root, rootScored ? "kept" : "unscored", rule, plan.reason));
3003
+ return {
3004
+ claim: claimOf(plan.finalists.map((finalist) => ({
3005
+ nodeId: finalist.nodeId,
3006
+ estimate: null,
3007
+ test: null,
3008
+ promote: false
3009
+ })), keepRoot, plan.test, plan.reason),
3010
+ decisions
3011
+ };
3012
+ }
3013
+ const power = plan.power;
3014
+ if (!("adequate" in power)) throw new Error("a claim that runs its test cells has a known power");
3015
+ const k = plan.finalists.length;
3016
+ const confidence = bonferroni(k);
3017
+ const testUnits = unitIds(header.splits.test.tasks);
3018
+ const maximize = header.objective.direction === "maximize";
3019
+ const rootUnits = state.unitScores(root, "test");
3020
+ const tested = plan.finalists.map(({ nodeId }) => {
3021
+ const estimate = estimateNode(state, nodeId, {
3022
+ against: root,
3023
+ split: "test"
3024
+ });
3025
+ return {
3026
+ nodeId,
3027
+ estimate,
3028
+ ...pairedTest({
3029
+ root: rootUnits,
3030
+ node: state.unitScores(nodeId, "test"),
3031
+ testUnits,
3032
+ confidence,
3033
+ estimator: power.estimator,
3034
+ maximize,
3035
+ seed: seedFromDigest(estimate.cellSetDigest)
3036
+ })
3037
+ };
3038
+ });
3039
+ const winner = tested.map((entry, rank) => ({
3040
+ ...entry,
3041
+ rank
3042
+ })).filter((entry) => entry.promote).sort((a, b) => b.improvement - a.improvement || a.rank - b.rank)[0] ?? null;
3043
+ const blockers = [];
3044
+ if (!header.splits.heldOutUnits) blockers.push("the test units also appear in train or selection");
3045
+ if ("unknown" in header.objective.judge) blockers.push("the judge is not pinned");
3046
+ const ship = winner !== null && blockers.length === 0;
3047
+ const selected = ship ? winner.nodeId : keepRoot;
3048
+ const reason = ship ? `finalist ${winner.nodeId} beat the root on the ${testUnits.length} test units at confidence ${round(confidence)} (${k} finalist${k === 1 ? "" : "s"}, family-wise ${FAMILY_CONFIDENCE})` : winner !== null ? `finalist ${winner.nodeId} beat the root, but the claim cannot ship: ${blockers.join("; ")}` : `no finalist beat the root on the test split at confidence ${round(confidence)} (${k} finalist${k === 1 ? "" : "s"}, family-wise ${FAMILY_CONFIDENCE})`;
3049
+ for (const entry of tested) decisions.push({
3050
+ nodeId: entry.nodeId,
3051
+ decision: { status: ship && entry.nodeId === winner.nodeId ? "selected" : "rejected" },
3052
+ basis: entry.estimate,
3053
+ rule,
3054
+ reason: ship && entry.nodeId === winner.nodeId ? reason : entry.promote ? ship ? `promoted, but finalist ${winner.nodeId} improved more on the test split` : reason : entry.why
3055
+ });
3056
+ decisions.push(rootDecision(root, ship ? "replaced" : rootScored ? "kept" : "unscored", rule, reason));
3057
+ return {
3058
+ claim: claimOf(tested.map((entry) => ({
3059
+ nodeId: entry.nodeId,
3060
+ estimate: entry.estimate,
3061
+ test: entry.test,
3062
+ promote: entry.promote
3063
+ })), selected, ship ? "ship" : "hold", reason),
3064
+ decisions
3065
+ };
3066
+ }
3067
+ /** Per-finalist confidence for `k` finalists. */
3068
+ function bonferroni(k) {
3069
+ return 1 - (1 - FAMILY_CONFIDENCE) / k;
3070
+ }
3071
+ function rootDecision(root, fate, rule, reason) {
3072
+ return {
3073
+ nodeId: root,
3074
+ decision: { status: fate === "kept" ? "selected" : "rejected" },
3075
+ basis: null,
3076
+ rule,
3077
+ reason: fate === "kept" ? `the search keeps the root: ${reason}` : fate === "replaced" ? `a finalist replaces the root: ${reason}` : "the root has no scored cell, so the search keeps nothing"
3078
+ };
3079
+ }
3080
+ /** Nodes eligible to go to test, best first, at most `SEARCH_CLAIM_MAX_FINALISTS`. */
3081
+ function rankFinalists(state, selectionUnits) {
3082
+ const sign = requireHeader(state).objective.direction === "maximize" ? 1 : -1;
3083
+ const root = state.rootNodeId;
3084
+ const rootMeans = new Map(state.unitScores(root, "selection").map((unit) => [unit.unitId, unit.mean]));
3085
+ const ranked = [];
3086
+ for (const node of state.nodes()) {
3087
+ if (node.nodeId === root || node.status === "invalid") continue;
3088
+ const units = state.unitScores(node.nodeId, "selection");
3089
+ if (units.length !== selectionUnits.length) continue;
3090
+ let gain = 0;
3091
+ let shared = 0;
3092
+ for (const unit of units) {
3093
+ const rootMean = rootMeans.get(unit.unitId);
3094
+ if (rootMean === void 0) continue;
3095
+ gain += sign * (unit.mean - rootMean);
3096
+ shared += 1;
3097
+ }
3098
+ if (shared === 0 || gain <= 0) continue;
3099
+ ranked.push({
3100
+ nodeId: node.nodeId,
3101
+ selectionMean: mean$2(units.map((unit) => unit.mean)),
3102
+ ordinal: node.ordinal
3103
+ });
3104
+ }
3105
+ ranked.sort((a, b) => sign * (b.selectionMean - a.selectionMean) || a.ordinal - b.ordinal);
3106
+ return ranked.slice(0, 3).map(({ nodeId, selectionMean }) => ({
3107
+ nodeId,
3108
+ selectionMean
3109
+ }));
3110
+ }
3111
+ /**
3112
+ * The paired decision's estimator, fixed from pre-test data. A per-unit mean
3113
+ * is binary only when each unit holds one task at one repeat and every score
3114
+ * seen before the test is 0 or one positive value.
3115
+ */
3116
+ function claimEstimator(state, reps) {
3117
+ const header = requireHeader(state);
3118
+ if (![
3119
+ "train",
3120
+ "selection",
3121
+ "test"
3122
+ ].every((split) => {
3123
+ const tasks = header.splits[split].tasks;
3124
+ return unitIds(tasks).length === tasks.length;
3125
+ }) || reps !== 1) return { kind: "continuous" };
3126
+ let scale = null;
3127
+ for (const nodeId of state.nodeIds()) for (const split of ["train", "selection"]) for (const cell of state.scoredCells(nodeId, split)) {
3128
+ if (cell.score === 0) continue;
3129
+ if (cell.score < 0 || scale !== null && cell.score !== scale) return { kind: "continuous" };
3130
+ scale = cell.score;
3131
+ }
3132
+ return scale === null ? { kind: "continuous" } : {
3133
+ kind: "binary",
3134
+ scale
3135
+ };
3136
+ }
3137
+ function passRate(units, estimator) {
3138
+ if (estimator.kind !== "binary" || units.length === 0) return 0;
3139
+ return units.filter((unit) => unit.mean === estimator.scale).length / units.length;
3140
+ }
3141
+ function claimPower(input) {
3142
+ const { units, finalists, minimumEffect, pooledVariance, estimator } = input;
3143
+ const confidence = bonferroni(finalists);
3144
+ let alternative;
3145
+ let call;
3146
+ if (estimator.kind === "binary") {
3147
+ const s = estimator.scale;
3148
+ const shift = Math.min(1, minimumEffect / s);
3149
+ const discordance = Math.min(.5, pooledVariance / (2 * s * s));
3150
+ const loss = Math.max(0, discordance - shift / 2);
3151
+ const win = Math.min(1 - loss, loss + shift);
3152
+ const concordant = Math.max(0, 1 - win - loss);
3153
+ alternative = { cells: [
3154
+ {
3155
+ probability: win,
3156
+ pass: {
3157
+ control: false,
3158
+ treatment: true
3159
+ },
3160
+ delta: {
3161
+ kind: "point",
3162
+ value: s
3163
+ }
3164
+ },
3165
+ {
3166
+ probability: loss,
3167
+ pass: {
3168
+ control: true,
3169
+ treatment: false
3170
+ },
3171
+ delta: {
3172
+ kind: "point",
3173
+ value: -s
3174
+ }
3175
+ },
3176
+ {
3177
+ probability: concordant * input.rootPassRate,
3178
+ pass: {
3179
+ control: true,
3180
+ treatment: true
3181
+ },
3182
+ delta: {
3183
+ kind: "point",
3184
+ value: 0
3185
+ }
3186
+ },
3187
+ {
3188
+ probability: concordant * (1 - input.rootPassRate),
3189
+ pass: {
3190
+ control: false,
3191
+ treatment: false
3192
+ },
3193
+ delta: {
3194
+ kind: "point",
3195
+ value: 0
3196
+ }
3197
+ }
3198
+ ] };
3199
+ call = {
3200
+ outcome: "pass",
3201
+ options: {
3202
+ binaryScale: s,
3203
+ confidence,
3204
+ threshold: 0
3205
+ }
3206
+ };
3207
+ } else {
3208
+ alternative = { cells: [{
3209
+ probability: 1,
3210
+ delta: {
3211
+ kind: "normal",
3212
+ mean: minimumEffect,
3213
+ sd: Math.sqrt(pooledVariance)
3214
+ }
3215
+ }] };
3216
+ call = {
3217
+ outcome: "delta",
3218
+ options: {
3219
+ continuous: true,
3220
+ confidence,
3221
+ threshold: 0,
3222
+ resamples: RESAMPLES
3223
+ }
3224
+ };
3225
+ }
3226
+ const simulated = pairedPromotionPower({
3227
+ n: units,
3228
+ alternative,
3229
+ calls: [call],
3230
+ simulations: POWER_SIMULATIONS,
3231
+ seed: seedFromDigest(hashCanonical([input.seed, finalists]))
3232
+ });
3233
+ return {
3234
+ adequate: simulated.power >= TARGET_POWER,
3235
+ minimumEffect,
3236
+ powerAtMinimumEffect: simulated.power,
3237
+ targetPower: TARGET_POWER,
3238
+ units,
3239
+ finalists,
3240
+ pooledVariance,
3241
+ estimator
3242
+ };
3243
+ }
3244
+ /**
3245
+ * One finalist against the root on the test split, in the objective's
3246
+ * direction, requiring every test unit scored by both.
3247
+ */
3248
+ function pairedTest(input) {
3249
+ const rootMeans = new Map(input.root.map((unit) => [unit.unitId, unit.mean]));
3250
+ const before = [];
3251
+ const after = [];
3252
+ for (const unit of input.node) {
3253
+ const rootMean = rootMeans.get(unit.unitId);
3254
+ if (rootMean === void 0) continue;
3255
+ before.push(input.maximize ? rootMean : unit.mean);
3256
+ after.push(input.maximize ? unit.mean : rootMean);
3257
+ }
3258
+ const pairs = before.length;
3259
+ if (pairs === 0) return {
3260
+ test: null,
3261
+ promote: false,
3262
+ improvement: null,
3263
+ why: "no test unit was scored by both it and the root"
3264
+ };
3265
+ const { estimator } = input;
3266
+ if (estimator.kind === "binary" && [...before, ...after].some((value) => value !== 0 && value !== estimator.scale)) return {
3267
+ test: null,
3268
+ promote: false,
3269
+ improvement: null,
3270
+ why: `a test score left the two-point scale {0, ${estimator.scale}} the claim fixed before the test`
3271
+ };
3272
+ const decision = decidePairedPromotion(before, after, {
3273
+ confidence: input.confidence,
3274
+ threshold: 0,
3275
+ minPairs: input.testUnits.length,
3276
+ resamples: RESAMPLES,
3277
+ seed: input.seed,
3278
+ ...estimator.kind === "binary" ? { binaryScale: estimator.scale } : { continuous: true }
3279
+ });
3280
+ const test = {
3281
+ pairs,
3282
+ confidence: input.confidence,
3283
+ method: decision.method,
3284
+ delta: plain(input.maximize ? decision.delta : -decision.delta),
3285
+ interval: input.maximize ? [plain(decision.low), plain(decision.high)] : [plain(-decision.high), plain(-decision.low)]
3286
+ };
3287
+ const complete = pairs === input.testUnits.length;
3288
+ const promote = decision.promote && complete;
3289
+ const why = !complete ? `it and the root scored ${pairs} of ${input.testUnits.length} test units, and a claim needs every one` : decision.promote ? "promoted" : !decision.sufficient ? `${pairs} test units are below the ${decision.minimumPairs} its paired test needs` : decision.indeterminate ? `its test interval has no width: ${decision.indeterminateCause}` : decision.exactTestVetoes ? "McNemar's exact test vetoes the improvement" : `its improvement interval [${round(decision.low)}, ${round(decision.high)}] at confidence ${round(input.confidence)} does not exclude 0`;
3290
+ return {
3291
+ test,
3292
+ promote,
3293
+ improvement: decision.delta,
3294
+ why
3295
+ };
3296
+ }
3297
+ function requireHeader(state) {
3298
+ const header = state.header;
3299
+ if (!header) throw new Error(`search ${state.searchId} has not been opened`);
3300
+ if (state.rootNodeId === null) throw new Error(`search ${state.searchId} has no root node`);
3301
+ return header;
3302
+ }
3303
+ function unitIds(tasks) {
3304
+ return [...new Set(tasks.map((task) => task.unitId))];
3305
+ }
3306
+ function mean$2(values) {
3307
+ let total = 0;
3308
+ for (const value of values) total += value;
3309
+ return total / values.length;
3310
+ }
3311
+ function round(value) {
3312
+ return Math.round(value * 1e4) / 1e4;
3313
+ }
3314
+ /** Canonical JSON has one zero; never hand it a negative one. */
3315
+ function plain(value) {
3316
+ return value === 0 ? 0 : value;
3317
+ }
3318
+ /** The kernel every `runSearch` ledger names as its search implementation. */
3319
+ const SEARCH_KERNEL_SOURCE = {
3320
+ uri: "npm:@tangle-network/agent-eval#runSearch",
3321
+ revision: hashCanonical({
3322
+ name: "tangle.search-kernel.2026-09",
3323
+ dispatch: "claim, then rung and root, then screen, then train; first allocated first within a stage",
3324
+ expansion: "when no cell waits, fewer than twice the lanes capacity run, and the cap admits one proposal and its expected screens",
3325
+ reservation: {
3326
+ hard: "the lane per-cell maximum",
3327
+ estimate: "1.5 times the p99 of the lane settled cells once 20 settled, else the lane prior"
3328
+ },
3329
+ retries: "a retryable errored attempt runs again, up to maxAttempts",
3330
+ resume: "an unrecorded operation is recorded failed at an unknown cost with floor 0; a recorded proposal is finished from its stored output; an unsettled cell is offered to adopt before it runs",
3331
+ writers: "one kernel per ledger file on a host, by a pid lock beside the ledger",
3332
+ divergence: "a node whose train mean over shared units rises above its parent while its descriptive or bootstrap selection interval against the parent lies wholly on the worse side is decided invalid when its cells finish",
3333
+ claim: "with a test split: after expansion stops, the claim plan is stored on the claim operation, the finalists are decided, the root and finalist test cells run together, and the claim decides; the kernel refuses to start a claim without a selection split, a minimumEffect, a pinned judge, or (under a cap) a claim reserve for the root and 3 finalists plus a cap for one screening round",
3334
+ close: "without a test split, the policy leader is selected when it has a scored cell; every other undecided node is rejected"
3335
+ })
3336
+ };
3337
+ const USD_TOLERANCE = 1e-9;
3338
+ /** The operation that holds the claim's plan. */
3339
+ const CLAIM_OPERATION = "claim";
3340
+ /** Settled cells a lane needs before its own cost distribution sets its estimate. */
3341
+ const ESTIMATE_FROM_CELLS = 20;
3342
+ const ESTIMATE_MARGIN = 1.5;
3343
+ const DEFAULT_MAX_ATTEMPTS = 3;
3344
+ /** Dispatch order: claim cells first, then rungs and the root, then screens, then train. */
3345
+ const STAGE_PRIORITY = {
3346
+ claim: 0,
3347
+ rung: 1,
3348
+ root: 1,
3349
+ screen: 2,
3350
+ train: 3,
3351
+ external: 3
3352
+ };
3353
+ /** Run a search to its close, or continue one from its ledger. */
3354
+ async function runSearch(options) {
3355
+ return new SearchKernel(options).run();
3356
+ }
3357
+ /** The expansion index a kernel operation id encodes, or null. */
3358
+ function searchExpansionIndex(operationId) {
3359
+ const match = /^expand-(\d+)$/.exec(operationId);
3360
+ return match ? Number(match[1]) : null;
3361
+ }
3362
+ var SearchKernel = class {
3363
+ options;
3364
+ recorder;
3365
+ maxAttempts;
3366
+ now;
3367
+ state;
3368
+ lanes = /* @__PURE__ */ new Map();
3369
+ inFlight = /* @__PURE__ */ new Map();
3370
+ proposal = null;
3371
+ proposalController = null;
3372
+ completions = [];
3373
+ wake = null;
3374
+ /** Unfinished cells per node. */
3375
+ pending = /* @__PURE__ */ new Map();
3376
+ /** Admitted nodes whose screen finished, in registration order. */
3377
+ screened = [];
3378
+ screenedSet = /* @__PURE__ */ new Set();
3379
+ admitted = /* @__PURE__ */ new Set();
3380
+ expansionOf = /* @__PURE__ */ new Map();
3381
+ artifacts = /* @__PURE__ */ new Map();
3382
+ /** Children a proposal returned, by artifact digest, until they are registered. */
3383
+ proposed = /* @__PURE__ */ new Map();
3384
+ /** Cells the ledger showed unsettled at start: an earlier process may hold their attempt. */
3385
+ adoptable = /* @__PURE__ */ new Set();
3386
+ operationCosts = [];
3387
+ /** Operations started: the next one's id. */
3388
+ expansions = 0;
3389
+ /** Proposals that completed, by operation index. A proposal an interrupted
3390
+ * process lost counts toward none of the stop rules, so a resumed search
3391
+ * makes the proposals the uninterrupted one would have made. */
3392
+ completed = [];
3393
+ stopReason = null;
3394
+ failure = null;
3395
+ /** The claim's design once expansion stopped; the search then only claims. */
3396
+ claimPlan = null;
3397
+ constructor(options) {
3398
+ this.options = options;
3399
+ this.recorder = options.recorder;
3400
+ this.maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
3401
+ this.now = options.now ?? Date.now;
3402
+ if (!Number.isSafeInteger(this.maxAttempts) || this.maxAttempts < 1) throw new TypeError("runSearch: maxAttempts must be a positive integer");
3403
+ const { maxExpansions } = options;
3404
+ if (maxExpansions !== void 0 && (!Number.isSafeInteger(maxExpansions) || maxExpansions < 0)) throw new TypeError("runSearch: maxExpansions must be a non-negative integer");
3405
+ for (const lane of options.executor.lanes()) {
3406
+ if (!Number.isSafeInteger(lane.capacity) || lane.capacity < 1) throw new TypeError(`runSearch: lane ${lane.name} needs a positive integer capacity`);
3407
+ if (!Number.isFinite(lane.cellUsd) || lane.cellUsd < 0) throw new TypeError(`runSearch: lane ${lane.name} needs a finite non-negative cellUsd`);
3408
+ if (this.lanes.has(lane.name)) throw new TypeError(`runSearch: lane ${lane.name} is declared twice`);
3409
+ this.lanes.set(lane.name, {
3410
+ ...lane,
3411
+ inFlight: 0,
3412
+ queues: [
3413
+ [],
3414
+ [],
3415
+ [],
3416
+ []
3417
+ ],
3418
+ settledCosts: []
3419
+ });
3420
+ }
3421
+ if (this.lanes.size === 0) throw new TypeError("runSearch: the executor declares no lane");
3422
+ }
3423
+ async run() {
3424
+ const { ledger } = this.recorder;
3425
+ const lock = ledger instanceof FileSearchLedger ? acquireSingleRunLock({ lockPath: `${ledger.path}.run.lock` }) : null;
3426
+ try {
3427
+ return await this.runLocked();
3428
+ } finally {
3429
+ lock?.release();
3430
+ }
3431
+ }
3432
+ async runLocked() {
3433
+ await this.refresh();
3434
+ const header = this.state.header;
3435
+ if (!header) throw new Error(`runSearch: search ${this.recorder.searchId} has not been opened`);
3436
+ if (header.policy.expansion !== this.options.policy.name || header.policy.allocation !== this.options.allocation.name) throw new Error(`runSearch: search ${this.recorder.searchId} was opened with policy ${header.policy.expansion} and allocation ${header.policy.allocation}, not ${this.options.policy.name} and ${this.options.allocation.name}`);
3437
+ if (this.state.closed) return this.closedResult();
3438
+ await this.restore();
3439
+ const { signal } = this.options;
3440
+ const onAbort = () => {
3441
+ for (const controller of this.inFlight.values()) controller.abort(signal?.reason);
3442
+ this.proposalController?.abort(signal?.reason);
3443
+ this.wakeUp();
3444
+ };
3445
+ signal?.addEventListener("abort", onAbort, { once: true });
3446
+ try {
3447
+ if (this.claimPlan) await this.applyClaimPlan(this.claimPlan);
3448
+ await this.loop();
3449
+ if (!this.failure && !this.claimPlan && this.state.header.splits.test.tasks.length > 0) {
3450
+ await this.beginClaim();
3451
+ await this.loop();
3452
+ }
3453
+ } finally {
3454
+ signal?.removeEventListener("abort", onAbort);
3455
+ }
3456
+ if (this.failure) throw this.failure.error;
3457
+ await this.close();
3458
+ return this.closedResult();
3459
+ }
3460
+ async restore() {
3461
+ const rootInput = this.options.codec.node(this.recorder, this.options.root);
3462
+ const rootId = this.state.rootNodeId;
3463
+ if (rootId === null) {
3464
+ const { nodeId } = await this.recorder.registerNode(rootInput);
3465
+ await this.recorder.recordEdge({
3466
+ childNodeId: nodeId,
3467
+ parents: [],
3468
+ operator: "seed",
3469
+ attribution: "explicit",
3470
+ proposer: null,
3471
+ proposalKey: "root",
3472
+ rationale: { unknown: "the root is the caller-supplied starting artifact" },
3473
+ diffs: [],
3474
+ label: "root"
3475
+ });
3476
+ await this.refresh();
3477
+ } else if (this.state.node(rootId).artifactDigest !== rootInput.artifactDigest) throw new Error(`runSearch: search ${this.recorder.searchId} started from another root artifact; a changed start is a derived search`);
3478
+ const root = this.state.rootNodeId;
3479
+ this.artifacts.set(root, this.options.root);
3480
+ this.expansionOf.set(root, -1);
3481
+ if (this.state.audit.cells.allocated === 0) this.assertClaimReady();
3482
+ await this.restoreClaimPlan();
3483
+ const recorded = [];
3484
+ for (let expansion = 0;; expansion++) {
3485
+ const operationId = `expand-${expansion}`;
3486
+ const operation = this.state.operation(operationId);
3487
+ if (!operation) break;
3488
+ this.expansions = expansion + 1;
3489
+ if (operation.recorded) {
3490
+ this.operationCosts.push(operation.spentUsd);
3491
+ recorded.push(expansion);
3492
+ if (operation.outcome === "completed") this.completed.push(expansion);
3493
+ continue;
3494
+ }
3495
+ await this.recorder.recordOperation({
3496
+ operationId,
3497
+ operationKind: "candidate-generation",
3498
+ execution: this.options.proposer.execution,
3499
+ outcome: {
3500
+ status: "failed",
3501
+ failure: {
3502
+ code: "interrupted",
3503
+ message: "the search stopped while this proposal ran; its output was lost"
3504
+ }
3505
+ },
3506
+ accounting: interruptedAccounting()
3507
+ });
3508
+ await this.refresh();
3509
+ }
3510
+ for (const node of this.state.nodes()) {
3511
+ if (node.edgeIds.length === 0) continue;
3512
+ const edge = this.state.edge(node.edgeIds[0]);
3513
+ const expansion = edge.proposer?.operationId ? searchExpansionIndex(edge.proposer.operationId) : null;
3514
+ this.expansionOf.set(node.nodeId, expansion ?? -1);
3515
+ if (node.status !== "invalid") this.admitted.add(node.nodeId);
3516
+ }
3517
+ for (const cell of this.state.cells()) {
3518
+ const lane = cell.lane === null ? void 0 : this.lanes.get(cell.lane);
3519
+ if (cell.final && lane) lane.settledCosts.push(cell.spentUsd);
3520
+ if (this.cellDone(cell)) continue;
3521
+ if (!lane) throw new Error(`runSearch: cell ${cell.cellId} was placed on lane ${String(cell.lane)}, which the executor does not declare`);
3522
+ this.adoptable.add(cell.cellId);
3523
+ this.enqueue(cell);
3524
+ if (cell.stage !== "claim") this.pending.set(cell.nodeId, (this.pending.get(cell.nodeId) ?? 0) + 1);
3525
+ }
3526
+ if (this.claimPlan) return;
3527
+ for (const expansion of recorded) await this.finishProposal(expansion);
3528
+ for (const node of this.state.nodes()) if (this.admitted.has(node.nodeId) && !isTerminal(node)) await this.allocateFor(node.nodeId);
3529
+ for (const nodeId of this.state.nodeIds()) if (this.admitted.has(nodeId) && (this.pending.get(nodeId) ?? 0) === 0) await this.screenDone(nodeId);
3530
+ }
3531
+ /**
3532
+ * A search that will claim must be able to: finalists come from a selection
3533
+ * split, the power check needs the claim's minimum effect, a ship needs a
3534
+ * pinned judge, and under a cap the claim reserve must cover the root and 3
3535
+ * finalists on every test task while the rest of the cap covers one
3536
+ * screening round. Checked before the first cell, so a search that cannot
3537
+ * claim spends nothing.
3538
+ */
3539
+ assertClaimReady() {
3540
+ const header = this.state.header;
3541
+ const testTasks = header.splits.test.tasks.length;
3542
+ if (testTasks === 0) return;
3543
+ const refuse = (why) => {
3544
+ throw new Error(`runSearch: search ${this.recorder.searchId} has a test split, but ${why}`);
3545
+ };
3546
+ if (header.splits.selection.tasks.length === 0) refuse("no selection split to choose its finalists on");
3547
+ if (header.objective.claim.minimumEffect === void 0) refuse("its claim declares no minimumEffect for the power check");
3548
+ if ("unknown" in header.objective.judge) refuse("its judge is not pinned; a claim with an unknown judge cannot ship, and a resumed search could mix two judges");
3549
+ const { maxUsd, reservedClaimUsd } = header.budget;
3550
+ if (maxUsd === null) return;
3551
+ const cellUsd = this.maxCellReservation();
3552
+ const need = searchClaimReserveUsd({
3553
+ testTasks,
3554
+ reps: this.options.allocation.reps,
3555
+ cellUsd
3556
+ });
3557
+ if (reservedClaimUsd + USD_TOLERANCE < need) refuse(`its claim reserve $${reservedClaimUsd} is below the $${need} the root and 3 finalists need on ${testTasks} test tasks at $${cellUsd} a cell`);
3558
+ const rootCells = this.options.allocation.plan(this.state, this.state.rootNodeId).length;
3559
+ const screens = (this.options.proposer.childrenPerProposal ?? 1) * this.options.allocation.screenSize(this.state);
3560
+ const round = this.operationReservation().usd + (rootCells + screens) * cellUsd;
3561
+ if (maxUsd - reservedClaimUsd + USD_TOLERANCE < round) refuse(`its cap $${maxUsd} less the claim reserve $${reservedClaimUsd} does not cover the root and one screening round ($${round})`);
3562
+ }
3563
+ /** A claim an earlier process began: continue it from its stored plan. */
3564
+ async restoreClaimPlan() {
3565
+ const operation = this.state.operation(CLAIM_OPERATION);
3566
+ if (!operation) return;
3567
+ const ref = operation.artifacts.find((artifact) => artifact.role === "claim-plan");
3568
+ if (!ref) throw new Error(`runSearch: the ${CLAIM_OPERATION} operation has no stored plan`);
3569
+ const plan = this.recorder.readBlob(ref);
3570
+ if (!operation.recorded) await this.recordClaimOperation();
3571
+ this.claimPlan = plan;
3572
+ this.stopReason = plan.stopReason;
3573
+ }
3574
+ async loop() {
3575
+ for (;;) {
3576
+ if (this.failure || this.options.signal?.aborted) {
3577
+ await this.drain(this.failure?.error ?? this.options.signal?.reason);
3578
+ this.failure ??= { error: this.options.signal?.reason ?? /* @__PURE__ */ new Error("aborted") };
3579
+ return;
3580
+ }
3581
+ if (this.pastDeadline()) {
3582
+ this.stopReason ??= "deadline";
3583
+ if (this.queuedCount() > 0) await this.cancelQueued("deadline");
3584
+ }
3585
+ this.dispatch();
3586
+ const blocked = this.stopReason === null && this.proposal === null ? await this.tryExpand() : null;
3587
+ if (!(this.inFlight.size > 0 || this.proposal !== null)) {
3588
+ if (this.queuedCount() > 0) continue;
3589
+ this.stopReason ??= blocked ?? "converged";
3590
+ return;
3591
+ }
3592
+ await this.handle(await this.next());
3593
+ }
3594
+ }
3595
+ /** The next completion, or null when an abort woke the loop without one. */
3596
+ async next() {
3597
+ if (this.completions.length === 0) await new Promise((resolve) => {
3598
+ this.wake = resolve;
3599
+ });
3600
+ return this.completions.shift() ?? null;
3601
+ }
3602
+ push(completion) {
3603
+ this.completions.push(completion);
3604
+ this.wakeUp();
3605
+ }
3606
+ wakeUp() {
3607
+ const wake = this.wake;
3608
+ this.wake = null;
3609
+ wake?.();
3610
+ }
3611
+ async handle(completion) {
3612
+ if (completion === null) return;
3613
+ if (completion.kind === "cell") await this.settle(completion);
3614
+ else await this.recordProposal(completion);
3615
+ }
3616
+ /** Abort everything in flight and record what still completes. */
3617
+ async drain(reason) {
3618
+ for (const controller of this.inFlight.values()) controller.abort(reason);
3619
+ this.proposalController?.abort(reason);
3620
+ while (this.inFlight.size > 0 || this.proposal !== null) await this.handle(await this.next());
3621
+ }
3622
+ /** Fill free lane slots with queued cells. A queued cell was admitted when
3623
+ * it was allocated, so starting it needs no further decision. */
3624
+ dispatch() {
3625
+ if (this.failure || this.options.signal?.aborted) return;
3626
+ const claimOnly = this.pastDeadline();
3627
+ const cap = this.state.header.budget.maxConcurrency;
3628
+ for (const lane of this.lanes.values()) while (lane.inFlight < lane.capacity && (cap === null || this.inFlight.size < cap)) {
3629
+ const cellId = (claimOnly ? lane.queues[STAGE_PRIORITY.claim] : lane.queues.find((waiting) => waiting.length > 0))?.shift();
3630
+ if (cellId === void 0) break;
3631
+ this.start(cellId, lane);
3632
+ }
3633
+ }
3634
+ start(cellId, lane) {
3635
+ const cell = this.state.cell(cellId);
3636
+ const attempt = cell.attempts + 1;
3637
+ const controller = new AbortController();
3638
+ const work = {
3639
+ searchId: this.recorder.searchId,
3640
+ cellId,
3641
+ nodeId: cell.nodeId,
3642
+ taskId: cell.taskId,
3643
+ unitId: cell.unitId,
3644
+ split: cell.split,
3645
+ rep: cell.rep,
3646
+ stage: cell.stage,
3647
+ lane: lane.name,
3648
+ attempt,
3649
+ runId: `${cellId}:${attempt}`,
3650
+ artifact: this.artifact(cell.nodeId),
3651
+ signal: controller.signal
3652
+ };
3653
+ const adopt = this.adoptable.delete(cellId);
3654
+ this.inFlight.set(cellId, controller);
3655
+ lane.inFlight += 1;
3656
+ const { executor } = this.options;
3657
+ (async () => (adopt ? await executor.adopt(work) : null) ?? await executor.run(work))().then((result) => this.push({
3658
+ kind: "cell",
3659
+ cellId,
3660
+ attempt,
3661
+ result
3662
+ }), (error) => this.push({
3663
+ kind: "cell",
3664
+ cellId,
3665
+ attempt,
3666
+ error
3667
+ }));
3668
+ }
3669
+ async settle(completion) {
3670
+ const { cellId, attempt } = completion;
3671
+ this.inFlight.delete(cellId);
3672
+ const before = this.state.cell(cellId);
3673
+ const lane = this.lanes.get(before.lane);
3674
+ lane.inFlight -= 1;
3675
+ if (!completion.result) {
3676
+ this.failure ??= { error: completion.error };
3677
+ return;
3678
+ }
3679
+ this.dispatch();
3680
+ if ((this.failure || this.options.signal?.aborted) && completion.result.outcome.status === "errored") return;
3681
+ await this.recorder.settleCell({
3682
+ ...completion.result,
3683
+ cellId,
3684
+ attempt
3685
+ });
3686
+ await this.refresh();
3687
+ const cell = this.state.cell(cellId);
3688
+ if (cell.final) lane.settledCosts.push(cell.spentUsd);
3689
+ if (!this.cellDone(cell)) {
3690
+ if (this.failure || this.options.signal?.aborted) return;
3691
+ this.enqueue(cell);
3692
+ return;
3693
+ }
3694
+ if (cell.stage === "claim") return;
3695
+ const left = (this.pending.get(cell.nodeId) ?? 1) - 1;
3696
+ this.pending.set(cell.nodeId, left);
3697
+ if (left === 0) await this.onScreenDone(cell.nodeId);
3698
+ }
3699
+ async onScreenDone(nodeId) {
3700
+ if (!await this.screenDone(nodeId)) return;
3701
+ if (this.stopReason === null && !this.failure) await this.allocateFor(nodeId);
3702
+ }
3703
+ /**
3704
+ * A node's allocated cells all finished. A node that diverges (its train
3705
+ * mean rose over its parent's while its selection interval against the
3706
+ * parent lies wholly on the worse side) is decided `invalid` and never
3707
+ * becomes a parent; any other node is screened. Returns whether it was.
3708
+ */
3709
+ async screenDone(nodeId) {
3710
+ if (!this.claimPlan && !this.screenedSet.has(nodeId)) {
3711
+ const divergence = searchDivergence(this.state, nodeId);
3712
+ if (divergence) {
3713
+ await this.recorder.decideNode({
3714
+ nodeId,
3715
+ decision: { status: "invalid" },
3716
+ basis: divergence.basis,
3717
+ rule: "divergence",
3718
+ reason: divergence.reason
3719
+ });
3720
+ await this.refresh();
3721
+ this.admitted.delete(nodeId);
3722
+ return false;
3723
+ }
3724
+ }
3725
+ this.markScreened(nodeId);
3726
+ return true;
3727
+ }
3728
+ markScreened(nodeId) {
3729
+ if (this.screenedSet.has(nodeId)) return;
3730
+ this.screenedSet.add(nodeId);
3731
+ const ordinal = this.state.node(nodeId).ordinal;
3732
+ let index = this.screened.length;
3733
+ while (index > 0 && this.state.node(this.screened[index - 1]).ordinal > ordinal) index--;
3734
+ this.screened = [
3735
+ ...this.screened.slice(0, index),
3736
+ nodeId,
3737
+ ...this.screened.slice(index)
3738
+ ];
3739
+ }
3740
+ /** Allocate the cells the allocator plans for a node and the ledger lacks. */
3741
+ async allocateFor(nodeId) {
3742
+ const plans = this.options.allocation.plan(this.state, nodeId).filter((plan) => !this.state.cell(searchCellId(this.recorder.searchId, nodeId, plan.taskId, plan.split, plan.rep)));
3743
+ if (plans.length === 0) return;
3744
+ const placed = plans.map((plan) => {
3745
+ const laneName = this.options.executor.place({
3746
+ ...plan,
3747
+ nodeId
3748
+ });
3749
+ const lane = this.lanes.get(laneName);
3750
+ if (!lane) throw new Error(`runSearch: the executor placed a cell on unknown lane ${laneName}`);
3751
+ return {
3752
+ plan,
3753
+ lane,
3754
+ reservation: this.cellReservation(lane)
3755
+ };
3756
+ });
3757
+ const hold = placed.reduce((sum, { reservation }) => sum + reservation.usd, 0);
3758
+ const { headroomUsd } = this.state.budget;
3759
+ if (headroomUsd !== null && hold > headroomUsd + USD_TOLERANCE) return;
3760
+ const { maxCells } = this.state.header.budget;
3761
+ if (maxCells !== null && this.state.audit.cells.allocated + placed.length > maxCells) return;
3762
+ for (const { plan, lane, reservation } of placed) await this.recorder.allocateCell({
3763
+ nodeId,
3764
+ taskId: plan.taskId,
3765
+ split: plan.split,
3766
+ rep: plan.rep,
3767
+ stage: plan.stage,
3768
+ lane: lane.name,
3769
+ reservation
3770
+ });
3771
+ await this.refresh();
3772
+ for (const { plan } of placed) {
3773
+ const cellId = searchCellId(this.recorder.searchId, nodeId, plan.taskId, plan.split, plan.rep);
3774
+ this.enqueue(this.state.cell(cellId));
3775
+ }
3776
+ this.pending.set(nodeId, (this.pending.get(nodeId) ?? 0) + placed.length);
3777
+ this.dispatch();
3778
+ }
3779
+ enqueue(cell) {
3780
+ this.lanes.get(cell.lane).queues[STAGE_PRIORITY[cell.stage]].push(cell.cellId);
3781
+ }
3782
+ queuedCount() {
3783
+ let count = 0;
3784
+ for (const lane of this.lanes.values()) for (const queue of lane.queues) count += queue.length;
3785
+ return count;
3786
+ }
3787
+ /** Fix the claim's design, store it, and start its test cells. */
3788
+ async beginClaim() {
3789
+ await this.refresh();
3790
+ const stopReason = this.stopReason ?? "converged";
3791
+ const header = this.state.header;
3792
+ const reps = this.options.allocation.reps;
3793
+ const plan = planSearchClaim(this.state, {
3794
+ stopReason,
3795
+ reps,
3796
+ affordable: (finalists) => {
3797
+ const placed = this.claimCells(finalists, this.state.rootNodeId);
3798
+ const hold = placed.reduce((sum, cell) => sum + cell.reservation.usd, 0);
3799
+ const { headroomUsd, claimReserveUsd } = this.state.budget;
3800
+ const { maxCells } = header.budget;
3801
+ return (headroomUsd === null || hold <= headroomUsd + claimReserveUsd + USD_TOLERANCE) && (maxCells === null || this.state.audit.cells.allocated + placed.length <= maxCells);
3802
+ }
3803
+ });
3804
+ await this.recorder.startOperation({
3805
+ operationId: CLAIM_OPERATION,
3806
+ operationKind: "selection",
3807
+ reservation: null,
3808
+ artifacts: [this.recorder.blob("claim-plan", plan)]
3809
+ });
3810
+ await this.recordClaimOperation();
3811
+ this.claimPlan = plan;
3812
+ this.stopReason = stopReason;
3813
+ await this.applyClaimPlan(plan);
3814
+ }
3815
+ async recordClaimOperation() {
3816
+ await this.recorder.recordOperation({
3817
+ operationId: CLAIM_OPERATION,
3818
+ operationKind: "selection",
3819
+ execution: {
3820
+ kind: "deterministic",
3821
+ source: SEARCH_CLAIM_RULE
3822
+ },
3823
+ outcome: { status: "completed" },
3824
+ accounting: {
3825
+ tokens: {
3826
+ status: "known",
3827
+ inputTokens: 0,
3828
+ outputTokens: 0,
3829
+ cachedTokens: 0
3830
+ },
3831
+ cost: {
3832
+ status: "known",
3833
+ usd: 0,
3834
+ source: "free"
3835
+ }
3836
+ }
3837
+ });
3838
+ await this.refresh();
3839
+ }
3840
+ /**
3841
+ * Decide the plan's finalists and allocate the test cells of the root and
3842
+ * the finalists, interleaved by task so the arms of one task run side by
3843
+ * side. Allocation finishes before any claim cell starts. Idempotent.
3844
+ */
3845
+ async applyClaimPlan(plan) {
3846
+ const root = this.state.rootNodeId;
3847
+ const bases = plan.finalists.map((finalist) => estimateNode(this.state, finalist.nodeId, {
3848
+ against: root,
3849
+ split: "selection"
3850
+ }));
3851
+ for (const [rank, finalist] of plan.finalists.entries()) await this.recorder.decideNode({
3852
+ nodeId: finalist.nodeId,
3853
+ decision: { status: "finalist" },
3854
+ basis: bases[rank],
3855
+ rule: SEARCH_CLAIM_RULE_NAME,
3856
+ reason: `rank ${rank + 1} of ${plan.finalists.length} by selection mean (${finalist.selectionMean}); ${plan.reason}`
3857
+ });
3858
+ await this.refresh();
3859
+ if (plan.test !== "run") return;
3860
+ const missing = this.claimCells(plan.finalists.length, root, plan).map((cell) => ({
3861
+ ...cell,
3862
+ cellId: searchCellId(this.recorder.searchId, cell.nodeId, cell.plan.taskId, "test", cell.plan.rep)
3863
+ })).filter((cell) => !this.state.cell(cell.cellId));
3864
+ for (const cell of missing) await this.recorder.allocateCell({
3865
+ nodeId: cell.nodeId,
3866
+ taskId: cell.plan.taskId,
3867
+ split: "test",
3868
+ rep: cell.plan.rep,
3869
+ stage: "claim",
3870
+ lane: cell.lane.name,
3871
+ reservation: cell.reservation
3872
+ });
3873
+ await this.refresh();
3874
+ for (const cell of missing) this.enqueue(this.state.cell(cell.cellId));
3875
+ }
3876
+ /** The test cells of the root and the first `finalists` of the plan (or of
3877
+ * the finalist count being priced), task by task, with their lanes and holds. */
3878
+ claimCells(finalists, root, plan) {
3879
+ const header = this.state.header;
3880
+ const arms = [root, ...plan ? plan.finalists.map((entry) => entry.nodeId) : []];
3881
+ while (arms.length < finalists + 1) arms.push(root);
3882
+ const cells = [];
3883
+ for (const task of header.splits.test.tasks) for (let rep = 0; rep < this.options.allocation.reps; rep++) for (const nodeId of arms) {
3884
+ const cellPlan = {
3885
+ taskId: task.taskId,
3886
+ split: "test",
3887
+ rep,
3888
+ stage: "claim"
3889
+ };
3890
+ const laneName = this.options.executor.place({
3891
+ ...cellPlan,
3892
+ nodeId
3893
+ });
3894
+ const lane = this.lanes.get(laneName);
3895
+ if (!lane) throw new Error(`runSearch: the executor placed a cell on unknown lane ${laneName}`);
3896
+ cells.push({
3897
+ nodeId,
3898
+ plan: cellPlan,
3899
+ lane,
3900
+ reservation: this.cellReservation(lane)
3901
+ });
3902
+ }
3903
+ return cells;
3904
+ }
3905
+ async cancelQueued(reason) {
3906
+ for (const lane of this.lanes.values()) for (const [priority, queue] of lane.queues.entries()) {
3907
+ if (priority === STAGE_PRIORITY.claim) continue;
3908
+ for (const cellId of queue.splice(0)) {
3909
+ const cell = this.state.cell(cellId);
3910
+ if (cell.attempts === 0) {
3911
+ await this.recorder.cancelCell({
3912
+ cellId,
3913
+ reason
3914
+ });
3915
+ await this.refresh();
3916
+ }
3917
+ const left = (this.pending.get(cell.nodeId) ?? 1) - 1;
3918
+ this.pending.set(cell.nodeId, left);
3919
+ if (left === 0) this.markScreened(cell.nodeId);
3920
+ }
3921
+ }
3922
+ }
3923
+ /** No further attempt will run: a final outcome, a cancellation, or a
3924
+ * retryable error that used up its attempts. */
3925
+ cellDone(cell) {
3926
+ return cell.final || cell.cancelled !== null || cell.outcome === "errored" && cell.attempts >= this.maxAttempts;
3927
+ }
3928
+ /** Start a proposal when the rules admit one; otherwise say why not. */
3929
+ async tryExpand() {
3930
+ const { maxNodes, maxCells } = this.state.header.budget;
3931
+ if (maxNodes !== null && this.state.audit.nodes >= maxNodes) return "max-nodes";
3932
+ const { maxExpansions } = this.options;
3933
+ if (maxExpansions !== void 0 && this.completed.length >= maxExpansions) return "max-nodes";
3934
+ if (this.queuedCount() > 0) return "converged";
3935
+ if (this.inFlight.size >= 2 * this.capacity()) return "converged";
3936
+ const view = this.policyView();
3937
+ const { policy } = this.options;
3938
+ const leader = policy.leader(view);
3939
+ if (policy.patience !== void 0 && this.completed.filter((index) => index > (this.expansionOf.get(leader) ?? -1)).length >= policy.patience) return "patience";
3940
+ const screen = this.options.allocation.screenSize(this.state);
3941
+ const operation = this.operationReservation();
3942
+ const children = this.options.proposer.childrenPerProposal ?? 1;
3943
+ const need = operation.usd + children * screen * this.maxCellReservation();
3944
+ const { headroomUsd } = this.state.budget;
3945
+ if (headroomUsd !== null && need > headroomUsd + USD_TOLERANCE) return "budget";
3946
+ if (maxCells !== null && this.state.audit.cells.allocated + children * screen > maxCells) return "budget";
3947
+ const expansion = policy.expand(view);
3948
+ if (expansion === null) return "converged";
3949
+ const index = this.expansions++;
3950
+ const operationId = `expand-${index}`;
3951
+ await this.recorder.startOperation({
3952
+ operationId,
3953
+ operationKind: "candidate-generation",
3954
+ reservation: operation
3955
+ });
3956
+ await this.refresh();
3957
+ const controller = new AbortController();
3958
+ const request = {
3959
+ expansion: index,
3960
+ operationId,
3961
+ operator: expansion.operator,
3962
+ parents: expansion.parents.map((nodeId) => ({
3963
+ nodeId,
3964
+ artifact: this.artifact(nodeId)
3965
+ })),
3966
+ leader,
3967
+ signal: controller.signal
3968
+ };
3969
+ this.proposalController = controller;
3970
+ this.proposal = this.options.proposer.propose(request).then((result) => this.push({
3971
+ kind: "proposal",
3972
+ expansion: index,
3973
+ operationId,
3974
+ request,
3975
+ selection: expansion.selection,
3976
+ result
3977
+ }), (error) => this.push({
3978
+ kind: "proposal",
3979
+ expansion: index,
3980
+ operationId,
3981
+ request,
3982
+ selection: expansion.selection,
3983
+ error
3984
+ }));
3985
+ return null;
3986
+ }
3987
+ async recordProposal(completion) {
3988
+ this.proposal = null;
3989
+ this.proposalController = null;
3990
+ const { operationId, expansion } = completion;
3991
+ const { proposer, codec, admit } = this.options;
3992
+ const result = completion.result;
3993
+ if (!result) {
3994
+ await this.recorder.recordOperation({
3995
+ operationId,
3996
+ operationKind: "candidate-generation",
3997
+ execution: proposer.execution,
3998
+ outcome: {
3999
+ status: "failed",
4000
+ failure: {
4001
+ code: "proposer-error",
4002
+ message: errorMessage(completion.error)
4003
+ }
4004
+ },
4005
+ accounting: interruptedAccounting()
4006
+ });
4007
+ await this.refresh();
4008
+ this.failure ??= { error: completion.error };
4009
+ return;
4010
+ }
4011
+ const { maxNodes } = this.state.header.budget;
4012
+ const room = maxNodes === null ? Number.POSITIVE_INFINITY : maxNodes - this.state.audit.nodes;
4013
+ const parents = completion.request.parents;
4014
+ const blob = {
4015
+ kind: "search-proposal",
4016
+ operator: completion.request.operator,
4017
+ parents: parents.map((parent) => parent.nodeId),
4018
+ selection: completion.selection,
4019
+ children: result.children.slice(0, Math.max(0, room)).map((child) => ({
4020
+ node: codec.node(this.recorder, child.artifact),
4021
+ diffs: parents.map((parent) => codec.diff(this.recorder, parent.artifact, child.artifact)),
4022
+ label: redactText(child.label, { profile: "share" }),
4023
+ rationale: redactText(child.rationale, { profile: "share" }),
4024
+ attribution: child.attribution ?? null,
4025
+ invalid: admit?.(child.artifact) ?? null
4026
+ }))
4027
+ };
4028
+ await this.recorder.recordOperation({
4029
+ operationId,
4030
+ operationKind: "candidate-generation",
4031
+ execution: result.execution ?? proposer.execution,
4032
+ outcome: { status: "completed" },
4033
+ accounting: result.accounting,
4034
+ artifacts: [this.recorder.blob("proposal", blob)]
4035
+ });
4036
+ await this.refresh();
4037
+ this.operationCosts.push(this.state.operation(operationId).spentUsd);
4038
+ this.completed.push(expansion);
4039
+ for (const [index, child] of blob.children.entries()) this.proposed.set(child.node.artifactDigest, result.children[index].artifact);
4040
+ if (result.stop !== void 0 || blob.children.length === 0) this.stopReason ??= "converged";
4041
+ await this.finishProposal(expansion);
4042
+ }
4043
+ /** Register every child a recorded proposal produced, each once. */
4044
+ async finishProposal(expansion) {
4045
+ const operationId = `expand-${expansion}`;
4046
+ const operation = this.state.operation(operationId);
4047
+ const ref = operation?.artifacts.find((artifact) => artifact.role === "proposal");
4048
+ if (!operation?.recorded || operation.outcome !== "completed" || !ref) return;
4049
+ const blob = this.recorder.readBlob(ref);
4050
+ const { proposer } = this.options;
4051
+ for (const [index, child] of blob.children.entries()) {
4052
+ const { nodeId } = await this.recorder.registerNode(child.node);
4053
+ const held = this.proposed.get(child.node.artifactDigest);
4054
+ if (held !== void 0) {
4055
+ this.proposed.delete(child.node.artifactDigest);
4056
+ if (!this.artifacts.has(nodeId)) this.artifacts.set(nodeId, held);
4057
+ }
4058
+ const edgeId = await this.recorder.recordEdge({
4059
+ childNodeId: nodeId,
4060
+ parents: blob.parents,
4061
+ operator: blob.operator,
4062
+ attribution: "explicit",
4063
+ proposer: {
4064
+ kind: proposer.kind,
4065
+ name: proposer.name,
4066
+ operationId,
4067
+ source: proposer.source
4068
+ },
4069
+ proposalKey: `${operationId}:${index}`,
4070
+ selection: blob.selection,
4071
+ rationale: child.rationale,
4072
+ diffs: child.diffs,
4073
+ label: child.label
4074
+ });
4075
+ await this.refresh();
4076
+ if (this.state.node(nodeId).edgeIds[0] !== edgeId) continue;
4077
+ this.expansionOf.set(nodeId, expansion);
4078
+ if (child.invalid !== null) {
4079
+ await this.recorder.decideNode({
4080
+ nodeId,
4081
+ decision: { status: "invalid" },
4082
+ rule: "admission",
4083
+ reason: child.invalid
4084
+ });
4085
+ await this.refresh();
4086
+ continue;
4087
+ }
4088
+ if (this.admitted.has(nodeId)) continue;
4089
+ this.admitted.add(nodeId);
4090
+ await this.allocateFor(nodeId);
4091
+ if ((this.pending.get(nodeId) ?? 0) === 0) this.markScreened(nodeId);
4092
+ }
4093
+ }
4094
+ async close() {
4095
+ await this.refresh();
4096
+ if (this.claimPlan) {
4097
+ await this.closeWithClaim(this.claimPlan);
4098
+ return;
4099
+ }
4100
+ const view = this.policyView();
4101
+ const leader = this.options.policy.leader(view);
4102
+ const { split } = view;
4103
+ const root = this.state.rootNodeId;
4104
+ const rule = this.options.policy.name;
4105
+ const decisions = this.state.nodes().filter((node) => !isTerminal(node)).map((node) => {
4106
+ const scored = this.state.scoredCells(node.nodeId, split).length > 0;
4107
+ const lead = node.nodeId === leader && scored;
4108
+ const against = node.nodeId === leader ? root : leader;
4109
+ const basis = node.nodeId === against || !scored ? null : estimateNode(this.state, node.nodeId, {
4110
+ against,
4111
+ split
4112
+ });
4113
+ return {
4114
+ nodeId: node.nodeId,
4115
+ decision: { status: lead ? "selected" : "rejected" },
4116
+ basis,
4117
+ rule,
4118
+ reason: lead ? `the leader when the search stopped (${this.stopReason}): a budget decision on the ${split} split, which claims nothing` : !this.screenedSet.has(node.nodeId) || !scored ? "the search stopped before this node was measured" : !view.complete(node.nodeId) ? "it left a unit of its screen unscored, so it could not lead" : "it did not beat the leader on the units they share"
4119
+ };
4120
+ });
4121
+ for (const decision of decisions) await this.recorder.decideNode(decision);
4122
+ await this.recorder.close({
4123
+ reason: this.stopReason ?? "converged",
4124
+ claim: null
4125
+ });
4126
+ await this.refresh();
4127
+ }
4128
+ /** Close with the claim: the claim's decisions, then every other open node
4129
+ * rejected with its selection estimate against the root. */
4130
+ async closeWithClaim(plan) {
4131
+ const { claim, decisions } = decideSearchClaim(this.state, plan);
4132
+ const decided = new Set(decisions.map((decision) => decision.nodeId));
4133
+ const root = this.state.rootNodeId;
4134
+ const rest = this.state.nodes().filter((node) => !decided.has(node.nodeId) && !isTerminal(node)).map((node) => {
4135
+ const scored = this.state.scoredCells(node.nodeId, "selection").length > 0;
4136
+ return {
4137
+ nodeId: node.nodeId,
4138
+ decision: { status: "rejected" },
4139
+ basis: scored ? estimateNode(this.state, node.nodeId, {
4140
+ against: root,
4141
+ split: "selection"
4142
+ }) : null,
4143
+ rule: SEARCH_CLAIM_RULE_NAME,
4144
+ reason: scored ? "not a finalist: the claim tests at most 3 nodes that scored every selection unit and beat the root, best selection mean first" : "the search stopped before this node was measured on the selection split"
4145
+ };
4146
+ });
4147
+ for (const decision of [...decisions, ...rest]) await this.recorder.decideNode(decision);
4148
+ await this.recorder.close({
4149
+ reason: plan.stopReason,
4150
+ claim
4151
+ });
4152
+ await this.refresh();
4153
+ }
4154
+ closedResult() {
4155
+ const state = this.state;
4156
+ return {
4157
+ state,
4158
+ leader: state.audit.selectedNodeId ?? state.rootNodeId,
4159
+ reason: state.closed.reason,
4160
+ claim: state.closed.claim
4161
+ };
4162
+ }
4163
+ policyView() {
4164
+ return searchPolicyView(this.state, {
4165
+ screened: this.screened,
4166
+ screening: this.admitted.size - this.screenedSet.size,
4167
+ expansions: this.completed.length
4168
+ });
4169
+ }
4170
+ artifact(nodeId) {
4171
+ const held = this.artifacts.get(nodeId);
4172
+ if (held !== void 0) return held;
4173
+ const artifact = this.options.codec.load(this.recorder, this.state.node(nodeId));
4174
+ this.artifacts.set(nodeId, artifact);
4175
+ return artifact;
4176
+ }
4177
+ capacity() {
4178
+ let total = 0;
4179
+ for (const lane of this.lanes.values()) total += lane.capacity;
4180
+ const cap = this.state.header.budget.maxConcurrency;
4181
+ return cap === null ? total : Math.min(cap, total);
4182
+ }
4183
+ /** A hard lane holds its enforced maximum. An estimate lane holds 1.5 times
4184
+ * the 99th percentile of its settled cells once 20 settled, else its prior. */
4185
+ cellReservation(lane) {
4186
+ if (lane.costCap === "hard") return {
4187
+ kind: "hard",
4188
+ usd: lane.cellUsd
4189
+ };
4190
+ return {
4191
+ kind: "estimate",
4192
+ usd: estimate(lane.settledCosts, lane.cellUsd)
4193
+ };
4194
+ }
4195
+ maxCellReservation() {
4196
+ let most = 0;
4197
+ for (const lane of this.lanes.values()) most = Math.max(most, this.cellReservation(lane).usd);
4198
+ return most;
4199
+ }
4200
+ operationReservation() {
4201
+ return {
4202
+ kind: "estimate",
4203
+ usd: estimate(this.operationCosts, this.options.proposer.reservationUsd ?? 0)
4204
+ };
4205
+ }
4206
+ pastDeadline() {
4207
+ const deadline = this.state.header.budget.deadline;
4208
+ return deadline !== null && this.now() >= Date.parse(deadline);
4209
+ }
4210
+ async refresh() {
4211
+ this.state = await this.recorder.state();
4212
+ }
4213
+ };
4214
+ /**
4215
+ * A policy's read of a search: `screened` lists the admitted nodes whose screen
4216
+ * finished, in registration order; `screening` counts the admitted nodes still
4217
+ * being screened. Nodes are ranked on the selection split, or on train when
4218
+ * the search declares no selection split. The view reads no test cell.
4219
+ */
4220
+ function searchPolicyView(state, input) {
4221
+ const header = state.header;
4222
+ if (!header) throw new Error(`search ${state.searchId} has not been opened`);
4223
+ const split = header.splits.selection.tasks.length > 0 ? "selection" : "train";
4224
+ return {
4225
+ searchId: state.searchId,
4226
+ seed: header.policy.seed,
4227
+ direction: header.objective.direction,
4228
+ split,
4229
+ rootNodeId: state.rootNodeId,
4230
+ expansions: input.expansions,
4231
+ screened: [...input.screened],
4232
+ screening: input.screening,
4233
+ complete: (nodeId) => {
4234
+ const cells = state.cells({ nodeId }).filter((cell) => cell.split === split);
4235
+ return cells.length > 0 && cells.every((cell) => cell.score !== null);
4236
+ },
4237
+ unitScores: (nodeId) => state.unitScores(nodeId, split),
4238
+ estimate: (nodeId, against) => estimateNode(state, nodeId, {
4239
+ against,
4240
+ split
4241
+ })
4242
+ };
4243
+ }
4244
+ /**
4245
+ * The divergence rule: a node whose train mean rose over its parent's on the
4246
+ * train units they share while its selection interval against the parent (6
4247
+ * or more units, not indeterminate) lies wholly on the worse side. Train is
4248
+ * what the proposer reads; a gain there that selection contradicts is the
4249
+ * signature of fitting the feedback instead of the task.
4250
+ */
4251
+ function searchDivergence(state, nodeId) {
4252
+ const header = state.header;
4253
+ const parent = state.node(nodeId)?.primaryParentId ?? null;
4254
+ if (!header || parent === null || header.splits.selection.tasks.length === 0) return null;
4255
+ const selection = estimateNode(state, nodeId, {
4256
+ against: parent,
4257
+ split: "selection"
4258
+ });
4259
+ if (selection.interval === null) return null;
4260
+ const maximize = header.objective.direction === "maximize";
4261
+ const [low, high] = selection.interval;
4262
+ if (maximize ? high >= 0 : low <= 0) return null;
4263
+ const parentTrain = new Map(state.unitScores(parent, "train").map((unit) => [unit.unitId, unit.mean]));
4264
+ let gain = 0;
4265
+ let shared = 0;
4266
+ for (const unit of state.unitScores(nodeId, "train")) {
4267
+ const before = parentTrain.get(unit.unitId);
4268
+ if (before === void 0) continue;
4269
+ gain += maximize ? unit.mean - before : before - unit.mean;
4270
+ shared += 1;
4271
+ }
4272
+ if (shared === 0 || gain <= 0) return null;
4273
+ return {
4274
+ basis: selection,
4275
+ reason: `its train mean rose ${Math.round(gain / shared * 1e4) / 1e4} over its parent's on ${shared} shared train unit${shared === 1 ? "" : "s"}, while its selection interval against the parent, [${round4(low)}, ${round4(high)}] on ${selection.pairs} units, lies wholly on the worse side`
4276
+ };
4277
+ }
4278
+ function round4(value) {
4279
+ return Math.round(value * 1e4) / 1e4;
4280
+ }
4281
+ function isTerminal(node) {
4282
+ return node.status !== null && node.status !== "advanced";
4283
+ }
4284
+ function estimate(costs, prior) {
4285
+ if (costs.length < ESTIMATE_FROM_CELLS) return prior;
4286
+ const sorted = [...costs].sort((a, b) => a - b);
4287
+ const p99 = sorted[Math.min(sorted.length - 1, Math.ceil(.99 * sorted.length) - 1)];
4288
+ return Math.round(ESTIMATE_MARGIN * p99 * 1e9) / 1e9;
4289
+ }
4290
+ /** Spend of work whose result was lost: unknown, with no proven floor. */
4291
+ function interruptedAccounting() {
4292
+ return {
4293
+ tokens: {
4294
+ status: "unknown",
4295
+ reason: "the result of this work was lost"
4296
+ },
4297
+ cost: {
4298
+ status: "unknown",
4299
+ knownLowerBoundUsd: 0,
4300
+ reason: "the result of this work was lost, so its spend is unknown"
4301
+ }
4302
+ };
4303
+ }
4304
+ function errorMessage(error) {
4305
+ const text = error instanceof Error ? error.message : String(error);
4306
+ return text.trim().length > 0 ? text : "the proposer failed without a message";
2549
4307
  }
2550
4308
  //#endregion
2551
4309
  //#region src/campaign/search-history-receipt.ts
@@ -2842,6 +4600,20 @@ var SearchRecorder = class SearchRecorder {
2842
4600
  byteLength: bytes.byteLength
2843
4601
  };
2844
4602
  }
4603
+ /**
4604
+ * Read back a blob this recorder stored, by its content address. The bytes
4605
+ * must hash to the reference's digest and length, so a resumed search reads
4606
+ * exactly what the ledger bound.
4607
+ */
4608
+ readBlob(ref) {
4609
+ const hex = ref.sha256.slice(7);
4610
+ const path = join(this.blobDir, `${hex}.json`);
4611
+ const text = this.storage.read(path);
4612
+ if (text === void 0) throw new Error(`search ${this.searchId}: blob ${ref.sha256} (${ref.role}) is missing at ${path}`);
4613
+ const bytes = Buffer.from(text, "utf8");
4614
+ if (createHash("sha256").update(bytes).digest("hex") !== hex || bytes.byteLength !== ref.byteLength) throw new Error(`search ${this.searchId}: blob at ${path} does not match ${ref.sha256}`);
4615
+ return JSON.parse(text);
4616
+ }
2845
4617
  /** Register a node, or return the existing node with the same artifact digest. */
2846
4618
  async registerNode(input) {
2847
4619
  const existing = (await this.ledger.state()).nodeIdForDigest(input.artifactDigest);
@@ -2904,7 +4676,7 @@ var SearchRecorder = class SearchRecorder {
2904
4676
  kind: "operation-started",
2905
4677
  eventId: `operation-started:${input.operationId}`,
2906
4678
  occurredAt: this.stamp(),
2907
- artifacts: [],
4679
+ artifacts: input.artifacts ?? [],
2908
4680
  operationId: input.operationId,
2909
4681
  operationKind: input.operationKind,
2910
4682
  reservation: input.reservation ?? null
@@ -3047,306 +4819,874 @@ function surfaceNode(recorder, surface) {
3047
4819
  surface
3048
4820
  });
3049
4821
  return {
3050
- artifactDigest: surfaceContentHash(surface),
3051
- artifact,
3052
- surfaces: declaredSurfaces(surface, artifact)
4822
+ artifactDigest: surfaceContentHash(surface),
4823
+ artifact,
4824
+ surfaces: declaredSurfaces(surface, artifact)
4825
+ };
4826
+ }
4827
+ /** A parent-to-child diff of two mutable surfaces, stored as a blob. */
4828
+ function surfaceDiff(recorder, parent, child) {
4829
+ return recorder.blob("diff", {
4830
+ kind: "surface-diff",
4831
+ from: surfaceContentHash(parent),
4832
+ to: surfaceContentHash(child),
4833
+ text: renderSurfaceDiff(child, parent)
4834
+ });
4835
+ }
4836
+ /** Declared surfaces of one node. A component surface declares one surface per
4837
+ * named component, so per-component evidence stays addressable. */
4838
+ function declaredSurfaces(surface, artifact) {
4839
+ if (typeof surface === "string") return [{
4840
+ surfaceId: "prompt",
4841
+ kind: "prompt",
4842
+ artifact
4843
+ }];
4844
+ if (surface.kind === "code") return [{
4845
+ surfaceId: "code",
4846
+ kind: "code",
4847
+ artifact
4848
+ }];
4849
+ return Object.keys(surface.components).sort().map((name) => ({
4850
+ surfaceId: `component:${name}`,
4851
+ kind: "prompt",
4852
+ artifact
4853
+ }));
4854
+ }
4855
+ function boundedLabel(label) {
4856
+ const redacted = redactText(label.trim(), { profile: "share" });
4857
+ return redacted.length <= MAX_LABEL_CHARS ? redacted : `${redacted.slice(0, MAX_LABEL_CHARS - 1)}…`;
4858
+ }
4859
+ /** The header without its stamp, for comparing a resumed opening to the stored one. */
4860
+ function headerMaterial(header) {
4861
+ const { occurredAt: _occurredAt, ...material } = header;
4862
+ return canonicalString(material);
4863
+ }
4864
+ /** The header as the ledger would store it, so the comparison ignores ordering. */
4865
+ function canonicalHeader(header) {
4866
+ return validateSearchLedgerEvent({
4867
+ ...header,
4868
+ occurredAt: "2000-01-01T00:00:00.000Z"
4869
+ });
4870
+ }
4871
+ //#endregion
4872
+ //#region src/campaign/search-policy.ts
4873
+ /**
4874
+ * Expansion policies: which node a search expands next, with which operator,
4875
+ * and which node it currently keeps.
4876
+ *
4877
+ * A policy reads a `SearchPolicyView`: nodes whose screen finished and their
4878
+ * per-unit scores on the policy's split. The view holds no test cells, so a
4879
+ * policy cannot see the sealed claim data. Its decisions spend budget and
4880
+ * claim nothing; a claim comes only from the sealed test split.
4881
+ */
4882
+ /**
4883
+ * The hill climb. The leader starts at the root; a screened node that scored
4884
+ * every unit of its screen takes the lead when its mean on the units it shares
4885
+ * with the leader beats the leader's mean on those units. Nodes are taken in
4886
+ * registration order, so the leader is a pure function of the ledger. The
4887
+ * policy expands the leader, and only once every earlier child is screened,
4888
+ * so each proposal sees every result before it.
4889
+ */
4890
+ function incumbent(options = {}) {
4891
+ const { patience } = options;
4892
+ if (patience !== void 0 && (!Number.isSafeInteger(patience) || patience < 1)) throw new TypeError(`incumbent: patience must be a positive integer, got ${String(patience)}`);
4893
+ return hillClimb({
4894
+ name: patience === void 0 ? "incumbent" : `incumbent(patience=${patience})`,
4895
+ patience,
4896
+ parent: (_view, leader) => ({
4897
+ nodeId: leader,
4898
+ evidence: {}
4899
+ })
4900
+ });
4901
+ }
4902
+ /**
4903
+ * The hill climb with a different parent: each expansion draws two distinct
4904
+ * members of the Pareto frontier (per-unit means on the policy split) with a
4905
+ * PRNG seeded from `seed` and the expansion index, and keeps the one with the
4906
+ * larger crowding distance, the more isolated. Boundary members carry infinite
4907
+ * distance. Ties fall to the higher mean, then to the earlier node. The leader
4908
+ * rule is the incumbent's: a frontier parent explores, and only a node that
4909
+ * beats the leader leads.
4910
+ */
4911
+ function crowdedFrontierParent(options) {
4912
+ const { seed } = options;
4913
+ if (!Number.isInteger(seed)) throw new TypeError(`crowdedFrontierParent: seed must be an integer, got ${String(seed)}`);
4914
+ return hillClimb({
4915
+ name: `crowded-frontier(seed=${seed})`,
4916
+ parent: (view, leader) => {
4917
+ const frontier = paretoMembers(view);
4918
+ if (frontier.length <= 1) return {
4919
+ nodeId: frontier[0]?.nodeId ?? leader,
4920
+ evidence: {}
4921
+ };
4922
+ const rng = mulberry32(seed ^ Math.imul(view.expansions + 1, 2654435761) | 0);
4923
+ const first = Math.floor(rng() * frontier.length);
4924
+ const offset = Math.floor(rng() * (frontier.length - 1));
4925
+ const second = offset >= first ? offset + 1 : offset;
4926
+ const a = frontier[first];
4927
+ const b = frontier[second];
4928
+ const winner = compareCrowded(a, b) <= 0 ? a : b;
4929
+ return {
4930
+ nodeId: winner.nodeId,
4931
+ evidence: {
4932
+ frontierSize: frontier.length,
4933
+ crowding: Number.isFinite(winner.distance) ? winner.distance : -1
4934
+ }
4935
+ };
4936
+ }
4937
+ });
4938
+ }
4939
+ function hillClimb(spec) {
4940
+ const leader = (view) => {
4941
+ let current = view.rootNodeId;
4942
+ for (const nodeId of view.screened) {
4943
+ if (nodeId === current || !view.complete(nodeId)) continue;
4944
+ if (beats(view, nodeId, current)) current = nodeId;
4945
+ }
4946
+ return current;
4947
+ };
4948
+ return {
4949
+ name: spec.name,
4950
+ ...spec.patience === void 0 ? {} : { patience: spec.patience },
4951
+ leader,
4952
+ expand(view) {
4953
+ if (view.screening > 0) return null;
4954
+ const kept = leader(view);
4955
+ const chosen = spec.parent(view, kept);
4956
+ const units = view.unitScores(chosen.nodeId);
4957
+ return {
4958
+ parents: [chosen.nodeId],
4959
+ operator: "improve",
4960
+ selection: {
4961
+ rule: spec.name,
4962
+ evidence: {
4963
+ ...chosen.evidence,
4964
+ units: units.length,
4965
+ ...units.length > 0 ? { mean: mean$1(units.map((unit) => unit.mean)) } : {},
4966
+ isLeader: chosen.nodeId === kept ? 1 : 0
4967
+ }
4968
+ }
4969
+ };
4970
+ }
4971
+ };
4972
+ }
4973
+ /** True when `challenger` improves on `holder` over the units both scored. */
4974
+ function beats(view, challenger, holder) {
4975
+ const held = new Map(view.unitScores(holder).map((unit) => [unit.unitId, unit.mean]));
4976
+ let challengerSum = 0;
4977
+ let holderSum = 0;
4978
+ let shared = 0;
4979
+ for (const unit of view.unitScores(challenger)) {
4980
+ const other = held.get(unit.unitId);
4981
+ if (other === void 0) continue;
4982
+ challengerSum += unit.mean;
4983
+ holderSum += other;
4984
+ shared += 1;
4985
+ }
4986
+ if (shared === 0) return false;
4987
+ return view.direction === "maximize" ? challengerSum > holderSum : challengerSum < holderSum;
4988
+ }
4989
+ /** Non-dominated complete screened nodes over the root's units, with crowding. */
4990
+ function paretoMembers(view) {
4991
+ const units = view.unitScores(view.rootNodeId).map((unit) => unit.unitId);
4992
+ const sign = view.direction === "maximize" ? 1 : -1;
4993
+ const members = [];
4994
+ view.screened.forEach((nodeId, ordinal) => {
4995
+ if (!view.complete(nodeId)) return;
4996
+ const objectives = new Map(view.unitScores(nodeId).map((unit) => [unit.unitId, unit.mean]));
4997
+ if (units.some((unit) => !objectives.has(unit))) return;
4998
+ members.push({
4999
+ nodeId,
5000
+ ordinal,
5001
+ mean: sign * mean$1([...objectives.values()]),
5002
+ objectives,
5003
+ distance: 0
5004
+ });
5005
+ });
5006
+ if (members.length === 0 || units.length === 0) return members;
5007
+ return paretoFrontierWithCrowding(members, units.map((unit) => ({
5008
+ name: unit,
5009
+ direction: view.direction,
5010
+ value: (member) => member.objectives.get(unit)
5011
+ }))).map(({ candidate, distance }) => ({
5012
+ ...candidate,
5013
+ distance
5014
+ }));
5015
+ }
5016
+ /** Negative when `a` wins the tournament. */
5017
+ function compareCrowded(a, b) {
5018
+ if (a.distance !== b.distance) return a.distance > b.distance ? -1 : 1;
5019
+ if (a.mean !== b.mean) return a.mean > b.mean ? -1 : 1;
5020
+ return a.ordinal - b.ordinal;
5021
+ }
5022
+ function mean$1(values) {
5023
+ let total = 0;
5024
+ for (const value of values) total += value;
5025
+ return total / values.length;
5026
+ }
5027
+ //#endregion
5028
+ //#region src/campaign/types.ts
5029
+ /** Type guard: a proposal carrying its rationale vs a bare
5030
+ * surface. The loop branches on this to populate `GenerationCandidate`. */
5031
+ function isProposedCandidate(value) {
5032
+ return typeof value === "object" && value !== null && "surface" in value && "label" in value && "rationale" in value;
5033
+ }
5034
+ const LABEL_TRUST_RANK = {
5035
+ unverified: 0,
5036
+ "verified-signal": 1,
5037
+ "human-rated": 2
5038
+ };
5039
+ /** Ordinal rank for a label-trust tier; absent ⇒ `unverified` (rank 0). */
5040
+ function labelTrustRank(trust) {
5041
+ return LABEL_TRUST_RANK[trust ?? "unverified"];
5042
+ }
5043
+ //#endregion
5044
+ //#region src/campaign/presets/run-optimization.ts
5045
+ /**
5046
+ * `runOptimization` improves a mutable surface with a caller-owned proposer,
5047
+ * as a search on the one kernel (`runSearch`).
5048
+ *
5049
+ * The baseline is the seeded root. The scenarios are the train split: the
5050
+ * proposer reads their scores, and the policy ranks on them because the loop
5051
+ * has no private selection split. A "generation" is one proposal of
5052
+ * `populationSize` candidates from the node the policy chose (by default the
5053
+ * incumbent, once every earlier candidate is measured). Every candidate is a
5054
+ * node measured on every scenario at every repeat, one cell at a time on the
5055
+ * run's lanes. The kept surface is the policy's leader: a budget decision on
5056
+ * the train split that claims nothing; `runImprovementLoop` compares it with
5057
+ * the baseline on separate cases.
5058
+ *
5059
+ * The search ledger is always written, beside the run (`<runDir>/search/`) or
5060
+ * where `searchLedger` puts it, and it is the run's only checkpoint: running
5061
+ * again on the same run directory and options continues an interrupted run
5062
+ * instead of starting over, and reruns nothing that settled.
5063
+ */
5064
+ /** Improve a surface as a search on the kernel; see the module comment. */
5065
+ async function runOptimization(opts) {
5066
+ const candidateConcurrency = opts.candidateConcurrency ?? 1;
5067
+ if (typeof opts.runDir !== "string" || opts.runDir.trim().length === 0) throw new Error("runOptimization: runDir is required and must be a non-empty string");
5068
+ if (!Number.isInteger(candidateConcurrency) || candidateConcurrency < 1) throw new Error("runOptimization: candidateConcurrency must be a positive integer");
5069
+ for (const [name, value] of [["populationSize", opts.populationSize], ["maxGenerations", opts.maxGenerations]]) if (!Number.isSafeInteger(value) || value < 0) throw new Error(`runOptimization: ${name} must be a non-negative integer`);
5070
+ const initialFindings = immutableProposalSnapshot(assertProposalFindings(opts.findings ?? [], "runOptimization initial proposal findings"), "initial findings");
5071
+ const baselineSurface = immutableProposalSnapshot(opts.baselineSurface, "baseline surface");
5072
+ const runDir = resolveRunDir(opts.runDir, opts.repo);
5073
+ const storage = opts.storage ?? fsCampaignStorage();
5074
+ const costLedger = opts.costLedger ?? createRunCostLedger({
5075
+ storage,
5076
+ runDir,
5077
+ costCeilingUsd: opts.costCeiling
5078
+ });
5079
+ const reps = opts.reps ?? 1;
5080
+ const seed = opts.seed ?? 42;
5081
+ const requireJudgeScore = (opts.judges?.length ?? 0) > 0;
5082
+ const premeasured = opts.premeasuredBaseline ? validatedPremeasuredBaseline({
5083
+ input: opts.premeasuredBaseline,
5084
+ baselineSurface,
5085
+ scenarios: opts.scenarios,
5086
+ reps,
5087
+ seed,
5088
+ judges: opts.judges ?? [],
5089
+ dispatchRef: surfaceDispatchRef(baselineSurface, opts.dispatchRef),
5090
+ requireJudgeScore
5091
+ }) : void 0;
5092
+ const splitDigest = campaignSplitDigest(opts.scenarios, reps);
5093
+ const binding = opts.searchLedger ?? await defaultBinding(opts, runDir, storage);
5094
+ const { identity } = binding;
5095
+ const execution = {
5096
+ model: identity.model,
5097
+ agent: identity.agent,
5098
+ benchmark: {
5099
+ uri: "campaign://scenarios",
5100
+ revision: splitDigest
5101
+ }
5102
+ };
5103
+ const policy = opts.policy ?? incumbent();
5104
+ const allocation = uniform({ reps });
5105
+ const recorder = await SearchRecorder.open({
5106
+ ledger: binding.ledger,
5107
+ storage
5108
+ }, {
5109
+ subject: identity.subject ?? opts.proposer.kind,
5110
+ process: {
5111
+ name: "runOptimization",
5112
+ executionRef: identity.search
5113
+ },
5114
+ artifactKind: artifactKindOf(baselineSurface),
5115
+ objective: {
5116
+ metric: "composite",
5117
+ direction: "maximize",
5118
+ judge: identity.judge ?? { unknown: "runOptimization receives its judges as functions without a pinned source" },
5119
+ claim: identity.claim ?? developmentClaim(splitDigest)
5120
+ },
5121
+ splits: {
5122
+ train: opts.scenarios.map((scenario) => ({
5123
+ taskId: scenario.id,
5124
+ unitId: scenario.id,
5125
+ source: {
5126
+ uri: `scenario://${scenario.id}`,
5127
+ revision: hashCanonical(scenario)
5128
+ }
5129
+ })),
5130
+ selection: [],
5131
+ test: [],
5132
+ heldOutUnits: true
5133
+ },
5134
+ policy: {
5135
+ expansion: policy.name,
5136
+ allocation: allocation.name,
5137
+ seed
5138
+ },
5139
+ budget: {
5140
+ maxUsd: opts.costCeiling ?? null,
5141
+ maxCells: null,
5142
+ maxNodes: 1 + opts.populationSize * opts.maxGenerations,
5143
+ deadline: null,
5144
+ maxConcurrency: null,
5145
+ reservedClaimUsd: 0
5146
+ },
5147
+ containment: null,
5148
+ derivedFrom: null,
5149
+ identity: execution
5150
+ });
5151
+ const nodes = new SurfaceNodes({
5152
+ opts,
5153
+ recorder,
5154
+ runDir,
5155
+ costLedger,
5156
+ execution,
5157
+ premeasured
5158
+ });
5159
+ const scenarioById = new Map(opts.scenarios.map((scenario) => [scenario.id, scenario]));
5160
+ let findings = initialFindings;
5161
+ const executor = {
5162
+ lanes: () => [{
5163
+ name: "campaign",
5164
+ capacity: candidateConcurrency * (opts.maxConcurrency ?? 2),
5165
+ costCap: "estimate",
5166
+ cellUsd: 0
5167
+ }],
5168
+ place: () => "campaign",
5169
+ run: (work) => nodes.runCell(work, scenarioById),
5170
+ adopt: (work) => nodes.adoptCell(work)
5171
+ };
5172
+ const proposer = opts.proposer;
5173
+ const proposerSource = identity.proposer.source;
5174
+ const port = {
5175
+ name: proposer.kind,
5176
+ kind: "optimizer",
5177
+ source: proposerSource,
5178
+ execution: identity.proposer,
5179
+ childrenPerProposal: opts.populationSize,
5180
+ async propose(request) {
5181
+ const phases = [
5182
+ "search.proposal",
5183
+ "analysis.baseline",
5184
+ "analysis.generation"
5185
+ ];
5186
+ const before = new Set(phases.flatMap((phase) => costLedger.list({ phase })).map(receiptKey));
5187
+ const accounting = () => {
5188
+ const fresh = phases.flatMap((phase) => costLedger.list({ phase })).filter((receipt) => !before.has(receiptKey(receipt)));
5189
+ return {
5190
+ accounting: receiptAccounting(fresh),
5191
+ execution: opts.searchLedger ? identity.proposer : proposalExecution(fresh, identity.model.provider, proposerSource)
5192
+ };
5193
+ };
5194
+ const earlier = await recorder.state();
5195
+ const root = earlier.rootNodeId;
5196
+ const rootCampaign = nodes.campaign(earlier, root);
5197
+ const rootCoverage = campaignCoverage(rootCampaign.cells, opts.scenarios, reps, requireJudgeScore);
5198
+ if (!rootCoverage.complete) throw new Error(`runOptimization: ${premeasured ? "premeasured baseline" : "baseline"} is incomplete (${rootCoverage.scorableCellIds.length}/${rootCoverage.expectedCellIds.length} designed cells scorable) — ${formatCoverageFailures(rootCoverage)}. Refusing to optimize against an incomplete incumbent.`);
5199
+ const generation = nodes.generations(earlier).length;
5200
+ if (opts.analyzeGeneration) {
5201
+ const previous = generation - 1;
5202
+ const candidates = (previous < 0 ? [root] : nodes.expansionNodes(earlier, nodes.generations(earlier)[previous])).map((nodeId) => {
5203
+ const campaign = nodes.campaign(earlier, nodeId);
5204
+ return {
5205
+ surfaceHash: surfaceHash(nodes.surface(earlier, nodeId)),
5206
+ campaign,
5207
+ composite: campaignMeanCompositeOrNull(campaign)
5208
+ };
5209
+ });
5210
+ const history = nodes.history(earlier, policy);
5211
+ if (candidates.some(({ campaign }) => campaign.cells.length > 0)) {
5212
+ const fresh = await opts.analyzeGeneration({
5213
+ generation: previous,
5214
+ runDir: previous < 0 ? rootCampaign.runDir : `${runDir}/candidates`,
5215
+ candidates,
5216
+ history,
5217
+ costLedger,
5218
+ costPhase: previous < 0 ? "analysis.baseline" : "analysis.generation"
5219
+ });
5220
+ if (!Array.isArray(fresh)) throw new TypeError("runOptimization: analyzeGeneration must return an array");
5221
+ findings = immutableProposalSnapshot(assertProposalFindings(fresh, "runOptimization analysis findings"), "analysis findings");
5222
+ }
5223
+ }
5224
+ const state = await recorder.state();
5225
+ const proposalHistory = immutableProposalSnapshot(nodes.history(state, policy), "history");
5226
+ const parent = request.parents[0];
5227
+ const context = Object.freeze({
5228
+ currentSurface: immutableProposalSnapshot(parent.artifact, "current surface"),
5229
+ operator: request.operator,
5230
+ parents: immutableProposalSnapshot(request.parents.map(({ nodeId, artifact }) => ({
5231
+ nodeId,
5232
+ artifact: immutableProposalSnapshot(artifact, "parent surface")
5233
+ })), "parents"),
5234
+ train: searchProposerView(state),
5235
+ summary: renderSearchSummary(state, { split: "train" }),
5236
+ history: proposalHistory,
5237
+ findings: immutableProposalSnapshot(assertProposalFindings(findings, "runOptimization proposal findings"), "findings"),
5238
+ populationSize: opts.populationSize,
5239
+ generation,
5240
+ signal: request.signal,
5241
+ baselineOutcome: immutableProposalSnapshot(nodes.outcome(state, root), "baseline"),
5242
+ incumbentOutcome: immutableProposalSnapshot(nodes.outcome(state, request.leader), "incumbent outcome"),
5243
+ parentOutcome: immutableProposalSnapshot(nodes.outcome(state, parent.nodeId), "parent outcome"),
5244
+ maxImprovementShots: opts.maxImprovementShots,
5245
+ paretoParents: immutableProposalSnapshot(nodes.frontier(state), "Pareto parents"),
5246
+ costLedger,
5247
+ costPhase: "search.proposal"
5248
+ });
5249
+ if (proposer.decide?.({ history: proposalHistory }).stop) return {
5250
+ children: [],
5251
+ stop: "the proposer decided to stop",
5252
+ ...accounting()
5253
+ };
5254
+ const proposed = await proposer.propose(context);
5255
+ if (!Array.isArray(proposed)) throw new TypeError("runOptimization: proposer must return an array");
5256
+ return {
5257
+ children: immutableProposalSnapshot(proposed, "candidate outputs").slice(0, opts.populationSize).map((candidate) => isProposedCandidate(candidate) ? {
5258
+ artifact: candidate.surface,
5259
+ label: candidate.label,
5260
+ rationale: candidate.rationale,
5261
+ ...candidate.attribution ? { attribution: candidate.attribution } : {}
5262
+ } : {
5263
+ artifact: candidate,
5264
+ label: "",
5265
+ rationale: ""
5266
+ }),
5267
+ ...accounting()
5268
+ };
5269
+ }
5270
+ };
5271
+ const result = await runSearch({
5272
+ recorder,
5273
+ root: baselineSurface,
5274
+ codec: surfaceCodec,
5275
+ policy,
5276
+ allocation,
5277
+ proposer: port,
5278
+ executor,
5279
+ maxExpansions: opts.maxGenerations,
5280
+ signal: opts.signal
5281
+ });
5282
+ const state = result.state;
5283
+ const winnerSurface = nodes.surface(state, result.leader);
5284
+ const firstEdge = state.edge(state.node(result.leader).edgeIds[0]);
5285
+ const winnerRationale = firstEdge.operator === "seed" || !("sha256" in firstEdge.rationale) ? void 0 : recorder.readBlob(firstEdge.rationale).text;
5286
+ const expansions = nodes.generations(state);
5287
+ return {
5288
+ generations: nodes.history(state, policy).map((record) => ({
5289
+ record,
5290
+ surfaces: nodes.expansionNodes(state, expansions[record.generationIndex]).map((nodeId) => {
5291
+ const surface = nodes.surface(state, nodeId);
5292
+ return {
5293
+ surfaceHash: surfaceHash(surface),
5294
+ surface,
5295
+ campaign: nodes.campaign(state, nodeId)
5296
+ };
5297
+ })
5298
+ })),
5299
+ baselineSurface,
5300
+ winnerSurface,
5301
+ winnerSurfaceHash: surfaceHash(winnerSurface),
5302
+ ...firstEdge.operator !== "seed" && firstEdge.label ? { winnerLabel: firstEdge.label } : {},
5303
+ ...winnerRationale ? { winnerRationale } : {},
5304
+ baselineCampaign: nodes.campaign(state, state.rootNodeId),
5305
+ cost: costLedger.summary(),
5306
+ searchHistory: recorder.receipt({
5307
+ producerId: proposer.kind,
5308
+ runId: runDir
5309
+ }),
5310
+ paretoFrontier: nodes.frontier(state)
3053
5311
  };
3054
5312
  }
3055
- /** A parent-to-child diff of two mutable surfaces, stored as a blob. */
3056
- function surfaceDiff(recorder, parent, child) {
3057
- return recorder.blob("diff", {
3058
- kind: "surface-diff",
3059
- from: surfaceContentHash(parent),
3060
- to: surfaceContentHash(child),
3061
- text: renderSurfaceDiff(child, parent)
3062
- });
3063
- }
3064
- /** Declared surfaces of one node. A component surface declares one surface per
3065
- * named component, so per-component evidence stays addressable. */
3066
- function declaredSurfaces(surface, artifact) {
3067
- if (typeof surface === "string") return [{
3068
- surfaceId: "prompt",
3069
- kind: "prompt",
3070
- artifact
3071
- }];
3072
- if (surface.kind === "code") return [{
3073
- surfaceId: "code",
3074
- kind: "code",
3075
- artifact
3076
- }];
3077
- return Object.keys(surface.components).sort().map((name) => ({
3078
- surfaceId: `component:${name}`,
3079
- kind: "prompt",
3080
- artifact
3081
- }));
3082
- }
3083
- function boundedLabel(label) {
3084
- const redacted = redactText(label.trim(), { profile: "share" });
3085
- return redacted.length <= MAX_LABEL_CHARS ? redacted : `${redacted.slice(0, MAX_LABEL_CHARS - 1)}…`;
3086
- }
3087
- /** The header without its stamp, for comparing a resumed opening to the stored one. */
3088
- function headerMaterial(header) {
3089
- const { occurredAt: _occurredAt, ...material } = header;
3090
- return canonicalString(material);
3091
- }
3092
- /** The header as the ledger would store it, so the comparison ignores ordering. */
3093
- function canonicalHeader(header) {
3094
- return validateSearchLedgerEvent({
3095
- ...header,
3096
- occurredAt: "2000-01-01T00:00:00.000Z"
3097
- });
3098
- }
3099
- //#endregion
3100
- //#region src/campaign/presets/run-optimization-recording.ts
3101
- var OptimizationSearch = class OptimizationSearch {
5313
+ /** A mutable surface as a search artifact. */
5314
+ const surfaceCodec = {
5315
+ node: surfaceNode,
5316
+ diff: surfaceDiff,
5317
+ load: (recorder, node) => {
5318
+ const stored = recorder.readBlob(node.artifact);
5319
+ if (stored.kind !== "mutable-surface" || stored.surface === void 0) throw new Error(`runOptimization: node ${node.nodeId} does not hold a mutable surface`);
5320
+ return stored.surface;
5321
+ }
5322
+ };
5323
+ /**
5324
+ * The run's view of its surface nodes: where each one's cells run, its
5325
+ * campaign, and the proposer's reads over them. Everything here derives from
5326
+ * the ledger and the per-cell campaign cache, so a resumed run reads the same.
5327
+ */
5328
+ var SurfaceNodes = class {
5329
+ opts;
3102
5330
  recorder;
3103
- binding;
5331
+ runDir;
3104
5332
  costLedger;
3105
- scenarioIds;
3106
- reps;
3107
- identity;
3108
- proposerName;
3109
- proposalReceiptCount = 0;
3110
- constructor(args) {
3111
- this.recorder = args.recorder;
3112
- this.binding = args.binding;
3113
- this.costLedger = args.costLedger;
3114
- this.scenarioIds = args.scenarioIds;
3115
- this.reps = args.reps;
3116
- this.identity = args.identity;
3117
- this.proposerName = args.proposerName;
3118
- }
3119
- /** Open the search and record the measured baseline as its seeded root. */
3120
- static async open(args) {
3121
- const { binding } = args;
3122
- const identity = {
3123
- model: binding.identity.model,
3124
- agent: binding.identity.agent,
3125
- benchmark: {
3126
- uri: "campaign://scenarios",
3127
- revision: args.splitDigest
3128
- }
3129
- };
3130
- const train = args.scenarios.map((scenario) => ({
3131
- taskId: scenario.id,
3132
- unitId: scenario.id,
3133
- source: {
3134
- uri: `scenario://${scenario.id}`,
3135
- revision: hashCanonical(scenario)
3136
- }
5333
+ execution;
5334
+ premeasured;
5335
+ storage;
5336
+ campaigns = /* @__PURE__ */ new Map();
5337
+ proposals = /* @__PURE__ */ new Map();
5338
+ /** Cells this process ran or adopted, by node, keyed `scenarioId:rep`. */
5339
+ cells = /* @__PURE__ */ new Map();
5340
+ constructor(input) {
5341
+ this.opts = input.opts;
5342
+ this.recorder = input.recorder;
5343
+ this.runDir = input.runDir;
5344
+ this.costLedger = input.costLedger;
5345
+ this.execution = input.execution;
5346
+ this.premeasured = input.premeasured;
5347
+ this.storage = input.opts.storage ?? fsCampaignStorage();
5348
+ }
5349
+ /** `<runDir>/baseline` for the root, `<runDir>/candidates/<surfaceHash>`
5350
+ * for every other node: content-addressed, so a cell's directory needs no
5351
+ * ledger read and one surface keeps one cache across the run's searches. */
5352
+ nodeDir(surface, root) {
5353
+ return root ? `${this.runDir}/baseline` : `${this.runDir}/candidates/${surfaceHash(surface)}`;
5354
+ }
5355
+ surface(state, nodeId) {
5356
+ return surfaceCodec.load(this.recorder, state.node(nodeId));
5357
+ }
5358
+ /** Nodes proposal `expansion` registered, in the order it returned them. */
5359
+ expansionNodes(state, expansion) {
5360
+ const blob = this.proposal(state, expansion);
5361
+ if (!blob) return [];
5362
+ const ids = [];
5363
+ for (const child of blob.children) {
5364
+ const nodeId = state.nodeIdForDigest(child.node.artifactDigest);
5365
+ if (nodeId === void 0 || ids.includes(nodeId)) continue;
5366
+ if (this.origin(state, nodeId).expansion === expansion) ids.push(nodeId);
5367
+ }
5368
+ return ids;
5369
+ }
5370
+ /**
5371
+ * The scored attempt of a cell an earlier process ran, from its node's
5372
+ * cache; null otherwise, so the kernel runs the cell. A failure is not
5373
+ * adopted: the stop that interrupted the earlier process may have caused it.
5374
+ */
5375
+ async adoptCell(work) {
5376
+ if (work.stage === "root" && this.premeasured) return this.premeasuredCell(work);
5377
+ const dir = this.nodeDir(work.artifact, work.stage === "root");
5378
+ const cached = this.storage.read(cellCachePath(dir, `${work.taskId}:${work.rep}`));
5379
+ const cell = cached === void 0 ? void 0 : JSON.parse(cached);
5380
+ if (cell?.manifestHash !== this.manifestFor(work.artifact)) return null;
5381
+ this.remember(work.nodeId, cell);
5382
+ return this.cellResult(cell, work);
5383
+ }
5384
+ /** Run one cell as a one-cell campaign in its node's directory; a cached
5385
+ * cell is read back, not dispatched again. */
5386
+ async runCell(work, scenarios) {
5387
+ if (work.stage === "root" && this.premeasured) return this.premeasuredCell(work);
5388
+ if (!scenarios.has(work.taskId)) throw new Error(`runOptimization: cell ${work.cellId} names unknown scenario ${work.taskId}`);
5389
+ const dir = this.nodeDir(work.artifact, work.stage === "root");
5390
+ const cell = (await this.campaignRun(dir, work.artifact, {
5391
+ signal: work.signal,
5392
+ costPhase: work.stage === "root" ? "search.baseline" : "search.candidate",
5393
+ cellFilter: ({ scenario, rep }) => scenario.id === work.taskId && rep === work.rep
5394
+ })).cells[0];
5395
+ if (!cell) throw new Error(`runOptimization: cell ${work.cellId} produced no campaign cell`);
5396
+ this.remember(work.nodeId, cell);
5397
+ return this.cellResult(cell, work);
5398
+ }
5399
+ /** The node's campaign over the cells the ledger settled: the cells this
5400
+ * process ran, else their cached results or failure receipts. */
5401
+ campaign(state, nodeId) {
5402
+ const root = nodeId === state.rootNodeId;
5403
+ if (root && this.premeasured) return this.premeasured;
5404
+ const surface = this.surface(state, nodeId);
5405
+ const dir = this.nodeDir(surface, root);
5406
+ const settled = state.cells({ nodeId }).filter((cell) => cell.attempts > 0).map((cell) => ({
5407
+ taskId: cell.taskId,
5408
+ rep: cell.rep
3137
5409
  }));
3138
- const recorder = await SearchRecorder.open({
3139
- ledger: binding.ledger,
3140
- storage: args.storage
3141
- }, {
3142
- subject: binding.identity.subject ?? args.proposerName,
3143
- process: {
3144
- name: "runOptimization",
3145
- executionRef: binding.identity.search
3146
- },
3147
- artifactKind: "prompt",
3148
- objective: {
3149
- metric: "composite",
3150
- direction: "maximize",
3151
- judge: binding.identity.judge ?? { unknown: "runOptimization receives its judges as functions without a pinned source" },
3152
- claim: binding.identity.claim ?? developmentClaim(args.splitDigest)
3153
- },
3154
- splits: {
3155
- train,
3156
- selection: [],
3157
- test: [],
3158
- heldOutUnits: true
3159
- },
3160
- policy: {
3161
- expansion: args.expansion,
3162
- allocation: "uniform",
3163
- seed: args.seed
3164
- },
3165
- budget: {
3166
- maxUsd: args.maxUsd,
3167
- maxCells: null,
3168
- maxNodes: null,
3169
- deadline: null,
3170
- maxConcurrency: null,
3171
- reservedClaimUsd: 0
3172
- },
3173
- containment: null,
3174
- derivedFrom: null,
3175
- identity
3176
- });
3177
- const search = new OptimizationSearch({
3178
- recorder,
3179
- binding,
3180
- costLedger: args.costLedger,
3181
- scenarioIds: args.scenarios.map((scenario) => scenario.id),
3182
- reps: args.reps,
3183
- identity,
3184
- proposerName: args.proposerName
5410
+ const key = settled.map(({ taskId, rep }) => `${taskId}:${rep}`).sort().join("\n");
5411
+ const held = this.campaigns.get(nodeId);
5412
+ if (held?.cells === key) return held.campaign;
5413
+ const known = this.cells.get(nodeId);
5414
+ const cells = settled.map(({ taskId, rep }) => {
5415
+ const cell = known?.get(`${taskId}:${rep}`) ?? this.storedCell(dir, surface, taskId, rep);
5416
+ if (!cell) throw new Error(`runOptimization: cell ${taskId}:${rep} of node ${nodeId} settled, but ${dir} holds neither its cached result nor its failure receipt`);
5417
+ return cell;
3185
5418
  });
3186
- const root = await recorder.registerNode(surfaceNode(recorder, args.baselineSurface));
3187
- await recorder.recordEdge({
3188
- childNodeId: root.nodeId,
3189
- parents: [],
3190
- operator: "seed",
3191
- attribution: "explicit",
3192
- proposer: null,
3193
- proposalKey: "baseline",
3194
- rationale: { unknown: "the baseline is the caller-supplied starting surface" },
3195
- diffs: [],
3196
- label: "baseline"
3197
- });
3198
- await search.recordCells(root.nodeId, "root", args.baselineCells);
3199
- return {
3200
- search,
3201
- rootNodeId: root.nodeId
5419
+ cells.sort((a, b) => a.cellId < b.cellId ? -1 : a.cellId > b.cellId ? 1 : 0);
5420
+ const reps = this.opts.reps ?? 1;
5421
+ const seed = this.opts.seed ?? 42;
5422
+ const judges = this.opts.judges ?? [];
5423
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5424
+ const durationMs = cells.reduce((sum, cell) => sum + cell.durationMs, 0);
5425
+ const campaign = {
5426
+ manifestHash: this.manifestFor(surface),
5427
+ splitDigest: campaignSplitDigest(this.opts.scenarios, reps),
5428
+ seed,
5429
+ reps,
5430
+ startedAt: now,
5431
+ endedAt: now,
5432
+ durationMs,
5433
+ cells,
5434
+ aggregates: computeAggregates(cells, judges, seed, this.costLedger.summary({ tags: { runDir: dir } })),
5435
+ runDir: dir,
5436
+ artifactsByPath: {},
5437
+ scenarios: this.opts.scenarios.map(campaignScenarioIdentity)
3202
5438
  };
3203
- }
3204
- /** Start one generation's candidate-generation operation, before `propose()`. */
3205
- async startGeneration(generation) {
3206
- await this.recorder.startOperation({
3207
- operationId: generationOperationId(generation),
3208
- operationKind: "candidate-generation"
3209
- });
3210
- }
3211
- /** Record the generation's proposal: its operation, then one node and edge per candidate. */
3212
- async recordProposal(input) {
3213
- const operationId = generationOperationId(input.generation);
3214
- await this.recorder.recordOperation({
3215
- operationId,
3216
- operationKind: "candidate-generation",
3217
- execution: this.binding.identity.proposer,
3218
- outcome: { status: "completed" },
3219
- accounting: this.proposalAccounting()
5439
+ this.campaigns.set(nodeId, {
5440
+ cells: key,
5441
+ campaign
3220
5442
  });
3221
- const nodeIds = [];
3222
- for (const [index, candidate] of input.candidates.entries()) {
3223
- const { nodeId } = await this.recorder.registerNode(surfaceNode(this.recorder, candidate.surface));
3224
- await this.recorder.recordEdge({
3225
- childNodeId: nodeId,
3226
- parents: [input.parent.nodeId],
3227
- operator: "improve",
3228
- attribution: "explicit",
3229
- proposer: {
3230
- kind: "optimizer",
3231
- name: this.proposerName,
3232
- operationId,
3233
- source: this.binding.identity.proposer.source
3234
- },
3235
- proposalKey: `gen-${input.generation}:candidate-${index}`,
3236
- selection: {
3237
- rule: input.selectionRule,
3238
- evidence: input.parent.composite === null ? {} : { parentComposite: input.parent.composite }
3239
- },
3240
- rationale: candidate.rationale,
3241
- diffs: [surfaceDiff(this.recorder, input.parent.surface, candidate.surface)],
3242
- label: candidate.label
5443
+ return campaign;
5444
+ }
5445
+ outcome(state, nodeId) {
5446
+ const campaign = this.campaign(state, nodeId);
5447
+ const reps = this.opts.reps ?? 1;
5448
+ const coverage = campaignCoverage(campaign.cells, this.opts.scenarios, reps, (this.opts.judges?.length ?? 0) > 0);
5449
+ const expansion = nodeId === state.rootNodeId ? -1 : this.generationOf(state, this.origin(state, nodeId).expansion);
5450
+ return toScoredSurfaceOutcome(surfaceHash(this.surface(state, nodeId)), campaign, coverage, expansion);
5451
+ }
5452
+ /** The Pareto frontier over every measured, complete node. */
5453
+ frontier(state) {
5454
+ const scored = [];
5455
+ for (const node of state.nodes()) {
5456
+ if (node.status === "invalid" || node.cellCount === 0) continue;
5457
+ const cells = state.cells({ nodeId: node.nodeId });
5458
+ if (cells.some((cell) => cell.attempts === 0 && cell.cancelled === null)) continue;
5459
+ if (cells.some((cell) => cell.score === null)) continue;
5460
+ const campaign = this.campaign(state, node.nodeId);
5461
+ const edge = state.edge(node.edgeIds[0]);
5462
+ scored.push(toParetoParent(this.surface(state, node.nodeId), campaign, node.nodeId === state.rootNodeId ? -1 : this.generationOf(state, this.origin(state, node.nodeId).expansion), edge.operator === "seed" ? void 0 : edge.label || void 0));
5463
+ }
5464
+ return computeParetoFrontier(scored);
5465
+ }
5466
+ /** Kernel expansions that registered a candidate, in order: generation g
5467
+ * is `generations(state)[g]`. */
5468
+ generations(state) {
5469
+ const expansions = [];
5470
+ for (let expansion = 0; state.operation(`expand-${expansion}`); expansion++) if (this.expansionNodes(state, expansion).length > 0) expansions.push(expansion);
5471
+ return expansions;
5472
+ }
5473
+ /** One record per generation, with its candidates and the one that took
5474
+ * the lead, if any. */
5475
+ history(state, policy) {
5476
+ const records = [];
5477
+ const measured = (nodeId) => state.node(nodeId).status !== "invalid" && state.cells({ nodeId }).every((cell) => cell.attempts > 0 || cell.cancelled !== null);
5478
+ const screened = [state.rootNodeId];
5479
+ let leader = policy.leader(searchPolicyView(state, {
5480
+ screened,
5481
+ screening: 0,
5482
+ expansions: 0
5483
+ }));
5484
+ for (const [generation, expansion] of this.generations(state).entries()) {
5485
+ const nodeIds = this.expansionNodes(state, expansion);
5486
+ screened.push(...nodeIds.filter(measured));
5487
+ screened.sort((a, b) => state.node(a).ordinal - state.node(b).ordinal);
5488
+ const next = policy.leader(searchPolicyView(state, {
5489
+ screened,
5490
+ screening: 0,
5491
+ expansions: expansion + 1
5492
+ }));
5493
+ const candidates = [];
5494
+ for (const nodeId of nodeIds) candidates.push(this.candidate(state, nodeId, expansion));
5495
+ records.push({
5496
+ generationIndex: generation,
5497
+ candidates,
5498
+ promoted: next !== leader && nodeIds.includes(next) ? [surfaceHash(this.surface(state, next))] : []
3243
5499
  });
3244
- nodeIds.push(nodeId);
5500
+ leader = next;
3245
5501
  }
3246
- return nodeIds;
5502
+ return records;
3247
5503
  }
3248
- /** Close a generation whose proposer produced nothing or failed. */
3249
- async failGeneration(generation, message) {
3250
- await this.recorder.recordOperation({
3251
- operationId: generationOperationId(generation),
3252
- operationKind: "candidate-generation",
3253
- execution: this.binding.identity.proposer,
3254
- outcome: {
3255
- status: "failed",
3256
- failure: {
3257
- code: "no-candidates",
3258
- message
3259
- }
5504
+ candidate(state, nodeId, expansion) {
5505
+ const campaign = this.campaign(state, nodeId);
5506
+ const coverage = campaignCoverage(campaign.cells, this.opts.scenarios, this.opts.reps ?? 1, (this.opts.judges?.length ?? 0) > 0);
5507
+ const breakdown = campaignBreakdown(campaign);
5508
+ const child = this.proposal(state, expansion).children.find((entry) => state.nodeIdForDigest(entry.node.artifactDigest) === nodeId);
5509
+ return {
5510
+ surfaceHash: surfaceHash(this.surface(state, nodeId)),
5511
+ composite: campaignMeanCompositeOrNull(campaign),
5512
+ eligibleForPromotion: coverage.complete,
5513
+ coverage: {
5514
+ expectedCells: coverage.expectedCellIds.length,
5515
+ scorableCells: coverage.scorableCellIds.length,
5516
+ unscorableCells: coverage.unscorableCells
3260
5517
  },
3261
- accounting: this.proposalAccounting()
3262
- });
5518
+ dimensions: breakdown.dimensions,
5519
+ scenarios: breakdown.scenarios,
5520
+ ...child.label ? { label: child.label } : {},
5521
+ ...child.rationale ? { rationale: child.rationale } : {},
5522
+ ...child.attribution ? { attribution: child.attribution } : {}
5523
+ };
3263
5524
  }
3264
- /** Allocate every designed cell of a node before its campaign runs. */
3265
- async allocate(nodeId) {
3266
- for (const taskId of this.scenarioIds) for (let rep = 0; rep < this.reps; rep++) await this.recorder.allocateCell({
3267
- nodeId,
3268
- taskId,
3269
- split: "train",
3270
- rep,
3271
- stage: "train"
3272
- });
5525
+ generationOf(state, expansion) {
5526
+ return this.generations(state).indexOf(expansion);
3273
5527
  }
3274
- /** Settle a node's campaign cells; allocates any the campaign ran beyond the design. */
3275
- async recordCells(nodeId, stage, cells) {
3276
- for (const cell of cells) {
3277
- const cellId = await this.recorder.allocateCell({
3278
- nodeId,
3279
- taskId: cell.scenarioId,
3280
- split: "train",
3281
- rep: cell.rep,
3282
- stage
3283
- });
3284
- await this.recorder.settleCell({
3285
- cellId,
3286
- attempt: 1,
3287
- outcome: cellOutcome(cell),
3288
- accounting: cellAccounting(cell),
3289
- identity: {
3290
- ...this.identity,
3291
- model: this.cellModel(cell)
3292
- },
3293
- wallMs: cell.durationMs,
3294
- traceRef: { unknown: "runCampaign reports no trace id per cell" }
3295
- });
3296
- }
5528
+ origin(state, nodeId) {
5529
+ const node = state.node(nodeId);
5530
+ const operationId = state.edge(node.edgeIds[0])?.proposer?.operationId ?? null;
5531
+ const expansion = operationId === null ? null : searchExpansionIndex(operationId);
5532
+ if (expansion === null) throw new Error(`runOptimization: node ${nodeId} was not proposed by this run's search`);
5533
+ return {
5534
+ expansion,
5535
+ index: this.proposal(state, expansion).children.findIndex((child) => child.node.artifactDigest === node.artifactDigest)
5536
+ };
3297
5537
  }
3298
- /**
3299
- * Decide every node and close. The winner is selected; every other node is
3300
- * rejected. Allocated cells a campaign never returned are cancelled.
3301
- */
3302
- async finish(input) {
3303
- const openCells = (await this.recorder.state()).cells().filter((cell) => cell.attempts === 0 && !cell.cancelled);
3304
- for (const cell of openCells) await this.recorder.cancelCell({
3305
- cellId: cell.cellId,
3306
- reason: "aborted"
5538
+ proposal(state, expansion) {
5539
+ const held = this.proposals.get(expansion);
5540
+ if (held) return held;
5541
+ const ref = state.operation(`expand-${expansion}`)?.artifacts.find((artifact) => artifact.role === "proposal");
5542
+ if (!ref) return void 0;
5543
+ const blob = this.recorder.readBlob(ref);
5544
+ this.proposals.set(expansion, blob);
5545
+ return blob;
5546
+ }
5547
+ async campaignRun(runDir, surface, input) {
5548
+ const { baselineSurface: _baselineSurface, premeasuredBaseline: _premeasuredBaseline, dispatchWithSurface, proposer: _proposer, populationSize: _populationSize, maxGenerations: _maxGenerations, candidateConcurrency: _candidateConcurrency, maxImprovementShots: _maxImprovementShots, findings: _findings, analyzeGeneration: _analyzeGeneration, policy: _policy, searchLedger: _searchLedger, ...campaignOptions } = this.opts;
5549
+ return runCampaign({
5550
+ ...campaignOptions,
5551
+ signal: input.signal ?? this.opts.signal,
5552
+ costLedger: this.costLedger,
5553
+ costPhase: input.costPhase,
5554
+ dispatchRef: surfaceDispatchRef(surface, this.opts.dispatchRef),
5555
+ dispatch: (scenario, ctx) => dispatchWithSurface(surface, scenario, ctx),
5556
+ runDir,
5557
+ cellFilter: input.cellFilter,
5558
+ resumable: true,
5559
+ maxConcurrency: 1
3307
5560
  });
3308
- for (const node of (await this.recorder.state()).nodes()) await this.recorder.decideNode({
3309
- nodeId: node.nodeId,
3310
- decision: node.nodeId === input.winnerNodeId ? { status: "selected" } : { status: "rejected" },
3311
- rule: "incumbent",
3312
- 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"
3313
- });
3314
- await this.recorder.close({
3315
- reason: input.reason,
3316
- claim: null
3317
- });
3318
- return this.recorder.receipt({
3319
- producerId: this.proposerName,
3320
- runId: input.runId
5561
+ }
5562
+ premeasuredCell(work) {
5563
+ const cell = this.premeasured.cells.find((candidate) => candidate.scenarioId === work.taskId && candidate.rep === work.rep);
5564
+ if (!cell) throw new Error(`runOptimization: premeasured baseline lacks ${work.taskId}:${work.rep}`);
5565
+ return this.cellResult(cell, work);
5566
+ }
5567
+ /** A cell's result as its campaign stored it: the cache of a scored cell,
5568
+ * the final failure receipt of a failed one. */
5569
+ /** A cell's result as its campaign stored it for this surface: the cache of
5570
+ * a scored cell, the final failure receipt of a failed one. A result another
5571
+ * surface left in the same directory does not count. */
5572
+ storedCell(dir, surface, taskId, rep) {
5573
+ const cellId = `${taskId}:${rep}`;
5574
+ const manifestHash = this.manifestFor(surface);
5575
+ const cached = this.storage.read(cellCachePath(dir, cellId));
5576
+ const scored = cached === void 0 ? void 0 : JSON.parse(cached);
5577
+ if (scored?.manifestHash === manifestHash) return scored;
5578
+ const receipt = this.storage.read(join(cellDirectory(dir, cellId), "failure-receipt.json"));
5579
+ const failed = receipt === void 0 ? void 0 : JSON.parse(receipt).cell;
5580
+ return failed?.manifestHash === manifestHash ? failed : void 0;
5581
+ }
5582
+ manifestFor(surface) {
5583
+ return computeManifestHash({
5584
+ scenarios: this.opts.scenarios,
5585
+ judges: this.opts.judges ?? [],
5586
+ dispatchRef: surfaceDispatchRef(surface, this.opts.dispatchRef),
5587
+ seed: this.opts.seed ?? 42,
5588
+ reps: this.opts.reps ?? 1
3321
5589
  });
3322
5590
  }
3323
- /** Spend booked to candidate generation since the previous generation. */
3324
- proposalAccounting() {
3325
- const receipts = this.costLedger.list({ phase: "search.proposal" });
3326
- const fresh = receipts.slice(this.proposalReceiptCount);
3327
- this.proposalReceiptCount = receipts.length;
3328
- return receiptAccounting(fresh);
3329
- }
3330
- cellModel(cell) {
3331
- const resolved = cell.resolvedModel;
3332
- if (resolved === void 0) return this.identity.model;
3333
- const provider = this.identity.model.provider;
3334
- return modelHasSnapshot(resolved) ? {
3335
- provider,
3336
- snapshot: resolved
3337
- } : {
3338
- provider,
3339
- alias: resolved,
3340
- unknown: "the provider reported a moving alias, not a snapshot"
5591
+ remember(nodeId, cell) {
5592
+ const cells = this.cells.get(nodeId) ?? /* @__PURE__ */ new Map();
5593
+ cells.set(`${cell.scenarioId}:${cell.rep}`, cell);
5594
+ this.cells.set(nodeId, cells);
5595
+ }
5596
+ cellResult(cell, work) {
5597
+ return {
5598
+ outcome: cellOutcome(cell),
5599
+ accounting: cellAccounting(cell),
5600
+ identity: {
5601
+ ...this.execution,
5602
+ model: cellModel(cell, this.execution.model)
5603
+ },
5604
+ wallMs: cell.durationMs,
5605
+ placement: {
5606
+ lane: work.lane,
5607
+ boxId: null
5608
+ },
5609
+ traceRef: { unknown: "runCampaign reports no trace id per cell" }
3341
5610
  };
3342
5611
  }
3343
5612
  };
3344
- function generationOperationId(generation) {
3345
- return `candidate-generation:gen-${generation}`;
5613
+ /** The default ledger and identities when the caller binds none. Revisions are
5614
+ * content digests of what the caller declared, and each uri names what its
5615
+ * digest covers: the dispatch ref, the proposer kind, the kernel. */
5616
+ async function defaultBinding(opts, runDir, storage) {
5617
+ const base = `optimization-${createHash("sha256").update(runDir).digest("hex").slice(0, 16)}`;
5618
+ const dispatchRef = opts.dispatchRef ?? "anonymous";
5619
+ const identity = {
5620
+ agent: {
5621
+ uri: `dispatch-ref:${dispatchRef}`,
5622
+ revision: hashCanonical({ dispatchRef })
5623
+ },
5624
+ proposer: {
5625
+ kind: "deterministic",
5626
+ source: {
5627
+ uri: `proposer:${opts.proposer.kind}`,
5628
+ revision: hashCanonical({ proposer: opts.proposer.kind })
5629
+ }
5630
+ },
5631
+ search: SEARCH_KERNEL_SOURCE,
5632
+ model: {
5633
+ provider: "unspecified",
5634
+ alias: "unspecified",
5635
+ unknown: "runOptimization was not told which model the agent runs"
5636
+ }
5637
+ };
5638
+ for (let slot = 1;; slot++) {
5639
+ const path = `${slot === 1 ? `${runDir}/search` : `${runDir}/search-${slot}`}/ledger.jsonl`;
5640
+ const searchId = slot === 1 ? base : `${base}-${slot}`;
5641
+ const ledger = storage.kind === "filesystem" ? openSearchLedger({
5642
+ path,
5643
+ searchId
5644
+ }) : openSearchLedger({
5645
+ path,
5646
+ searchId,
5647
+ store: storage
5648
+ });
5649
+ if (!(await ledger.state()).closed) return {
5650
+ ledger,
5651
+ identity
5652
+ };
5653
+ }
5654
+ }
5655
+ function artifactKindOf(surface) {
5656
+ return typeof surface === "object" && surface.kind === "code" ? "code" : "prompt";
5657
+ }
5658
+ function receiptKey(receipt) {
5659
+ return receipt.callId;
5660
+ }
5661
+ /** A proposal that made a paid call ran a model; one that made none ran code. */
5662
+ function proposalExecution(receipts, provider, source) {
5663
+ const model = receipts.find((receipt) => receipt.channel !== "judge")?.model;
5664
+ if (model === void 0) return {
5665
+ kind: "deterministic",
5666
+ source
5667
+ };
5668
+ return {
5669
+ kind: "model",
5670
+ model: modelIdentity(model, provider),
5671
+ source
5672
+ };
5673
+ }
5674
+ function modelIdentity(model, provider) {
5675
+ return modelHasSnapshot(model) ? {
5676
+ provider,
5677
+ snapshot: model
5678
+ } : {
5679
+ provider,
5680
+ alias: model,
5681
+ unknown: "the provider reported a moving alias, not a snapshot"
5682
+ };
5683
+ }
5684
+ function cellModel(cell, fallback) {
5685
+ return cell.resolvedModel === void 0 ? fallback : modelIdentity(cell.resolvedModel, fallback.provider);
3346
5686
  }
3347
5687
  function cellOutcome(cell) {
3348
- const scores = Object.entries(cell.judgeScores).filter(([, score]) => score.failed !== true && Number.isFinite(score.composite));
3349
- if (cell.error !== void 0 || scores.length === 0) return {
5688
+ const quality = projectCampaignCellQuality(cell);
5689
+ if (quality.score === void 0) return {
3350
5690
  status: "errored",
3351
5691
  metrics: {},
3352
5692
  error: {
@@ -3355,12 +5695,11 @@ function cellOutcome(cell) {
3355
5695
  retryable: false
3356
5696
  }
3357
5697
  };
3358
- const composite = scores.reduce((sum, [, score]) => sum + score.composite, 0) / scores.length;
3359
- const metrics = { composite };
3360
- for (const [judge, score] of scores) metrics[`judge.${judge}`] = score.composite;
5698
+ const metrics = { composite: quality.score };
5699
+ for (const [judge, score] of Object.entries(quality.successfulJudgeScores)) metrics[`judge.${judge}`] = score.composite;
3361
5700
  return {
3362
5701
  status: "passed",
3363
- score: composite,
5702
+ score: quality.score,
3364
5703
  metrics
3365
5704
  };
3366
5705
  }
@@ -3423,332 +5762,6 @@ function receiptAccounting(receipts) {
3423
5762
  }
3424
5763
  };
3425
5764
  }
3426
- //#endregion
3427
- //#region src/campaign/presets/run-optimization.ts
3428
- /**
3429
- * `runOptimization` runs a caller-owned candidate generator for a bounded
3430
- * number of rounds. Each candidate is measured on the same cases, and only a
3431
- * candidate that beats the current best becomes the incumbent. By default the
3432
- * incumbent is also the parent every generation mutates; a `selectParent`
3433
- * policy draws the parent from the Pareto frontier instead.
3434
- * The same loop accepts deterministic, model-backed, or agent-backed
3435
- * proposers; they differ only in how `propose()` picks candidates.
3436
- *
3437
- * `runImprovementLoop` adds a separate final comparison, a release decision,
3438
- * and optional pull request creation.
3439
- */
3440
- /**
3441
- * Improvement loop body: N generations of propose → campaign → rank, maintaining a Pareto frontier and one global incumbent across generations. The parent each generation mutates is the incumbent unless `selectParent` draws it from the frontier.
3442
- */
3443
- async function runOptimization(opts) {
3444
- const { proposer } = opts;
3445
- const candidateConcurrency = opts.candidateConcurrency ?? 1;
3446
- if (typeof opts.runDir !== "string" || opts.runDir.trim().length === 0) throw new Error("runOptimization: runDir is required and must be a non-empty string");
3447
- if (!Number.isInteger(candidateConcurrency) || candidateConcurrency < 1) throw new Error("runOptimization: candidateConcurrency must be a positive integer");
3448
- const initialFindings = immutableProposalSnapshot(assertProposalFindings(opts.findings ?? [], "runOptimization initial proposal findings"), "initial findings");
3449
- const baselineSurface = immutableProposalSnapshot(opts.baselineSurface, "baseline surface");
3450
- opts.runDir = resolveRunDir(opts.runDir, opts.repo);
3451
- const storage = opts.storage ?? fsCampaignStorage();
3452
- const costLedger = opts.costLedger ?? createRunCostLedger({
3453
- storage,
3454
- runDir: opts.runDir,
3455
- costCeilingUsd: opts.costCeiling
3456
- });
3457
- const requireJudgeScore = (opts.judges?.length ?? 0) > 0;
3458
- const reps = opts.reps ?? 1;
3459
- const premeasuredBaseline = opts.premeasuredBaseline;
3460
- const baselineCampaign = premeasuredBaseline ? validatedPremeasuredBaseline({
3461
- input: premeasuredBaseline,
3462
- baselineSurface,
3463
- scenarios: opts.scenarios,
3464
- reps,
3465
- seed: opts.seed ?? 42,
3466
- judges: opts.judges ?? [],
3467
- dispatchRef: surfaceDispatchRef(baselineSurface, opts.dispatchRef)
3468
- }) : await runCampaign({
3469
- ...opts,
3470
- costLedger,
3471
- costPhase: "search.baseline",
3472
- dispatchRef: surfaceDispatchRef(baselineSurface, opts.dispatchRef),
3473
- dispatch: (scenario, ctx) => opts.dispatchWithSurface(baselineSurface, scenario, ctx),
3474
- runDir: `${opts.runDir}/baseline`
3475
- });
3476
- const baselineCoverage = campaignCoverage(baselineCampaign.cells, opts.scenarios, reps, requireJudgeScore);
3477
- if (!baselineCoverage.complete) {
3478
- const label = opts.premeasuredBaseline ? "premeasured baseline" : "baseline";
3479
- 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.`);
3480
- }
3481
- const opened = opts.searchLedger ? await OptimizationSearch.open({
3482
- binding: opts.searchLedger,
3483
- storage,
3484
- costLedger,
3485
- scenarios: opts.scenarios,
3486
- reps,
3487
- seed: opts.seed ?? 42,
3488
- splitDigest: baselineCampaign.splitDigest,
3489
- proposerName: proposer.kind,
3490
- expansion: opts.selectParent ? "select-parent" : "incumbent",
3491
- maxUsd: opts.costCeiling ?? null,
3492
- baselineSurface,
3493
- baselineCells: baselineCampaign.cells
3494
- }) : void 0;
3495
- const search = opened?.search;
3496
- const nodeIdByHash = /* @__PURE__ */ new Map();
3497
- if (opened) nodeIdByHash.set(surfaceHash(baselineSurface), opened.rootNodeId);
3498
- const incompleteNodeIds = /* @__PURE__ */ new Set();
3499
- const promotedNodeIds = /* @__PURE__ */ new Set();
3500
- let stopReason = "max-nodes";
3501
- const generations = [];
3502
- const history = [];
3503
- let currentFindings = initialFindings;
3504
- const selectionRankKey = opts.selectionRankKey ?? ((campaign) => [campaignMeanComposite(campaign)]);
3505
- let winnerSurface = baselineSurface;
3506
- let winnerSurfaceHash = surfaceHash(baselineSurface);
3507
- const admittedCandidateHashes = /* @__PURE__ */ new Set([winnerSurfaceHash]);
3508
- let winnerComposite = campaignMeanComposite(baselineCampaign);
3509
- let winnerRankKey = selectionRankKey(baselineCampaign);
3510
- assertFiniteRankKey(winnerRankKey, "selectionRankKey for baseline");
3511
- const baselineOutcome = toScoredSurfaceOutcome(winnerSurfaceHash, baselineCampaign, baselineCoverage, -1);
3512
- let winnerOutcome = baselineOutcome;
3513
- let winnerLabel;
3514
- let winnerRationale;
3515
- const scored = [toParetoParent(baselineSurface, winnerSurfaceHash, baselineCampaign, -1)];
3516
- const measuredByHash = /* @__PURE__ */ new Map([[winnerSurfaceHash, {
3517
- parent: scored[0],
3518
- outcome: baselineOutcome
3519
- }]]);
3520
- if (opts.analyzeGeneration && opts.maxGenerations > 0 && baselineCampaign.cells.length > 0) {
3521
- const fresh = await opts.analyzeGeneration({
3522
- generation: -1,
3523
- runDir: baselineCampaign.runDir,
3524
- candidates: [{
3525
- surfaceHash: winnerSurfaceHash,
3526
- campaign: baselineCampaign,
3527
- composite: winnerComposite
3528
- }],
3529
- history,
3530
- costLedger,
3531
- costPhase: "analysis.baseline"
3532
- });
3533
- if (!Array.isArray(fresh)) throw new TypeError("runOptimization: analyzeGeneration must return an array");
3534
- currentFindings = immutableProposalSnapshot(assertProposalFindings(fresh, "runOptimization baseline analysis findings"), "baseline analysis findings");
3535
- }
3536
- for (let gen = 0; gen < opts.maxGenerations; gen++) {
3537
- const proposalHistory = immutableProposalSnapshot(history, "history");
3538
- if (proposer.decide?.({ history: proposalHistory }).stop) {
3539
- stopReason = "converged";
3540
- break;
3541
- }
3542
- const paretoParents = immutableProposalSnapshot(computeParetoFrontier(scored), "Pareto parents");
3543
- const incumbentOutcome = immutableProposalSnapshot(winnerOutcome, "incumbent outcome");
3544
- const selected = opts.selectParent ? resolveSelectedParent(opts.selectParent(Object.freeze({
3545
- frontier: paretoParents,
3546
- incumbent: incumbentOutcome,
3547
- history: proposalHistory,
3548
- generation: gen
3549
- })), measuredByHash, gen) : void 0;
3550
- const parentSurface = selected ? selected.parent.surface : winnerSurface;
3551
- const parentSurfaceHash = selected ? selected.parent.surfaceHash : winnerSurfaceHash;
3552
- const parentComposite = selected ? selected.outcome.composite : winnerComposite;
3553
- const parentOutcome = selected ? selected.outcome : winnerOutcome;
3554
- const proposalContext = Object.freeze({
3555
- currentSurface: immutableProposalSnapshot(parentSurface, "current surface"),
3556
- history: proposalHistory,
3557
- findings: immutableProposalSnapshot(assertProposalFindings(currentFindings, "runOptimization proposal findings"), "findings"),
3558
- populationSize: opts.populationSize,
3559
- generation: gen,
3560
- signal: opts.signal ?? new AbortController().signal,
3561
- baselineOutcome: immutableProposalSnapshot(baselineOutcome, "baseline outcome"),
3562
- incumbentOutcome,
3563
- parentOutcome: immutableProposalSnapshot(parentOutcome, "parent outcome"),
3564
- maxImprovementShots: opts.maxImprovementShots,
3565
- paretoParents,
3566
- costLedger,
3567
- costPhase: "search.proposal"
3568
- });
3569
- await search?.startGeneration(gen);
3570
- const proposed = await proposer.propose(proposalContext);
3571
- if (!Array.isArray(proposed)) throw new TypeError("runOptimization: proposer must return an array");
3572
- const proposalSnapshot = immutableProposalSnapshot(proposed, "candidate outputs");
3573
- if (proposalSnapshot.length === 0) {
3574
- await search?.failGeneration(gen, "the proposer returned no candidates");
3575
- stopReason = "converged";
3576
- break;
3577
- }
3578
- const candidates = proposalSnapshot.map((p) => isProposedCandidate(p) ? p : {
3579
- surface: p,
3580
- label: "",
3581
- rationale: ""
3582
- });
3583
- const generationHashes = /* @__PURE__ */ new Set();
3584
- for (const { surface } of candidates) {
3585
- const hash = surfaceHash(surface);
3586
- if (admittedCandidateHashes.has(hash) || generationHashes.has(hash)) throw new Error(`runOptimization: duplicate candidate surface hash "${hash}" in generation ${gen}; candidate surfaces must be unique`);
3587
- generationHashes.add(hash);
3588
- }
3589
- for (const hash of generationHashes) admittedCandidateHashes.add(hash);
3590
- if (search) {
3591
- const nodeIds = await search.recordProposal({
3592
- generation: gen,
3593
- parent: {
3594
- nodeId: nodeIdByHash.get(parentSurfaceHash),
3595
- surface: parentSurface,
3596
- composite: parentComposite
3597
- },
3598
- selectionRule: opts.selectParent ? "select-parent" : "incumbent",
3599
- candidates
3600
- });
3601
- for (const [index, nodeId] of nodeIds.entries()) {
3602
- nodeIdByHash.set(surfaceHash(candidates[index].surface), nodeId);
3603
- await search.allocate(nodeId);
3604
- }
3605
- }
3606
- const surfaceResults = await mapConcurrent(candidates, candidateConcurrency, async ({ surface, label, rationale, attribution }, i, signal) => {
3607
- const hash = surfaceHash(surface);
3608
- const campaign = await runCampaign({
3609
- ...opts,
3610
- signal,
3611
- costLedger,
3612
- costPhase: "search.candidate",
3613
- dispatchRef: surfaceDispatchRef(surface, opts.dispatchRef),
3614
- dispatch: (scenario, ctx) => opts.dispatchWithSurface(surface, scenario, ctx),
3615
- runDir: `${opts.runDir}/gen-${gen}/candidate-${i}`
3616
- });
3617
- const coverage = campaignCoverage(campaign.cells, opts.scenarios, opts.reps ?? 1, requireJudgeScore);
3618
- const composite = campaignMeanCompositeOrNull(campaign);
3619
- const rankKey = coverage.complete ? selectionRankKey(campaign) : null;
3620
- if (rankKey) assertFiniteRankKey(rankKey, `selectionRankKey for generation ${gen} candidate ${i}`, winnerRankKey.length);
3621
- return {
3622
- surfaceHash: hash,
3623
- surface,
3624
- label,
3625
- rationale,
3626
- ...attribution ? { attribution } : {},
3627
- campaign,
3628
- composite,
3629
- rankKey,
3630
- coverage
3631
- };
3632
- }, opts.signal);
3633
- for (const result of surfaceResults) {
3634
- const { surface, surfaceHash: hash, campaign, coverage, label, rationale } = result;
3635
- if (coverage.complete) {
3636
- const parent = toParetoParent(surface, hash, campaign, gen, label || void 0, rationale || void 0);
3637
- scored.push(parent);
3638
- measuredByHash.set(hash, {
3639
- parent,
3640
- outcome: toScoredSurfaceOutcome(hash, campaign, coverage, gen)
3641
- });
3642
- }
3643
- }
3644
- if (search) for (const result of surfaceResults) {
3645
- const nodeId = nodeIdByHash.get(result.surfaceHash);
3646
- await search.recordCells(nodeId, "train", result.campaign.cells);
3647
- if (!result.coverage.complete) incompleteNodeIds.add(nodeId);
3648
- }
3649
- surfaceResults.sort((a, b) => {
3650
- if (a.coverage.complete !== b.coverage.complete) return a.coverage.complete ? -1 : 1;
3651
- if (a.rankKey && b.rankKey) return compareRankKeys(b.rankKey, a.rankKey);
3652
- return a.surfaceHash.localeCompare(b.surfaceHash);
3653
- });
3654
- const top = surfaceResults.filter((result) => result.coverage.complete && result.composite !== null && result.rankKey !== null)[0];
3655
- const promoted = top && compareRankKeys(top.rankKey, winnerRankKey) > 0 ? [top] : [];
3656
- if (promoted[0]) {
3657
- const top = promoted[0];
3658
- const promotedNodeId = nodeIdByHash.get(top.surfaceHash);
3659
- if (promotedNodeId) promotedNodeIds.add(promotedNodeId);
3660
- winnerSurface = top.surface;
3661
- winnerSurfaceHash = top.surfaceHash;
3662
- winnerComposite = top.composite;
3663
- winnerRankKey = top.rankKey;
3664
- winnerOutcome = toScoredSurfaceOutcome(top.surfaceHash, top.campaign, top.coverage, gen);
3665
- winnerLabel = top.label || void 0;
3666
- winnerRationale = top.rationale || void 0;
3667
- }
3668
- const record = {
3669
- generationIndex: gen,
3670
- candidates: surfaceResults.map((s) => {
3671
- const breakdown = campaignBreakdown(s.campaign);
3672
- const candidate = {
3673
- surfaceHash: s.surfaceHash,
3674
- composite: s.composite,
3675
- ci95: null,
3676
- parentSurfaceHash,
3677
- parentComposite,
3678
- ...s.coverage.complete ? { observedDeltaFromParent: s.composite - parentComposite } : {},
3679
- eligibleForPromotion: s.coverage.complete,
3680
- coverage: {
3681
- expectedCells: s.coverage.expectedCellIds.length,
3682
- scorableCells: s.coverage.scorableCellIds.length,
3683
- unscorableCells: s.coverage.unscorableCells
3684
- },
3685
- dimensions: breakdown.dimensions,
3686
- scenarios: breakdown.scenarios
3687
- };
3688
- if (s.label) candidate.label = s.label;
3689
- if (s.rationale) candidate.rationale = s.rationale;
3690
- if (s.attribution) candidate.attribution = s.attribution;
3691
- return candidate;
3692
- }),
3693
- promoted: promoted.map((p) => p.surfaceHash)
3694
- };
3695
- history.push(record);
3696
- generations.push({
3697
- record,
3698
- surfaces: surfaceResults.map((s) => ({
3699
- surfaceHash: s.surfaceHash,
3700
- surface: s.surface,
3701
- campaign: s.campaign
3702
- }))
3703
- });
3704
- if (opts.analyzeGeneration && gen < opts.maxGenerations - 1) {
3705
- const fresh = await opts.analyzeGeneration({
3706
- generation: gen,
3707
- runDir: `${opts.runDir}/gen-${gen}`,
3708
- candidates: surfaceResults.map((s) => ({
3709
- surfaceHash: s.surfaceHash,
3710
- campaign: s.campaign,
3711
- composite: s.composite
3712
- })),
3713
- history,
3714
- costLedger,
3715
- costPhase: "analysis.generation"
3716
- });
3717
- if (!Array.isArray(fresh)) throw new TypeError("runOptimization: analyzeGeneration must return an array");
3718
- currentFindings = immutableProposalSnapshot(assertProposalFindings(fresh, "runOptimization generation analysis findings"), "generation analysis findings");
3719
- }
3720
- }
3721
- const searchHistory = await search?.finish({
3722
- winnerNodeId: nodeIdByHash.get(winnerSurfaceHash),
3723
- promotedNodeIds,
3724
- incompleteNodeIds,
3725
- reason: stopReason,
3726
- runId: opts.runDir
3727
- });
3728
- return {
3729
- generations,
3730
- baselineSurface,
3731
- winnerSurface,
3732
- winnerSurfaceHash,
3733
- winnerLabel,
3734
- winnerRationale,
3735
- baselineCampaign,
3736
- ...searchHistory ? { searchHistory } : {},
3737
- paretoFrontier: computeParetoFrontier(scored),
3738
- cost: costLedger.summary()
3739
- };
3740
- }
3741
- /** Resolve the parent a `selectParent` policy returned to the measured record
3742
- * this run holds for it. Refuses a surface the run never measured to
3743
- * completion, and a parent whose surface does not hash to its `surfaceHash`. */
3744
- function resolveSelectedParent(returned, measuredByHash, generation) {
3745
- if (typeof returned !== "object" || returned === null || typeof returned.surfaceHash !== "string") throw new TypeError(`runOptimization: selectParent must return a ParetoParent (generation ${generation})`);
3746
- const parent = returned;
3747
- const measured = measuredByHash.get(parent.surfaceHash);
3748
- if (!measured) throw new Error(`runOptimization: selectParent returned surface "${parent.surfaceHash}" in generation ${generation}, which this run has not measured to completion; a parent must be a scored surface from the frontier`);
3749
- if (!surfaceHashMatches(parent.surface, parent.surfaceHash)) throw new Error(`runOptimization: selectParent returned a parent whose surface does not match its surfaceHash "${parent.surfaceHash}" (generation ${generation})`);
3750
- return measured;
3751
- }
3752
5765
  function immutableProposalSnapshot(value, label) {
3753
5766
  try {
3754
5767
  return deepFreeze(structuredClone(value));
@@ -3782,58 +5795,42 @@ function validatedPremeasuredBaseline(args) {
3782
5795
  reps: args.reps
3783
5796
  });
3784
5797
  if (campaign.manifestHash !== expectedManifest) throw new Error("runOptimization: premeasured baseline evaluator identity does not match the requested dispatch and judges");
5798
+ const coverage = campaignCoverage(campaign.cells, args.scenarios, args.reps, args.requireJudgeScore);
5799
+ if (!coverage.complete) throw new Error(`runOptimization: premeasured baseline is incomplete (${coverage.scorableCellIds.length}/${coverage.expectedCellIds.length} designed cells scorable) — ${formatCoverageFailures(coverage)}. Refusing to optimize against an incomplete incumbent.`);
3785
5800
  return campaign;
3786
5801
  }
3787
- /** Build a `ParetoParent` from a scored campaign — objective vector =
3788
- * per-scenario composite, scalar = mean composite. */
3789
- function toParetoParent(surface, hash, campaign, generation, label, rationale) {
5802
+ /** A scored campaign as a frontier member: per-scenario composite objectives. */
5803
+ function toParetoParent(surface, campaign, generation, label) {
3790
5804
  const objectives = {};
3791
5805
  for (const { scenarioId, composite } of campaignBreakdown(campaign).scenarios) objectives[scenarioId] = composite;
3792
5806
  const parent = {
3793
5807
  surface,
3794
- surfaceHash: hash,
5808
+ surfaceHash: surfaceHash(surface),
3795
5809
  objectives,
3796
5810
  composite: campaignMeanComposite(campaign),
3797
5811
  generation
3798
5812
  };
3799
5813
  if (label) parent.label = label;
3800
- if (rationale) parent.rationale = rationale;
3801
5814
  return parent;
3802
5815
  }
3803
- /** The non-dominated set over the per-scenario objective vectors. Every
3804
- * scenario seen across the scored set becomes a `maximize` objective.
3805
- * `runOptimization` admits only complete campaigns to this set; the finite
3806
- * floor remains a defensive fallback for manually constructed/no-judge
3807
- * vectors. Delegates dominance to the package-canonical `paretoFrontier`. */
5816
+ /** The non-dominated set over the per-scenario objective vectors. */
3808
5817
  function computeParetoFrontier(scored) {
3809
5818
  if (scored.length <= 1) return [...scored];
3810
5819
  const ids = /* @__PURE__ */ new Set();
3811
5820
  for (const p of scored) for (const id of Object.keys(p.objectives)) ids.add(id);
3812
5821
  if (ids.size === 0) return [...scored];
3813
- const floor = {};
3814
- for (const id of ids) {
3815
- let min = Number.POSITIVE_INFINITY;
3816
- for (const p of scored) {
3817
- const v = p.objectives[id];
3818
- if (typeof v === "number" && Number.isFinite(v) && v < min) min = v;
3819
- }
3820
- floor[id] = Number.isFinite(min) ? min : 0;
3821
- }
3822
5822
  return paretoFrontier(scored, [...ids].map((id) => ({
3823
5823
  name: id,
3824
5824
  direction: "maximize",
3825
- value: (p) => {
3826
- const v = p.objectives[id];
3827
- return typeof v === "number" && Number.isFinite(v) ? v : floor[id] ?? 0;
3828
- }
5825
+ value: (p) => p.objectives[id] ?? Number.NEGATIVE_INFINITY
3829
5826
  }))).frontier;
3830
5827
  }
3831
- function toScoredSurfaceOutcome(surfaceHash, campaign, coverage, generation) {
5828
+ function toScoredSurfaceOutcome(hash, campaign, coverage, generation) {
3832
5829
  const breakdown = campaignBreakdown(campaign);
3833
5830
  return {
3834
5831
  split: "search",
3835
5832
  generation,
3836
- surfaceHash,
5833
+ surfaceHash: hash,
3837
5834
  composite: campaignMeanComposite(campaign),
3838
5835
  dimensions: breakdown.dimensions,
3839
5836
  scenarios: breakdown.scenarios,
@@ -5253,6 +7250,6 @@ function firstString(value) {
5253
7250
  return typeof value === "string" && value.trim() ? value : void 0;
5254
7251
  }
5255
7252
  //#endregion
5256
- export { readCachedCell as $, isProposedCandidate as A, redTeamDataset as B, assertCompleteSearchHistory as C, searchHistoryCoverageRow as D, createSearchHistoryReceipt as E, runFinalComparison as F, captureFinalEvidencePolicy as G, scoreRedTeamOutput as H, openAutoPr as I, runEval as J, evaluationUnitMap as K, captureJudge as L, dominates as M, paretoFrontier as N, verifySearchHistoryArtifact as O, paretoFrontierWithCrowding as P, tangleTracesRoot as Q, defaultProductionGate as R, SearchHistoryRequiredError as S, assertSearchHistoryMatchesState as T, runCanaries as U, redTeamReport as V, assertIndependentEvaluationSplit as W, planCampaignRun as X, runCampaign as Y, resolveRunDir as Z, SearchRecorder as _, transientDispatchFailure as a, surfaceDiff as b, compareOptimizationMethods as c, combineComparisonCosts as d, buildCellSchedule as et, costFromLedgerSummary as f, runOptimization as g, runImprovementLoop as h, quotaExhaustedUntil as i, labelTrustRank as j, verifySearchHistoryReceipt as k, optimizationTokenUsageFromSummary as l, readGepaCandidatePopulationArtifact as m, JudgeParseError as n, computeManifestHash as nt, aggregateRunScore as o, assertGepaCandidatePopulationSummary as p, reserveFinalEvidence as q, isTransientTransportFailure as r, clamp01 as s, llmJudge as t, cellCachePath as tt, executeOptimizationMethod as u, developmentClaim as v, assertSearchHistoryAdmissionOptions as w, surfaceNode as x, searchEdgeId as y, DEFAULT_RED_TEAM_CORPUS as z };
7253
+ export { redTeamDataset as $, createSearchHistoryReceipt as A, decideSearchClaim as B, searchEdgeId as C, assertCompleteSearchHistory as D, SearchHistoryRequiredError as E, runSearch as F, uniform as G, searchClaimReserveUsd as H, searchDivergence as I, runFinalComparison as J, dominates as K, searchExpansionIndex as L, verifySearchHistoryArtifact as M, verifySearchHistoryReceipt as N, assertSearchHistoryAdmissionOptions as O, SEARCH_KERNEL_SOURCE as P, DEFAULT_RED_TEAM_CORPUS as Q, searchPolicyView as R, developmentClaim as S, surfaceNode as T, pairedPromotionPower as U, planSearchClaim as V, requiredPairsForPairedPromotion as W, captureJudge as X, openAutoPr as Y, defaultProductionGate as Z, isProposedCandidate as _, transientDispatchFailure as a, evaluationUnitMap as at, incumbent as b, compareOptimizationMethods as c, runCampaign as ct, combineComparisonCosts as d, tangleTracesRoot as dt, redTeamReport as et, costFromLedgerSummary as f, readCachedCell as ft, runOptimization as g, runImprovementLoop as h, computeManifestHash as ht, quotaExhaustedUntil as i, captureFinalEvidencePolicy as it, searchHistoryCoverageRow as j, assertSearchHistoryMatchesState as k, optimizationTokenUsageFromSummary as l, planCampaignRun as lt, readGepaCandidatePopulationArtifact as m, cellCachePath as mt, JudgeParseError as n, runCanaries as nt, aggregateRunScore as o, reserveFinalEvidence as ot, assertGepaCandidatePopulationSummary as p, buildCellSchedule as pt, paretoFrontier as q, isTransientTransportFailure as r, assertIndependentEvaluationSplit as rt, clamp01 as s, runEval as st, llmJudge as t, scoreRedTeamOutput as tt, executeOptimizationMethod as u, resolveRunDir as ut, labelTrustRank as v, surfaceDiff as w, SearchRecorder as x, crowdedFrontierParent as y, SEARCH_CLAIM_RULE as z };
5257
7254
 
5258
- //# sourceMappingURL=llm-judge-NJOHM0iA.js.map
7255
+ //# sourceMappingURL=llm-judge-ChbWfeWl.js.map