@evomap/evolver 1.28.1

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 (52) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +290 -0
  3. package/README.zh-CN.md +236 -0
  4. package/SKILL.md +132 -0
  5. package/assets/gep/capsules.json +79 -0
  6. package/assets/gep/events.jsonl +7 -0
  7. package/assets/gep/genes.json +108 -0
  8. package/index.js +530 -0
  9. package/package.json +38 -0
  10. package/src/canary.js +13 -0
  11. package/src/evolve.js +1704 -0
  12. package/src/gep/a2a.js +173 -0
  13. package/src/gep/a2aProtocol.js +736 -0
  14. package/src/gep/analyzer.js +35 -0
  15. package/src/gep/assetCallLog.js +130 -0
  16. package/src/gep/assetStore.js +297 -0
  17. package/src/gep/assets.js +36 -0
  18. package/src/gep/bridge.js +71 -0
  19. package/src/gep/candidates.js +142 -0
  20. package/src/gep/contentHash.js +65 -0
  21. package/src/gep/deviceId.js +209 -0
  22. package/src/gep/envFingerprint.js +83 -0
  23. package/src/gep/hubReview.js +206 -0
  24. package/src/gep/hubSearch.js +237 -0
  25. package/src/gep/issueReporter.js +262 -0
  26. package/src/gep/llmReview.js +92 -0
  27. package/src/gep/memoryGraph.js +771 -0
  28. package/src/gep/memoryGraphAdapter.js +203 -0
  29. package/src/gep/mutation.js +186 -0
  30. package/src/gep/narrativeMemory.js +108 -0
  31. package/src/gep/paths.js +113 -0
  32. package/src/gep/personality.js +355 -0
  33. package/src/gep/prompt.js +566 -0
  34. package/src/gep/questionGenerator.js +212 -0
  35. package/src/gep/reflection.js +127 -0
  36. package/src/gep/sanitize.js +67 -0
  37. package/src/gep/selector.js +250 -0
  38. package/src/gep/signals.js +417 -0
  39. package/src/gep/skillDistiller.js +499 -0
  40. package/src/gep/solidify.js +1681 -0
  41. package/src/gep/strategy.js +126 -0
  42. package/src/gep/taskReceiver.js +528 -0
  43. package/src/gep/validationReport.js +55 -0
  44. package/src/ops/cleanup.js +80 -0
  45. package/src/ops/commentary.js +60 -0
  46. package/src/ops/health_check.js +106 -0
  47. package/src/ops/index.js +11 -0
  48. package/src/ops/innovation.js +67 -0
  49. package/src/ops/lifecycle.js +168 -0
  50. package/src/ops/self_repair.js +72 -0
  51. package/src/ops/skills_monitor.js +143 -0
  52. package/src/ops/trigger.js +33 -0
@@ -0,0 +1,212 @@
1
+ // ---------------------------------------------------------------------------
2
+ // questionGenerator -- analyzes evolution context (signals, session transcripts,
3
+ // recent events) and generates proactive questions for the Hub bounty system.
4
+ //
5
+ // Questions are sent via the A2A fetch payload.questions field. The Hub creates
6
+ // bounties from them, enabling multi-agent collaborative problem solving.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { getEvolutionDir } = require('./paths');
12
+
13
+ const QUESTION_STATE_FILE = path.join(getEvolutionDir(), 'question_generator_state.json');
14
+ const MIN_INTERVAL_MS = 3 * 60 * 60 * 1000; // at most once per 3 hours
15
+ const MAX_QUESTIONS_PER_CYCLE = 2;
16
+
17
+ function readState() {
18
+ try {
19
+ if (fs.existsSync(QUESTION_STATE_FILE)) {
20
+ return JSON.parse(fs.readFileSync(QUESTION_STATE_FILE, 'utf8'));
21
+ }
22
+ } catch (_) {}
23
+ return { lastAskedAt: null, recentQuestions: [] };
24
+ }
25
+
26
+ function writeState(state) {
27
+ try {
28
+ const dir = path.dirname(QUESTION_STATE_FILE);
29
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
30
+ fs.writeFileSync(QUESTION_STATE_FILE, JSON.stringify(state, null, 2) + '\n');
31
+ } catch (_) {}
32
+ }
33
+
34
+ function isDuplicate(question, recentQuestions) {
35
+ var qLower = question.toLowerCase();
36
+ for (var i = 0; i < recentQuestions.length; i++) {
37
+ var prev = String(recentQuestions[i] || '').toLowerCase();
38
+ if (prev === qLower) return true;
39
+ // fuzzy: if >70% overlap by word set
40
+ var qWords = new Set(qLower.split(/\s+/).filter(function(w) { return w.length > 2; }));
41
+ var pWords = new Set(prev.split(/\s+/).filter(function(w) { return w.length > 2; }));
42
+ if (qWords.size === 0 || pWords.size === 0) continue;
43
+ var overlap = 0;
44
+ qWords.forEach(function(w) { if (pWords.has(w)) overlap++; });
45
+ if (overlap / Math.max(qWords.size, pWords.size) > 0.7) return true;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ /**
51
+ * Generate proactive questions based on evolution context.
52
+ *
53
+ * @param {object} opts
54
+ * @param {string[]} opts.signals - current cycle signals
55
+ * @param {object[]} opts.recentEvents - recent EvolutionEvent objects
56
+ * @param {string} opts.sessionTranscript - recent session transcript
57
+ * @param {string} opts.memorySnippet - MEMORY.md content
58
+ * @returns {Array<{ question: string, amount: number, signals: string[] }>}
59
+ */
60
+ function generateQuestions(opts) {
61
+ var o = opts || {};
62
+ var signals = Array.isArray(o.signals) ? o.signals : [];
63
+ var recentEvents = Array.isArray(o.recentEvents) ? o.recentEvents : [];
64
+ var transcript = String(o.sessionTranscript || '');
65
+ var memory = String(o.memorySnippet || '');
66
+
67
+ var state = readState();
68
+
69
+ // Rate limit: don't ask too frequently
70
+ if (state.lastAskedAt) {
71
+ var elapsed = Date.now() - new Date(state.lastAskedAt).getTime();
72
+ if (elapsed < MIN_INTERVAL_MS) return [];
73
+ }
74
+
75
+ var candidates = [];
76
+ var signalSet = new Set(signals);
77
+
78
+ // --- Strategy 1: Recurring errors the agent cannot resolve ---
79
+ if (signalSet.has('recurring_error') || signalSet.has('high_failure_ratio')) {
80
+ var errSig = signals.find(function(s) { return s.startsWith('recurring_errsig'); });
81
+ if (errSig) {
82
+ var errDetail = errSig.replace(/^recurring_errsig\(\d+x\):/, '').trim().slice(0, 120);
83
+ candidates.push({
84
+ question: 'Recurring error in evolution cycle that auto-repair cannot resolve: ' + errDetail + ' -- What approaches or patches have worked for similar issues?',
85
+ amount: 0,
86
+ signals: ['recurring_error', 'auto_repair_failed'],
87
+ priority: 3,
88
+ });
89
+ }
90
+ }
91
+
92
+ // --- Strategy 2: Capability gaps detected from user conversations ---
93
+ if (signalSet.has('capability_gap') || signalSet.has('unsupported_input_type')) {
94
+ var gapContext = '';
95
+ var lines = transcript.split('\n');
96
+ for (var i = 0; i < lines.length; i++) {
97
+ if (/not supported|cannot|unsupported|not implemented/i.test(lines[i])) {
98
+ gapContext = lines[i].replace(/\s+/g, ' ').trim().slice(0, 150);
99
+ break;
100
+ }
101
+ }
102
+ if (gapContext) {
103
+ candidates.push({
104
+ question: 'Capability gap detected in agent environment: ' + gapContext + ' -- How can this be addressed or what alternative approaches exist?',
105
+ amount: 0,
106
+ signals: ['capability_gap'],
107
+ priority: 2,
108
+ });
109
+ }
110
+ }
111
+
112
+ // --- Strategy 3: Stagnation / saturation -- seek new directions ---
113
+ if (signalSet.has('evolution_saturation') || signalSet.has('force_steady_state')) {
114
+ var recentGenes = [];
115
+ var last5 = recentEvents.slice(-5);
116
+ for (var j = 0; j < last5.length; j++) {
117
+ var genes = last5[j].genes_used;
118
+ if (Array.isArray(genes) && genes.length > 0) {
119
+ recentGenes.push(genes[0]);
120
+ }
121
+ }
122
+ var uniqueGenes = Array.from(new Set(recentGenes));
123
+ candidates.push({
124
+ question: 'Agent evolution has reached saturation after exhausting genes: [' + uniqueGenes.join(', ') + ']. What new evolution directions, automation patterns, or capability genes would be most valuable?',
125
+ amount: 0,
126
+ signals: ['evolution_saturation', 'innovation_needed'],
127
+ priority: 1,
128
+ });
129
+ }
130
+
131
+ // --- Strategy 4: Consecutive failure streak -- seek external help ---
132
+ var failStreak = signals.find(function(s) { return s.startsWith('consecutive_failure_streak_'); });
133
+ if (failStreak) {
134
+ var streakCount = parseInt(failStreak.replace('consecutive_failure_streak_', ''), 10) || 0;
135
+ if (streakCount >= 4) {
136
+ var failGene = signals.find(function(s) { return s.startsWith('ban_gene:'); });
137
+ var failGeneId = failGene ? failGene.replace('ban_gene:', '') : 'unknown';
138
+ candidates.push({
139
+ question: 'Agent has failed ' + streakCount + ' consecutive evolution cycles (last gene: ' + failGeneId + '). The current approach is exhausted. What alternative strategies or environmental fixes should be tried?',
140
+ amount: 0,
141
+ signals: ['failure_streak', 'external_help_needed'],
142
+ priority: 3,
143
+ });
144
+ }
145
+ }
146
+
147
+ // --- Strategy 5: User feature requests the agent can amplify ---
148
+ if (signalSet.has('user_feature_request') || signals.some(function (s) { return String(s).startsWith('user_feature_request:'); })) {
149
+ var featureLines = transcript.split('\n').filter(function(l) {
150
+ return /\b(add|implement|create|build|i want|i need|please add)\b/i.test(l);
151
+ });
152
+ if (featureLines.length > 0) {
153
+ var featureContext = featureLines[0].replace(/\s+/g, ' ').trim().slice(0, 150);
154
+ candidates.push({
155
+ question: 'User requested a feature that may benefit from community solutions: ' + featureContext + ' -- Are there existing implementations or best practices for this?',
156
+ amount: 0,
157
+ signals: ['user_feature_request', 'community_solution_sought'],
158
+ priority: 1,
159
+ });
160
+ }
161
+ }
162
+
163
+ // --- Strategy 6: Performance bottleneck -- seek optimization patterns ---
164
+ if (signalSet.has('perf_bottleneck')) {
165
+ var perfLines = transcript.split('\n').filter(function(l) {
166
+ return /\b(slow|timeout|latency|bottleneck|high cpu|high memory)\b/i.test(l);
167
+ });
168
+ if (perfLines.length > 0) {
169
+ var perfContext = perfLines[0].replace(/\s+/g, ' ').trim().slice(0, 150);
170
+ candidates.push({
171
+ question: 'Performance bottleneck detected: ' + perfContext + ' -- What optimization strategies or architectural patterns address this?',
172
+ amount: 0,
173
+ signals: ['perf_bottleneck', 'optimization_sought'],
174
+ priority: 2,
175
+ });
176
+ }
177
+ }
178
+
179
+ if (candidates.length === 0) return [];
180
+
181
+ // Sort by priority (higher = more urgent)
182
+ candidates.sort(function(a, b) { return b.priority - a.priority; });
183
+
184
+ // De-duplicate against recently asked questions
185
+ var recentQTexts = Array.isArray(state.recentQuestions) ? state.recentQuestions : [];
186
+ var filtered = [];
187
+ for (var fi = 0; fi < candidates.length && filtered.length < MAX_QUESTIONS_PER_CYCLE; fi++) {
188
+ if (!isDuplicate(candidates[fi].question, recentQTexts)) {
189
+ filtered.push(candidates[fi]);
190
+ }
191
+ }
192
+
193
+ if (filtered.length === 0) return [];
194
+
195
+ // Update state
196
+ var newRecentQuestions = recentQTexts.concat(filtered.map(function(q) { return q.question; }));
197
+ // Keep only last 20 questions in history
198
+ if (newRecentQuestions.length > 20) {
199
+ newRecentQuestions = newRecentQuestions.slice(-20);
200
+ }
201
+ writeState({
202
+ lastAskedAt: new Date().toISOString(),
203
+ recentQuestions: newRecentQuestions,
204
+ });
205
+
206
+ // Strip internal priority field before returning
207
+ return filtered.map(function(q) {
208
+ return { question: q.question, amount: q.amount, signals: q.signals };
209
+ });
210
+ }
211
+
212
+ module.exports = { generateQuestions };
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { getReflectionLogPath, getEvolutionDir } = require('./paths');
6
+
7
+ const REFLECTION_INTERVAL_CYCLES = 5;
8
+ const REFLECTION_COOLDOWN_MS = 30 * 60 * 1000;
9
+
10
+ function ensureDir(dir) {
11
+ try { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } catch (_) {}
12
+ }
13
+
14
+ function shouldReflect({ cycleCount, recentEvents }) {
15
+ if (!Number.isFinite(cycleCount) || cycleCount < REFLECTION_INTERVAL_CYCLES) return false;
16
+ if (cycleCount % REFLECTION_INTERVAL_CYCLES !== 0) return false;
17
+
18
+ const logPath = getReflectionLogPath();
19
+ try {
20
+ if (fs.existsSync(logPath)) {
21
+ const stat = fs.statSync(logPath);
22
+ if (Date.now() - stat.mtimeMs < REFLECTION_COOLDOWN_MS) return false;
23
+ }
24
+ } catch (_) {}
25
+
26
+ return true;
27
+ }
28
+
29
+ function buildReflectionContext({ recentEvents, signals, memoryAdvice, narrative }) {
30
+ const parts = ['You are performing a strategic reflection on recent evolution cycles.'];
31
+ parts.push('Analyze the patterns below and provide concise strategic guidance.');
32
+ parts.push('');
33
+
34
+ if (Array.isArray(recentEvents) && recentEvents.length > 0) {
35
+ const last10 = recentEvents.slice(-10);
36
+ const successCount = last10.filter(e => e && e.outcome && e.outcome.status === 'success').length;
37
+ const failCount = last10.filter(e => e && e.outcome && e.outcome.status === 'failed').length;
38
+ const intents = {};
39
+ last10.forEach(e => {
40
+ const i = e && e.intent ? e.intent : 'unknown';
41
+ intents[i] = (intents[i] || 0) + 1;
42
+ });
43
+ const genes = {};
44
+ last10.forEach(e => {
45
+ const g = e && Array.isArray(e.genes_used) && e.genes_used[0] ? e.genes_used[0] : 'unknown';
46
+ genes[g] = (genes[g] || 0) + 1;
47
+ });
48
+
49
+ parts.push('## Recent Cycle Statistics (last 10)');
50
+ parts.push(`- Success: ${successCount}, Failed: ${failCount}`);
51
+ parts.push(`- Intent distribution: ${JSON.stringify(intents)}`);
52
+ parts.push(`- Gene usage: ${JSON.stringify(genes)}`);
53
+ parts.push('');
54
+ }
55
+
56
+ if (Array.isArray(signals) && signals.length > 0) {
57
+ parts.push('## Current Signals');
58
+ parts.push(signals.slice(0, 20).join(', '));
59
+ parts.push('');
60
+ }
61
+
62
+ if (memoryAdvice) {
63
+ parts.push('## Memory Graph Advice');
64
+ if (memoryAdvice.preferredGeneId) {
65
+ parts.push(`- Preferred gene: ${memoryAdvice.preferredGeneId}`);
66
+ }
67
+ if (Array.isArray(memoryAdvice.bannedGeneIds) && memoryAdvice.bannedGeneIds.length > 0) {
68
+ parts.push(`- Banned genes: ${memoryAdvice.bannedGeneIds.join(', ')}`);
69
+ }
70
+ if (memoryAdvice.explanation) {
71
+ parts.push(`- Explanation: ${memoryAdvice.explanation}`);
72
+ }
73
+ parts.push('');
74
+ }
75
+
76
+ if (narrative) {
77
+ parts.push('## Recent Evolution Narrative');
78
+ parts.push(String(narrative).slice(0, 3000));
79
+ parts.push('');
80
+ }
81
+
82
+ parts.push('## Questions to Answer');
83
+ parts.push('1. Are there persistent signals being ignored?');
84
+ parts.push('2. Is the gene selection strategy optimal, or are we stuck in a local maximum?');
85
+ parts.push('3. Should the balance between repair/optimize/innovate shift?');
86
+ parts.push('4. Are there capability gaps that no current gene addresses?');
87
+ parts.push('5. What single strategic adjustment would have the highest impact?');
88
+ parts.push('');
89
+ parts.push('Respond with a JSON object: { "insights": [...], "strategy_adjustment": "...", "priority_signals": [...] }');
90
+
91
+ return parts.join('\n');
92
+ }
93
+
94
+ function recordReflection(reflection) {
95
+ const logPath = getReflectionLogPath();
96
+ ensureDir(path.dirname(logPath));
97
+
98
+ const entry = JSON.stringify({
99
+ ts: new Date().toISOString(),
100
+ type: 'reflection',
101
+ ...reflection,
102
+ }) + '\n';
103
+
104
+ fs.appendFileSync(logPath, entry, 'utf8');
105
+ }
106
+
107
+ function loadRecentReflections(count) {
108
+ const n = Number.isFinite(count) ? count : 3;
109
+ const logPath = getReflectionLogPath();
110
+ try {
111
+ if (!fs.existsSync(logPath)) return [];
112
+ const lines = fs.readFileSync(logPath, 'utf8').trim().split('\n').filter(Boolean);
113
+ return lines.slice(-n).map(line => {
114
+ try { return JSON.parse(line); } catch (_) { return null; }
115
+ }).filter(Boolean);
116
+ } catch (_) {
117
+ return [];
118
+ }
119
+ }
120
+
121
+ module.exports = {
122
+ shouldReflect,
123
+ buildReflectionContext,
124
+ recordReflection,
125
+ loadRecentReflections,
126
+ REFLECTION_INTERVAL_CYCLES,
127
+ };
@@ -0,0 +1,67 @@
1
+ // Pre-publish payload sanitization.
2
+ // Removes sensitive tokens, local paths, emails, and env references
3
+ // from capsule payloads before broadcasting to the hub.
4
+
5
+ // Patterns to redact (replaced with placeholder)
6
+ const REDACT_PATTERNS = [
7
+ // API keys & tokens (generic)
8
+ /Bearer\s+[A-Za-z0-9\-._~+\/]+=*/g,
9
+ /sk-[A-Za-z0-9]{20,}/g,
10
+ /token[=:]\s*["']?[A-Za-z0-9\-._~+\/]{16,}["']?/gi,
11
+ /api[_-]?key[=:]\s*["']?[A-Za-z0-9\-._~+\/]{16,}["']?/gi,
12
+ /secret[=:]\s*["']?[A-Za-z0-9\-._~+\/]{16,}["']?/gi,
13
+ /password[=:]\s*["']?[^\s"',;)}\]]{6,}["']?/gi,
14
+ // GitHub tokens (ghp_, gho_, ghu_, ghs_, github_pat_)
15
+ /ghp_[A-Za-z0-9]{36,}/g,
16
+ /gho_[A-Za-z0-9]{36,}/g,
17
+ /ghu_[A-Za-z0-9]{36,}/g,
18
+ /ghs_[A-Za-z0-9]{36,}/g,
19
+ /github_pat_[A-Za-z0-9_]{22,}/g,
20
+ // AWS access keys
21
+ /AKIA[0-9A-Z]{16}/g,
22
+ // OpenAI / Anthropic tokens
23
+ /sk-proj-[A-Za-z0-9\-_]{20,}/g,
24
+ /sk-ant-[A-Za-z0-9\-_]{20,}/g,
25
+ // npm tokens
26
+ /npm_[A-Za-z0-9]{36,}/g,
27
+ // Private keys
28
+ /-----BEGIN\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----/g,
29
+ // Basic auth in URLs (redact only credentials, keep :// and @)
30
+ /(?<=:\/\/)[^@\s]+:[^@\s]+(?=@)/g,
31
+ // Local filesystem paths
32
+ /\/home\/[^\s"',;)}\]]+/g,
33
+ /\/Users\/[^\s"',;)}\]]+/g,
34
+ /[A-Z]:\\[^\s"',;)}\]]+/g,
35
+ // Email addresses
36
+ /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
37
+ // .env file references
38
+ /\.env(?:\.[a-zA-Z]+)?/g,
39
+ ];
40
+
41
+ const REDACTED = '[REDACTED]';
42
+
43
+ function redactString(str) {
44
+ if (typeof str !== 'string') return str;
45
+ let result = str;
46
+ for (const pattern of REDACT_PATTERNS) {
47
+ // Reset lastIndex for global regexes
48
+ pattern.lastIndex = 0;
49
+ result = result.replace(pattern, REDACTED);
50
+ }
51
+ return result;
52
+ }
53
+
54
+ /**
55
+ * Deep-clone and sanitize a capsule payload.
56
+ * Returns a new object with sensitive values redacted.
57
+ * Does NOT modify the original.
58
+ */
59
+ function sanitizePayload(capsule) {
60
+ if (!capsule || typeof capsule !== 'object') return capsule;
61
+ return JSON.parse(JSON.stringify(capsule), (_key, value) => {
62
+ if (typeof value === 'string') return redactString(value);
63
+ return value;
64
+ });
65
+ }
66
+
67
+ module.exports = { sanitizePayload, redactString };
@@ -0,0 +1,250 @@
1
+ function matchPatternToSignals(pattern, signals) {
2
+ if (!pattern || !signals || signals.length === 0) return false;
3
+ const p = String(pattern);
4
+ const sig = signals.map(s => String(s));
5
+
6
+ // Regex pattern: /body/flags
7
+ const regexLike = p.length >= 2 && p.startsWith('/') && p.lastIndexOf('/') > 0;
8
+ if (regexLike) {
9
+ const lastSlash = p.lastIndexOf('/');
10
+ const body = p.slice(1, lastSlash);
11
+ const flags = p.slice(lastSlash + 1);
12
+ try {
13
+ const re = new RegExp(body, flags || 'i');
14
+ return sig.some(s => re.test(s));
15
+ } catch (e) {
16
+ // fallback to substring
17
+ }
18
+ }
19
+
20
+ // Multi-language alias: "en_term|zh_term|ja_term" -- any branch matching = hit
21
+ if (p.includes('|') && !p.startsWith('/')) {
22
+ const branches = p.split('|').map(b => b.trim().toLowerCase()).filter(Boolean);
23
+ return branches.some(needle => sig.some(s => s.toLowerCase().includes(needle)));
24
+ }
25
+
26
+ const needle = p.toLowerCase();
27
+ return sig.some(s => s.toLowerCase().includes(needle));
28
+ }
29
+
30
+ function scoreGene(gene, signals) {
31
+ if (!gene || gene.type !== 'Gene') return 0;
32
+ const patterns = Array.isArray(gene.signals_match) ? gene.signals_match : [];
33
+ if (patterns.length === 0) return 0;
34
+ let score = 0;
35
+ for (const pat of patterns) {
36
+ if (matchPatternToSignals(pat, signals)) score += 1;
37
+ }
38
+ return score;
39
+ }
40
+
41
+ // Population-size-dependent drift intensity.
42
+ // In population genetics, genetic drift is stronger in small populations (Ne).
43
+ // driftIntensity: 0 = pure selection, 1 = pure drift (random).
44
+ // Formula: intensity = 1 / sqrt(Ne) where Ne = effective population size.
45
+ // This replaces the binary driftEnabled flag with a continuous spectrum.
46
+ function computeDriftIntensity(opts) {
47
+ // If explicitly enabled/disabled, use that as the baseline
48
+ var driftEnabled = !!(opts && opts.driftEnabled);
49
+
50
+ // Effective population size: active gene count in the pool
51
+ var effectivePopulationSize = opts && Number.isFinite(Number(opts.effectivePopulationSize))
52
+ ? Number(opts.effectivePopulationSize)
53
+ : null;
54
+
55
+ // If no Ne provided, fall back to gene pool size
56
+ var genePoolSize = opts && Number.isFinite(Number(opts.genePoolSize))
57
+ ? Number(opts.genePoolSize)
58
+ : null;
59
+
60
+ var ne = effectivePopulationSize || genePoolSize || null;
61
+
62
+ if (driftEnabled) {
63
+ // Explicit drift: use moderate-to-high intensity
64
+ return ne && ne > 1 ? Math.min(1, 1 / Math.sqrt(ne) + 0.3) : 0.7;
65
+ }
66
+
67
+ if (ne != null && ne > 0) {
68
+ // Population-dependent drift: small population = more drift
69
+ // Ne=1: intensity=1.0 (pure drift), Ne=25: intensity=0.2, Ne=100: intensity=0.1
70
+ return Math.min(1, 1 / Math.sqrt(ne));
71
+ }
72
+
73
+ return 0; // No drift info available, pure selection
74
+ }
75
+
76
+ function selectGene(genes, signals, opts) {
77
+ const genesList = Array.isArray(genes) ? genes : [];
78
+ const bannedGeneIds = opts && opts.bannedGeneIds ? opts.bannedGeneIds : new Set();
79
+ const driftEnabled = !!(opts && opts.driftEnabled);
80
+ const preferredGeneId = opts && typeof opts.preferredGeneId === 'string' ? opts.preferredGeneId : null;
81
+
82
+ // Compute continuous drift intensity based on effective population size
83
+ var driftIntensity = computeDriftIntensity({
84
+ driftEnabled: driftEnabled,
85
+ effectivePopulationSize: opts && opts.effectivePopulationSize,
86
+ genePoolSize: genesList.length,
87
+ });
88
+ var useDrift = driftEnabled || driftIntensity > 0.15;
89
+
90
+ var DISTILLED_PREFIX = 'gene_distilled_';
91
+ var DISTILLED_SCORE_FACTOR = 0.8;
92
+
93
+ const scored = genesList
94
+ .map(g => {
95
+ var s = scoreGene(g, signals);
96
+ if (s > 0 && g.id && String(g.id).startsWith(DISTILLED_PREFIX)) s *= DISTILLED_SCORE_FACTOR;
97
+ return { gene: g, score: s };
98
+ })
99
+ .filter(x => x.score > 0)
100
+ .sort((a, b) => b.score - a.score);
101
+
102
+ if (scored.length === 0) return { selected: null, alternatives: [], driftIntensity: driftIntensity };
103
+
104
+ // Memory graph preference: only override when the preferred gene is already a match candidate.
105
+ if (preferredGeneId) {
106
+ const preferred = scored.find(x => x.gene && x.gene.id === preferredGeneId);
107
+ if (preferred && (useDrift || !bannedGeneIds.has(preferredGeneId))) {
108
+ const rest = scored.filter(x => x.gene && x.gene.id !== preferredGeneId);
109
+ const filteredRest = useDrift ? rest : rest.filter(x => x.gene && !bannedGeneIds.has(x.gene.id));
110
+ return {
111
+ selected: preferred.gene,
112
+ alternatives: filteredRest.slice(0, 4).map(x => x.gene),
113
+ driftIntensity: driftIntensity,
114
+ };
115
+ }
116
+ }
117
+
118
+ // Low-efficiency suppression: do not repeat low-confidence paths unless drift is active.
119
+ const filtered = useDrift ? scored : scored.filter(x => x.gene && !bannedGeneIds.has(x.gene.id));
120
+ if (filtered.length === 0) return { selected: null, alternatives: scored.slice(0, 4).map(x => x.gene), driftIntensity: driftIntensity };
121
+
122
+ // Stochastic selection under drift: with probability proportional to driftIntensity,
123
+ // pick a random gene from the top candidates instead of always picking the best.
124
+ var selectedIdx = 0;
125
+ if (driftIntensity > 0 && filtered.length > 1 && Math.random() < driftIntensity) {
126
+ // Weighted random selection from top candidates (favor higher-scoring but allow lower)
127
+ var topN = Math.min(filtered.length, Math.max(2, Math.ceil(filtered.length * driftIntensity)));
128
+ selectedIdx = Math.floor(Math.random() * topN);
129
+ }
130
+
131
+ return {
132
+ selected: filtered[selectedIdx].gene,
133
+ alternatives: filtered.filter(function(_, i) { return i !== selectedIdx; }).slice(0, 4).map(x => x.gene),
134
+ driftIntensity: driftIntensity,
135
+ };
136
+ }
137
+
138
+ function selectCapsule(capsules, signals) {
139
+ const scored = (capsules || [])
140
+ .map(c => {
141
+ const triggers = Array.isArray(c.trigger) ? c.trigger : [];
142
+ const score = triggers.reduce((acc, t) => (matchPatternToSignals(t, signals) ? acc + 1 : acc), 0);
143
+ return { capsule: c, score };
144
+ })
145
+ .filter(x => x.score > 0)
146
+ .sort((a, b) => b.score - a.score);
147
+ return scored.length ? scored[0].capsule : null;
148
+ }
149
+
150
+ function computeSignalOverlap(signalsA, signalsB) {
151
+ if (!Array.isArray(signalsA) || !Array.isArray(signalsB)) return 0;
152
+ if (signalsA.length === 0 || signalsB.length === 0) return 0;
153
+ var setB = new Set(signalsB.map(function (s) { return String(s).toLowerCase(); }));
154
+ var hits = 0;
155
+ for (var i = 0; i < signalsA.length; i++) {
156
+ if (setB.has(String(signalsA[i]).toLowerCase())) hits++;
157
+ }
158
+ return hits / Math.max(signalsA.length, 1);
159
+ }
160
+
161
+ var FAILED_CAPSULE_BAN_THRESHOLD = 2;
162
+ var FAILED_CAPSULE_OVERLAP_MIN = 0.6;
163
+
164
+ function banGenesFromFailedCapsules(failedCapsules, signals, existingBans) {
165
+ var bans = existingBans instanceof Set ? new Set(existingBans) : new Set();
166
+ if (!Array.isArray(failedCapsules) || failedCapsules.length === 0) return bans;
167
+ var geneFailCounts = {};
168
+ for (var i = 0; i < failedCapsules.length; i++) {
169
+ var fc = failedCapsules[i];
170
+ if (!fc || !fc.gene) continue;
171
+ var overlap = computeSignalOverlap(signals, fc.trigger || []);
172
+ if (overlap < FAILED_CAPSULE_OVERLAP_MIN) continue;
173
+ var gid = String(fc.gene);
174
+ geneFailCounts[gid] = (geneFailCounts[gid] || 0) + 1;
175
+ }
176
+ var keys = Object.keys(geneFailCounts);
177
+ for (var j = 0; j < keys.length; j++) {
178
+ if (geneFailCounts[keys[j]] >= FAILED_CAPSULE_BAN_THRESHOLD) {
179
+ bans.add(keys[j]);
180
+ }
181
+ }
182
+ return bans;
183
+ }
184
+
185
+ function selectGeneAndCapsule({ genes, capsules, signals, memoryAdvice, driftEnabled, failedCapsules }) {
186
+ const bannedGeneIds =
187
+ memoryAdvice && memoryAdvice.bannedGeneIds instanceof Set ? memoryAdvice.bannedGeneIds : new Set();
188
+ const preferredGeneId = memoryAdvice && memoryAdvice.preferredGeneId ? memoryAdvice.preferredGeneId : null;
189
+
190
+ var effectiveBans = banGenesFromFailedCapsules(
191
+ Array.isArray(failedCapsules) ? failedCapsules : [],
192
+ signals,
193
+ bannedGeneIds
194
+ );
195
+
196
+ const { selected, alternatives, driftIntensity } = selectGene(genes, signals, {
197
+ bannedGeneIds: effectiveBans,
198
+ preferredGeneId,
199
+ driftEnabled: !!driftEnabled,
200
+ });
201
+ const capsule = selectCapsule(capsules, signals);
202
+ const selector = buildSelectorDecision({
203
+ gene: selected,
204
+ capsule,
205
+ signals,
206
+ alternatives,
207
+ memoryAdvice,
208
+ driftEnabled,
209
+ driftIntensity,
210
+ });
211
+ return {
212
+ selectedGene: selected,
213
+ capsuleCandidates: capsule ? [capsule] : [],
214
+ selector,
215
+ driftIntensity,
216
+ };
217
+ }
218
+
219
+ function buildSelectorDecision({ gene, capsule, signals, alternatives, memoryAdvice, driftEnabled, driftIntensity }) {
220
+ const reason = [];
221
+ if (gene) reason.push('signals match gene.signals_match');
222
+ if (capsule) reason.push('capsule trigger matches signals');
223
+ if (!gene) reason.push('no matching gene found; new gene may be required');
224
+ if (signals && signals.length) reason.push(`signals: ${signals.join(', ')}`);
225
+
226
+ if (memoryAdvice && Array.isArray(memoryAdvice.explanation) && memoryAdvice.explanation.length) {
227
+ reason.push(`memory_graph: ${memoryAdvice.explanation.join(' | ')}`);
228
+ }
229
+ if (driftEnabled) {
230
+ reason.push('random_drift_override: true');
231
+ }
232
+ if (Number.isFinite(driftIntensity) && driftIntensity > 0) {
233
+ reason.push(`drift_intensity: ${driftIntensity.toFixed(3)}`);
234
+ }
235
+
236
+ return {
237
+ selected: gene ? gene.id : null,
238
+ reason,
239
+ alternatives: Array.isArray(alternatives) ? alternatives.map(g => g.id) : [],
240
+ };
241
+ }
242
+
243
+ module.exports = {
244
+ selectGeneAndCapsule,
245
+ selectGene,
246
+ selectCapsule,
247
+ buildSelectorDecision,
248
+ matchPatternToSignals,
249
+ };
250
+