Files
el/ui/runtime/src/activation.js
T

122 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}