textopt 0.0.0 → 0.1.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 (55) hide show
  1. package/README.md +44 -22
  2. package/dist/bootstrap-search/index.cjs +153 -73
  3. package/dist/bootstrap-search/index.d.cts +32 -10
  4. package/dist/bootstrap-search/index.d.mts +32 -10
  5. package/dist/bootstrap-search/index.mjs +144 -66
  6. package/dist/{demos-B0pVQjYC.d.mts → demos-1b7JiUue.d.mts} +10 -3
  7. package/dist/{demos-BTuzFNsp.d.cts → demos-CU9dy2oT.d.cts} +10 -3
  8. package/dist/demos-D2o0qSSo.cjs +244 -0
  9. package/dist/demos-DE2oxNWX.mjs +215 -0
  10. package/dist/file-cache.cjs +11 -3
  11. package/dist/file-cache.mjs +11 -3
  12. package/dist/gepa/index.cjs +76 -71
  13. package/dist/gepa/index.d.cts +12 -6
  14. package/dist/gepa/index.d.mts +12 -6
  15. package/dist/gepa/index.mjs +49 -46
  16. package/dist/index.cjs +129 -27
  17. package/dist/index.d.cts +145 -7
  18. package/dist/index.d.mts +145 -7
  19. package/dist/index.mjs +113 -15
  20. package/dist/{math-COOofUyv.cjs → math-BhlziRPc.cjs} +60 -9
  21. package/dist/math-Dqme4rYz.mjs +123 -0
  22. package/dist/mipro/index.cjs +98 -70
  23. package/dist/mipro/index.d.cts +17 -14
  24. package/dist/mipro/index.d.mts +17 -14
  25. package/dist/mipro/index.mjs +84 -58
  26. package/dist/opro/index.cjs +130 -51
  27. package/dist/opro/index.d.cts +17 -9
  28. package/dist/opro/index.d.mts +17 -9
  29. package/dist/opro/index.mjs +115 -38
  30. package/dist/{optimizer-B7SpRwl7.d.cts → optimizer-Bh5DPRMH.d.cts} +50 -4
  31. package/dist/{optimizer-DqCoth_w.d.mts → optimizer-Ck6-e_8o.d.mts} +50 -4
  32. package/dist/random-search/index.cjs +93 -49
  33. package/dist/random-search/index.d.cts +15 -13
  34. package/dist/random-search/index.d.mts +15 -13
  35. package/dist/random-search/index.mjs +83 -41
  36. package/dist/{reflection-CQToe-5B.d.cts → reflection-Dt3QrXhM.d.cts} +7 -11
  37. package/dist/{reflection-Cr_upzU0.d.mts → reflection-LRaAZP4e.d.mts} +7 -11
  38. package/dist/{evaluation-OZOp6TB7.cjs → reporting-CNHzbJC-.cjs} +165 -5
  39. package/dist/reporting-DQbAohc9.d.cts +240 -0
  40. package/dist/reporting-DQbAohc9.d.mts +240 -0
  41. package/dist/{evaluation-BV0nSZVx.mjs → reporting-DY-DC4HG.mjs} +124 -6
  42. package/dist/simba/index.cjs +210 -83
  43. package/dist/simba/index.d.cts +32 -11
  44. package/dist/simba/index.d.mts +32 -11
  45. package/dist/simba/index.mjs +200 -75
  46. package/dist/testing.cjs +1 -0
  47. package/dist/testing.d.cts +5 -3
  48. package/dist/testing.d.mts +5 -3
  49. package/dist/testing.mjs +1 -1
  50. package/package.json +4 -3
  51. package/dist/demos-B9BJiNKz.cjs +0 -143
  52. package/dist/demos-Degx6UmP.mjs +0 -126
  53. package/dist/math-DhrDmpFS.mjs +0 -78
  54. package/dist/types-CWv4IQFF.d.cts +0 -129
  55. package/dist/types-CWv4IQFF.d.mts +0 -129
package/dist/index.d.mts CHANGED
@@ -1,8 +1,8 @@
1
- import { a as EvaluationContext, c as RolloutUsage, d as UsageTotals, f as componentNames, i as EvaluationBatch, l as ScoreResult, n as Candidate, o as EvaluationPhase, r as EvaluateArgs, s as EvaluationSplit, t as Adapter, u as TextModel } from "./types-CWv4IQFF.mjs";
1
+ import { _ as TextModel, a as RunFinished, c as Adapter, d as EvaluationBatch, f as EvaluationContext, g as ScoreResult, h as RolloutUsage, i as Reporter, l as Candidate, m as EvaluationSplit, n as OptimizerEvent, o as isCandidateAccepted, p as EvaluationPhase, r as ReportableEvent, s as isRunFinished, t as CandidateAccepted, u as EvaluateArgs, v as UsageTotals, y as componentNames } from "./reporting-DQbAohc9.mjs";
2
2
  import { n as EvaluationCache, r as createMemoryCache, t as CachedScore } from "./cache-CuSo0NJ8.mjs";
3
3
  import { t as Rng } from "./rng-BR5MOedA.mjs";
4
- import { a as formatDemos, i as bootstrapDemos, n as Demo, o as parseDemos, r as DemoRenderer, t as BootstrapResult } from "./demos-B0pVQjYC.mjs";
5
- import { a as EvaluateBatchArgs, c as Evaluator, d as costExhausted, f as createEvaluator, g as createBudget, h as Budget, i as BudgetExhausted, l as RetryPolicy, m as requireMeasuredMean, n as OptimizerResult, o as EvaluateTracedArgs, p as measuredMean, r as OptimizerTask, s as EvaluationEvent, t as Optimizer, u as ScoredBatch } from "./optimizer-DqCoth_w.mjs";
4
+ import { a as harvestFewShotExamples, i as formatDemos, n as Demo, o as parseDemos, r as DemoRenderer, t as BootstrapResult } from "./demos-1b7JiUue.mjs";
5
+ import { a as EvaluateBatchArgs, c as Evaluator, d as costExhausted, f as createEvaluator, g as createBudget, h as Budget, i as BudgetExhausted, l as RetryPolicy, m as requireMeasuredMean, n as OptimizerResult, o as EvaluateTracedArgs, p as measuredMean, r as OptimizerTask, s as EvaluationEvent, t as Optimizer, u as ScoredBatch } from "./optimizer-Ck6-e_8o.mjs";
6
6
  import { t as BatchSampler } from "./sampling-DFo_7RNJ.mjs";
7
7
  //#region src/checkpoint.d.ts
8
8
  /**
@@ -46,6 +46,10 @@ interface ComparisonRun {
46
46
  bestScore: number;
47
47
  testScore?: number;
48
48
  metricCalls: number;
49
+ /** Rollouts this run got from the cache instead of paying for. */
50
+ cacheHits: number;
51
+ /** Calls to a proposal or reflection model, outside the metric budget. */
52
+ reflectionCalls: number;
49
53
  costUsd: number;
50
54
  stopReason: string;
51
55
  }
@@ -58,12 +62,33 @@ interface ComparisonSummary {
58
62
  maxScore: number;
59
63
  meanMetricCalls: number;
60
64
  meanCostUsd: number;
65
+ meanCacheHits: number;
66
+ meanReflectionCalls: number;
67
+ /**
68
+ * How many distinct values this entrant's score took across its seeds.
69
+ * 1 means every seed landed on the same number — the seed changed nothing
70
+ * about the outcome, whatever the search did internally with it.
71
+ */
72
+ distinctScores: number;
61
73
  /**
62
74
  * How often the winner's margin over this entrant would arise if the two
63
- * were equally good and each seed's outcome were a coin flip. Absent for the
64
- * winner itself.
75
+ * were equally good and each seed's outcome were a coin flip. Absent for
76
+ * the winner itself, and also absent when every seed produced the exact
77
+ * same margin: a sign-flip test over n seeds is answering a question about
78
+ * n independent trials, and identical margins mean the seed never actually
79
+ * put that to the test — there was one realization, repeated. Reporting a
80
+ * p-value there would state a precision (as fine as 2^-n) that the run
81
+ * never earned, so it is withheld rather than printed misleadingly small.
65
82
  */
66
83
  pValueVsWinner?: number;
84
+ /**
85
+ * `pValueVsWinner` after Holm-Bonferroni step-down across the other
86
+ * entrants in this same `compare()` call — the family the raw p-value
87
+ * would otherwise be read against in isolation. Absent wherever the raw
88
+ * p-value is: a withheld comparison has nothing to adjust, but it still
89
+ * occupies a slot in the family the other comparisons are corrected for.
90
+ */
91
+ pValueVsWinnerHolm?: number;
67
92
  }
68
93
  interface Comparison {
69
94
  /** Highest mean score. Read `pValueVsWinner` before believing it. */
@@ -85,7 +110,7 @@ interface Comparison {
85
110
  * seed is constructor config and every optimizer here is deterministic given
86
111
  * one — comparing two entrants at a single seed compares two anecdotes. It also
87
112
  * leaves the task where the caller builds it, which is the only place the
88
- * optimizer-specific parts of it (`reflect`, `cache`, `onEvent`) are known.
113
+ * optimizer-specific parts of it (`reflect`, `cache`, `reporters`) are known.
89
114
  */
90
115
  declare function compare<K extends string, Output = unknown>(args: {
91
116
  entrants: Record<string, (args: {
@@ -136,6 +161,119 @@ declare function createDeadline(args: {
136
161
  now?: () => number;
137
162
  }): Deadline;
138
163
  //#endregion
164
+ //#region src/harvest.d.ts
165
+ /**
166
+ * One rollout worth keeping: what went in, what the system produced, and how
167
+ * that output scored. Harvested, never written — the value of the pair is that
168
+ * the system actually produced the output and the metric actually rewarded it.
169
+ */
170
+ interface Rollout<Datum = unknown, Output = unknown> {
171
+ input: Datum;
172
+ output: Output;
173
+ score: number;
174
+ }
175
+ interface HarvestResult<Datum, Output> {
176
+ rollouts: Rollout<Datum, Output>[];
177
+ /** Rollouts this cost. Harvesting is cheap, not free. */
178
+ metricCalls: number;
179
+ /**
180
+ * Tokens and dollars this cost. Harvesting runs on its own evaluator, so a
181
+ * caller that bounds spend has to fold these into its own totals.
182
+ */
183
+ usage: UsageTotals;
184
+ /** Instances run, including the ones the metric did not reward. */
185
+ attempted: number;
186
+ }
187
+ /**
188
+ * Run a candidate over data and keep the rollouts the metric rewarded.
189
+ *
190
+ * The library's one paid collection primitive, with two consumers: a few-shot
191
+ * block wants four of these, and a distillation set wants thousands. Both are
192
+ * the same pass — run the candidate, score it, keep what cleared the bar — so
193
+ * both share the budget, retry and transient-failure handling that pass needs.
194
+ *
195
+ * Which data to sweep is the caller's decision and the consequential one. A
196
+ * validation set is the wrong choice: it is the set that selected the candidate,
197
+ * so the rollouts it yields are enriched for the candidate's fit to those
198
+ * instances rather than to the task. Prefer the training set, or a pool held
199
+ * out of the run entirely.
200
+ */
201
+ declare function harvestRollouts<Datum, Trajectory, Output, K extends string = string>(args: {
202
+ adapter: Adapter<Datum, Trajectory, Output, K>;
203
+ /** The candidate to run. Usually a run's winner, sometimes the seed. */
204
+ candidate: Candidate<K>;
205
+ data: readonly Datum[];
206
+ /**
207
+ * Score a rollout must reach to be kept. Unset keeps every rollout the
208
+ * metric rewarded at all, which is what MIPROv2's bootstrapper does without
209
+ * a `metric_threshold`: it keeps a trace on any truthy score and only
210
+ * compares against a number once one is configured.
211
+ *
212
+ * Demanding a perfect score instead is the right call for a boolean metric
213
+ * and the wrong one for a graded metric, where it throws away every rollout
214
+ * that was most of the way there — which on a hard task is all of them.
215
+ */
216
+ minScore?: number;
217
+ /** Rollouts to collect before stopping. Unset sweeps the whole pool. */
218
+ maxRollouts?: number;
219
+ /**
220
+ * Instances per rollout batch. Smaller batches stop closer to the moment
221
+ * enough rollouts exist, at the cost of less concurrency inside the adapter.
222
+ */
223
+ batchSize?: number;
224
+ /** Ceiling on rollouts run. Defaults to one pass over `data`. */
225
+ maxMetricCalls?: number;
226
+ /**
227
+ * Ceiling on dollars this pass may spend, checked between batches. Rollout
228
+ * counts are a poor proxy for spend, and a caller bounding dollars cannot
229
+ * bound this pass from outside: it runs on its own evaluator.
230
+ */
231
+ maxCostUsd?: number;
232
+ /** Shuffles `data` first, so rollouts are not all drawn from its head. */
233
+ rng?: Rng;
234
+ signal?: AbortSignal;
235
+ }): Promise<HarvestResult<Datum, Output>>;
236
+ //#endregion
237
+ //#region src/distillation.d.ts
238
+ /** A turn in a training example, in the shape trainers read. */
239
+ interface ChatMessage {
240
+ role: "system" | "user" | "assistant";
241
+ content: string;
242
+ }
243
+ interface TrainingExample {
244
+ messages: ChatMessage[];
245
+ }
246
+ /**
247
+ * Turns one harvested rollout into the example to train on, or `null` to skip
248
+ * it. The callback exists because only the caller knows how to render a
249
+ * `Datum` — the library never sees inside one.
250
+ *
251
+ * It is also where the consequential decision lives: how much of the optimized
252
+ * candidate to leave in the input. Dropping it entirely moves the whole prompt
253
+ * into weights and gives up the ability to steer the model with text
254
+ * afterwards; keeping a short task statement distills away only the tokens the
255
+ * search accreted. See `docs/distillation.md`.
256
+ */
257
+ type TrainingExampleRenderer<Datum, Output> = (args: {
258
+ rollout: Rollout<Datum, Output>;
259
+ index: number;
260
+ }) => TrainingExample | null;
261
+ /**
262
+ * Serialize harvested rollouts as JSONL, one training example per line.
263
+ *
264
+ * The chat-messages shape rather than any one vendor's: it is what Axolotl,
265
+ * Together, Fireworks and the Hugging Face trainers all ingest, and the
266
+ * providers that read it outlive the ones that do not.
267
+ *
268
+ * Returns the text rather than writing it. Only `file-cache` touches the
269
+ * filesystem, and a caller uploading this straight to a provider should not
270
+ * have to round-trip it through a file to do so.
271
+ */
272
+ declare function toTrainingJsonl<Datum, Output>(args: {
273
+ rollouts: readonly Rollout<Datum, Output>[];
274
+ render: TrainingExampleRenderer<Datum, Output>;
275
+ }): string;
276
+ //#endregion
139
277
  //#region src/judge.d.ts
140
278
  /** One thing the judge grades, and what a perfect answer looks like for it. */
141
279
  interface JudgeCriterion {
@@ -218,4 +356,4 @@ declare function priceUsage(args: {
218
356
  pricing?: TokenPricing;
219
357
  }): RolloutUsage;
220
358
  //#endregion
221
- export { type Adapter, type BatchSampler, type BootstrapResult, type Budget, BudgetExhausted, type CachedScore, type Candidate, type Comparison, type ComparisonRun, type ComparisonSummary, type Deadline, type Demo, type DemoRenderer, type EvaluateArgs, type EvaluateBatchArgs, type EvaluateTracedArgs, type EvaluationBatch, type EvaluationCache, type EvaluationContext, type EvaluationEvent, type EvaluationPhase, type EvaluationSplit, type Evaluator, type Judge, type JudgeCriterion, type JudgePromptBuilder, type Optimizer, type OptimizerResult, type OptimizerTask, type RetryPolicy, type Rng, type RolloutUsage, type ScoreResult, type ScoredBatch, type TextModel, type TokenPricing, type UsageTotals, assertResumable, bootstrapDemos, buildJudgePrompt, candidateFingerprint, compare, componentNames, costExhausted, createBudget, createDeadline, createEvaluator, createJudge, createMemoryCache, formatDemos, mapWithConcurrency, measuredMean, parseDemos, parseProposedText, priceUsage, requireMeasuredMean, runFingerprint };
359
+ export { type Adapter, type BatchSampler, type BootstrapResult, type Budget, BudgetExhausted, type CachedScore, type Candidate, type CandidateAccepted, type ChatMessage, type Comparison, type ComparisonRun, type ComparisonSummary, type Deadline, type Demo, type DemoRenderer, type EvaluateArgs, type EvaluateBatchArgs, type EvaluateTracedArgs, type EvaluationBatch, type EvaluationCache, type EvaluationContext, type EvaluationEvent, type EvaluationPhase, type EvaluationSplit, type Evaluator, type HarvestResult, type Judge, type JudgeCriterion, type JudgePromptBuilder, type Optimizer, type OptimizerEvent, type OptimizerResult, type OptimizerTask, type ReportableEvent, type Reporter, type RetryPolicy, type Rng, type Rollout, type RolloutUsage, type RunFinished, type ScoreResult, type ScoredBatch, type TextModel, type TokenPricing, type TrainingExample, type TrainingExampleRenderer, type UsageTotals, assertResumable, buildJudgePrompt, candidateFingerprint, compare, componentNames, costExhausted, createBudget, createDeadline, createEvaluator, createJudge, createMemoryCache, formatDemos, harvestFewShotExamples, harvestRollouts, isCandidateAccepted, isRunFinished, mapWithConcurrency, measuredMean, parseDemos, parseProposedText, priceUsage, requireMeasuredMean, runFingerprint, toTrainingJsonl };
package/dist/index.mjs CHANGED
@@ -1,10 +1,12 @@
1
- import { a as requireMeasuredMean, c as candidateFingerprint, d as createMemoryCache, i as measuredMean, l as runFingerprint, n as costExhausted, o as createDeadline, p as createBudget, r as createEvaluator, s as assertResumable, t as BudgetExhausted, u as componentNames } from "./evaluation-BV0nSZVx.mjs";
1
+ import { _ as createMemoryCache, a as isRunFinished, b as createBudget, c as createEvaluator, d as createDeadline, f as assertResumable, h as componentNames, i as isCandidateAccepted, l as measuredMean, m as runFingerprint, o as BudgetExhausted, p as candidateFingerprint, s as costExhausted, u as requireMeasuredMean } from "./reporting-DY-DC4HG.mjs";
2
2
  import { t as mapWithConcurrency } from "./concurrency-D58PWeSk.mjs";
3
- import { r as signFlipPValue } from "./math-DhrDmpFS.mjs";
4
- import { n as formatDemos, r as parseDemos, t as bootstrapDemos } from "./demos-Degx6UmP.mjs";
3
+ import { i as signFlipPValue, n as holmAdjust } from "./math-Dqme4rYz.mjs";
4
+ import { a as harvestRollouts, n as harvestFewShotExamples, r as parseDemos, t as formatDemos } from "./demos-DE2oxNWX.mjs";
5
5
  import { t as parseProposedText } from "./text--v4Ffbus.mjs";
6
6
  //#region src/compare.ts
7
- const EXACT_LIMIT = 16;
7
+ const EXACT_LIMIT = 20;
8
+ /** Well above float subtraction noise (~1e-16), well below a real margin. */
9
+ const DEGENERACY_TOLERANCE = 1e-9;
8
10
  /**
9
11
  * Run several optimizers over the same seeds and report which one actually won.
10
12
  *
@@ -19,7 +21,7 @@ const EXACT_LIMIT = 16;
19
21
  * seed is constructor config and every optimizer here is deterministic given
20
22
  * one — comparing two entrants at a single seed compares two anecdotes. It also
21
23
  * leaves the task where the caller builds it, which is the only place the
22
- * optimizer-specific parts of it (`reflect`, `cache`, `onEvent`) are known.
24
+ * optimizer-specific parts of it (`reflect`, `cache`, `reporters`) are known.
23
25
  */
24
26
  async function compare(args) {
25
27
  const { entrants, seeds, concurrency = 1 } = args;
@@ -42,6 +44,8 @@ async function compare(args) {
42
44
  bestScore: result.bestScore,
43
45
  ...result.testScore === void 0 ? {} : { testScore: result.testScore },
44
46
  metricCalls: result.metricCalls,
47
+ cacheHits: result.cacheHits,
48
+ reflectionCalls: result.reflectionCalls ?? 0,
45
49
  costUsd: result.usage.costUsd,
46
50
  stopReason: result.stopReason
47
51
  };
@@ -52,17 +56,25 @@ async function compare(args) {
52
56
  runs: runs.filter((run) => run.entrant === entrant)
53
57
  }));
54
58
  const winner = summaries.reduce((best, summary) => summary.meanScore > best.meanScore ? summary : best);
59
+ const rawPValues = summaries.map((summary) => summary.entrant === winner.entrant ? void 0 : margin({
60
+ winner: winner.entrant,
61
+ entrant: summary.entrant,
62
+ runs,
63
+ seeds
64
+ }));
65
+ const holmAdjusted = holmAdjustSparse({
66
+ pValues: rawPValues,
67
+ familySize: names.length - 1
68
+ });
55
69
  return {
56
70
  winner: winner.entrant,
57
- summaries: summaries.map((summary) => summary.entrant === winner.entrant ? summary : {
71
+ summaries: summaries.map((summary, index) => ({
58
72
  ...summary,
59
- pValueVsWinner: margin({
60
- winner: winner.entrant,
61
- entrant: summary.entrant,
62
- runs,
63
- seeds
64
- })
65
- }),
73
+ ...rawPValues[index] === void 0 ? {} : {
74
+ pValueVsWinner: rawPValues[index],
75
+ pValueVsWinnerHolm: holmAdjusted[index]
76
+ }
77
+ })),
66
78
  runs
67
79
  };
68
80
  }
@@ -77,13 +89,28 @@ function summarize(args) {
77
89
  minScore: Math.min(...scores),
78
90
  maxScore: Math.max(...scores),
79
91
  meanMetricCalls: mean(runs.map((run) => run.metricCalls)),
80
- meanCostUsd: mean(runs.map((run) => run.costUsd))
92
+ meanCostUsd: mean(runs.map((run) => run.costUsd)),
93
+ meanCacheHits: mean(runs.map((run) => run.cacheHits)),
94
+ meanReflectionCalls: mean(runs.map((run) => run.reflectionCalls)),
95
+ distinctScores: new Set(scores).size
81
96
  };
82
97
  }
83
98
  /**
84
99
  * Paired across seeds rather than pooled: the same seed puts both entrants on
85
100
  * the same sampling order, so the difference at a seed is a comparison and the
86
101
  * spread between seeds is not.
102
+ *
103
+ * Returns `undefined` when every seed produced the exact same nonzero margin.
104
+ * That is not evidence of an n-seed-strong result — it is one realization the
105
+ * seed never varied, and a sign-flip p-value would report a precision from n
106
+ * independent trials that never happened. A margin of exactly zero every seed
107
+ * is not this case: `signFlipPValue` already reports that honestly as 1, no
108
+ * significance claimed either way, which is not a fabricated number.
109
+ *
110
+ * "Exact same" is judged within `DEGENERACY_TOLERANCE`, not `===`: subtracting
111
+ * two scores that are equal in substance can still land a few ULPs apart
112
+ * (0.95 - 0.55 and 0.9 - 0.5 differ at the 16th digit), and treating that as
113
+ * n real trials would be the same fabrication this check exists to prevent.
87
114
  */
88
115
  function margin(args) {
89
116
  const { winner, entrant, runs, seeds } = args;
@@ -98,6 +125,10 @@ function margin(args) {
98
125
  seed
99
126
  });
100
127
  });
128
+ const [first] = differences;
129
+ if (first !== void 0) {
130
+ if (differences.reduce((widest, difference) => Math.max(widest, Math.abs(difference - first)), 0) < DEGENERACY_TOLERANCE && Math.abs(first) > DEGENERACY_TOLERANCE) return;
131
+ }
101
132
  return signFlipPValue({
102
133
  differences,
103
134
  observed: differences.reduce((total, value) => total + value, 0),
@@ -108,6 +139,27 @@ function scoreOf(args) {
108
139
  const { runs, entrant, seed } = args;
109
140
  return runs.find((candidate) => candidate.entrant === entrant && candidate.seed === seed)?.score ?? 0;
110
141
  }
142
+ /**
143
+ * `holmAdjust` over the raw p-values that exist, skipping the slots a
144
+ * withheld comparison left `undefined` — those still count toward
145
+ * `familySize`, they just have nothing of their own to adjust.
146
+ */
147
+ function holmAdjustSparse(args) {
148
+ const { pValues, familySize } = args;
149
+ const present = pValues.map((pValue, index) => ({
150
+ pValue,
151
+ index
152
+ })).filter((entry) => entry.pValue !== void 0);
153
+ const adjusted = holmAdjust({
154
+ pValues: present.map((entry) => entry.pValue),
155
+ familySize
156
+ });
157
+ const result = new Array(pValues.length).fill(void 0);
158
+ present.forEach((entry, rank) => {
159
+ result[entry.index] = adjusted[rank];
160
+ });
161
+ return result;
162
+ }
111
163
  function mean(values) {
112
164
  return values.reduce((total, value) => total + value, 0) / values.length;
113
165
  }
@@ -118,6 +170,51 @@ function standardDeviation(values) {
118
170
  return Math.sqrt(variance);
119
171
  }
120
172
  //#endregion
173
+ //#region src/distillation.ts
174
+ const ROLES = /* @__PURE__ */ new Set([
175
+ "system",
176
+ "user",
177
+ "assistant"
178
+ ]);
179
+ /**
180
+ * Serialize harvested rollouts as JSONL, one training example per line.
181
+ *
182
+ * The chat-messages shape rather than any one vendor's: it is what Axolotl,
183
+ * Together, Fireworks and the Hugging Face trainers all ingest, and the
184
+ * providers that read it outlive the ones that do not.
185
+ *
186
+ * Returns the text rather than writing it. Only `file-cache` touches the
187
+ * filesystem, and a caller uploading this straight to a provider should not
188
+ * have to round-trip it through a file to do so.
189
+ */
190
+ function toTrainingJsonl(args) {
191
+ const { rollouts, render } = args;
192
+ const lines = [];
193
+ for (const [index, rollout] of rollouts.entries()) {
194
+ const example = render({
195
+ rollout,
196
+ index
197
+ });
198
+ if (example === null) continue;
199
+ assertUsable(example, index);
200
+ lines.push(JSON.stringify(example));
201
+ }
202
+ return lines.join("\n");
203
+ }
204
+ /**
205
+ * Checked here rather than left to the provider. A malformed line surfaces as
206
+ * a rejected upload hours later, naming a line number in a file the caller
207
+ * never wrote by hand; naming the rollout at the point it was rendered is the
208
+ * same error while it is still fixable.
209
+ */
210
+ function assertUsable(example, index) {
211
+ if (!Array.isArray(example.messages) || example.messages.length === 0) throw new Error(`rollout ${index} rendered no messages`);
212
+ for (const message of example.messages) {
213
+ if (!ROLES.has(message.role)) throw new Error(`rollout ${index} rendered the unknown role ${message.role}`);
214
+ if (typeof message.content !== "string") throw new Error(`rollout ${index} rendered non-text content`);
215
+ }
216
+ }
217
+ //#endregion
121
218
  //#region src/judge.ts
122
219
  const DEFAULT_SCALE = 5;
123
220
  const SCORE = /<score\s+name="([^"]+)"\s*>\s*([\d.]+)\s*<\/score>/g;
@@ -140,6 +237,7 @@ const FEEDBACK = /<feedback>([\s\S]*?)<\/feedback>/;
140
237
  function createJudge(args) {
141
238
  const { model, criteria, scale = DEFAULT_SCALE, renderInput = stringify, renderOutput = stringify, buildPrompt = buildJudgePrompt } = args;
142
239
  if (criteria.length === 0) throw new Error("createJudge requires at least one criterion");
240
+ if (!Number.isFinite(scale) || scale <= 0) throw new Error(`scale must be a positive number, received ${scale}`);
143
241
  return async ({ input, output, expected, signal }) => {
144
242
  return readVerdict({
145
243
  response: await model({
@@ -242,4 +340,4 @@ function priceUsage(args) {
242
340
  };
243
341
  }
244
342
  //#endregion
245
- export { BudgetExhausted, assertResumable, bootstrapDemos, buildJudgePrompt, candidateFingerprint, compare, componentNames, costExhausted, createBudget, createDeadline, createEvaluator, createJudge, createMemoryCache, formatDemos, mapWithConcurrency, measuredMean, parseDemos, parseProposedText, priceUsage, requireMeasuredMean, runFingerprint };
343
+ export { BudgetExhausted, assertResumable, buildJudgePrompt, candidateFingerprint, compare, componentNames, costExhausted, createBudget, createDeadline, createEvaluator, createJudge, createMemoryCache, formatDemos, harvestFewShotExamples, harvestRollouts, isCandidateAccepted, isRunFinished, mapWithConcurrency, measuredMean, parseDemos, parseProposedText, priceUsage, requireMeasuredMean, runFingerprint, toTrainingJsonl };
@@ -44,17 +44,62 @@ function signFlipPValue(args) {
44
44
  differences: moved,
45
45
  observed
46
46
  });
47
- const assignments = 2 ** moved.length;
48
- let atLeastAsExtreme = 0;
49
- for (let mask = 0; mask < assignments; mask += 1) {
50
- let total = 0;
51
- for (let index = 0; index < moved.length; index += 1) {
52
- const sign = mask >> index & 1 ? -1 : 1;
53
- total += sign * moved[index];
47
+ const sums = achievableSums(moved);
48
+ return sums.filter((total) => total >= observed).length / sums.length;
49
+ }
50
+ /**
51
+ * Holm-Bonferroni step-down adjustment: the p-value each comparison would need
52
+ * to survive on its own if the whole family were held to one error rate,
53
+ * rather than letting the smallest of several tests read as significant by
54
+ * volume alone. Sorted ascending, each rank is scaled by how many comparisons
55
+ * are still in contention at that rank, and the running maximum keeps a later,
56
+ * less-scaled rank from reporting looser than an earlier one already has.
57
+ *
58
+ * `familySize` may exceed `pValues.length`: a comparison `signFlipPValue`
59
+ * could not test at all (every paired difference identical, so no p reflects
60
+ * a real margin) is still a member of the family being controlled for, and
61
+ * excluding it from the denominator would understate the correction owed to
62
+ * the comparisons that could be tested.
63
+ */
64
+ function holmAdjust(args) {
65
+ const { pValues, familySize } = args;
66
+ const ranked = pValues.map((pValue, index) => ({
67
+ pValue,
68
+ index
69
+ })).sort((a, b) => a.pValue - b.pValue);
70
+ const adjusted = new Array(pValues.length);
71
+ let runningMax = 0;
72
+ ranked.forEach(({ pValue, index }, rank) => {
73
+ runningMax = Math.max(runningMax, Math.min(1, pValue * (familySize - rank)));
74
+ adjusted[index] = runningMax;
75
+ });
76
+ return adjusted;
77
+ }
78
+ /**
79
+ * Every total reachable by flipping some subset of `differences`' signs,
80
+ * built by doubling rather than by scoring each of the 2^n sign masks
81
+ * independently: after `k` differences there are 2^k sums, and folding in
82
+ * difference `k+1` only ever adds or subtracts it from each of them, so the
83
+ * whole enumeration costs O(2^n) instead of the O(2^n * n) a per-mask loop
84
+ * pays for re-summing n terms every time. Measured at n=20, twenty seeds
85
+ * being the bench's ceiling: about 63ms scoring masks one at a time against
86
+ * about 16ms building sums this way — the difference between affording exact
87
+ * enumeration through a full twenty-seed run and falling back to the normal
88
+ * approximation, which is what raising `EXACT_LIMIT` in compare.ts to 20
89
+ * relies on.
90
+ */
91
+ function achievableSums(differences) {
92
+ let sums = [0];
93
+ for (const difference of differences) {
94
+ const next = new Array(sums.length * 2);
95
+ for (let index = 0; index < sums.length; index += 1) {
96
+ const total = sums[index];
97
+ next[index] = total + difference;
98
+ next[index + sums.length] = total - difference;
54
99
  }
55
- if (total >= observed) atLeastAsExtreme += 1;
100
+ sums = next;
56
101
  }
57
- return atLeastAsExtreme / assignments;
102
+ return sums;
58
103
  }
59
104
  /**
60
105
  * The same tail probability from a normal approximation, for batches too large
@@ -81,6 +126,12 @@ Object.defineProperty(exports, "argmax", {
81
126
  return argmax;
82
127
  }
83
128
  });
129
+ Object.defineProperty(exports, "holmAdjust", {
130
+ enumerable: true,
131
+ get: function() {
132
+ return holmAdjust;
133
+ }
134
+ });
84
135
  Object.defineProperty(exports, "mean", {
85
136
  enumerable: true,
86
137
  get: function() {
@@ -0,0 +1,123 @@
1
+ //#region src/math.ts
2
+ function argmax(values) {
3
+ let bestIndex = 0;
4
+ let bestValue = Number.NEGATIVE_INFINITY;
5
+ for (let index = 0; index < values.length; index += 1) {
6
+ const value = values[index];
7
+ if (value > bestValue) {
8
+ bestValue = value;
9
+ bestIndex = index;
10
+ }
11
+ }
12
+ return bestIndex;
13
+ }
14
+ /** Mean over the values that exist; unscored instances are not zeros. */
15
+ function mean(values) {
16
+ let total = 0;
17
+ let count = 0;
18
+ for (const value of values) if (value !== void 0) {
19
+ total += value;
20
+ count += 1;
21
+ }
22
+ return count === 0 ? 0 : total / count;
23
+ }
24
+ function sum(values) {
25
+ let total = 0;
26
+ for (const value of values) total += value;
27
+ return total;
28
+ }
29
+ /**
30
+ * One-sided p-value from a paired sign-flip (permutation) test: how often a
31
+ * total this large arises when each difference is equally likely to have come
32
+ * out the other way. Exact by enumeration for small samples, normal-approximated
33
+ * past `maxExact` where 2^n stops being cheap.
34
+ *
35
+ * Paired and distribution-free, which is what a comparison of two runs over the
36
+ * same instances needs — the scores are not independent draws and are not
37
+ * normal, so a t-test on them is answering a different question.
38
+ */
39
+ function signFlipPValue(args) {
40
+ const { differences, observed, maxExact } = args;
41
+ const moved = differences.filter((difference) => difference !== 0);
42
+ if (moved.length === 0) return 1;
43
+ if (moved.length > maxExact) return normalTailProbability({
44
+ differences: moved,
45
+ observed
46
+ });
47
+ const sums = achievableSums(moved);
48
+ return sums.filter((total) => total >= observed).length / sums.length;
49
+ }
50
+ /**
51
+ * Holm-Bonferroni step-down adjustment: the p-value each comparison would need
52
+ * to survive on its own if the whole family were held to one error rate,
53
+ * rather than letting the smallest of several tests read as significant by
54
+ * volume alone. Sorted ascending, each rank is scaled by how many comparisons
55
+ * are still in contention at that rank, and the running maximum keeps a later,
56
+ * less-scaled rank from reporting looser than an earlier one already has.
57
+ *
58
+ * `familySize` may exceed `pValues.length`: a comparison `signFlipPValue`
59
+ * could not test at all (every paired difference identical, so no p reflects
60
+ * a real margin) is still a member of the family being controlled for, and
61
+ * excluding it from the denominator would understate the correction owed to
62
+ * the comparisons that could be tested.
63
+ */
64
+ function holmAdjust(args) {
65
+ const { pValues, familySize } = args;
66
+ const ranked = pValues.map((pValue, index) => ({
67
+ pValue,
68
+ index
69
+ })).sort((a, b) => a.pValue - b.pValue);
70
+ const adjusted = new Array(pValues.length);
71
+ let runningMax = 0;
72
+ ranked.forEach(({ pValue, index }, rank) => {
73
+ runningMax = Math.max(runningMax, Math.min(1, pValue * (familySize - rank)));
74
+ adjusted[index] = runningMax;
75
+ });
76
+ return adjusted;
77
+ }
78
+ /**
79
+ * Every total reachable by flipping some subset of `differences`' signs,
80
+ * built by doubling rather than by scoring each of the 2^n sign masks
81
+ * independently: after `k` differences there are 2^k sums, and folding in
82
+ * difference `k+1` only ever adds or subtracts it from each of them, so the
83
+ * whole enumeration costs O(2^n) instead of the O(2^n * n) a per-mask loop
84
+ * pays for re-summing n terms every time. Measured at n=20, twenty seeds
85
+ * being the bench's ceiling: about 63ms scoring masks one at a time against
86
+ * about 16ms building sums this way — the difference between affording exact
87
+ * enumeration through a full twenty-seed run and falling back to the normal
88
+ * approximation, which is what raising `EXACT_LIMIT` in compare.ts to 20
89
+ * relies on.
90
+ */
91
+ function achievableSums(differences) {
92
+ let sums = [0];
93
+ for (const difference of differences) {
94
+ const next = new Array(sums.length * 2);
95
+ for (let index = 0; index < sums.length; index += 1) {
96
+ const total = sums[index];
97
+ next[index] = total + difference;
98
+ next[index + sums.length] = total - difference;
99
+ }
100
+ sums = next;
101
+ }
102
+ return sums;
103
+ }
104
+ /**
105
+ * The same tail probability from a normal approximation, for batches too large
106
+ * to enumerate. Under the sign-flip null each difference has mean zero and
107
+ * variance equal to its square, so the total's variance is their sum.
108
+ */
109
+ function normalTailProbability(args) {
110
+ const { differences, observed } = args;
111
+ const variance = differences.reduce((total, difference) => total + difference * difference, 0);
112
+ if (variance === 0) return observed > 0 ? 0 : 1;
113
+ return 1 - standardNormalCdf(observed / Math.sqrt(variance));
114
+ }
115
+ /** Abramowitz and Stegun 7.1.26, which is accurate to about 1e-7. */
116
+ function standardNormalCdf(value) {
117
+ const sign = value < 0 ? -1 : 1;
118
+ const scaled = Math.abs(value) / Math.SQRT2;
119
+ const t = 1 / (1 + .3275911 * scaled);
120
+ return .5 * (1 + sign * (1 - t * (.254829592 + t * (-.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429)))) * Math.exp(-scaled * scaled)));
121
+ }
122
+ //#endregion
123
+ export { sum as a, signFlipPValue as i, holmAdjust as n, mean as r, argmax as t };