@tangle-network/agent-eval 0.123.0 → 0.123.2

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 (85) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +151 -161
  3. package/dist/analyst/index.d.ts +9 -1
  4. package/dist/analyst/index.js +5 -5
  5. package/dist/authenticity/index.js +3 -2
  6. package/dist/authenticity/index.js.map +1 -1
  7. package/dist/belief-state/index.d.ts +45 -5
  8. package/dist/belief-state/index.js +41 -3
  9. package/dist/belief-state/index.js.map +1 -1
  10. package/dist/benchmarks/index.d.ts +2 -1
  11. package/dist/benchmarks/index.js +6 -6
  12. package/dist/campaign/index.d.ts +29 -33
  13. package/dist/campaign/index.js +6 -6
  14. package/dist/{chunk-A5S77LSE.js → chunk-4SOQ4ND2.js} +2 -2
  15. package/dist/{chunk-VJ7T5WIO.js → chunk-5YMKIFYP.js} +3 -3
  16. package/dist/{chunk-U5CHZ5M3.js → chunk-DNVPOYUS.js} +4 -4
  17. package/dist/{chunk-6WX7CBAR.js → chunk-E3HAD4A3.js} +19 -8
  18. package/dist/chunk-E3HAD4A3.js.map +1 -0
  19. package/dist/{chunk-LBAHQOBI.js → chunk-EBDOTTZJ.js} +37 -11
  20. package/dist/chunk-EBDOTTZJ.js.map +1 -0
  21. package/dist/{chunk-XJYR7XFV.js → chunk-GC4ATIKK.js} +1 -1
  22. package/dist/chunk-GC4ATIKK.js.map +1 -0
  23. package/dist/{chunk-HZJF4IUO.js → chunk-HQY7LBV2.js} +3 -3
  24. package/dist/{chunk-NJC7U437.js → chunk-J7S4YM27.js} +6 -5
  25. package/dist/chunk-J7S4YM27.js.map +1 -0
  26. package/dist/{chunk-S3UZOQ5Y.js → chunk-LOW3U7JZ.js} +2 -2
  27. package/dist/{chunk-OYZAPX5G.js → chunk-R226UZOI.js} +2 -2
  28. package/dist/{chunk-GS3FJGUF.js → chunk-RQP5UTK5.js} +120 -14
  29. package/dist/chunk-RQP5UTK5.js.map +1 -0
  30. package/dist/{chunk-DTJ6QUQB.js → chunk-VGRCHJON.js} +39 -7
  31. package/dist/chunk-VGRCHJON.js.map +1 -0
  32. package/dist/{chunk-G2GPNLSX.js → chunk-WMJR67FX.js} +3 -3
  33. package/dist/{chunk-FC5NDO3E.js → chunk-WXQTVEKM.js} +3 -3
  34. package/dist/cli.js +100 -10
  35. package/dist/cli.js.map +1 -1
  36. package/dist/contract/index.d.ts +97 -5
  37. package/dist/contract/index.js +9 -7
  38. package/dist/contract/index.js.map +1 -1
  39. package/dist/control.js +3 -3
  40. package/dist/fuzz.js +3 -2
  41. package/dist/fuzz.js.map +1 -1
  42. package/dist/hosted/index.d.ts +8 -2
  43. package/dist/index.d.ts +10 -2
  44. package/dist/index.js +13 -13
  45. package/dist/index.js.map +1 -1
  46. package/dist/openapi.json +1 -1
  47. package/dist/rl.d.ts +48 -12
  48. package/dist/rl.js +5 -5
  49. package/dist/storyboard/index.js +1 -1
  50. package/dist/storyboard/index.js.map +1 -1
  51. package/dist/traces.js +3 -3
  52. package/dist/wire/index.d.ts +61 -4
  53. package/dist/wire/index.js +2 -2
  54. package/docs/adapters-observability.md +6 -6
  55. package/docs/building-doctrine.md +5 -5
  56. package/docs/concepts.md +29 -29
  57. package/docs/customer-journeys.md +80 -155
  58. package/docs/design/loop-taxonomy.md +26 -27
  59. package/docs/design.md +70 -0
  60. package/docs/distributed-driver.md +14 -14
  61. package/docs/eval-surface-map.md +11 -11
  62. package/docs/hosted-ingest-spec.md +4 -4
  63. package/docs/improvement-glossary.md +38 -38
  64. package/docs/insight-report.md +32 -27
  65. package/docs/multi-shot-optimization.md +8 -8
  66. package/docs/research-report-methodology.md +9 -9
  67. package/docs/self-improvement-map.md +13 -13
  68. package/docs/trace-analysis.md +2 -2
  69. package/docs/wire-protocol.md +16 -16
  70. package/package.json +2 -1
  71. package/dist/chunk-6WX7CBAR.js.map +0 -1
  72. package/dist/chunk-DTJ6QUQB.js.map +0 -1
  73. package/dist/chunk-GS3FJGUF.js.map +0 -1
  74. package/dist/chunk-LBAHQOBI.js.map +0 -1
  75. package/dist/chunk-NJC7U437.js.map +0 -1
  76. package/dist/chunk-XJYR7XFV.js.map +0 -1
  77. package/docs/auto-research-loop-end-to-end.md +0 -186
  78. /package/dist/{chunk-A5S77LSE.js.map → chunk-4SOQ4ND2.js.map} +0 -0
  79. /package/dist/{chunk-VJ7T5WIO.js.map → chunk-5YMKIFYP.js.map} +0 -0
  80. /package/dist/{chunk-U5CHZ5M3.js.map → chunk-DNVPOYUS.js.map} +0 -0
  81. /package/dist/{chunk-HZJF4IUO.js.map → chunk-HQY7LBV2.js.map} +0 -0
  82. /package/dist/{chunk-S3UZOQ5Y.js.map → chunk-LOW3U7JZ.js.map} +0 -0
  83. /package/dist/{chunk-OYZAPX5G.js.map → chunk-R226UZOI.js.map} +0 -0
  84. /package/dist/{chunk-G2GPNLSX.js.map → chunk-WMJR67FX.js.map} +0 -0
  85. /package/dist/{chunk-FC5NDO3E.js.map → chunk-WXQTVEKM.js.map} +0 -0
@@ -1,7 +1,7 @@
1
- # Eval surface map which primitive, when
1
+ # Eval surface map: which primitive, when
2
2
 
3
3
  The eval surface is a small set of orthogonal primitives. They compose; they do
4
- not overlap. If two seem interchangeable, read the "use when" the distinction
4
+ not overlap. If two seem interchangeable, read the "use when": the distinction
5
5
  is real and load-bearing. **Do not add a new wrapper to bridge two of these; the
6
6
  composition point already exists (see Produced-state grading below).**
7
7
 
@@ -10,15 +10,15 @@ composition point already exists (see Produced-state grading below).**
10
10
  | Primitive | Use when | Returns |
11
11
  |---|---|---|
12
12
  | `runCampaign` | The measurement primitive. Run a dispatch over scenarios × seeds × reps, score each with judges, aggregate. Caller owns the dispatch. | `CampaignResult` |
13
- | `runEval` | The simplest preset over `runCampaign` just score, no loop, no gate. The 80% "I want a scorecard" case. | `CampaignResult` |
13
+ | `runEval` | The simplest preset over `runCampaign`: just score, no loop, no gate. The 80% "I want a scorecard" case. | `CampaignResult` |
14
14
  | `runProfileMatrix` | Factor the SAME scenarios across N agent **profiles** (models / prompt variants), with RunRecord stamping + a real-backend integrity guard. | `RunRecord[]` |
15
15
  | `runOptimization` | GENERATE: measured or validated premeasured baseline → N generations of propose → measure → rank → promote. No release gate. | generations + winner |
16
16
  | `runImprovementLoop` | The release-gate shell around `runOptimization`: adds a held-out re-score + a promotion gate (+ optional auto-PR). | gate decision + winner |
17
- | `runEvalCampaign` | Inversion-of-control variant of `runCampaign` the runner is handed a pre-wired trace/sink/emitter and integrity gating as a precondition. Use when you need full capture by construction. | `CampaignResult` + records |
17
+ | `runEvalCampaign` | Inversion-of-control variant of `runCampaign`: the runner is handed a pre-wired trace/sink/emitter and integrity gating as a precondition. Use when you need full capture by construction. | `CampaignResult` + records |
18
18
 
19
19
  When variants of the same task run inside one `runCampaign`, give those scenarios the same `seedGroup` so each repetition uses common randomness.
20
20
  Use `runProfileMatrix` instead when profiles are separate campaign axes.
21
- | `runAgentMatrix` | The bare N-axis cartesian scheduler with concurrency control. The layer beneath the eval surface reach for it only when you need raw scheduling, not eval semantics. | cell results |
21
+ | `runAgentMatrix` | The bare N-axis cartesian scheduler with concurrency control. The layer beneath the eval surface: reach for it only when you need raw scheduling, not eval semantics. | cell results |
22
22
 
23
23
  Mental model: **measure** (`runCampaign`/`runEval`) → **factor** (`runProfileMatrix`) →
24
24
  **generate** (`runOptimization`) → **gate** (`runImprovementLoop`). `runEvalCampaign`
@@ -27,11 +27,11 @@ is `runCampaign` with capture inverted; `runAgentMatrix` is the scheduler undern
27
27
  Merging any two of these conflates distinct mental models (measure ≠ search ≠
28
28
  release-gate). Keep them separate; pick by the table.
29
29
 
30
- ## Produced-state grading there is NO persona-dispatch wrapper
30
+ ## Produced-state grading: there is NO persona-dispatch wrapper
31
31
 
32
32
  To grade what an agent actually **produced** (filed the proposal, wrote the
33
33
  artifact) rather than what it said, the composition point is a **judge that wraps
34
- `verifyCompletion`** not a dedicated runner. The pipeline:
34
+ `verifyCompletion`**: not a dedicated runner. The pipeline:
35
35
 
36
36
  ```
37
37
  runtime/app-tool events ──► extractProducedState(events) ──► ProducedState
@@ -43,20 +43,20 @@ runtime/app-tool events ──► extractProducedState(events) ──► Produce
43
43
 
44
44
  `extractProducedState` is a pure function over the produced-event stream; the
45
45
  judge calls it inline. This is why **`runProducedStatePersonaDispatch` does not
46
- exist and should not be built** it would be a fourth layer over a composition
46
+ exist and should not be built**: it would be a fourth layer over a composition
47
47
  that is already one judge. (Archetype: `playback.ts` `scoreUserStory`.)
48
48
 
49
49
  ### The in-band body contract
50
50
 
51
- Produced events carry their **body in-band** the grader never reaches into a
51
+ Produced events carry their **body in-band**: the grader never reaches into a
52
52
  product database to recover it:
53
53
 
54
54
  - `artifact` events carry `content` (the persisted file body).
55
- - `proposal_created` events carry `content` (the `submit_proposal` description)
55
+ - `proposal_created` events carry `content` (the `submit_proposal` description) -
56
56
  same role, same field name. A title-only filing omits it; a content-less
57
57
  proposal is graded presence-only (and, by the completion oracle's rule, does
58
58
  not count as a completed deliverable).
59
59
 
60
60
  A consumer that finds itself re-fetching a deliverable's body from its own DB to
61
- grade it is working around a thin event fix the event (carry `content`), don't
61
+ grade it is working around a thin event: fix the event (carry `content`), don't
62
62
  add an enrichment band-aid.
@@ -1,4 +1,4 @@
1
- # Hosted-ingest wire spec `2026-05-26.v1`
1
+ # Hosted-ingest wire spec: `2026-05-26.v1`
2
2
 
3
3
  The schema **every** orchestrator (ours, partners' self-hosted ones,
4
4
  any future open implementation) must accept. Frozen under semver:
@@ -149,7 +149,7 @@ Any orchestrator implementing this spec MUST:
149
149
  5. **Persist eval-runs durably**: at least the event + cell scores must
150
150
  survive an orchestrator restart. Trace spans MAY be best-effort.
151
151
  6. **Provide read access**: GET endpoints for the tenant to list + fetch
152
- their own runs. Wire format for reads is NOT part of this spec each
152
+ their own runs. Wire format for reads is NOT part of this spec: each
153
153
  orchestrator can pick its own (REST + JSON, gRPC, GraphQL).
154
154
 
155
155
  Servers SHOULD also:
@@ -163,7 +163,7 @@ Servers SHOULD also:
163
163
 
164
164
  ## Reference implementation
165
165
 
166
- `examples/hosted-ingest-server/` a minimal hono-based receiver. ~200
166
+ `examples/hosted-ingest-server/`: a minimal hono-based receiver. ~200
167
167
  LOC. Validates auth, accepts ingest, stores in memory, exposes a
168
168
  read endpoint. Runs anywhere Node runs.
169
169
 
@@ -200,4 +200,4 @@ receiver's `GET /v1/runs` lists it back.
200
200
  Servers MUST reject requests with `X-Tangle-Wire-Version` they don't
201
201
  support, with a 400 listing the versions they DO accept.
202
202
 
203
- The version string IS the spec id pin against it.
203
+ The version string IS the spec id: pin against it.
@@ -1,12 +1,12 @@
1
1
  # Improvement glossary + proposer chooser + composition
2
2
 
3
- > **In plain terms:** this is the dictionary for the *improvement* half of the stack the words that show up when you optimize an agent (proposer, surface, candidate, generation, holdout, lift, gate…) rather than when you *run* one (driver, worker, iteration those live in [`agent-runtime/docs/glossary.md`](../../agent-runtime/docs/glossary.md)).
3
+ > **In plain terms:** this is the dictionary for the *improvement* half of the stack: the words that show up when you optimize an agent (proposer, surface, candidate, generation, holdout, lift, gate…) rather than when you *run* one (driver, worker, iteration: those live in [`agent-runtime/docs/glossary.md`](../../agent-runtime/docs/glossary.md)).
4
4
  > Read this once and you can read any improvement result, pick a proposer, and wire two of them together.
5
5
 
6
6
  **Who this is for.**
7
7
  A **novice** (never seen the repo) should be able to read a `CampaignResult`, a `ProposeContext`, and a proposer chooser table without opening the source.
8
- An **expert** who knows DSPy/GEPA should be able to map their existing mental model onto our names in about a minute every core term below carries a *"if you know DSPy/GEPA"* line.
9
- If code and this file disagree, the code wins fix this file the same turn (the anti-staleness law).
8
+ An **expert** who knows DSPy/GEPA should be able to map their existing mental model onto our names in about a minute: every core term below carries a *"if you know DSPy/GEPA"* line.
9
+ If code and this file disagree, the code wins: fix this file the same turn (the anti-staleness law).
10
10
 
11
11
  Neighbors, so this page does not duplicate them: [`concepts.md`](./concepts.md) (eval mental model), [`self-improvement-map.md`](./self-improvement-map.md) (one loop / four roles / proposer catalog), [`campaign-proposers.md`](./campaign-proposers.md) (proposer ELI5), [`eval-surface-map.md`](./eval-surface-map.md) (which `run*` primitive), [`design/loop-taxonomy.md`](./design/loop-taxonomy.md) (execution vs proposer layering).
12
12
 
@@ -24,63 +24,63 @@ baseline surface
24
24
  repeat for maxGenerations
25
25
  ```
26
26
 
27
- ## Glossary one plain sentence each
27
+ ## Glossary: one plain sentence each
28
28
 
29
29
  Grounded to `agent-eval/src/campaign/types.ts` unless noted.
30
30
 
31
31
  | Term | Plain sentence | If you know DSPy/GEPA |
32
32
  |---|---|---|
33
- | **surface** | The one thing being changed this run a prompt string, a JSON config string, or a finalized code candidate (`MutableSurface = string \| CodeSurface`, `types.ts`). | The optimized artifact: a signature/predictor's instruction text, or the module config. |
34
- | **proposer** | The strategy that, given the current surface + what failed, proposes the next batch of candidate surfaces to measure it does **not** run the agent or score anything (`SurfaceProposer.propose`, `types.ts:286`). | The optimizer / teleprompter (MIPRO, BootstrapFewShot, GEPA's reflective proposer). |
33
+ | **surface** | The one thing being changed this run: a prompt string, a JSON config string, or a finalized code candidate (`MutableSurface = string \| CodeSurface`, `types.ts`). | The optimized artifact: a signature/predictor's instruction text, or the module config. |
34
+ | **proposer** | The strategy that, given the current surface + what failed, proposes the next batch of candidate surfaces to measure: it does **not** run the agent or score anything (`SurfaceProposer.propose`, `types.ts:286`). | The optimizer / teleprompter (MIPRO, BootstrapFewShot, GEPA's reflective proposer). |
35
35
  | **candidate** | One proposed surface plus its human `label` and `rationale`, ready to be measured (`ProposedCandidate`, `types.ts:166`). | One trial instruction/program the optimizer wants to evaluate. |
36
36
  | **generation** | One round of *propose → measure → rank → promote*; the loop runs up to `maxGenerations` of them (`GenerationRecord`, `types.ts:549`). | One GEPA iteration / optimization step. |
37
- | **populationSize** | BREADTH how many candidate surfaces the proposer returns *this* generation (`ProposeContext.populationSize`, `types.ts:237`). Paired with `maxGenerations` (DEPTH) as the search budget. | Beam width / number of minibatch candidates per step. |
38
- | **population budget** | Informal name for the pair `{ populationSize, maxGenerations }` the total candidates the search may evaluate (breadth × depth). | Optimizer trial budget. |
37
+ | **populationSize** | BREADTH: how many candidate surfaces the proposer returns *this* generation (`ProposeContext.populationSize`, `types.ts:237`). Paired with `maxGenerations` (DEPTH) as the search budget. | Beam width / number of minibatch candidates per step. |
38
+ | **population budget** | Informal name for the pair `{ populationSize, maxGenerations }`: the total candidates the search may evaluate (breadth × depth). | Optimizer trial budget. |
39
39
  | **campaign** | One measurement: run a dispatch over scenarios × seeds × reps, judge each output, aggregate → `CampaignResult` (`runCampaign`; a "campaign" = a coordinated batch of measurements). | One evaluation pass of a candidate over a valset. |
40
- | **cell** | The atomic measurement unit exactly one `(scenario, rep)` execution producing one artifact + its judge scores + its cost (`CampaignCellResult`, `types.ts:509`; `DispatchContext.cellId/rep`). | One (example, seed) evaluation datapoint. |
40
+ | **cell** | The atomic measurement unit: exactly one `(scenario, rep)` execution producing one artifact + its judge scores + its cost (`CampaignCellResult`, `types.ts:509`; `DispatchContext.cellId/rep`). | One (example, seed) evaluation datapoint. |
41
41
  | **scenario** | One input case with a stable `id` + `kind` (consumers attach their payload: persona, task, requirement) (`Scenario`, `types.ts:23`). | One dataset example / `dspy.Example`. |
42
- | **rep** | The repetition index the same scenario run more than once so noise/variance is measurable rather than mistaken for signal (`ctx.rep`). | Repeated sampling of the same example (temperature/seed variance). |
42
+ | **rep** | The repetition index: the same scenario run more than once so noise/variance is measurable rather than mistaken for signal (`ctx.rep`). | Repeated sampling of the same example (temperature/seed variance). |
43
43
  | **judge** | A scorer: given an artifact, return dimensions + a single `composite` + free-form `notes`; it throws on failure rather than silently scoring 0 (`JudgeConfig` → `JudgeScore`, `types.ts:94/116`). | The metric function, but pluggable (LLM-judge, deterministic checks, or an ensemble). |
44
- | **composite** | The single 0..1 number that combines all judge dimensions the number you rank and gate on (`JudgeScore.composite`, `types.ts:116`). | The scalar metric value. |
45
- | **findings** | The failure analysis handed to the proposer so it edits from evidence, not guesses worst cells + judge reasons, or a trace-analyst's clusters (`ProposeContext.findings`). | GEPA's reflective feedback / the "textual gradient". |
44
+ | **composite** | The single 0..1 number that combines all judge dimensions: the number you rank and gate on (`JudgeScore.composite`, `types.ts:116`). | The scalar metric value. |
45
+ | **findings** | The failure analysis handed to the proposer so it edits from evidence, not guesses: worst cells + judge reasons, or a trace-analyst's clusters (`ProposeContext.findings`). | GEPA's reflective feedback / the "textual gradient". |
46
46
  | **baseline** | The starting surface (the current prompt/config) every candidate is measured against (`baselineSurface`). | The unoptimized program you compare lift against. |
47
- | **holdout** | A separate scenario split the winner is re-scored on and that the proposer is *never* allowed to see a compile-time firewall (`ProposeContext.judgeScores: never`, `types.ts:265`) keeps held-out verdicts out of proposal so the optimizer can't game the acceptance axis. | The held-out valset/testset but here it is *write-only* to the optimizer. |
48
- | **lift** | Winner minus baseline `composite` on the holdout the actual improvement, reported with a bootstrap confidence interval (`ImproveResult.lift`, `ProposerScore.lift`). | Δ metric between optimized and baseline program. |
49
- | **MDE** | Minimum Detectable Effect the smallest lift your budget could statistically distinguish from noise, computed up front from baseline cells; a structurally-hopeless budget warns before you spend (surfaced as `result.power` / "power preflight", `agent-optimization-map.md`). Not spelled "MDE" in code yet this is the standard name for it. | Power analysis on the eval set; the reason a tiny valset can't certify a small gain. |
50
- | **Pareto frontier** | The set of surfaces that are non-dominated across the per-scenario score vectors a candidate worse on the mean but uniquely best on one hard scenario survives, so its lesson is not discarded (`ParetoParent`, `types.ts:198`; GEPA, arXiv:2507.19457). | Exactly GEPA's Pareto candidate pool same paper, same idea. |
47
+ | **holdout** | A separate scenario split the winner is re-scored on and that the proposer is *never* allowed to see: a compile-time firewall (`ProposeContext.judgeScores: never`, `types.ts:265`) keeps held-out verdicts out of proposal so the optimizer can't game the acceptance axis. | The held-out valset/testset: but here it is *write-only* to the optimizer. |
48
+ | **lift** | Winner minus baseline `composite` on the holdout: the actual improvement, reported with a bootstrap confidence interval (`ImproveResult.lift`, `ProposerScore.lift`). | Δ metric between optimized and baseline program. |
49
+ | **MDE** | Minimum Detectable Effect: the smallest lift your budget could statistically distinguish from noise, computed up front from baseline cells; a structurally-hopeless budget warns before you spend (surfaced as `result.power` / "power preflight", `agent-optimization-map.md`). Not spelled "MDE" in code yet: this is the standard name for it. | Power analysis on the eval set; the reason a tiny valset can't certify a small gain. |
50
+ | **Pareto frontier** | The set of surfaces that are non-dominated across the per-scenario score vectors: a candidate worse on the mean but uniquely best on one hard scenario survives, so its lesson is not discarded (`ParetoParent`, `types.ts:198`; GEPA, arXiv:2507.19457). | Exactly GEPA's Pareto candidate pool: same paper, same idea. |
51
51
  | **gate** | The promotion decision: does the winner beat baseline on the holdout with significance, returning one of five verdicts `ship / hold / need_more_work / model_ceiling / arch_ceiling` (`Gate`, `GateDecision`, `types.ts:315/340`). | The accept/reject rule on the held-out valset, plus a significance test and ceiling diagnosis. |
52
- | **promotion** | What happens on a `ship` verdict the winning surface is written back into the profile field it came from (`GenerationRecord.promoted`; `applyWinnerToProfile`, `improve.ts`). | Committing the optimized program as the new default. |
52
+ | **promotion** | What happens on a `ship` verdict: the winning surface is written back into the profile field it came from (`GenerationRecord.promoted`; `applyWinnerToProfile`, `improve.ts`). | Committing the optimized program as the new default. |
53
53
 
54
- ## Which proposer the chooser
54
+ ## Which proposer: the chooser
55
55
 
56
56
  Every optimizer is a factory `xProposer(opts): SurfaceProposer`, all exported from `@tangle-network/agent-eval/campaign`.
57
57
  Start at the top row and only move down when the row's *"reach for it when"* matches your failure mode.
58
58
 
59
59
  | Proposer factory | Reach for it when | Surface it edits | Wired to the paved path? |
60
60
  |---|---|---|---|
61
- | `gepaProposer` | You want the strong default: reflective full-surface prompt rewrites, grounded in findings, keeping a Pareto frontier of complementary winners. | prompt string | **Yes `improve({ surface: 'prompt' })` default.** Proven live. |
62
- | `skillOptProposer` | You are editing a structured `SKILL.md`/runbook and want small anchored add/delete/replace patches that preserve earlier rules. | skill/prompt string | **Yes `improve({ surface: 'skills' })` default.** Not yet proven live. |
63
- | `parameterSweepProposer` | The likely fix is a config knob, not words `retrieval.k`, `temperature`, `max_tokens`. You give it candidate patches; it applies them to a JSON surface. | JSON config string | Yes, but you supply the candidate list. |
64
- | `fapoProposer` | You want *evidence to decide when to escalate*: try prompt edits first, move to parameters, then to structural code one scoped change per cycle, only escalating when the cheaper level is exhausted. | whatever its level proposers return | Exported; you wire the level proposers. |
61
+ | `gepaProposer` | You want the strong default: reflective full-surface prompt rewrites, grounded in findings, keeping a Pareto frontier of complementary winners. | prompt string | **Yes: `improve({ surface: 'prompt' })` default.** Proven live. |
62
+ | `skillOptProposer` | You are editing a structured `SKILL.md`/runbook and want small anchored add/delete/replace patches that preserve earlier rules. | skill/prompt string | **Yes: `improve({ surface: 'skills' })` default.** Not yet proven live. |
63
+ | `parameterSweepProposer` | The likely fix is a config knob, not words: `retrieval.k`, `temperature`, `max_tokens`. You give it candidate patches; it applies them to a JSON surface. | JSON config string | Yes, but you supply the candidate list. |
64
+ | `fapoProposer` | You want *evidence to decide when to escalate*: try prompt edits first, move to parameters, then to structural code: one scoped change per cycle, only escalating when the cheaper level is exhausted. | whatever its level proposers return | Exported; you wire the level proposers. |
65
65
  | `compositeProposer` | You want several proposers to share one candidate-generation budget in the same round. It allocates the population by declared weights, preserves member provenance, deduplicates surfaces, and isolates a member failure unless every member fails. | whatever its member proposers return | Exported; you wire the member proposers. |
66
66
  | `aceProposer` | You are accumulating hard-won lessons into a playbook and must **never** summarize an old lesson away (append-only, provenance-tagged). | playbook string | Exported. |
67
67
  | `memoryCurationProposer` | Same as ACE but you want a compact, deduped, re-ranked memory instead of append-only growth. | memory string | Exported. |
68
- | `evolutionaryProposer` | You want blind population search (mutate → measure → select) with no reflection over findings a cheap control or a baseline to beat. | any string | Exported. |
68
+ | `evolutionaryProposer` | You want blind population search (mutate → measure → select) with no reflection over findings: a cheap control or a baseline to beat. | any string | Exported. |
69
69
  | `traceAnalystProposer` | Bench-only: race our trace-analysis evidence engine head-to-head inside `compareProposers`. | prompt string | Bench-only. |
70
70
  | `haloProposer` | Bench-only: race the external `halo-engine` analysis against ours. | prompt string | Bench-only, external. |
71
71
 
72
72
  Default path: `gepaProposer` for prompts; add `parameterSweepProposer` when a config knob is the suspect; wrap levels in `fapoProposer` when the loop should decide *when* to escalate, or use `compositeProposer` when multiple proposer families must split one fixed population budget.
73
73
 
74
- ## Composing proposers four distinct shapes
74
+ ## Composing proposers: four distinct shapes
75
75
 
76
76
  Choose the shape that matches the experiment:
77
77
 
78
- 1. **Portfolio** `compositeProposer` splits one generation's population across member proposers by fixed weights and returns one provenance-labelled pool.
79
- 2. **Escalate** `fapoProposer` wraps prompt + parameter + structural levels into one proposer and spends on the cheapest level until evidence says to escalate.
80
- 3. **Race** `compareProposers` gives proposers separate loops, then re-scores their winners on one holdout and returns per-proposer lift intervals plus pairwise results.
81
- 4. **Plug in** hand any proposer to `runImprovementLoop({ proposer })`, or use `improve({ surface, generator })` in `@tangle-network/agent-runtime`.
78
+ 1. **Portfolio**: `compositeProposer` splits one generation's population across member proposers by fixed weights and returns one provenance-labelled pool.
79
+ 2. **Escalate**: `fapoProposer` wraps prompt + parameter + structural levels into one proposer and spends on the cheapest level until evidence says to escalate.
80
+ 3. **Race**: `compareProposers` gives proposers separate loops, then re-scores their winners on one holdout and returns per-proposer lift intervals plus pairwise results.
81
+ 4. **Plug in**: hand any proposer to `runImprovementLoop({ proposer })`, or use `improve({ surface, generator })` in `@tangle-network/agent-runtime`.
82
82
 
83
- ### 2 + 4 compose by escalation, then run the improvement loop
83
+ ### 2 + 4: compose by escalation, then run the improvement loop
84
84
 
85
85
  ```ts
86
86
  import {
@@ -110,8 +110,8 @@ const proposer = fapoProposer({
110
110
  }),
111
111
  })
112
112
 
113
- // dispatchWithSurface scores ONE surface on ONE scenario this is the topology-
114
- // opaque seam (one LLM call, one worker, or a whole fleet the loop can't tell).
113
+ // dispatchWithSurface scores ONE surface on ONE scenario: this is the topology-
114
+ // opaque seam (one LLM call, one worker, or a whole fleet: the loop can't tell).
115
115
  const dispatchWithSurface = async (surface, scenario, ctx) =>
116
116
  runYourAgent({ surface, scenario, signal: ctx.signal }) // returns the artifact to judge
117
117
 
@@ -148,7 +148,7 @@ const out = await improve(profile, findings, {
148
148
  if (out.shipped) deploy(out.profile) // out.lift is the held-out winner − baseline
149
149
  ```
150
150
 
151
- ### 3 race proposers head-to-head for a lift CI
151
+ ### 3: race proposers head-to-head for a lift CI
152
152
 
153
153
  ```ts
154
154
  import {
@@ -186,19 +186,19 @@ const comparison = await compareProposers({
186
186
  })
187
187
 
188
188
  comparison.best // highest-lift proposer
189
- comparison.scores // per-proposer { lift, liftCi:{low,high}, cost } low>0 ⇒ real gain
189
+ comparison.scores // per-proposer { lift, liftCi:{low,high}, cost }: low>0 ⇒ real gain
190
190
  comparison.pairwise // best vs each other, paired-bootstrap: 'a' | 'b' | 'tie'
191
191
  ```
192
192
 
193
- Every entrant is re-scored on the **same** holdout with the **same** judges, so the comparison never trusts how a proposer measured itself the only variable is proposal quality.
193
+ Every entrant is re-scored on the **same** holdout with the **same** judges, so the comparison never trusts how a proposer measured itself: the only variable is proposal quality.
194
194
 
195
195
  ## Common traps (they cost the most)
196
196
 
197
- - Do not put eval logic inside a proposer scoring lives in `dispatch` + `judges`, proposing lives in the proposer.
198
- - Do not let a proposer read held-out judge scores `ProposeContext` makes that a compile error on purpose; a proposer that games the acceptance axis is an oracle, not an optimizer.
199
- - Do not read `lift` without `result.power`/MDE a "+4" on a valset too small to detect +4 is noise wearing a number.
197
+ - Do not put eval logic inside a proposer: scoring lives in `dispatch` + `judges`, proposing lives in the proposer.
198
+ - Do not let a proposer read held-out judge scores: `ProposeContext` makes that a compile error on purpose; a proposer that games the acceptance axis is an oracle, not an optimizer.
199
+ - Do not read `lift` without `result.power`/MDE: a "+4" on a valset too small to detect +4 is noise wearing a number.
200
200
  - Do not confuse `compositeProposer` with `fapoProposer`: the former allocates one fixed population across peers, while the latter escalates through ordered levels from cheaper to more structural changes.
201
201
 
202
- ### neutralizationGate the placebo / content-causality control
202
+ ### neutralizationGate: the placebo / content-causality control
203
203
 
204
204
  Standard gates prove a candidate *beat baseline*. `neutralizationGate` proves the candidate's **content** caused the lift, not the extra prompt bytes: it blanks the candidate's added content to byte-length-matched filler, holds everything else fixed, and requires the lift to vanish. A fully-neutralized candidate that still scores is decorative and is rejected. Exports: `neutralizationGate` (`src/campaign/gates/neutralization-gate.ts`), `neutralizeText` (`src/campaign/neutralize.ts`). Since 0.107.0. Pair it with `heldOutGate` in any loop that promotes authored artifacts (prompts, tool docs, knowledge) so a lift that is really just added prompt size cannot be shipped.
@@ -1,11 +1,11 @@
1
- # `InsightReport` the decision packet
1
+ # `InsightReport`: the report
2
2
 
3
3
  The single shape every analysis call returns. `selfImprove()` embeds it in `SelfImproveResult.insight`; `analyzeRuns()` returns it directly. The hosted-tier wire format carries it on `EvalRunEvent.insightReport?`.
4
4
 
5
5
  Use `summarizeExecution({ runs })` when observed traces have no task-quality labels.
6
6
  It returns only `execution` and `costProvenance`, so callers do not need to fabricate a quality score to report runtime facts.
7
7
 
8
- Every section is **opt-in based on what your data supports** the function never invents signal. If your runs don't carry judge scores, `judges` is empty. If there's no baseline/candidate split, `lift` is undefined. The shape is consistent; population is honest.
8
+ Every section is **opt-in based on what your data supports**: the function never invents signal. If your runs don't carry judge scores, `judges` is empty. If there's no baseline/candidate split, `lift` is undefined. The shape is consistent; population is honest.
9
9
 
10
10
  This page walks every section with a real (synthetic) example and explains how to act on it.
11
11
 
@@ -27,13 +27,13 @@ interface InsightReport {
27
27
  contamination?: ContaminationInsight // when canaryScenarios supplied
28
28
  outcomeCorrelation?: OutcomeCorrelationInsight // when outcomeSignal supplied
29
29
  release: ReleaseSummary // always
30
- recommendations: Recommendation[] // always read this FIRST
30
+ recommendations: Recommendation[] // always: read this FIRST
31
31
  }
32
32
  ```
33
33
 
34
34
  ---
35
35
 
36
- ## `execution` runtime facts, separate from quality
36
+ ## `execution`: runtime facts, separate from quality
37
37
 
38
38
  Always present.
39
39
  It reports duration, optional queue time, direct input, output, reasoning, cache-read, and cache-write tokens, model-call coverage, model cohorts, explicit failures, and separately reported orchestration aggregates.
@@ -73,7 +73,7 @@ Cost remains in `costQuality`, where observed, estimated, and uncaptured USD sta
73
73
 
74
74
  ---
75
75
 
76
- ## `n` + `composite` + `perDimension` distributional summary
76
+ ## `n` + `composite` + `perDimension`: distributional summary
77
77
 
78
78
  Always present. The basic "where are my numbers" view.
79
79
 
@@ -99,13 +99,13 @@ Always present. The basic "where are my numbers" view.
99
99
 
100
100
  **Read first:** the `composite.mean`. If it's < 0.5, your agent has a ceiling problem, not a tuning problem.
101
101
 
102
- **Read next:** `perDimension`. If `clarity` is high but `concision` is low, your prompts get the right ideas in too many words different fix than "wrong ideas."
102
+ **Read next:** `perDimension`. If `clarity` is high but `concision` is low, your prompts get the right ideas in too many words: different fix than "wrong ideas."
103
103
 
104
104
  **Use the histogram for:** finding bimodal failure modes. A bin with `count > 0` near zero and another > 0 near 1 means your agent has two distinct behaviors, not one noisy one.
105
105
 
106
106
  ---
107
107
 
108
- ## `costQuality` cost-vs-quality Pareto
108
+ ## `costQuality`: cost-vs-quality Pareto
109
109
 
110
110
  Always present. `cost.histogram` is the per-run cost distribution; `pareto` is the substrate's `ParetoFigureSpec`.
111
111
 
@@ -131,11 +131,11 @@ Always present. `cost.histogram` is the per-run cost distribution; `pareto` is t
131
131
 
132
132
  **Use this when:** comparing prompts, models, or candidate surfaces. The Pareto frontier is your menu of "best you can do at each cost level."
133
133
 
134
- **Render with:** any chart library `points` is plain JSON. Hosted-tier dashboards render this as a scatter with the frontier highlighted.
134
+ **Render with:** any chart library: `points` is plain JSON. Hosted-tier dashboards render this as a scatter with the frontier highlighted.
135
135
 
136
136
  ---
137
137
 
138
- ## `judges` per-judge mean
138
+ ## `judges`: per-judge mean
139
139
 
140
140
  Populated when run records carry `outcome.judgeScores`.
141
141
 
@@ -154,20 +154,23 @@ The substrate's full judge-calibration suite (positional bias, self-preference,
154
154
 
155
155
  ---
156
156
 
157
- ## `interRater` multi-rater agreement + disagreement triage
157
+ ## `interRater`: multi-rater agreement and disagreement review
158
158
 
159
- Populated when `analyzeRuns({ raterScores })` is supplied typically via `fromFeedbackTable()`.
159
+ Populated when `analyzeRuns({ raterScores })` is supplied: typically via `fromFeedbackTable()`.
160
160
 
161
161
  ```jsonc
162
162
  {
163
163
  "interRater": {
164
164
  "raters": 3,
165
165
  "jointlyRated": 30,
166
- "kappa": 0.71,
166
+ "kappa": 0.40,
167
+ "icc": 0.42,
168
+ "pearson": 0.43,
169
+ "spearman": 0.41,
167
170
  "perPair": {
168
- "alice::bob": 0.78,
169
- "alice::carol": 0.65,
170
- "bob::carol": 0.69
171
+ "alice::bob": 0.53,
172
+ "alice::carol": 0.47,
173
+ "bob::carol": 0.19
171
174
  },
172
175
  "disagreementCases": [
173
176
  { "runId": "claim-7", "range": 1.00,
@@ -180,13 +183,15 @@ Populated when `analyzeRuns({ raterScores })` is supplied — typically via `fro
180
183
  }
181
184
  ```
182
185
 
183
- **Read first:** the mean `kappa`. < 0.5 means raters disagree on what "good" looks like — surface the disagreement cases at the next review meeting.
186
+ **Read first:** `kappa` and `icc`, which measure absolute agreement.
187
+ Pearson and Spearman measure correlation and can remain high when raters use different score levels.
188
+ When absolute agreement is low, review the largest disagreement cases before automating the rubric.
184
189
 
185
190
  **Use this when:** building per-rater LLM judges. Each rater's individual scores are the gold signal you calibrate against. Once a calibrated LLM matches the human ≥85%, you can auto-grade and escalate only the disagreement cases.
186
191
 
187
192
  ---
188
193
 
189
- ## `lift` paired-bootstrap statistical lift
194
+ ## `lift`: paired-bootstrap statistical lift
190
195
 
191
196
  Populated when baseline + candidate candidates are present (auto-detected from two distinct `candidateId`s, or explicit via `baselineCandidateId` + `candidateCandidateId`).
192
197
 
@@ -211,13 +216,13 @@ Populated when baseline + candidate candidates are present (auto-detected from t
211
216
  - `ci95[0] ≤ threshold < ci95[1]` → **INCONCLUSIVE.** Expand the corpus or wait for more data.
212
217
  - `ci95[1] ≤ threshold` → **HOLD.** No evidence the candidate is better.
213
218
 
214
- The `recommendations` array surfaces exactly this decision (`kind: 'ship' | 'hold' | 'expand-corpus'`) that's what consumers should read.
219
+ The `recommendations` array surfaces exactly this decision (`kind: 'ship' | 'hold' | 'expand-corpus'`): that's what consumers should read.
215
220
 
216
221
  **Why bootstrap, not t-test alone:** paired bootstrap is distribution-free. Your judge scores are bounded in [0,1] and almost never normal; the bootstrap CI is the honest one.
217
222
 
218
223
  ---
219
224
 
220
- ## `failureClusters` grouped failure modes
225
+ ## `failureClusters`: grouped failure modes
221
226
 
222
227
  Populated when an `AnalystRegistry` is passed via `analyzeRuns({ analyst })`. The substrate runs each failed run through the registered analysts and groups findings by `analyst_id` / `area`.
223
228
 
@@ -245,7 +250,7 @@ Populated when an `AnalystRegistry` is passed via `analyzeRuns({ analyst })`. Th
245
250
 
246
251
  ---
247
252
 
248
- ## `contamination` canary check
253
+ ## `contamination`: canary check
249
254
 
250
255
  Populated when canary scenarios are passed via `analyzeRuns({ canaryScenarios })`. Each canary carries a sentinel string the agent should never emit; the report counts leaks.
251
256
 
@@ -277,7 +282,7 @@ When `leaks > 0`:
277
282
 
278
283
  ---
279
284
 
280
- ## `outcomeCorrelation` closing the loop on real outcomes
285
+ ## `outcomeCorrelation`: closing the loop on real outcomes
281
286
 
282
287
  Populated when `outcomeSignal: { metric, valueByRunId }` is supplied.
283
288
 
@@ -307,9 +312,9 @@ This is the layer that says **"does my judge's taste actually predict the metric
307
312
 
308
313
  ---
309
314
 
310
- ## `release` pass/warn/fail axes
315
+ ## `release`: pass/warn/fail axes
311
316
 
312
- Always present. Roll-up across three axes quality lift, contamination, composite distribution.
317
+ Always present. Roll-up across three axes: quality lift, contamination, composite distribution.
313
318
 
314
319
  ```jsonc
315
320
  {
@@ -334,7 +339,7 @@ Overall `status` is `fail` if any axis fails; `warn` if any warn; `pass` otherwi
334
339
 
335
340
  ---
336
341
 
337
- ## `recommendations` the actionable layer
342
+ ## `recommendations`: the actionable layer
338
343
 
339
344
  Always present. Read this first.
340
345
 
@@ -342,7 +347,7 @@ Always present. Read this first.
342
347
  {
343
348
  "recommendations": [
344
349
  { "priority": "critical", "kind": "ship",
345
- "title": "Ship lift 0.070 (95% CI 0.040..0.100)",
350
+ "title": "Ship: lift 0.070 (95% CI 0.040..0.100)",
346
351
  "detail": "Holdout lift exceeds threshold 0.02 with 95% bootstrap confidence (n=40, p=0.0008, d=0.41).",
347
352
  "evidencePath": "lift" },
348
353
  { "priority": "high", "kind": "investigate",
@@ -357,7 +362,7 @@ Always present. Read this first.
357
362
  |---|---|
358
363
  | `ship` | lift CI lower bound > threshold |
359
364
  | `hold` | lift CI upper bound ≤ threshold |
360
- | `expand-corpus` | lift CI straddles threshold more data needed |
365
+ | `expand-corpus` | lift CI straddles threshold: more data needed |
361
366
  | `fix` | canary contamination detected |
362
367
  | `recalibrate` | inter-rater κ < 0.5, OR outcome correlation < 0.3 |
363
368
  | `investigate` | top failure cluster > some-share |
@@ -378,4 +383,4 @@ Always present. Read this first.
378
383
  | `contamination` | `canaryScenarios` passed in |
379
384
  | `outcomeCorrelation` | `outcomeSignal` passed in |
380
385
 
381
- All sections beyond the always-present ones are `T | undefined`, never empty objects. If a section is missing, your inputs didn't support it the report is honest about that.
386
+ All sections beyond the always-present ones are `T | undefined`, never empty objects. If a section is missing, your inputs didn't support it: the report is honest about that.
@@ -6,7 +6,7 @@
6
6
  > live API; see also [feature-guide.md](./feature-guide.md) and [concepts.md](./concepts.md).
7
7
 
8
8
  `runImprovementLoop` is the public entry for GEPA-style optimization over a whole
9
- task trajectory the thing you improve is not a single model call but an agent
9
+ task trajectory: the thing you improve is not a single model call but an agent
10
10
  system prompt, tool descriptions, a routing policy, or any scaffolding that affects
11
11
  the entire run. It is the OUTER loop: it improves the SURFACE the inner workers run.
12
12
 
@@ -15,13 +15,13 @@ the entire run. It is the OUTER loop: it improves the SURFACE the inner workers
15
15
  You own a few seams; the loop owns the release-critical glue (paired seeds, the
16
16
  held-out re-score, the promotion gate, provenance):
17
17
 
18
- - **`baselineSurface`** the current surface (a prompt string, or a `CodeSurface`).
19
- - **`dispatchWithSurface(surface, scenario, ctx)`** run one task to completion
18
+ - **`baselineSurface`**: the current surface (a prompt string, or a `CodeSurface`).
19
+ - **`dispatchWithSurface(surface, scenario, ctx)`**: run one task to completion
20
20
  under a candidate surface; return the artifact the judges score.
21
- - **`judges`** score the artifact (`{ composite, dimensions }`).
22
- - **`proposer`** proposes candidate surfaces each generation: `gepaProposer`
21
+ - **`judges`**: score the artifact (`{ composite, dimensions }`).
22
+ - **`proposer`**: proposes candidate surfaces each generation: `gepaProposer`
23
23
  (reflective + Pareto frontier) or `evolutionaryProposer` (mutator).
24
- - **`gate`** `defaultProductionGate` (held-out significance + red-team +
24
+ - **`gate`**: `defaultProductionGate` (held-out significance + red-team +
25
25
  reward-hacking + canary). Ships ONLY on a CI-lower-bound held-out lift.
26
26
 
27
27
  ## Minimal example
@@ -36,7 +36,7 @@ import {
36
36
  const result = await runImprovementLoop({
37
37
  baselineSurface: currentSystemPrompt,
38
38
  scenarios: trainScenarios, // optimizer-visible
39
- holdoutScenarios, // DISJOINT only the gate sees these
39
+ holdoutScenarios, // DISJOINT: only the gate sees these
40
40
  dispatchWithSurface: async (surface, scenario) =>
41
41
  runYourAgentToCompletion({ scenario, prompt: String(surface) }),
42
42
  judges: [myJudge],
@@ -64,7 +64,7 @@ if (result.gateResult.decision === 'ship') {
64
64
  paired-bootstrap CI lower bound clears `deltaThreshold`; a few-instance swing at
65
65
  thin `n` is held (`few_runs`), not promoted.
66
66
  - **No-op never ships.** If no candidate beats the baseline, the winner IS the
67
- baseline (empty diff) and the loop forces `hold` it does not score
67
+ baseline (empty diff) and the loop forces `hold`: it does not score
68
68
  baseline-vs-itself and read model noise as lift.
69
69
  - **Provenance falls out.** `result.promotedDiff` + `emitLoopProvenance` give the
70
70
  auditable candidate→gate→promote chain (rationale, content hashes, a held-out lift
@@ -1,4 +1,4 @@
1
- # researchReport methodology
1
+ # researchReport: methodology
2
2
 
3
3
  This document is the methodological brief for `researchReport` (exported from
4
4
  `@tangle-network/agent-eval` and `@tangle-network/agent-eval/reporting`). It
@@ -11,14 +11,14 @@ corresponding function or option so the doc and the code don't drift.
11
11
 
12
12
  ## Inputs
13
13
 
14
- - `runs: RunRecord[]` every record carries `runId`, `candidateId`, `seed`,
14
+ - `runs: RunRecord[]`: every record carries `runId`, `candidateId`, `seed`,
15
15
  `experimentId`, `splitTag`, and an `outcome` with the configured score.
16
- - `comparator: string` the candidate id treated as the null reference. Must
16
+ - `comparator: string`: the candidate id treated as the null reference. Must
17
17
  be selected before data inspection; `preregistrationHash` should pin this.
18
- - `split: 'search' | 'holdout'` defaults to `holdout`. Decisions on `search`
18
+ - `split: 'search' | 'holdout'`: defaults to `holdout`. Decisions on `search`
19
19
  are descriptive only; promotion calls require the holdout.
20
- - `rope: { low, high }` Region of Practical Equivalence on the paired delta,
21
- in score units. Must come from the domain owner there is no
20
+ - `rope: { low, high }`: Region of Practical Equivalence on the paired delta,
21
+ in score units. Must come from the domain owner: there is no
22
22
  statistically-defensible default.
23
23
  - `minPairs` (soft floor, default 20) and `RESEARCH_REPORT_HARD_PAIR_FLOOR`
24
24
  (hard floor, 6). Below the soft floor, the verdict is `needs_more_data` and
@@ -35,7 +35,7 @@ with non-finite scores are dropped before pairing.
35
35
 
36
36
  ## Decision rule
37
37
 
38
- In order first match wins:
38
+ In order: first match wins:
39
39
 
40
40
  1. `comparator` itself → `hold` (baseline).
41
41
  2. No comparator → `hold` if on the cost/quality Pareto frontier, else
@@ -46,7 +46,7 @@ In order — first match wins:
46
46
  "below hard floor" reason. Bootstrap CIs degenerate at this size.
47
47
  5. ROPE configured AND paired-delta CI ⊂ ROPE → `equivalent`.
48
48
  6. Paired-delta CI upper bound < 0 → `reject` (CI excludes a non-negative
49
- effect). Note: this uses **paired delta only** not the marginal mean.
49
+ effect). Note: this uses **paired delta only**: not the marginal mean.
50
50
  7. Paired N < `minPairs` (soft floor) → `needs_more_data` with the MDE at
51
51
  current N attached so the verdict is actionable.
52
52
  8. BH-adjusted q ≤ `fdr` AND CI lower bound > 0 → `promote`. The BH q-value
@@ -71,7 +71,7 @@ The Pr(Δ>0) and Pr(Δ∈ROPE) summaries use the bootstrap-prior duality of
71
71
  [Rubin 1981]: under a non-informative Dirichlet prior, the bootstrap
72
72
  distribution of a sample statistic is its posterior. We expose these as
73
73
  posterior summaries on the **mean** delta and the bootstrap CI on the
74
- **median** delta the median is more robust to the heavy-tailed score
74
+ **median** delta: the median is more robust to the heavy-tailed score
75
75
  distributions seen in agent benchmarks; the mean lets us read off the
76
76
  Bayesian-style probability of superiority in a single number.
77
77