@hmharness/evolution 0.12.1 → 0.13.3

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/bench.js CHANGED
@@ -101,6 +101,22 @@ export async function runBench(home, run) {
101
101
  }
102
102
  }
103
103
  const passed = results.filter((r) => r.pass).length;
104
+ // persist a history record: readiness's "eval-regression-stable" condition
105
+ // reads evolution/benches/*.json - without these records the RL gate's
106
+ // stability condition can never be met (day-20 audit gap)
107
+ try {
108
+ const dir = join(home, 'evolution', 'benches');
109
+ await mkdir(dir, { recursive: true });
110
+ const record = {
111
+ time: new Date().toISOString(),
112
+ total: cases.length,
113
+ passed,
114
+ passRate: cases.length === 0 ? 1 : passed / cases.length,
115
+ results: results.map((r) => ({ name: r.name, pass: r.pass, detail: r.detail.slice(0, 200) })),
116
+ };
117
+ await writeFile(join(dir, `${record.time.replace(/[:.]/g, '-')}.json`), JSON.stringify(record, null, 2) + '\n', 'utf8');
118
+ }
119
+ catch { /* history is best-effort; the run result still returns */ }
104
120
  return { results, passRate: cases.length === 0 ? 1 : passed / cases.length };
105
121
  }
106
122
  /** Starter cases so the evolve gate has a signal from day one. */
package/dist/evolve.js CHANGED
@@ -178,6 +178,19 @@ export async function runEvolution(opts) {
178
178
  }
179
179
  }
180
180
  report.proposals = proposals;
181
+ // 4b. Promotion quality floor (day-16 meta-audit): "no regression" alone
182
+ // can promote a candidate that is still terrible when the BASELINE itself
183
+ // is weak (baseline 20% -> candidate 25% passes the old gate). An absolute
184
+ // minimum train pass rate closes that hole. Configurable via
185
+ // config.json evolution.minPassRate (default 0.6).
186
+ let minPassRate = 0.6;
187
+ try {
188
+ const cfg = JSON.parse(await readFile(join(home, 'config.json'), 'utf8'));
189
+ if (typeof cfg.evolution?.minPassRate === 'number' && cfg.evolution.minPassRate >= 0 && cfg.evolution.minPassRate <= 1) {
190
+ minPassRate = cfg.evolution.minPassRate;
191
+ }
192
+ }
193
+ catch { /* default floor */ }
181
194
  // 5. A/B gate each proposal on the train set.
182
195
  for (const p of proposals.slice(0, maxProposals)) {
183
196
  say(`candidate "${p.name}": drafting + candidate bench`);
@@ -236,6 +249,20 @@ export async function runEvolution(opts) {
236
249
  say(` rejected (${regression ? 'regression' : 'lower pass rate'})`);
237
250
  continue;
238
251
  }
252
+ if (candRate < minPassRate) {
253
+ await deleteDraft(home, p.name);
254
+ await recordParetoEntry(home, { name: p.name, parentInsights: insightIds, rejectedReason: `below quality floor (${candRate.toFixed(2)} < ${minPassRate})`, scores: { train: candRate }, metaModel: provider.model, at: new Date().toISOString() });
255
+ report.outcomes.push({
256
+ name: p.name,
257
+ action: 'rejected',
258
+ reason: `below quality floor: pass rate ${candRate} < min ${minPassRate} (non-regression vs a weak baseline ${baseRate} is not good enough)`,
259
+ baseline: { passRate: baseRate, cases: summary(baseResults) },
260
+ candidate: { passRate: candRate, cases: summary(candResults) },
261
+ lineage: { parentInsights: insightIds, scores: { train: candRate }, metaModel: provider.model, decidedAt: new Date().toISOString() },
262
+ });
263
+ say(` rejected (below quality floor ${minPassRate})`);
264
+ continue;
265
+ }
239
266
  if (costRegressions.length > 0) {
240
267
  await deleteDraft(home, p.name);
241
268
  await recordParetoEntry(home, { name: p.name, parentInsights: insightIds, rejectedReason: `cost regression on ${costRegressions.join(', ')}`, scores: { train: candRate }, metaModel: provider.model, at: new Date().toISOString() });
@@ -303,7 +330,7 @@ export async function runEvolution(opts) {
303
330
  report.outcomes.push({
304
331
  name: p.name,
305
332
  action: 'promoted',
306
- reason: `no regression (train ${(candRate * 100).toFixed(0)}% vs ${(baseRate * 100).toFixed(0)}%${holdout.length ? `, holdout ${(holdoutRate * 100).toFixed(0)}%` : ''}) - promoted to CANARY (20% sessions, impact-gated full promotion)${archivedPrevious ? '; previous version archived' : ''}${weakGate ? ' [WEAK GATE: no holdout cases defined]' : ''}`,
333
+ reason: `no regression (train ${(candRate * 100).toFixed(0)}% vs ${(baseRate * 100).toFixed(0)}%${holdout.length ? `, holdout ${(holdoutRate * 100).toFixed(0)}%` : ''}; floor ≥${(minPassRate * 100).toFixed(0)}%) - promoted to CANARY (20% sessions, impact-gated full promotion)${archivedPrevious ? '; previous version archived' : ''}${weakGate ? ' [WEAK GATE: no holdout cases defined]' : ''}`,
307
334
  baseline: { passRate: baseRate, cases: summary(baseResults) },
308
335
  candidate: { passRate: candRate, cases: summary(candResults) },
309
336
  ...(holdout.length ? { holdout: { baselineRate: holdoutBaseRate, candidateRate: holdoutRate } } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hmharness/evolution",
3
- "version": "0.12.1",
3
+ "version": "0.13.3",
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",