feat: el-ui — activation-based frontend framework, spreading activation reactivity, graph state
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
Reference in New Issue
Block a user