feat: live memory materialization — nodes spawn in 3D graph in real-time

When memories are created, promoted, deleted, or dreamed via MCP tools,
the 3D graph now shows spectacular live animations:

- Rainbow particle burst + elastic scale-up on MemoryCreated
- Ripple wave cascading to nearby nodes
- Green pulse + node growth on MemoryPromoted
- Implosion + dissolution on MemoryDeleted
- Edge growth animation on ConnectionDiscovered
- Purple cascade on DreamStarted/DreamProgress/DreamCompleted
- FIFO eviction at 50 live nodes to guard performance

Also: graph center defaults to most-connected node, legacy HTML
redirects to SvelteKit dashboard, CSS height chain fix in layout.

Testing: 150 unit tests (vitest), 11 e2e tests (Playwright with
MCP Streamable HTTP client), 22 proof screenshots.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-03-03 14:04:31 -06:00
parent 816b577f69
commit 9bdcc69ce3
76 changed files with 5915 additions and 332 deletions

View file

@ -8,7 +8,7 @@
import { ParticleSystem } from '$lib/graph/particles';
import { EffectManager } from '$lib/graph/effects';
import { DreamMode } from '$lib/graph/dream-mode';
import { mapEventToEffects } from '$lib/graph/events';
import { mapEventToEffects, type GraphMutationContext, type GraphMutation } from '$lib/graph/events';
import { createNebulaBackground, updateNebula } from '$lib/graph/shaders/nebula.frag';
import { createPostProcessing, updatePostProcessing, type PostProcessingStack } from '$lib/graph/shaders/post-processing';
import type * as THREE from 'three';
@ -20,9 +20,10 @@
events?: VestigeEvent[];
isDreaming?: boolean;
onSelect?: (nodeId: string) => void;
onGraphMutation?: (mutation: GraphMutation) => void;
}
let { nodes, edges, centerId, events = [], isDreaming = false, onSelect }: Props = $props();
let { nodes, edges, centerId, events = [], isDreaming = false, onSelect, onGraphMutation }: Props = $props();
let container: HTMLDivElement;
let ctx: SceneContext;
@ -41,6 +42,9 @@
// Event tracking
let processedEventCount = 0;
// Internal tracking: initial nodes + live-added nodes
let allNodes: GraphNode[] = [];
onMount(() => {
ctx = createScene(container);
@ -63,6 +67,9 @@
edgeManager.createEdges(edges, positions);
forceSim = new ForceSimulation(positions);
// Track all nodes (initial set)
allNodes = [...nodes];
ctx.scene.add(edgeManager.group);
ctx.scene.add(nodeManager.group);
@ -96,9 +103,12 @@
nodeManager.updatePositions();
edgeManager.updatePositions(nodeManager.positions);
// Animate edge growth/dissolution
edgeManager.animateEdges(nodeManager.positions);
// Animate
particles.animate(time);
nodeManager.animate(time, nodes, ctx.camera);
nodeManager.animate(time, allNodes, ctx.camera);
// Dream mode
dreamMode.setActive(isDreaming);
@ -116,7 +126,7 @@
// Events + effects
processEvents();
effects.update(nodeManager.meshMap, ctx.camera);
effects.update(nodeManager.meshMap, ctx.camera, nodeManager.positions);
ctx.controls.update();
ctx.composer.render();
@ -128,8 +138,26 @@
const newEvents = events.slice(processedEventCount);
processedEventCount = events.length;
const mutationCtx: GraphMutationContext = {
effects,
nodeManager,
edgeManager,
forceSim,
camera: ctx.camera,
onMutation: (mutation: GraphMutation) => {
// Update internal allNodes tracking
if (mutation.type === 'nodeAdded') {
allNodes = [...allNodes, mutation.node];
} else if (mutation.type === 'nodeRemoved') {
allNodes = allNodes.filter((n) => n.id !== mutation.nodeId);
}
// Notify parent
onGraphMutation?.(mutation);
},
};
for (const event of newEvents) {
mapEventToEffects(event, effects, nodeManager.positions, nodeManager.meshMap, ctx.camera);
mapEventToEffects(event, mutationCtx, allNodes);
}
}