function matchPatternToSignals(pattern, signals) { if (!pattern || !signals || signals.length === 0) return false; const p = String(pattern); const sig = signals.map(s => String(s)); const regexLike = p.length >= 2 && p.startsWith('/') && p.lastIndexOf('/') > 0; if (regexLike) { const lastSlash = p.lastIndexOf('/'); const body = p.slice(1, lastSlash); const flags = p.slice(lastSlash + 1); try { const re = new RegExp(body, flags || 'i'); return sig.some(s => re.test(s)); } catch (e) { // fallback to substring } } const needle = p.toLowerCase(); return sig.some(s => s.toLowerCase().includes(needle)); } function scoreGene(gene, signals) { if (!gene || gene.type !== 'Gene') return 0; const patterns = Array.isArray(gene.signals_match) ? gene.signals_match : []; if (patterns.length === 0) return 0; let score = 0; for (const pat of patterns) { if (matchPatternToSignals(pat, signals)) score += 1; } return score; } function selectGene(genes, signals, opts) { const bannedGeneIds = opts && opts.bannedGeneIds ? opts.bannedGeneIds : new Set(); const driftEnabled = !!(opts && opts.driftEnabled); const preferredGeneId = opts && typeof opts.preferredGeneId === 'string' ? opts.preferredGeneId : null; const scored = genes .map(g => ({ gene: g, score: scoreGene(g, signals) })) .filter(x => x.score > 0) .sort((a, b) => b.score - a.score); if (scored.length === 0) return { selected: null, alternatives: [] }; // Memory graph preference: only override when the preferred gene is already a match candidate. if (preferredGeneId) { const preferred = scored.find(x => x.gene && x.gene.id === preferredGeneId); if (preferred && (driftEnabled || !bannedGeneIds.has(preferredGeneId))) { const rest = scored.filter(x => x.gene && x.gene.id !== preferredGeneId); const filteredRest = driftEnabled ? rest : rest.filter(x => x.gene && !bannedGeneIds.has(x.gene.id)); return { selected: preferred.gene, alternatives: filteredRest.slice(0, 4).map(x => x.gene), }; } } // Low-efficiency suppression: do not repeat low-confidence paths unless drift is explicit. const filtered = driftEnabled ? scored : scored.filter(x => x.gene && !bannedGeneIds.has(x.gene.id)); if (filtered.length === 0) return { selected: null, alternatives: scored.slice(0, 4).map(x => x.gene) }; return { selected: filtered[0].gene, alternatives: filtered.slice(1, 4).map(x => x.gene), }; } function selectCapsule(capsules, signals) { const scored = (capsules || []) .map(c => { const triggers = Array.isArray(c.trigger) ? c.trigger : []; const score = triggers.reduce((acc, t) => (matchPatternToSignals(t, signals) ? acc + 1 : acc), 0); return { capsule: c, score }; }) .filter(x => x.score > 0) .sort((a, b) => b.score - a.score); return scored.length ? scored[0].capsule : null; } function selectGeneAndCapsule({ genes, capsules, signals, memoryAdvice, driftEnabled }) { const bannedGeneIds = memoryAdvice && memoryAdvice.bannedGeneIds instanceof Set ? memoryAdvice.bannedGeneIds : new Set(); const preferredGeneId = memoryAdvice && memoryAdvice.preferredGeneId ? memoryAdvice.preferredGeneId : null; const { selected, alternatives } = selectGene(genes, signals, { bannedGeneIds, preferredGeneId, driftEnabled: !!driftEnabled, }); const capsule = selectCapsule(capsules, signals); const selector = buildSelectorDecision({ gene: selected, capsule, signals, alternatives, memoryAdvice, driftEnabled, }); return { selectedGene: selected, capsuleCandidates: capsule ? [capsule] : [], selector, }; } function buildSelectorDecision({ gene, capsule, signals, alternatives, memoryAdvice, driftEnabled }) { const reason = []; if (gene) reason.push('signals match gene.signals_match'); if (capsule) reason.push('capsule trigger matches signals'); if (!gene) reason.push('no matching gene found; new gene may be required'); if (signals && signals.length) reason.push(`signals: ${signals.join(', ')}`); if (memoryAdvice && Array.isArray(memoryAdvice.explanation) && memoryAdvice.explanation.length) { reason.push(`memory_graph: ${memoryAdvice.explanation.join(' | ')}`); } if (driftEnabled) { reason.push('random_drift_override: true'); } return { selected: gene ? gene.id : null, reason, alternatives: Array.isArray(alternatives) ? alternatives.map(g => g.id) : [], }; } module.exports = { selectGeneAndCapsule, selectGene, selectCapsule, buildSelectorDecision, };