135 lines
4.6 KiB
JavaScript
135 lines
4.6 KiB
JavaScript
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,
|
|
};
|
|
|