@hmharness/evolution 0.14.14 → 0.14.16

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.
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export * from './readiness.ts';
7
7
  export * from './labels.ts';
8
8
  export * from './reward-model.ts';
9
9
  export * from './judge.ts';
10
+ export * from './sequential.ts';
10
11
  export * from './insights.ts';
11
12
  export * from './skills.ts';
12
13
  export * from './bench.ts';
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export * from "./readiness.js";
7
7
  export * from "./labels.js";
8
8
  export * from "./reward-model.js";
9
9
  export * from "./judge.js";
10
+ export * from "./sequential.js";
10
11
  export * from "./insights.js";
11
12
  export * from "./skills.js";
12
13
  export * from "./bench.js";
@@ -64,11 +64,19 @@ export declare function exportDpoPairs(labels: HumanLabel[], insights: InsightLi
64
64
  crossBucketPrefix?: number;
65
65
  sources?: Map<string, 'human' | 'judge'>;
66
66
  }): DpoPair[];
67
- /** Deterministic ~80/20 train/eval split by pair identity. Pure. */
67
+ /** Deterministic ~80/20 split BY PROMPT (group split): bench templates
68
+ * mint many pairs per prompt, and a pair-level split leaked ~4.9k eval
69
+ * prompts into train (audit 2026-09-21) - every pair of one prompt must
70
+ * land on the same side or offline metrics are inflated. Pure. */
68
71
  export declare function splitDpoPairs(pairs: DpoPair[], salt?: string): {
69
72
  train: DpoPair[];
70
73
  eval: DpoPair[];
71
74
  };
75
+ /** Exact-duplicate removal (same prompt+chosen+rejected): the 5-star army
76
+ * against the same few degraded sessions minted 11k+ permutation copies
77
+ * (audit 2026-09-21) - training would just over-weight those templates.
78
+ * Keeps the first occurrence (deterministic). Pure. */
79
+ export declare function dedupeDpoPairs(pairs: DpoPair[]): DpoPair[];
72
80
  /** Leakage/quality audit before training consumes the pairs: empty sides,
73
81
  * exact duplicates, and eval pairs whose prompt ALSO appears in train
74
82
  * (prompt leakage inflates offline metrics). Pure. */
@@ -202,15 +202,34 @@ function fnv1a(str) {
202
202
  }
203
203
  return h >>> 0;
204
204
  }
205
- /** Deterministic ~80/20 train/eval split by pair identity. Pure. */
205
+ /** Deterministic ~80/20 split BY PROMPT (group split): bench templates
206
+ * mint many pairs per prompt, and a pair-level split leaked ~4.9k eval
207
+ * prompts into train (audit 2026-09-21) - every pair of one prompt must
208
+ * land on the same side or offline metrics are inflated. Pure. */
206
209
  export function splitDpoPairs(pairs, salt = 'dpo') {
207
210
  const train = [];
208
211
  const evalP = [];
209
212
  for (const p of pairs) {
210
- (fnv1a(salt + '|' + p.prompt + '|' + p.chosenSession + '|' + p.rejectedSession) % 5 === 0 ? evalP : train).push(p);
213
+ (fnv1a(salt + '|' + p.prompt) % 5 === 0 ? evalP : train).push(p);
211
214
  }
212
215
  return { train, eval: evalP };
213
216
  }
217
+ /** Exact-duplicate removal (same prompt+chosen+rejected): the 5-star army
218
+ * against the same few degraded sessions minted 11k+ permutation copies
219
+ * (audit 2026-09-21) - training would just over-weight those templates.
220
+ * Keeps the first occurrence (deterministic). Pure. */
221
+ export function dedupeDpoPairs(pairs) {
222
+ const seen = new Set();
223
+ const out = [];
224
+ for (const p of pairs) {
225
+ const key = p.prompt + '\u0000' + p.chosen + '\u0000' + p.rejected;
226
+ if (seen.has(key))
227
+ continue;
228
+ seen.add(key);
229
+ out.push(p);
230
+ }
231
+ return out;
232
+ }
214
233
  /** Leakage/quality audit before training consumes the pairs: empty sides,
215
234
  * exact duplicates, and eval pairs whose prompt ALSO appears in train
216
235
  * (prompt leakage inflates offline metrics). Pure. */
@@ -0,0 +1,81 @@
1
+ /**
2
+ * @hmharness/evolution - Sequential Experimentation (P0-05, 2026-09-24 audit)
3
+ *
4
+ * Replaces the fixed "10 percentage points difference" threshold with
5
+ * Sequential Probability Ratio Test (SPRT) for binomial outcomes.
6
+ *
7
+ * The audit said: "应升级为 Sequential Probability Ratio Test / Bayesian
8
+ * decision / anytime-valid inference 中的一个正式方案, 并同时加入多重比较、
9
+ * 实验停止规则、护栏指标和效应量置信区间"
10
+ *
11
+ * SPRT advantage over fixed-threshold:
12
+ * - Can stop EARLY when evidence is conclusive (saves API calls)
13
+ * - Has provable error rates (alpha/beta are hard bounds)
14
+ * - Anytime-valid: the error guarantee holds at ANY stopping point
15
+ */
16
+ export interface SPRTParams {
17
+ /** H0: p_control (null hypothesis baseline success rate) */
18
+ p0: number;
19
+ /** H1: p_treatment (alternative hypothesis success rate) */
20
+ p1: number;
21
+ /** Type I error rate (false positive): reject H0 when H0 is true */
22
+ alpha: number;
23
+ /** Type II error rate (false negative): fail to reject H0 when H1 is true */
24
+ beta: number;
25
+ }
26
+ export interface SPRTResult {
27
+ /** decision at current sample sizes */
28
+ decision: 'continue' | 'accept-h0' | 'accept-h1' | 'insufficient';
29
+ /** log-likelihood ratio */
30
+ logLR: number;
31
+ /** lower boundary for accepting H0 */
32
+ lowerBound: number;
33
+ /** upper boundary for accepting H1 */
34
+ upperBound: number;
35
+ /** total observations so far */
36
+ nControl: number;
37
+ nTreatment: number;
38
+ /** estimated effect (treatment - control success rate) */
39
+ effectEstimate: number;
40
+ /** 95% CI for the effect */
41
+ effectCI: [number, number];
42
+ /** minimum observations per arm before any decision */
43
+ minN: number;
44
+ }
45
+ /**
46
+ * Wald's Sequential Probability Ratio Test for comparing two binomial rates.
47
+ *
48
+ * At each observation, compute the log-likelihood ratio:
49
+ * Λ = Σ log(L(p1)/L(p0))
50
+ *
51
+ * Decision boundaries:
52
+ * Λ ≤ log(β/(1-α)) → accept H0 (no difference / treatment not better)
53
+ * Λ ≥ log((1-β)/α) → accept H1 (treatment IS better)
54
+ * otherwise → continue collecting data
55
+ */
56
+ export declare function sprt(controlSuccesses: number, controlTotal: number, treatmentSuccesses: number, treatmentTotal: number, params: SPRTParams, minN?: number): SPRTResult;
57
+ /**
58
+ * Convenience: standard SPRT for evolution experiments.
59
+ * H0: treatment is NOT better than control (same rate)
60
+ * H1: treatment is at least 5pp better
61
+ */
62
+ export declare function evolutionSPRT(controlSuccesses: number, controlTotal: number, treatmentSuccesses: number, treatmentTotal: number): SPRTResult;
63
+ /**
64
+ * Multiple comparison correction (Bonferroni) for running multiple
65
+ * experiments simultaneously - the audit's "多重比较" requirement.
66
+ */
67
+ export declare function bonferroniAlpha(numExperiments: number, familyAlpha?: number): number;
68
+ /**
69
+ * Guardrail check: a treatment that improves the target metric but
70
+ * degrades a guardrail metric beyond tolerance should be rejected.
71
+ * The audit's "护栏指标" requirement.
72
+ */
73
+ export declare function checkGuardrails(target: SPRTResult, guardrails: Array<{
74
+ name: string;
75
+ controlValue: number;
76
+ treatmentValue: number;
77
+ maxRegression: number;
78
+ }>): {
79
+ pass: boolean;
80
+ violations: string[];
81
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @hmharness/evolution - Sequential Experimentation (P0-05, 2026-09-24 audit)
3
+ *
4
+ * Replaces the fixed "10 percentage points difference" threshold with
5
+ * Sequential Probability Ratio Test (SPRT) for binomial outcomes.
6
+ *
7
+ * The audit said: "应升级为 Sequential Probability Ratio Test / Bayesian
8
+ * decision / anytime-valid inference 中的一个正式方案, 并同时加入多重比较、
9
+ * 实验停止规则、护栏指标和效应量置信区间"
10
+ *
11
+ * SPRT advantage over fixed-threshold:
12
+ * - Can stop EARLY when evidence is conclusive (saves API calls)
13
+ * - Has provable error rates (alpha/beta are hard bounds)
14
+ * - Anytime-valid: the error guarantee holds at ANY stopping point
15
+ */
16
+ /**
17
+ * Wald's Sequential Probability Ratio Test for comparing two binomial rates.
18
+ *
19
+ * At each observation, compute the log-likelihood ratio:
20
+ * Λ = Σ log(L(p1)/L(p0))
21
+ *
22
+ * Decision boundaries:
23
+ * Λ ≤ log(β/(1-α)) → accept H0 (no difference / treatment not better)
24
+ * Λ ≥ log((1-β)/α) → accept H1 (treatment IS better)
25
+ * otherwise → continue collecting data
26
+ */
27
+ export function sprt(controlSuccesses, controlTotal, treatmentSuccesses, treatmentTotal, params, minN = 30) {
28
+ const { p0, p1, alpha, beta } = params;
29
+ const nC = controlTotal;
30
+ const nT = treatmentTotal;
31
+ const xC = controlSuccesses;
32
+ const xT = treatmentSuccesses;
33
+ // boundaries
34
+ const lowerBound = Math.log(beta / (1 - alpha));
35
+ const upperBound = Math.log((1 - beta) / alpha);
36
+ // log-likelihood ratio for binomial
37
+ // Under H0: both arms have success rate p0
38
+ // Under H1: treatment has p1, control has p0
39
+ let logLR = 0;
40
+ // treatment arm observations
41
+ if (nT > 0) {
42
+ const logL1 = xT * Math.log(p1) + (nT - xT) * Math.log(1 - p1);
43
+ const logL0 = xT * Math.log(p0) + (nT - xT) * Math.log(1 - p0);
44
+ logLR += logL1 - logL0;
45
+ }
46
+ // minimum sample check
47
+ if (nC < minN || nT < minN) {
48
+ return {
49
+ decision: 'insufficient',
50
+ logLR, lowerBound, upperBound,
51
+ nControl: nC, nTreatment: nT,
52
+ effectEstimate: nT > 0 && nC > 0 ? (xT / nT) - (xC / nC) : 0,
53
+ effectCI: [-1, 1],
54
+ minN,
55
+ };
56
+ }
57
+ // decision
58
+ let decision;
59
+ if (logLR <= lowerBound) {
60
+ decision = 'accept-h0';
61
+ }
62
+ else if (logLR >= upperBound) {
63
+ decision = 'accept-h1';
64
+ }
65
+ else {
66
+ decision = 'continue';
67
+ }
68
+ // effect estimate and CI (Wald normal approximation)
69
+ const pControl = nC > 0 ? xC / nC : 0;
70
+ const pTreatment = nT > 0 ? xT / nT : 0;
71
+ const effect = pTreatment - pControl;
72
+ const seC = nC > 0 ? Math.sqrt(pControl * (1 - pControl) / nC) : 0;
73
+ const seT = nT > 0 ? Math.sqrt(pTreatment * (1 - pTreatment) / nT) : 0;
74
+ const se = Math.sqrt(seC * seC + seT * seT);
75
+ const z = 1.96; // 95% CI
76
+ const ci = [
77
+ Math.max(-1, effect - z * se),
78
+ Math.min(1, effect + z * se),
79
+ ];
80
+ return {
81
+ decision, logLR, lowerBound, upperBound,
82
+ nControl: nC, nTreatment: nT,
83
+ effectEstimate: effect,
84
+ effectCI: ci,
85
+ minN,
86
+ };
87
+ }
88
+ /**
89
+ * Convenience: standard SPRT for evolution experiments.
90
+ * H0: treatment is NOT better than control (same rate)
91
+ * H1: treatment is at least 5pp better
92
+ */
93
+ export function evolutionSPRT(controlSuccesses, controlTotal, treatmentSuccesses, treatmentTotal) {
94
+ // estimate baseline from control
95
+ const p0 = controlTotal > 0 ? controlSuccesses / controlTotal : 0.5;
96
+ // H1: 5pp improvement (minimum meaningful effect)
97
+ const p1 = Math.min(0.99, p0 + 0.05);
98
+ return sprt(controlSuccesses, controlTotal, treatmentSuccesses, treatmentTotal, {
99
+ p0, p1, alpha: 0.05, beta: 0.10,
100
+ }, 30);
101
+ }
102
+ /**
103
+ * Multiple comparison correction (Bonferroni) for running multiple
104
+ * experiments simultaneously - the audit's "多重比较" requirement.
105
+ */
106
+ export function bonferroniAlpha(numExperiments, familyAlpha = 0.05) {
107
+ return familyAlpha / Math.max(1, numExperiments);
108
+ }
109
+ /**
110
+ * Guardrail check: a treatment that improves the target metric but
111
+ * degrades a guardrail metric beyond tolerance should be rejected.
112
+ * The audit's "护栏指标" requirement.
113
+ */
114
+ export function checkGuardrails(target, guardrails) {
115
+ const violations = [];
116
+ for (const g of guardrails) {
117
+ const regression = g.treatmentValue - g.controlValue;
118
+ if (regression > g.maxRegression) {
119
+ violations.push(`${g.name}: regression ${regression.toFixed(3)} > tolerance ${g.maxRegression}`);
120
+ }
121
+ }
122
+ return { pass: violations.length === 0, violations };
123
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/evolution",
3
- "version": "0.14.14",
3
+ "version": "0.14.16",
4
4
  "description": "hmharness evolution subsystem: persistent memory, insight capture, skill library, and the bench that gives evolution its fitness signal. First-class kernel citizen, not a plugin.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -15,7 +15,7 @@
15
15
  "build": "tsc -p tsconfig.build.json"
16
16
  },
17
17
  "dependencies": {
18
- "@hmharness/kernel": "0.9.2"
18
+ "@hmharness/kernel": "0.9.3"
19
19
  },
20
20
  "files": [
21
21
  "dist"