@hmharness/evolution 0.9.0 → 0.13.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 (2) hide show
  1. package/dist/evolve.js +40 -1
  2. package/package.json +1 -1
package/dist/evolve.js CHANGED
@@ -59,14 +59,26 @@ export async function runEvolution(opts) {
59
59
  // anything but cycles - the documented budget was decorative).
60
60
  const budget = await readBudget(home);
61
61
  const today = new Date().toISOString().slice(0, 10);
62
+ // Skipped cycles ARE data (SELFFEED honesty rule #2: absent/empty days must
63
+ // be visible). The early returns used to bypass the durable log entirely -
64
+ // a day's skips vanished without a trace and "0 skips" was unauditable
65
+ // (caught by the day-16 meta-audit). Persist before returning.
66
+ const persistSkip = async () => {
67
+ report.estTokens = 0;
68
+ const logDir = join(home, 'evolution');
69
+ await mkdir(logDir, { recursive: true });
70
+ await appendFile(join(logDir, 'log.jsonl'), JSON.stringify(report) + '\n', 'utf8');
71
+ };
62
72
  if (budget.maxCyclesPerDay && budget.cyclesToday >= budget.maxCyclesPerDay) {
63
73
  report.outcomes.push({ name: '(budget)', action: 'error', reason: `daily cycle limit reached (${budget.cyclesToday}/${budget.maxCyclesPerDay} today) - skipped` });
74
+ await persistSkip();
64
75
  return report;
65
76
  }
66
77
  if (budget.maxCyclesPerDay && budget.maxTokensPerCycle) {
67
78
  const dailyCap = budget.maxCyclesPerDay * budget.maxTokensPerCycle;
68
79
  if ((budget.tokensToday ?? 0) >= dailyCap) {
69
80
  report.outcomes.push({ name: '(budget)', action: 'error', reason: `daily token limit reached (~${budget.tokensToday}/${dailyCap} est-tokens today) - skipped` });
81
+ await persistSkip();
70
82
  return report;
71
83
  }
72
84
  }
@@ -166,6 +178,19 @@ export async function runEvolution(opts) {
166
178
  }
167
179
  }
168
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 */ }
169
194
  // 5. A/B gate each proposal on the train set.
170
195
  for (const p of proposals.slice(0, maxProposals)) {
171
196
  say(`candidate "${p.name}": drafting + candidate bench`);
@@ -224,6 +249,20 @@ export async function runEvolution(opts) {
224
249
  say(` rejected (${regression ? 'regression' : 'lower pass rate'})`);
225
250
  continue;
226
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
+ }
227
266
  if (costRegressions.length > 0) {
228
267
  await deleteDraft(home, p.name);
229
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() });
@@ -291,7 +330,7 @@ export async function runEvolution(opts) {
291
330
  report.outcomes.push({
292
331
  name: p.name,
293
332
  action: 'promoted',
294
- 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]' : ''}`,
295
334
  baseline: { passRate: baseRate, cases: summary(baseResults) },
296
335
  candidate: { passRate: candRate, cases: summary(candResults) },
297
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.9.0",
3
+ "version": "0.13.0",
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",