@evomap/evolver 1.31.0 → 1.32.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.
@@ -1,15 +1,16 @@
1
1
  'use strict';
2
2
 
3
- var fs = require('fs');
4
- var path = require('path');
5
- var crypto = require('crypto');
6
- var paths = require('./paths');
7
-
8
- var DISTILLER_MIN_CAPSULES = parseInt(process.env.DISTILLER_MIN_CAPSULES || '10', 10) || 10;
9
- var DISTILLER_INTERVAL_HOURS = parseInt(process.env.DISTILLER_INTERVAL_HOURS || '24', 10) || 24;
10
- var DISTILLER_MIN_SUCCESS_RATE = parseFloat(process.env.DISTILLER_MIN_SUCCESS_RATE || '0.7') || 0.7;
11
- var DISTILLED_MAX_FILES = 12;
12
- var DISTILLED_ID_PREFIX = 'gene_distilled_';
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const paths = require('./paths');
7
+ const learningSignals = require('./learningSignals');
8
+
9
+ const DISTILLER_MIN_CAPSULES = parseInt(process.env.DISTILLER_MIN_CAPSULES || '10', 10) || 10;
10
+ const DISTILLER_INTERVAL_HOURS = parseInt(process.env.DISTILLER_INTERVAL_HOURS || '24', 10) || 24;
11
+ const DISTILLER_MIN_SUCCESS_RATE = parseFloat(process.env.DISTILLER_MIN_SUCCESS_RATE || '0.7') || 0.7;
12
+ const DISTILLED_MAX_FILES = 12;
13
+ const DISTILLED_ID_PREFIX = 'gene_distilled_';
13
14
 
14
15
  function ensureDir(dir) {
15
16
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -18,7 +19,7 @@ function ensureDir(dir) {
18
19
  function readJsonIfExists(filePath, fallback) {
19
20
  try {
20
21
  if (!fs.existsSync(filePath)) return fallback;
21
- var raw = fs.readFileSync(filePath, 'utf8');
22
+ const raw = fs.readFileSync(filePath, 'utf8');
22
23
  if (!raw.trim()) return fallback;
23
24
  return JSON.parse(raw);
24
25
  } catch (e) {
@@ -29,7 +30,7 @@ function readJsonIfExists(filePath, fallback) {
29
30
  function readJsonlIfExists(filePath) {
30
31
  try {
31
32
  if (!fs.existsSync(filePath)) return [];
32
- var raw = fs.readFileSync(filePath, 'utf8');
33
+ const raw = fs.readFileSync(filePath, 'utf8');
33
34
  return raw.split('\n').map(function (l) { return l.trim(); }).filter(Boolean).map(function (l) {
34
35
  try { return JSON.parse(l); } catch (e) { return null; }
35
36
  }).filter(Boolean);
@@ -57,13 +58,13 @@ function readDistillerState() {
57
58
 
58
59
  function writeDistillerState(state) {
59
60
  ensureDir(path.dirname(distillerStatePath()));
60
- var tmp = distillerStatePath() + '.tmp';
61
+ const tmp = distillerStatePath() + '.tmp';
61
62
  fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8');
62
63
  fs.renameSync(tmp, distillerStatePath());
63
64
  }
64
65
 
65
66
  function computeDataHash(capsules) {
66
- var ids = capsules.map(function (c) { return c.id || ''; }).sort();
67
+ const ids = capsules.map(function (c) { return c.id || ''; }).sort();
67
68
  return crypto.createHash('sha256').update(ids.join('|')).digest('hex').slice(0, 16);
68
69
  }
69
70
 
@@ -71,40 +72,40 @@ function computeDataHash(capsules) {
71
72
  // Step 1: collectDistillationData
72
73
  // ---------------------------------------------------------------------------
73
74
  function collectDistillationData() {
74
- var assetsDir = paths.getGepAssetsDir();
75
- var evoDir = paths.getEvolutionDir();
75
+ const assetsDir = paths.getGepAssetsDir();
76
+ const evoDir = paths.getEvolutionDir();
76
77
 
77
- var capsulesJson = readJsonIfExists(path.join(assetsDir, 'capsules.json'), { capsules: [] });
78
- var capsulesJsonl = readJsonlIfExists(path.join(assetsDir, 'capsules.jsonl'));
79
- var allCapsules = [].concat(capsulesJson.capsules || [], capsulesJsonl);
78
+ const capsulesJson = readJsonIfExists(path.join(assetsDir, 'capsules.json'), { capsules: [] });
79
+ const capsulesJsonl = readJsonlIfExists(path.join(assetsDir, 'capsules.jsonl'));
80
+ let allCapsules = [].concat(capsulesJson.capsules || [], capsulesJsonl);
80
81
 
81
- var unique = new Map();
82
+ const unique = new Map();
82
83
  allCapsules.forEach(function (c) { if (c && c.id) unique.set(String(c.id), c); });
83
84
  allCapsules = Array.from(unique.values());
84
85
 
85
- var successCapsules = allCapsules.filter(function (c) {
86
+ const successCapsules = allCapsules.filter(function (c) {
86
87
  if (!c || !c.outcome) return false;
87
- var status = typeof c.outcome === 'string' ? c.outcome : c.outcome.status;
88
+ const status = typeof c.outcome === 'string' ? c.outcome : c.outcome.status;
88
89
  if (status !== 'success') return false;
89
- var score = c.outcome && Number.isFinite(Number(c.outcome.score)) ? Number(c.outcome.score) : 1;
90
+ const score = c.outcome && Number.isFinite(Number(c.outcome.score)) ? Number(c.outcome.score) : 1;
90
91
  return score >= DISTILLER_MIN_SUCCESS_RATE;
91
92
  });
92
93
 
93
- var events = readJsonlIfExists(path.join(assetsDir, 'events.jsonl'));
94
+ const events = readJsonlIfExists(path.join(assetsDir, 'events.jsonl'));
94
95
 
95
- var memGraphPath = process.env.MEMORY_GRAPH_PATH || path.join(evoDir, 'memory_graph.jsonl');
96
- var graphEntries = readJsonlIfExists(memGraphPath);
96
+ const memGraphPath = process.env.MEMORY_GRAPH_PATH || path.join(evoDir, 'memory_graph.jsonl');
97
+ const graphEntries = readJsonlIfExists(memGraphPath);
97
98
 
98
- var grouped = {};
99
+ const grouped = {};
99
100
  successCapsules.forEach(function (c) {
100
- var geneId = c.gene || c.gene_id || 'unknown';
101
+ const geneId = c.gene || c.gene_id || 'unknown';
101
102
  if (!grouped[geneId]) {
102
103
  grouped[geneId] = {
103
104
  gene_id: geneId, capsules: [], total_count: 0,
104
105
  total_score: 0, triggers: [], summaries: [],
105
106
  };
106
107
  }
107
- var g = grouped[geneId];
108
+ const g = grouped[geneId];
108
109
  g.capsules.push(c);
109
110
  g.total_count += 1;
110
111
  g.total_score += (c.outcome && Number.isFinite(Number(c.outcome.score))) ? Number(c.outcome.score) : 0.8;
@@ -113,7 +114,7 @@ function collectDistillationData() {
113
114
  });
114
115
 
115
116
  Object.keys(grouped).forEach(function (id) {
116
- var g = grouped[id];
117
+ const g = grouped[id];
117
118
  g.avg_score = g.total_count > 0 ? g.total_score / g.total_count : 0;
118
119
  });
119
120
 
@@ -131,8 +132,8 @@ function collectDistillationData() {
131
132
  // Step 2: analyzePatterns
132
133
  // ---------------------------------------------------------------------------
133
134
  function analyzePatterns(data) {
134
- var grouped = data.grouped;
135
- var report = {
135
+ const grouped = data.grouped;
136
+ const report = {
136
137
  high_frequency: [],
137
138
  strategy_drift: [],
138
139
  coverage_gaps: [],
@@ -142,26 +143,26 @@ function analyzePatterns(data) {
142
143
  };
143
144
 
144
145
  Object.keys(grouped).forEach(function (geneId) {
145
- var g = grouped[geneId];
146
+ const g = grouped[geneId];
146
147
  if (g.total_count >= 5) {
147
- var flat = [];
148
+ let flat = [];
148
149
  g.triggers.forEach(function (t) { if (Array.isArray(t)) flat = flat.concat(t); });
149
- var freq = {};
150
- flat.forEach(function (t) { var k = String(t).toLowerCase(); freq[k] = (freq[k] || 0) + 1; });
151
- var top = Object.keys(freq).sort(function (a, b) { return freq[b] - freq[a]; }).slice(0, 5);
150
+ const freq = {};
151
+ flat.forEach(function (t) { const k = String(t).toLowerCase(); freq[k] = (freq[k] || 0) + 1; });
152
+ const top = Object.keys(freq).sort(function (a, b) { return freq[b] - freq[a]; }).slice(0, 5);
152
153
  report.high_frequency.push({ gene_id: geneId, count: g.total_count, avg_score: Math.round(g.avg_score * 100) / 100, top_triggers: top });
153
154
  }
154
155
 
155
156
  if (g.summaries.length >= 3) {
156
- var first = g.summaries[0];
157
- var last = g.summaries[g.summaries.length - 1];
157
+ const first = g.summaries[0];
158
+ const last = g.summaries[g.summaries.length - 1];
158
159
  if (first !== last) {
159
- var fw = new Set(first.toLowerCase().split(/\s+/));
160
- var lw = new Set(last.toLowerCase().split(/\s+/));
161
- var inter = 0;
160
+ const fw = new Set(first.toLowerCase().split(/\s+/));
161
+ const lw = new Set(last.toLowerCase().split(/\s+/));
162
+ let inter = 0;
162
163
  fw.forEach(function (w) { if (lw.has(w)) inter++; });
163
- var union = fw.size + lw.size - inter;
164
- var sim = union > 0 ? inter / union : 1;
164
+ const union = fw.size + lw.size - inter;
165
+ const sim = union > 0 ? inter / union : 1;
165
166
  if (sim < 0.6) {
166
167
  report.strategy_drift.push({ gene_id: geneId, similarity: Math.round(sim * 100) / 100, early_summary: first.slice(0, 120), recent_summary: last.slice(0, 120) });
167
168
  }
@@ -169,19 +170,19 @@ function analyzePatterns(data) {
169
170
  }
170
171
  });
171
172
 
172
- var signalFreq = {};
173
+ const signalFreq = {};
173
174
  (data.events || []).forEach(function (evt) {
174
175
  if (evt && Array.isArray(evt.signals)) {
175
- evt.signals.forEach(function (s) { var k = String(s).toLowerCase(); signalFreq[k] = (signalFreq[k] || 0) + 1; });
176
+ evt.signals.forEach(function (s) { const k = String(s).toLowerCase(); signalFreq[k] = (signalFreq[k] || 0) + 1; });
176
177
  }
177
178
  });
178
- var covered = new Set();
179
+ const covered = new Set();
179
180
  Object.keys(grouped).forEach(function (geneId) {
180
181
  grouped[geneId].triggers.forEach(function (t) {
181
182
  if (Array.isArray(t)) t.forEach(function (s) { covered.add(String(s).toLowerCase()); });
182
183
  });
183
184
  });
184
- var gaps = Object.keys(signalFreq)
185
+ const gaps = Object.keys(signalFreq)
185
186
  .filter(function (s) { return signalFreq[s] >= 3 && !covered.has(s); })
186
187
  .sort(function (a, b) { return signalFreq[b] - signalFreq[a]; })
187
188
  .slice(0, 10);
@@ -196,16 +197,16 @@ function analyzePatterns(data) {
196
197
  // Step 3: LLM response parsing
197
198
  // ---------------------------------------------------------------------------
198
199
  function extractJsonFromLlmResponse(text) {
199
- var str = String(text || '');
200
- var buffer = '';
201
- var depth = 0;
202
- for (var i = 0; i < str.length; i++) {
203
- var ch = str[i];
200
+ const str = String(text || '');
201
+ let buffer = '';
202
+ let depth = 0;
203
+ for (let i = 0; i < str.length; i++) {
204
+ const ch = str[i];
204
205
  if (ch === '{') { if (depth === 0) buffer = ''; depth++; buffer += ch; }
205
206
  else if (ch === '}') {
206
207
  depth--; buffer += ch;
207
208
  if (depth === 0 && buffer.length > 2) {
208
- try { var obj = JSON.parse(buffer); if (obj && typeof obj === 'object' && obj.type === 'Gene') return obj; } catch (e) {}
209
+ try { const obj = JSON.parse(buffer); if (obj && typeof obj === 'object' && obj.type === 'Gene') return obj; } catch (e) {}
209
210
  buffer = '';
210
211
  }
211
212
  if (depth < 0) depth = 0;
@@ -215,10 +216,10 @@ function extractJsonFromLlmResponse(text) {
215
216
  }
216
217
 
217
218
  function buildDistillationPrompt(analysis, existingGenes, sampleCapsules) {
218
- var genesRef = existingGenes.map(function (g) {
219
+ const genesRef = existingGenes.map(function (g) {
219
220
  return { id: g.id, category: g.category || null, signals_match: g.signals_match || [] };
220
221
  });
221
- var samples = sampleCapsules.slice(0, 8).map(function (c) {
222
+ const samples = sampleCapsules.slice(0, 8).map(function (c) {
222
223
  return { gene: c.gene || c.gene_id || null, trigger: c.trigger || [], summary: (c.summary || '').slice(0, 200), outcome: c.outcome || null };
223
224
  });
224
225
 
@@ -318,7 +319,7 @@ function distillRequestPath() {
318
319
  // Derive a descriptive ID from gene content when the LLM gives a bad name
319
320
  // ---------------------------------------------------------------------------
320
321
  function deriveDescriptiveId(gene) {
321
- var words = [];
322
+ let words = [];
322
323
  if (Array.isArray(gene.signals_match)) {
323
324
  gene.signals_match.slice(0, 3).forEach(function (s) {
324
325
  String(s).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
@@ -327,7 +328,7 @@ function deriveDescriptiveId(gene) {
327
328
  });
328
329
  }
329
330
  if (words.length < 3 && gene.summary) {
330
- var STOP = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'into', 'when', 'are', 'was', 'has', 'had']);
331
+ const STOP = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'into', 'when', 'are', 'was', 'has', 'had']);
331
332
  String(gene.summary).toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim().split(/\s+/).forEach(function (w) {
332
333
  if (w.length >= 3 && !STOP.has(w) && words.length < 6) words.push(w);
333
334
  });
@@ -338,8 +339,8 @@ function deriveDescriptiveId(gene) {
338
339
  });
339
340
  }
340
341
  if (words.length < 2) words = ['auto', 'distilled', 'strategy'];
341
- var unique = [];
342
- var seen = new Set();
342
+ const unique = [];
343
+ const seen = new Set();
343
344
  words.forEach(function (w) { if (!seen.has(w)) { seen.add(w); unique.push(w); } });
344
345
  return DISTILLED_ID_PREFIX + unique.slice(0, 5).join('-');
345
346
  }
@@ -349,9 +350,9 @@ function deriveDescriptiveId(gene) {
349
350
  // ---------------------------------------------------------------------------
350
351
  function sanitizeSignalsMatch(signals) {
351
352
  if (!Array.isArray(signals)) return [];
352
- var cleaned = [];
353
+ const cleaned = [];
353
354
  signals.forEach(function (s) {
354
- var sig = String(s || '').trim().toLowerCase();
355
+ let sig = String(s || '').trim().toLowerCase();
355
356
  if (!sig) return;
356
357
  // Strip trailing timestamps (10+ digits) and random suffixes
357
358
  sig = sig.replace(/[_-]\d{10,}$/g, '');
@@ -368,7 +369,7 @@ function sanitizeSignalsMatch(signals) {
368
369
  cleaned.push(sig);
369
370
  });
370
371
  // Deduplicate
371
- var seen = {};
372
+ const seen = {};
372
373
  return cleaned.filter(function (s) { if (seen[s]) return false; seen[s] = true; return true; });
373
374
  }
374
375
 
@@ -376,7 +377,7 @@ function sanitizeSignalsMatch(signals) {
376
377
  // Step 4: validateSynthesizedGene
377
378
  // ---------------------------------------------------------------------------
378
379
  function validateSynthesizedGene(gene, existingGenes) {
379
- var errors = [];
380
+ const errors = [];
380
381
  if (!gene || typeof gene !== 'object') return { valid: false, errors: ['gene is not an object'] };
381
382
 
382
383
  if (gene.type !== 'Gene') errors.push('missing or wrong type (must be "Gene")');
@@ -404,17 +405,16 @@ function validateSynthesizedGene(gene, existingGenes) {
404
405
  }
405
406
 
406
407
  if (gene.id) {
407
- var suffix = String(gene.id).replace(DISTILLED_ID_PREFIX, '');
408
- // Strip ALL embedded timestamps (10+ digit sequences) anywhere in the id
408
+ let suffix = String(gene.id).replace(DISTILLED_ID_PREFIX, '');
409
409
  suffix = suffix.replace(/[-_]?\d{10,}[-_]?/g, '-').replace(/[-_]+/g, '-').replace(/^[-_]+|[-_]+$/g, '');
410
- var needsRename = /^\d+$/.test(suffix) || /^\d{10,}/.test(suffix)
410
+ const needsRename = /^\d+$/.test(suffix) || /^\d{10,}/.test(suffix)
411
411
  || /^(cursor|vscode|vim|emacs|windsurf|copilot|cline|codex)[-_]?\d*$/i.test(suffix);
412
412
  if (needsRename) {
413
413
  gene.id = deriveDescriptiveId(gene);
414
414
  } else {
415
415
  gene.id = DISTILLED_ID_PREFIX + suffix;
416
416
  }
417
- var cleanSuffix = String(gene.id).replace(DISTILLED_ID_PREFIX, '');
417
+ const cleanSuffix = String(gene.id).replace(DISTILLED_ID_PREFIX, '');
418
418
  if (cleanSuffix.replace(/[-_]/g, '').length < 6) {
419
419
  gene.id = deriveDescriptiveId(gene);
420
420
  }
@@ -447,14 +447,14 @@ function validateSynthesizedGene(gene, existingGenes) {
447
447
  }
448
448
 
449
449
  // --- Validation command sanitization ---
450
- var ALLOWED_PREFIXES = ['node ', 'npm ', 'npx '];
450
+ const ALLOWED_PREFIXES = ['node ', 'npm ', 'npx '];
451
451
  if (Array.isArray(gene.validation)) {
452
452
  gene.validation = gene.validation.filter(function (cmd) {
453
- var c = String(cmd || '').trim();
453
+ const c = String(cmd || '').trim();
454
454
  if (!c) return false;
455
455
  if (!ALLOWED_PREFIXES.some(function (p) { return c.startsWith(p); })) return false;
456
456
  if (/`|\$\(/.test(c)) return false;
457
- var stripped = c.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, '');
457
+ const stripped = c.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, '');
458
458
  return !/[;&|><]/.test(stripped);
459
459
  });
460
460
  }
@@ -463,19 +463,19 @@ function validateSynthesizedGene(gene, existingGenes) {
463
463
  if (!gene.schema_version) gene.schema_version = '1.6.0';
464
464
 
465
465
  // --- Duplicate ID check ---
466
- var existingIds = new Set((existingGenes || []).map(function (g) { return g.id; }));
466
+ const existingIds = new Set((existingGenes || []).map(function (g) { return g.id; }));
467
467
  if (gene.id && existingIds.has(gene.id)) {
468
468
  gene.id = gene.id + '_' + Date.now().toString(36);
469
469
  }
470
470
 
471
471
  // --- Signal overlap check ---
472
472
  if (gene.signals_match && existingGenes && existingGenes.length > 0) {
473
- var newSet = new Set(gene.signals_match.map(function (s) { return String(s).toLowerCase(); }));
474
- for (var i = 0; i < existingGenes.length; i++) {
475
- var eg = existingGenes[i];
476
- var egSet = new Set((eg.signals_match || []).map(function (s) { return String(s).toLowerCase(); }));
473
+ const newSet = new Set(gene.signals_match.map(function (s) { return String(s).toLowerCase(); }));
474
+ for (let i = 0; i < existingGenes.length; i++) {
475
+ const eg = existingGenes[i];
476
+ const egSet = new Set((eg.signals_match || []).map(function (s) { return String(s).toLowerCase(); }));
477
477
  if (newSet.size > 0 && egSet.size > 0) {
478
- var overlap = 0;
478
+ let overlap = 0;
479
479
  newSet.forEach(function (s) { if (egSet.has(s)) overlap++; });
480
480
  if (overlap === newSet.size && overlap === egSet.size) {
481
481
  errors.push('signals_match fully overlaps with existing gene: ' + eg.id);
@@ -493,24 +493,24 @@ function validateSynthesizedGene(gene, existingGenes) {
493
493
  function shouldDistill() {
494
494
  if (String(process.env.SKILL_DISTILLER || 'true').toLowerCase() === 'false') return false;
495
495
 
496
- var state = readDistillerState();
496
+ const state = readDistillerState();
497
497
  if (state.last_distillation_at) {
498
- var elapsed = Date.now() - new Date(state.last_distillation_at).getTime();
498
+ const elapsed = Date.now() - new Date(state.last_distillation_at).getTime();
499
499
  if (elapsed < DISTILLER_INTERVAL_HOURS * 3600000) return false;
500
500
  }
501
501
 
502
- var assetsDir = paths.getGepAssetsDir();
503
- var capsulesJson = readJsonIfExists(path.join(assetsDir, 'capsules.json'), { capsules: [] });
504
- var capsulesJsonl = readJsonlIfExists(path.join(assetsDir, 'capsules.jsonl'));
505
- var all = [].concat(capsulesJson.capsules || [], capsulesJsonl);
502
+ const assetsDir = paths.getGepAssetsDir();
503
+ const capsulesJson = readJsonIfExists(path.join(assetsDir, 'capsules.json'), { capsules: [] });
504
+ const capsulesJsonl = readJsonlIfExists(path.join(assetsDir, 'capsules.jsonl'));
505
+ const all = [].concat(capsulesJson.capsules || [], capsulesJsonl);
506
506
 
507
- var recent = all.slice(-10);
508
- var recentSuccess = recent.filter(function (c) {
507
+ const recent = all.slice(-10);
508
+ const recentSuccess = recent.filter(function (c) {
509
509
  return c && c.outcome && (c.outcome.status === 'success' || c.outcome === 'success');
510
510
  }).length;
511
511
  if (recentSuccess < 7) return false;
512
512
 
513
- var totalSuccess = all.filter(function (c) {
513
+ const totalSuccess = all.filter(function (c) {
514
514
  return c && c.outcome && (c.outcome.status === 'success' || c.outcome === 'success');
515
515
  }).length;
516
516
  if (totalSuccess < DISTILLER_MIN_CAPSULES) return false;
@@ -524,7 +524,7 @@ function shouldDistill() {
524
524
  function prepareDistillation() {
525
525
  console.log('[Distiller] Preparing skill distillation...');
526
526
 
527
- var data = collectDistillationData();
527
+ const data = collectDistillationData();
528
528
  console.log('[Distiller] Collected ' + data.successCapsules.length + ' successful capsules across ' + Object.keys(data.grouped).length + ' gene groups.');
529
529
 
530
530
  if (data.successCapsules.length < DISTILLER_MIN_CAPSULES) {
@@ -532,29 +532,29 @@ function prepareDistillation() {
532
532
  return { ok: false, reason: 'insufficient_data' };
533
533
  }
534
534
 
535
- var state = readDistillerState();
535
+ const state = readDistillerState();
536
536
  if (state.last_data_hash === data.dataHash) {
537
537
  console.log('[Distiller] Data unchanged since last distillation (hash: ' + data.dataHash + '). Skipping.');
538
538
  return { ok: false, reason: 'idempotent_skip' };
539
539
  }
540
540
 
541
- var analysis = analyzePatterns(data);
541
+ const analysis = analyzePatterns(data);
542
542
  console.log('[Distiller] Analysis: high_freq=' + analysis.high_frequency.length + ' drift=' + analysis.strategy_drift.length + ' gaps=' + analysis.coverage_gaps.length);
543
543
 
544
- var assetsDir = paths.getGepAssetsDir();
545
- var existingGenesJson = readJsonIfExists(path.join(assetsDir, 'genes.json'), { genes: [] });
546
- var existingGenes = existingGenesJson.genes || [];
544
+ const assetsDir = paths.getGepAssetsDir();
545
+ const existingGenesJson = readJsonIfExists(path.join(assetsDir, 'genes.json'), { genes: [] });
546
+ const existingGenes = existingGenesJson.genes || [];
547
547
 
548
- var prompt = buildDistillationPrompt(analysis, existingGenes, data.successCapsules);
548
+ const prompt = buildDistillationPrompt(analysis, existingGenes, data.successCapsules);
549
549
 
550
- var memDir = paths.getMemoryDir();
550
+ const memDir = paths.getMemoryDir();
551
551
  ensureDir(memDir);
552
- var promptFileName = 'distill_prompt_' + Date.now() + '.txt';
553
- var promptPath = path.join(memDir, promptFileName);
552
+ const promptFileName = 'distill_prompt_' + Date.now() + '.txt';
553
+ const promptPath = path.join(memDir, promptFileName);
554
554
  fs.writeFileSync(promptPath, prompt, 'utf8');
555
555
 
556
- var reqPath = distillRequestPath();
557
- var requestData = {
556
+ const reqPath = distillRequestPath();
557
+ const requestData = {
558
558
  type: 'DistillationRequest',
559
559
  created_at: new Date().toISOString(),
560
560
  prompt_path: promptPath,
@@ -573,19 +573,139 @@ function prepareDistillation() {
573
573
  return { ok: true, promptPath: promptPath, requestPath: reqPath, dataHash: data.dataHash };
574
574
  }
575
575
 
576
+ function inferCategoryFromSignals(signals) {
577
+ const list = Array.isArray(signals) ? signals.map(function (s) { return String(s).toLowerCase(); }) : [];
578
+ if (list.some(function (s) { return s.indexOf('error') !== -1 || s.indexOf('fail') !== -1 || s.indexOf('reliability') !== -1; })) {
579
+ return 'repair';
580
+ }
581
+ if (list.some(function (s) { return s.indexOf('feature') !== -1 || s.indexOf('capability') !== -1 || s.indexOf('stagnation') !== -1; })) {
582
+ return 'innovate';
583
+ }
584
+ return 'optimize';
585
+ }
586
+
587
+ function chooseDistillationSource(data, analysis) {
588
+ const grouped = data && data.grouped ? data.grouped : {};
589
+ let best = null;
590
+ Object.keys(grouped).forEach(function (geneId) {
591
+ const g = grouped[geneId];
592
+ if (!g || g.total_count <= 0) return;
593
+ const score = (g.total_count * 2) + (g.avg_score || 0);
594
+ if (!best || score > best.score) {
595
+ best = { gene_id: geneId, group: g, score: score };
596
+ }
597
+ });
598
+ return best;
599
+ }
600
+
601
+ function synthesizeGeneFromPatterns(data, analysis, existingGenes) {
602
+ const source = chooseDistillationSource(data, analysis);
603
+ if (!source || !source.group) return null;
604
+
605
+ const group = source.group;
606
+ const existing = Array.isArray(existingGenes) ? existingGenes : [];
607
+ const sourceGene = existing.find(function (g) { return g && g.id === source.gene_id; }) || null;
608
+
609
+ const triggerFreq = {};
610
+ (group.triggers || []).forEach(function (arr) {
611
+ (Array.isArray(arr) ? arr : []).forEach(function (s) {
612
+ const k = String(s).toLowerCase();
613
+ triggerFreq[k] = (triggerFreq[k] || 0) + 1;
614
+ });
615
+ });
616
+ let signalsMatch = Object.keys(triggerFreq)
617
+ .sort(function (a, b) { return triggerFreq[b] - triggerFreq[a]; })
618
+ .slice(0, 6);
619
+ const summaryText = (group.summaries || []).slice(0, 5).join(' ');
620
+ const derivedTags = learningSignals.expandSignals(signalsMatch, summaryText)
621
+ .filter(function (tag) { return tag.indexOf('problem:') === 0 || tag.indexOf('area:') === 0; })
622
+ .slice(0, 4);
623
+ signalsMatch = Array.from(new Set(signalsMatch.concat(derivedTags)));
624
+ if (signalsMatch.length === 0 && sourceGene && Array.isArray(sourceGene.signals_match)) {
625
+ signalsMatch = sourceGene.signals_match.slice(0, 6);
626
+ }
627
+
628
+ const category = sourceGene && sourceGene.category ? sourceGene.category : inferCategoryFromSignals(signalsMatch);
629
+ const idSeed = {
630
+ type: 'Gene',
631
+ id: DISTILLED_ID_PREFIX + source.gene_id.replace(/^gene_/, '').replace(/^gene_distilled_/, ''),
632
+ category: category,
633
+ signals_match: signalsMatch,
634
+ strategy: sourceGene && Array.isArray(sourceGene.strategy) && sourceGene.strategy.length > 0
635
+ ? sourceGene.strategy.slice(0, 4)
636
+ : [
637
+ 'Identify the dominant repeated trigger pattern.',
638
+ 'Apply the smallest targeted change for that pattern.',
639
+ 'Run the narrowest validation that proves the regression is gone.',
640
+ 'Rollback immediately if validation fails.',
641
+ ],
642
+ };
643
+
644
+ let summaryBase = (group.summaries && group.summaries[0]) ? String(group.summaries[0]) : '';
645
+ if (!summaryBase) {
646
+ summaryBase = 'Reusable strategy for repeated successful pattern: ' + signalsMatch.slice(0, 3).join(', ');
647
+ }
648
+
649
+ const gene = {
650
+ type: 'Gene',
651
+ id: deriveDescriptiveId(idSeed),
652
+ summary: summaryBase.slice(0, 200),
653
+ category: category,
654
+ signals_match: signalsMatch,
655
+ preconditions: sourceGene && Array.isArray(sourceGene.preconditions) && sourceGene.preconditions.length > 0
656
+ ? sourceGene.preconditions.slice(0, 4)
657
+ : ['repeated success pattern observed in recent capsules'],
658
+ strategy: idSeed.strategy,
659
+ constraints: {
660
+ max_files: sourceGene && sourceGene.constraints && Number(sourceGene.constraints.max_files) > 0
661
+ ? Math.min(DISTILLED_MAX_FILES, Number(sourceGene.constraints.max_files))
662
+ : DISTILLED_MAX_FILES,
663
+ forbidden_paths: sourceGene && sourceGene.constraints && Array.isArray(sourceGene.constraints.forbidden_paths)
664
+ ? sourceGene.constraints.forbidden_paths.slice(0, 6)
665
+ : ['.git', 'node_modules'],
666
+ },
667
+ validation: sourceGene && Array.isArray(sourceGene.validation) && sourceGene.validation.length > 0
668
+ ? sourceGene.validation.slice(0, 4)
669
+ : ['node --test'],
670
+ };
671
+
672
+ return gene;
673
+ }
674
+
675
+ function finalizeDistilledGene(gene, requestLike, status) {
676
+ const state = readDistillerState();
677
+ state.last_distillation_at = new Date().toISOString();
678
+ state.last_data_hash = requestLike.data_hash;
679
+ state.last_gene_id = gene.id;
680
+ state.distillation_count = (state.distillation_count || 0) + 1;
681
+ writeDistillerState(state);
682
+
683
+ appendJsonl(distillerLogPath(), {
684
+ timestamp: new Date().toISOString(),
685
+ data_hash: requestLike.data_hash,
686
+ input_capsule_count: requestLike.input_capsule_count,
687
+ analysis_summary: requestLike.analysis_summary,
688
+ synthesized_gene_id: gene.id,
689
+ validation_passed: true,
690
+ validation_errors: [],
691
+ status: status || 'success',
692
+ gene: gene,
693
+ });
694
+ }
695
+
576
696
  // ---------------------------------------------------------------------------
577
697
  // Step 5b: completeDistillation -- validate LLM response and save gene
578
698
  // ---------------------------------------------------------------------------
579
699
  function completeDistillation(responseText) {
580
- var reqPath = distillRequestPath();
581
- var request = readJsonIfExists(reqPath, null);
700
+ const reqPath = distillRequestPath();
701
+ const request = readJsonIfExists(reqPath, null);
582
702
 
583
703
  if (!request) {
584
704
  console.warn('[Distiller] No pending distillation request found.');
585
705
  return { ok: false, reason: 'no_request' };
586
706
  }
587
707
 
588
- var rawGene = extractJsonFromLlmResponse(responseText);
708
+ const rawGene = extractJsonFromLlmResponse(responseText);
589
709
  if (!rawGene) {
590
710
  appendJsonl(distillerLogPath(), {
591
711
  timestamp: new Date().toISOString(),
@@ -597,13 +717,13 @@ function completeDistillation(responseText) {
597
717
  return { ok: false, reason: 'no_gene_in_response' };
598
718
  }
599
719
 
600
- var assetsDir = paths.getGepAssetsDir();
601
- var existingGenesJson = readJsonIfExists(path.join(assetsDir, 'genes.json'), { genes: [] });
602
- var existingGenes = existingGenesJson.genes || [];
720
+ const assetsDir = paths.getGepAssetsDir();
721
+ const existingGenesJson = readJsonIfExists(path.join(assetsDir, 'genes.json'), { genes: [] });
722
+ const existingGenes = existingGenesJson.genes || [];
603
723
 
604
- var validation = validateSynthesizedGene(rawGene, existingGenes);
724
+ const validation = validateSynthesizedGene(rawGene, existingGenes);
605
725
 
606
- var logEntry = {
726
+ const logEntry = {
607
727
  timestamp: new Date().toISOString(),
608
728
  data_hash: request.data_hash,
609
729
  input_capsule_count: request.input_capsule_count,
@@ -620,18 +740,18 @@ function completeDistillation(responseText) {
620
740
  return { ok: false, reason: 'validation_failed', errors: validation.errors };
621
741
  }
622
742
 
623
- var gene = validation.gene;
743
+ const gene = validation.gene;
624
744
  gene._distilled_meta = {
625
745
  distilled_at: new Date().toISOString(),
626
746
  source_capsule_count: request.input_capsule_count,
627
747
  data_hash: request.data_hash,
628
748
  };
629
749
 
630
- var assetStore = require('./assetStore');
750
+ const assetStore = require('./assetStore');
631
751
  assetStore.upsertGene(gene);
632
752
  console.log('[Distiller] Gene "' + gene.id + '" written to genes.json.');
633
753
 
634
- var state = readDistillerState();
754
+ const state = readDistillerState();
635
755
  state.last_distillation_at = new Date().toISOString();
636
756
  state.last_data_hash = request.data_hash;
637
757
  state.last_gene_id = gene.id;
@@ -649,7 +769,7 @@ function completeDistillation(responseText) {
649
769
 
650
770
  if (process.env.SKILL_AUTO_PUBLISH !== '0') {
651
771
  try {
652
- var skillPublisher = require('./skillPublisher');
772
+ const skillPublisher = require('./skillPublisher');
653
773
  skillPublisher.publishSkillToHub(gene).then(function (res) {
654
774
  if (res.ok) {
655
775
  console.log('[Distiller] Skill published to Hub: ' + (res.result?.skill_id || gene.id));
@@ -665,11 +785,67 @@ function completeDistillation(responseText) {
665
785
  return { ok: true, gene: gene };
666
786
  }
667
787
 
788
+ function autoDistill() {
789
+ const data = collectDistillationData();
790
+ if (data.successCapsules.length < DISTILLER_MIN_CAPSULES) {
791
+ return { ok: false, reason: 'insufficient_data' };
792
+ }
793
+
794
+ const state = readDistillerState();
795
+ if (state.last_data_hash === data.dataHash) {
796
+ return { ok: false, reason: 'idempotent_skip' };
797
+ }
798
+
799
+ const analysis = analyzePatterns(data);
800
+ const assetsDir = paths.getGepAssetsDir();
801
+ const existingGenesJson = readJsonIfExists(path.join(assetsDir, 'genes.json'), { genes: [] });
802
+ const existingGenes = existingGenesJson.genes || [];
803
+ const rawGene = synthesizeGeneFromPatterns(data, analysis, existingGenes);
804
+ if (!rawGene) return { ok: false, reason: 'no_candidate_gene' };
805
+
806
+ const validation = validateSynthesizedGene(rawGene, existingGenes);
807
+ if (!validation.valid) {
808
+ appendJsonl(distillerLogPath(), {
809
+ timestamp: new Date().toISOString(),
810
+ data_hash: data.dataHash,
811
+ status: 'auto_validation_failed',
812
+ synthesized_gene_id: validation.gene ? validation.gene.id : null,
813
+ validation_errors: validation.errors,
814
+ });
815
+ return { ok: false, reason: 'validation_failed', errors: validation.errors };
816
+ }
817
+
818
+ const gene = validation.gene;
819
+ gene._distilled_meta = {
820
+ distilled_at: new Date().toISOString(),
821
+ source_capsule_count: data.successCapsules.length,
822
+ data_hash: data.dataHash,
823
+ auto_distilled: true,
824
+ };
825
+
826
+ const assetStore = require('./assetStore');
827
+ assetStore.upsertGene(gene);
828
+ finalizeDistilledGene(gene, {
829
+ data_hash: data.dataHash,
830
+ input_capsule_count: data.successCapsules.length,
831
+ analysis_summary: {
832
+ high_frequency_count: analysis.high_frequency.length,
833
+ drift_count: analysis.strategy_drift.length,
834
+ gap_count: analysis.coverage_gaps.length,
835
+ success_rate: Math.round(analysis.success_rate * 100) / 100,
836
+ },
837
+ }, 'auto_success');
838
+
839
+ return { ok: true, gene: gene, auto: true };
840
+ }
841
+
668
842
  module.exports = {
669
843
  collectDistillationData: collectDistillationData,
670
844
  analyzePatterns: analyzePatterns,
845
+ synthesizeGeneFromPatterns: synthesizeGeneFromPatterns,
671
846
  prepareDistillation: prepareDistillation,
672
847
  completeDistillation: completeDistillation,
848
+ autoDistill: autoDistill,
673
849
  validateSynthesizedGene: validateSynthesizedGene,
674
850
  sanitizeSignalsMatch: sanitizeSignalsMatch,
675
851
  shouldDistill: shouldDistill,