'use strict'; var fs = require('fs'); var path = require('path'); var crypto = require('crypto'); var paths = require('./paths'); var DISTILLER_MIN_CAPSULES = parseInt(process.env.DISTILLER_MIN_CAPSULES || '10', 10) || 10; var DISTILLER_INTERVAL_HOURS = parseInt(process.env.DISTILLER_INTERVAL_HOURS || '24', 10) || 24; var DISTILLER_MIN_SUCCESS_RATE = parseFloat(process.env.DISTILLER_MIN_SUCCESS_RATE || '0.7') || 0.7; var DISTILLED_MAX_FILES = 12; var DISTILLED_ID_PREFIX = 'gene_distilled_'; function ensureDir(dir) { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } function readJsonIfExists(filePath, fallback) { try { if (!fs.existsSync(filePath)) return fallback; var raw = fs.readFileSync(filePath, 'utf8'); if (!raw.trim()) return fallback; return JSON.parse(raw); } catch (e) { return fallback; } } function readJsonlIfExists(filePath) { try { if (!fs.existsSync(filePath)) return []; var raw = fs.readFileSync(filePath, 'utf8'); return raw.split('\n').map(function (l) { return l.trim(); }).filter(Boolean).map(function (l) { try { return JSON.parse(l); } catch (e) { return null; } }).filter(Boolean); } catch (e) { return []; } } function appendJsonl(filePath, obj) { ensureDir(path.dirname(filePath)); fs.appendFileSync(filePath, JSON.stringify(obj) + '\n', 'utf8'); } function distillerLogPath() { return path.join(paths.getMemoryDir(), 'distiller_log.jsonl'); } function distillerStatePath() { return path.join(paths.getMemoryDir(), 'distiller_state.json'); } function readDistillerState() { return readJsonIfExists(distillerStatePath(), {}); } function writeDistillerState(state) { ensureDir(path.dirname(distillerStatePath())); var tmp = distillerStatePath() + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(state, null, 2) + '\n', 'utf8'); fs.renameSync(tmp, distillerStatePath()); } function computeDataHash(capsules) { var ids = capsules.map(function (c) { return c.id || ''; }).sort(); return crypto.createHash('sha256').update(ids.join('|')).digest('hex').slice(0, 16); } // --------------------------------------------------------------------------- // Step 1: collectDistillationData // --------------------------------------------------------------------------- function collectDistillationData() { var assetsDir = paths.getGepAssetsDir(); var evoDir = paths.getEvolutionDir(); var capsulesJson = readJsonIfExists(path.join(assetsDir, 'capsules.json'), { capsules: [] }); var capsulesJsonl = readJsonlIfExists(path.join(assetsDir, 'capsules.jsonl')); var allCapsules = [].concat(capsulesJson.capsules || [], capsulesJsonl); var unique = new Map(); allCapsules.forEach(function (c) { if (c && c.id) unique.set(String(c.id), c); }); allCapsules = Array.from(unique.values()); var successCapsules = allCapsules.filter(function (c) { if (!c || !c.outcome) return false; var status = typeof c.outcome === 'string' ? c.outcome : c.outcome.status; if (status !== 'success') return false; var score = c.outcome && Number.isFinite(Number(c.outcome.score)) ? Number(c.outcome.score) : 1; return score >= DISTILLER_MIN_SUCCESS_RATE; }); var events = readJsonlIfExists(path.join(assetsDir, 'events.jsonl')); var memGraphPath = process.env.MEMORY_GRAPH_PATH || path.join(evoDir, 'memory_graph.jsonl'); var graphEntries = readJsonlIfExists(memGraphPath); var grouped = {}; successCapsules.forEach(function (c) { var geneId = c.gene || c.gene_id || 'unknown'; if (!grouped[geneId]) { grouped[geneId] = { gene_id: geneId, capsules: [], total_count: 0, total_score: 0, triggers: [], summaries: [], }; } var g = grouped[geneId]; g.capsules.push(c); g.total_count += 1; g.total_score += (c.outcome && Number.isFinite(Number(c.outcome.score))) ? Number(c.outcome.score) : 0.8; if (Array.isArray(c.trigger)) g.triggers.push(c.trigger); if (c.summary) g.summaries.push(String(c.summary)); }); Object.keys(grouped).forEach(function (id) { var g = grouped[id]; g.avg_score = g.total_count > 0 ? g.total_score / g.total_count : 0; }); return { successCapsules: successCapsules, allCapsules: allCapsules, events: events, graphEntries: graphEntries, grouped: grouped, dataHash: computeDataHash(successCapsules), }; } // --------------------------------------------------------------------------- // Step 2: analyzePatterns // --------------------------------------------------------------------------- function analyzePatterns(data) { var grouped = data.grouped; var report = { high_frequency: [], strategy_drift: [], coverage_gaps: [], total_success: data.successCapsules.length, total_capsules: data.allCapsules.length, success_rate: data.allCapsules.length > 0 ? data.successCapsules.length / data.allCapsules.length : 0, }; Object.keys(grouped).forEach(function (geneId) { var g = grouped[geneId]; if (g.total_count >= 5) { var flat = []; g.triggers.forEach(function (t) { if (Array.isArray(t)) flat = flat.concat(t); }); var freq = {}; flat.forEach(function (t) { var k = String(t).toLowerCase(); freq[k] = (freq[k] || 0) + 1; }); var top = Object.keys(freq).sort(function (a, b) { return freq[b] - freq[a]; }).slice(0, 5); report.high_frequency.push({ gene_id: geneId, count: g.total_count, avg_score: Math.round(g.avg_score * 100) / 100, top_triggers: top }); } if (g.summaries.length >= 3) { var first = g.summaries[0]; var last = g.summaries[g.summaries.length - 1]; if (first !== last) { var fw = new Set(first.toLowerCase().split(/\s+/)); var lw = new Set(last.toLowerCase().split(/\s+/)); var inter = 0; fw.forEach(function (w) { if (lw.has(w)) inter++; }); var union = fw.size + lw.size - inter; var sim = union > 0 ? inter / union : 1; if (sim < 0.6) { 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) }); } } } }); var signalFreq = {}; (data.events || []).forEach(function (evt) { if (evt && Array.isArray(evt.signals)) { evt.signals.forEach(function (s) { var k = String(s).toLowerCase(); signalFreq[k] = (signalFreq[k] || 0) + 1; }); } }); var covered = new Set(); Object.keys(grouped).forEach(function (geneId) { grouped[geneId].triggers.forEach(function (t) { if (Array.isArray(t)) t.forEach(function (s) { covered.add(String(s).toLowerCase()); }); }); }); var gaps = Object.keys(signalFreq) .filter(function (s) { return signalFreq[s] >= 3 && !covered.has(s); }) .sort(function (a, b) { return signalFreq[b] - signalFreq[a]; }) .slice(0, 10); if (gaps.length > 0) { report.coverage_gaps = gaps.map(function (s) { return { signal: s, frequency: signalFreq[s] }; }); } return report; } // --------------------------------------------------------------------------- // Step 3: LLM response parsing // --------------------------------------------------------------------------- function extractJsonFromLlmResponse(text) { var str = String(text || ''); var buffer = ''; var depth = 0; for (var i = 0; i < str.length; i++) { var ch = str[i]; if (ch === '{') { if (depth === 0) buffer = ''; depth++; buffer += ch; } else if (ch === '}') { depth--; buffer += ch; if (depth === 0 && buffer.length > 2) { try { var obj = JSON.parse(buffer); if (obj && typeof obj === 'object' && obj.type === 'Gene') return obj; } catch (e) {} buffer = ''; } if (depth < 0) depth = 0; } else if (depth > 0) { buffer += ch; } } return null; } function buildDistillationPrompt(analysis, existingGenes, sampleCapsules) { var genesRef = existingGenes.map(function (g) { return { id: g.id, category: g.category || null, signals_match: g.signals_match || [] }; }); var samples = sampleCapsules.slice(0, 8).map(function (c) { return { gene: c.gene || c.gene_id || null, trigger: c.trigger || [], summary: (c.summary || '').slice(0, 200), outcome: c.outcome || null }; }); return [ 'You are a Gene synthesis engine for the GEP (Genome Evolution Protocol).', '', 'Analyze the following successful evolution capsules and extract a reusable Gene.', '', 'RULES:', '- Strategy steps MUST be actionable operations, NOT summaries', '- Each step must be a concrete instruction an AI agent can execute', '- Do NOT describe what happened; describe what TO DO next time', '- The Gene MUST have a unique id starting with "' + DISTILLED_ID_PREFIX + '"', '- constraints.max_files MUST be <= ' + DISTILLED_MAX_FILES, '- constraints.forbidden_paths MUST include at least [".git", "node_modules"]', '- Output valid Gene JSON only (no markdown, no explanation)', '', 'SUCCESSFUL CAPSULES (grouped by pattern):', JSON.stringify(samples, null, 2), '', 'EXISTING GENES (avoid duplication):', JSON.stringify(genesRef, null, 2), '', 'ANALYSIS:', JSON.stringify(analysis, null, 2), '', 'Output a single Gene JSON object with these fields:', '{ "type": "Gene", "id": "gene_distilled_...", "category": "...", "signals_match": [...], "preconditions": [...], "strategy": [...], "constraints": { "max_files": N, "forbidden_paths": [...] }, "validation": [...] }', ].join('\n'); } function distillRequestPath() { return path.join(paths.getMemoryDir(), 'distill_request.json'); } // --------------------------------------------------------------------------- // Step 4: validateSynthesizedGene // --------------------------------------------------------------------------- function validateSynthesizedGene(gene, existingGenes) { var errors = []; if (!gene || typeof gene !== 'object') return { valid: false, errors: ['gene is not an object'] }; if (gene.type !== 'Gene') errors.push('missing or wrong type (must be "Gene")'); if (!gene.id || typeof gene.id !== 'string') errors.push('missing id'); if (!gene.category) errors.push('missing category'); if (!Array.isArray(gene.signals_match) || gene.signals_match.length === 0) errors.push('missing or empty signals_match'); if (!Array.isArray(gene.strategy) || gene.strategy.length === 0) errors.push('missing or empty strategy'); if (gene.id && !String(gene.id).startsWith(DISTILLED_ID_PREFIX)) { gene.id = DISTILLED_ID_PREFIX + String(gene.id).replace(/^gene_/, ''); } if (!gene.constraints || typeof gene.constraints !== 'object') gene.constraints = {}; if (!Array.isArray(gene.constraints.forbidden_paths) || gene.constraints.forbidden_paths.length === 0) { gene.constraints.forbidden_paths = ['.git', 'node_modules']; } if (!gene.constraints.forbidden_paths.some(function (p) { return p === '.git' || p === 'node_modules'; })) { errors.push('constraints.forbidden_paths must include .git or node_modules'); } if (!gene.constraints.max_files || gene.constraints.max_files > DISTILLED_MAX_FILES) { gene.constraints.max_files = DISTILLED_MAX_FILES; } var ALLOWED_PREFIXES = ['node ', 'npm ', 'npx ']; if (Array.isArray(gene.validation)) { gene.validation = gene.validation.filter(function (cmd) { var c = String(cmd || '').trim(); if (!c) return false; if (!ALLOWED_PREFIXES.some(function (p) { return c.startsWith(p); })) return false; if (/`|\$\(/.test(c)) return false; var stripped = c.replace(/"[^"]*"/g, '').replace(/'[^']*'/g, ''); return !/[;&|><]/.test(stripped); }); } var existingIds = new Set((existingGenes || []).map(function (g) { return g.id; })); if (gene.id && existingIds.has(gene.id)) { gene.id = gene.id + '_' + Date.now().toString(36); } if (gene.signals_match && existingGenes && existingGenes.length > 0) { var newSet = new Set(gene.signals_match.map(function (s) { return String(s).toLowerCase(); })); for (var i = 0; i < existingGenes.length; i++) { var eg = existingGenes[i]; var egSet = new Set((eg.signals_match || []).map(function (s) { return String(s).toLowerCase(); })); if (newSet.size > 0 && egSet.size > 0) { var overlap = 0; newSet.forEach(function (s) { if (egSet.has(s)) overlap++; }); if (overlap === newSet.size && overlap === egSet.size) { errors.push('signals_match fully overlaps with existing gene: ' + eg.id); } } } } return { valid: errors.length === 0, errors: errors, gene: gene }; } // --------------------------------------------------------------------------- // shouldDistill: gate check // --------------------------------------------------------------------------- function shouldDistill() { if (String(process.env.SKILL_DISTILLER || 'true').toLowerCase() === 'false') return false; var state = readDistillerState(); if (state.last_distillation_at) { var elapsed = Date.now() - new Date(state.last_distillation_at).getTime(); if (elapsed < DISTILLER_INTERVAL_HOURS * 3600000) return false; } var assetsDir = paths.getGepAssetsDir(); var capsulesJson = readJsonIfExists(path.join(assetsDir, 'capsules.json'), { capsules: [] }); var capsulesJsonl = readJsonlIfExists(path.join(assetsDir, 'capsules.jsonl')); var all = [].concat(capsulesJson.capsules || [], capsulesJsonl); var recent = all.slice(-10); var recentSuccess = recent.filter(function (c) { return c && c.outcome && (c.outcome.status === 'success' || c.outcome === 'success'); }).length; if (recentSuccess < 7) return false; var totalSuccess = all.filter(function (c) { return c && c.outcome && (c.outcome.status === 'success' || c.outcome === 'success'); }).length; if (totalSuccess < DISTILLER_MIN_CAPSULES) return false; return true; } // --------------------------------------------------------------------------- // Step 5a: prepareDistillation -- collect data, build prompt, write to file // --------------------------------------------------------------------------- function prepareDistillation() { console.log('[Distiller] Preparing skill distillation...'); var data = collectDistillationData(); console.log('[Distiller] Collected ' + data.successCapsules.length + ' successful capsules across ' + Object.keys(data.grouped).length + ' gene groups.'); if (data.successCapsules.length < DISTILLER_MIN_CAPSULES) { console.log('[Distiller] Not enough successful capsules (' + data.successCapsules.length + ' < ' + DISTILLER_MIN_CAPSULES + '). Skipping.'); return { ok: false, reason: 'insufficient_data' }; } var state = readDistillerState(); if (state.last_data_hash === data.dataHash) { console.log('[Distiller] Data unchanged since last distillation (hash: ' + data.dataHash + '). Skipping.'); return { ok: false, reason: 'idempotent_skip' }; } var analysis = analyzePatterns(data); console.log('[Distiller] Analysis: high_freq=' + analysis.high_frequency.length + ' drift=' + analysis.strategy_drift.length + ' gaps=' + analysis.coverage_gaps.length); var assetsDir = paths.getGepAssetsDir(); var existingGenesJson = readJsonIfExists(path.join(assetsDir, 'genes.json'), { genes: [] }); var existingGenes = existingGenesJson.genes || []; var prompt = buildDistillationPrompt(analysis, existingGenes, data.successCapsules); var memDir = paths.getMemoryDir(); ensureDir(memDir); var promptFileName = 'distill_prompt_' + Date.now() + '.txt'; var promptPath = path.join(memDir, promptFileName); fs.writeFileSync(promptPath, prompt, 'utf8'); var reqPath = distillRequestPath(); var requestData = { type: 'DistillationRequest', created_at: new Date().toISOString(), prompt_path: promptPath, data_hash: data.dataHash, input_capsule_count: data.successCapsules.length, analysis_summary: { high_frequency_count: analysis.high_frequency.length, drift_count: analysis.strategy_drift.length, gap_count: analysis.coverage_gaps.length, success_rate: Math.round(analysis.success_rate * 100) / 100, }, }; fs.writeFileSync(reqPath, JSON.stringify(requestData, null, 2) + '\n', 'utf8'); console.log('[Distiller] Prompt written to: ' + promptPath); return { ok: true, promptPath: promptPath, requestPath: reqPath, dataHash: data.dataHash }; } // --------------------------------------------------------------------------- // Step 5b: completeDistillation -- validate LLM response and save gene // --------------------------------------------------------------------------- function completeDistillation(responseText) { var reqPath = distillRequestPath(); var request = readJsonIfExists(reqPath, null); if (!request) { console.warn('[Distiller] No pending distillation request found.'); return { ok: false, reason: 'no_request' }; } var rawGene = extractJsonFromLlmResponse(responseText); if (!rawGene) { appendJsonl(distillerLogPath(), { timestamp: new Date().toISOString(), data_hash: request.data_hash, status: 'error', error: 'LLM response did not contain a valid Gene JSON', }); console.error('[Distiller] LLM response did not contain a valid Gene JSON.'); return { ok: false, reason: 'no_gene_in_response' }; } var assetsDir = paths.getGepAssetsDir(); var existingGenesJson = readJsonIfExists(path.join(assetsDir, 'genes.json'), { genes: [] }); var existingGenes = existingGenesJson.genes || []; var validation = validateSynthesizedGene(rawGene, existingGenes); var logEntry = { timestamp: new Date().toISOString(), data_hash: request.data_hash, input_capsule_count: request.input_capsule_count, analysis_summary: request.analysis_summary, synthesized_gene_id: validation.gene ? validation.gene.id : null, validation_passed: validation.valid, validation_errors: validation.errors, }; if (!validation.valid) { logEntry.status = 'validation_failed'; appendJsonl(distillerLogPath(), logEntry); console.warn('[Distiller] Gene failed validation: ' + validation.errors.join(', ')); return { ok: false, reason: 'validation_failed', errors: validation.errors }; } var gene = validation.gene; gene._distilled_meta = { distilled_at: new Date().toISOString(), source_capsule_count: request.input_capsule_count, data_hash: request.data_hash, }; var assetStore = require('./assetStore'); assetStore.upsertGene(gene); console.log('[Distiller] Gene "' + gene.id + '" written to genes.json.'); var state = readDistillerState(); state.last_distillation_at = new Date().toISOString(); state.last_data_hash = request.data_hash; state.last_gene_id = gene.id; state.distillation_count = (state.distillation_count || 0) + 1; writeDistillerState(state); logEntry.status = 'success'; logEntry.gene = gene; appendJsonl(distillerLogPath(), logEntry); try { fs.unlinkSync(reqPath); } catch (e) {} try { if (request.prompt_path) fs.unlinkSync(request.prompt_path); } catch (e) {} console.log('[Distiller] Distillation complete. New gene: ' + gene.id); return { ok: true, gene: gene }; } module.exports = { collectDistillationData: collectDistillationData, analyzePatterns: analyzePatterns, prepareDistillation: prepareDistillation, completeDistillation: completeDistillation, validateSynthesizedGene: validateSynthesizedGene, shouldDistill: shouldDistill, buildDistillationPrompt: buildDistillationPrompt, extractJsonFromLlmResponse: extractJsonFromLlmResponse, computeDataHash: computeDataHash, distillerLogPath: distillerLogPath, distillerStatePath: distillerStatePath, distillRequestPath: distillRequestPath, readDistillerState: readDistillerState, writeDistillerState: writeDistillerState, DISTILLED_ID_PREFIX: DISTILLED_ID_PREFIX, DISTILLED_MAX_FILES: DISTILLED_MAX_FILES, };