122 lines
4.1 KiB
JavaScript
122 lines
4.1 KiB
JavaScript
/**
|
||
* activation.js — Spreading activation utilities for el-ui.
|
||
*
|
||
* This module provides standalone activation functions that operate on any
|
||
* Graph instance. Useful for implementing custom reactive behavior outside
|
||
* of the standard component setState() lifecycle.
|
||
*
|
||
* The activation model mirrors engram-core/src/activation.rs exactly:
|
||
* - Best-first BFS from seed nodes
|
||
* - Multiplicative strength: parent_strength × edge.weight × target.importance
|
||
* - Winner-take-most: strongest path to each node wins
|
||
* - Prune: paths below threshold are cut (models attention filter)
|
||
*/
|
||
|
||
export const PRUNE_THRESHOLD = 0.01;
|
||
export const DEFAULT_MAX_DEPTH = 3;
|
||
export const DEFAULT_LIMIT = 20;
|
||
|
||
/**
|
||
* Run spreading activation from multiple seed nodes.
|
||
*
|
||
* @param {import('./graph.js').Graph} graph
|
||
* @param {string[]} seeds starting node IDs
|
||
* @param {object} opts
|
||
* @param {number} [opts.maxDepth=3]
|
||
* @param {number} [opts.limit=20]
|
||
* @param {number} [opts.pruneThreshold=0.01]
|
||
* @returns {Array<{nodeId: string, strength: number, hops: number, node: object}>}
|
||
*/
|
||
export function spreadActivation(graph, seeds, {
|
||
maxDepth = DEFAULT_MAX_DEPTH,
|
||
limit = DEFAULT_LIMIT,
|
||
pruneThreshold = PRUNE_THRESHOLD,
|
||
} = {}) {
|
||
/** @type {Map<string, {strength: number, hops: number}>} */
|
||
const bestStrength = new Map();
|
||
/** @type {Array<{id: string, strength: number, hops: number}>} */
|
||
const queue = [];
|
||
|
||
const seedSet = new Set(seeds);
|
||
|
||
for (const seed of seeds) {
|
||
const node = graph.get(seed);
|
||
if (!node) continue;
|
||
queue.push({ id: seed, strength: 1.0, hops: 0 });
|
||
bestStrength.set(seed, { strength: 1.0, hops: 0 });
|
||
}
|
||
|
||
while (queue.length > 0) {
|
||
// Best-first: pop the highest-strength candidate
|
||
let bestIdx = 0;
|
||
for (let i = 1; i < queue.length; i++) {
|
||
if (queue[i].strength > queue[bestIdx].strength) bestIdx = i;
|
||
}
|
||
const { id, strength, hops } = queue.splice(bestIdx, 1)[0];
|
||
|
||
if (hops >= maxDepth) continue;
|
||
|
||
const node = graph.get(id);
|
||
if (!node) continue;
|
||
|
||
for (const edgeId of node.edges) {
|
||
const edge = graph.edges.get(edgeId);
|
||
if (!edge) continue;
|
||
|
||
const target = graph.get(edge.to);
|
||
if (!target) continue;
|
||
|
||
const targetStrength = strength * edge.weight * Math.max(0, target.importance);
|
||
|
||
if (targetStrength <= pruneThreshold) continue;
|
||
|
||
const prev = bestStrength.get(edge.to);
|
||
if (!prev || targetStrength > prev.strength) {
|
||
const nextHops = hops + 1;
|
||
bestStrength.set(edge.to, { strength: targetStrength, hops: nextHops });
|
||
queue.push({ id: edge.to, strength: targetStrength, hops: nextHops });
|
||
}
|
||
}
|
||
}
|
||
|
||
// Collect results — exclude seeds, sort by strength
|
||
const results = [];
|
||
for (const [nodeId, { strength, hops }] of bestStrength) {
|
||
if (seedSet.has(nodeId)) continue;
|
||
const node = graph.get(nodeId);
|
||
if (node) results.push({ nodeId, strength, hops, node });
|
||
}
|
||
|
||
results.sort((a, b) => b.strength - a.strength);
|
||
return results.slice(0, limit);
|
||
}
|
||
|
||
/**
|
||
* Compute activation strength between two specific nodes.
|
||
* Returns 0 if no path exists within maxDepth.
|
||
*
|
||
* @param {import('./graph.js').Graph} graph
|
||
* @param {string} fromId
|
||
* @param {string} toId
|
||
* @param {number} [maxDepth=3]
|
||
* @returns {number}
|
||
*/
|
||
export function activationStrength(graph, fromId, toId, maxDepth = DEFAULT_MAX_DEPTH) {
|
||
const results = spreadActivation(graph, [fromId], { maxDepth });
|
||
const found = results.find(r => r.nodeId === toId);
|
||
return found ? found.strength : 0;
|
||
}
|
||
|
||
/**
|
||
* Find all nodes reachable from a seed within the activation surface.
|
||
* Unlike spreadActivation(), this includes the seed itself.
|
||
*
|
||
* @param {import('./graph.js').Graph} graph
|
||
* @param {string} seedId
|
||
* @param {number} [maxDepth=3]
|
||
* @returns {Set<string>}
|
||
*/
|
||
export function reachableNodes(graph, seedId, maxDepth = DEFAULT_MAX_DEPTH) {
|
||
return graph.activate(seedId, maxDepth);
|
||
}
|