The Observatory is a full-bleed, zero-library WebGPU surface that renders the memory graph as a living cognitive field. Five deterministic demo moments driven by a URL contract (?demo=<name>&seed=...&frame=N): recall-path, engram-birth, salience-rescue, forgetting-horizon, firewall. Capture mode (?frame=N) freezes the sim deterministically so the same URL produces identical pixels, the viral-clip primitive. Architecture: bare-metal WebGPU engine (no Three.js), seeded demo clock, per-demo plan + renderer modules, WGSL shaders (simulate, nodes, edges, path, birth particles, rescue, forgetting, firewall) plus a post-processing chain (tone mapping, MIP). DOM is instrument overlays only (telemetry strip, timeline spine, rescue verdict); the layout gives /observatory the same full-bleed bypass as marketing routes so recordings stay clean. Reads the real memory graph. Verified live: svelte-check 939 files 0 errors, 96 observatory unit tests green, all 5 demos load at 108-119fps with zero console errors against the live brain. Known follow-up: engram-birth capture-mode particle cluster needs a render fix pass before it is camera-ready; the other 4 demos are camera-ready. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
36 KiB
Vestige Cognitive Observatory WebGPU Implementation Spec
Status: RESEARCH -> SPEC ONLY. Do not implement app code from this file until Sam explicitly asks.
Goal: add a new full-bleed Cognitive Observatory route that reuses the existing graph/cinema/effects substrate, then layers a GPU-resident WebGPU simulation path for viral demo moments. Priority #1 is ?demo=recall-path: a deterministic loop where recall lights a causal path through real memory nodes.
Ground truth read before this spec:
apps/dashboard/src/lib/components/Graph3D.sveltecurrently owns the Three/WebGL scene, 60fps governor,ForceSimulation,NodeManager,EdgeManager,ParticleSystem,EffectManager, DreamMode, post-processing, and event mapping.apps/dashboard/src/lib/graph/force-sim.tsis a 101-line CPU simulation with O(N^2) repulsion, edge attraction, damping, centering, and frame-count cooldown.apps/dashboard/src/lib/graph/scene.tsis Three/WebGLWebGLRenderer+EffectComposer, not WebGPU.apps/dashboard/src/lib/graph/particles.tsandeffects.tsstill use CPU-updatedTHREE.BufferGeometryattributes andMath.random()bursts.apps/dashboard/src/lib/graph/cinema/pathfinder.tsis already deterministic and produces story beats/flow edges. Reuse this for recall paths.apps/dashboard/src/lib/graph/cinema/director.tsis renderer-agnostic camera choreography. Reuse its path semantics, not its renderer.apps/dashboard/src/routes/(app)/graph/+page.sveltehas protectedMemoryCinema; do not modify it. Add a separate route.
1. Exact WebGPU technique chosen per demo moment
Shared baseline: GPU-resident node/particle state
Chosen technique: WebGPU Samples compute-boids ping-pong storage-buffer update + instanced sprite render.
Why: it is the cleanest canonical WebGPU pattern: node/particle state is in GPU buffers created with GPUBufferUsage.VERTEX | GPUBufferUsage.STORAGE; a compute pass writes next state; a render pass reads the current/next state as an instance vertex buffer and draws one tiny sprite mesh per node/particle. The sample uses @compute @workgroup_size(64) and dispatches Math.ceil(numParticles / 64) workgroups.
Sources:
- https://webgpu.github.io/webgpu-samples/samples/computeBoids/
- https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/main.ts
- https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/updateSprites.wgsl
- https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/sprite.wgsl
- https://webgpufundamentals.org/webgpu/lessons/webgpu-storage-buffers.html
- https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html
Concrete pattern to use:
NodeStatestorage buffer, 16-byte aligned lanes:pos_radius: vec4f= xyz + visual radiusvel_retention: vec4f= xyz velocity + FSRS retentioncolor_flags: vec4f= rgb + packed visual flags as f32 for MVP; move to u32 later if neededdemo: vec4f= path phase, birth phase, ripple phase, shock phase
- Two ping-pong
GPUBuffers for simulation: previous read-only, next read-write. - One static
EdgeIndexstorage buffer:array<vec2u>source/target indices. - One
PathStepstorage buffer for demo path:array<vec4u>with source index, target index, beat frame, kind. - One uniform buffer per frame: frame index, fixed time, loop phase, node count, edge count, path count, viewport, camera matrices.
- Compute workgroup size: start at 64 for broad compatibility; allow 128 only after profiling.
- Every WGSL entry checks
if (id.x >= params.nodeCount) { return; }because dispatch is rounded up. - CPU writes graph data once on route load, then only writes uniforms and demo controls. No per-frame readback.
Moment A: ?demo=recall-path — PATH lighting through nodes
Chosen technique: GPU path-wave scalar + edge glow render from the PathStep buffer.
Implementation:
- Use existing
planCinemaPath(nodes, edges, centerId, maxBeats)to get deterministic beats andflowEdges. - Convert each beat/edge to node indices and upload to
PathStep. - Compute pass
recallPathPassruns per node. For each node, scan the small path buffer (max 7-12 beats) and compute:beatT = smoothstep(beatFrame - 18, beatFrame + 18, frameInLoop)decayT = 1.0 - smoothstep(beatFrame + 45, beatFrame + 180, frameInLoop)- path intensity = max of beat arrival envelope for matching node index.
- Edge render reads source/target node states and path steps. A second
PathEdgeInstancebuffer is optional for MVP; simpler first build draws only path edges as instanced line quads using thePathStepsource/target index. - Color: cyan-white core with violet afterglow. Node
demo.x= recall intensity. Edge alpha = wavefront envelope. - Render order: background -> non-path edges -> nodes -> path edges additive -> path node halos additive -> DOM overlay.
Sources:
- WebGPU boids storage/render pattern above.
apps/dashboard/src/lib/graph/cinema/pathfinder.tsfor deterministic story path and flow edges.- GraphWaGu edge vertex shader pattern, where the vertex shader fetches node positions from a storage buffer by source/target edge indices: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/edge_vert.wgsl
Moment B: node BORN — particle convergence into a node
Chosen technique: GPU particle attractor convergence with deterministic seed, not CPU Math.random() bursts.
Implementation:
- Add a
BirthParticlestorage buffer sized for e.g. 16k particles shared across all births. - For each birth event or demo beat, assign a deterministic particle slice: start positions are generated from
hash(seed, particleIndex, nodeIndex)on GPU or precomputed once on CPU with the same seeded PRNG. - Compute pass lerps particle position from shell/noise field toward target node position:
p = mix(start, target + curlNoiseOffset, easeOutCubic(t))- Fade from violet dust to node color; size shrinks on arrival.
- For MVP, render as instanced billboards. Later upgrade to screen-space point splatting if density gets high.
Sources:
- WebGPU Samples compute-boids for GPU particle update/render.
- Particle Life WebGPU for finite-radius particle update discipline and atomics when interactions become local-neighborhood based: https://lisyarus.github.io/blog/posts/particle-life-simulation-in-browser-using-webgpu.html
- Codrops WebGPU fluids on particle simulation and screen-space point-splatting as the real-time surface path: https://tympanus.net/codrops/2025/01/29/particles-progress-and-perseverance-a-journey-into-webgpu-fluids/
Moment C: backward RIPPLE — radial distortion moving from effect to cause
Chosen technique: screen-space radial distortion post pass + node intensity ring.
Implementation:
- Add a fullscreen post-process pipeline after node/edge render.
- Uniform
Ripple { originClip: vec2f, radius: f32, width: f32, strength: f32, direction: f32 }. - Fragment shader samples the scene texture with UV displaced along radial direction:
d = distance(uv, originClip)ring = smoothstep(radius + width, radius, d) * smoothstep(radius - width, radius, d)uv2 = uv + normalize(uv - originClip) * ring * strength * direction
- For backward causal recall,
direction = -1and radius contracts from the failure node toward the cause node, whilePathStepbeats are ordered effect -> cause.
Sources:
- WebGPU Fundamentals render/post basics: https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html
- Codrops Shader.se article for scroll/timeline-driven WebGPU scene transitions and shader masking: https://tympanus.net/codrops/2026/05/19/80s-business-tech-seamless-scene-transitions-inside-shader-ses-scroll-driven-webgpu-pipeline/
- Existing
apps/dashboard/src/lib/graph/effects.tsalready has CPUcreateRippleWave; reuse semantics, move rendering to post shader for Observatory.
Moment D: DRIFT toward a horizon — depth fog and horizon attractor
Chosen technique: GPU drift vector + depth/alpha fog in vertex/fragment shader.
Implementation:
- Add per-node
lifecyclescalar from retention/state: active=near, dormant=mid, silent=far, unavailable=horizon. - Compute pass applies a weak drift acceleration toward
horizon = vec3(0, -20, -220)for low-retention nodes; high-retention nodes resist drift. - Vertex shader writes depth/fog factor from camera-space z and lifecycle.
- Fragment shader mixes node color toward deep blue/black and reduces alpha with
smoothstep(fogNear, fogFar, depth).
Sources:
- Existing
scene.tsuses ThreeFogExp2; Observatory should port the semantic to WGSL rather than rely on Three. - Codrops False Earth for large WebGPU procedural world using compute shaders + indirect drawing; use as proof that compute-generated scene density belongs on GPU: https://tympanus.net/codrops/2026/04/21/false-earth-from-webgl-limits-to-a-webgpu-driven-world/
Moment E: crimson SHOCKWAVE + membrane — firewall / immune response
Chosen technique: expanding instanced ring/membrane mesh + fullscreen chromatic shock pass.
Implementation:
- Keep an
EventPulseuniform/storage ring buffer with origin, start frame, color, type. - Node pass reads pulses and adds red rim lighting when
abs(distance(worldPos, origin) - radius) < width. - Membrane pass draws a camera-facing ring/quad sphere impostor with fresnel alpha, crimson core, black outer edge.
- Post pass applies a one-frame high-strength radial distortion and red channel bias when shockfront crosses screen-space pixel.
Sources:
- Existing
effects.ts:createShockwaveuses a CPU ThreeRingGeometry; reuse the visual grammar, move to WebGPU instanced membrane. - Codrops WebGPU fluid articles for atomics/storage buffers/compute simulation made practical in browser: https://tympanus.net/codrops/2025/02/26/webgpu-fluid-simulations-high-performance-real-time-rendering/
- compute.toys gallery for WebGPU compute-shader idioms and GPU toy iteration: https://compute.toys/ and https://github.com/compute-toys/compute.toys
GPU force-directed graph layout
Chosen technique for v1: GPU force layout with storage buffers, edge-local spring pass, approximate repulsion pass, and integrate pass. Start with exact O(N^2) repulsion for <=2k Observatory demo nodes; add Barnes-Hut / spatial bins before 10k+.
Why: existing force-sim.ts is O(N^2) CPU. The right migration is not to immediately rebuild all GraphWaGu complexity; it is to preserve Vestige's force semantics on GPU with the boids ping-pong pattern, then graduate repulsion acceleration once visual requirements exceed a few thousand nodes.
Required compute passes:
clearForcesPass: one invocation per node, zeroforce.xyz.edgeSpringPass: one invocation per edge. Read source/target node positions, compute attraction, accumulate into source/target force. MVP avoids float atomics by writing one force contribution per edge endpoint toEdgeForceand reducing per node in pass 3; advanced path uses atomic fixed-point accumulation.repulsionPass: one invocation per node; loop all nodes for MVP. For 10k+, replace with GraphWaGu-style Barnes-Hut/Morton tree or Particle Life spatial bins.integratePass: one invocation per node; apply centering, damping, max velocity, retention/lifecycle drift, and ping-pong write to next node buffer.boundsPassoptional: compute bounds for fit camera; do not block v1 on GPU readback.
What moves from force-sim.ts:
positions: Map<string, THREE.Vector3>becomes CPU index map + GPUNodeStatebuffers.velocities: Map<string, THREE.Vector3>becomesvel_retention.xyzinNodeState.repulsionStrength,attractionStrength,dampening,alpha,maxStepsbecome uniform fields.tick(edges)becomes command encoding of the compute passes above.addNode/removeNode/resetbecome buffer rebuilds for v1; optimize incremental updates later.
Sources:
- GraphWaGu repo: https://github.com/harp-lab/GraphWaGu
- GraphWaGu force-directed TS with compute pipelines/buffers: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/webgpu/force_directed.ts
- GraphWaGu exact force WGSL: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/compute_forces.wgsl
- GraphWaGu Barnes-Hut force WGSL: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/compute_forcesBH.wgsl
- GraphWaGu apply-forces WGSL: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/apply_forces.wgsl
- GraphWaGu Morton/tree/radix sort: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/create_tree.wgsl and https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/radix_sort.wgsl
- cosmos.gl / Cosmograph GPU force graph: https://github.com/cosmograph-org/cosmos and https://cosmograph.app/
- cosmos.gl README says computations and drawing occur on GPU and targets hundreds of thousands of points/links: https://raw.githubusercontent.com/cosmograph-org/cosmos/main/README.md
2. Precise files to add/change
Add docs/spec only in this run:
apps/dashboard/OBSERVATORY-SPEC.md— this file.
Implementation files for the Qwen coder later:
Create:
apps/dashboard/src/routes/(app)/observatory/+page.svelte- New route only. Do not touch protected
graph/+page.svelteorMemoryCinemawiring. - Fetch graph data via existing
api.graphpattern from graph route. - Parse
?demo=and?seed=withURLSearchParams. - Render full-bleed
ObservatoryCanvasplus DOM overlay.
- New route only. Do not touch protected
apps/dashboard/src/lib/components/ObservatoryCanvas.svelte- Svelte wrapper with
onMount/onDestroy, canvas bind, ResizeObserver, WebGPU lifecycle.
- Svelte wrapper with
apps/dashboard/src/lib/observatory/engine.ts- Owns adapter/device/context, pipelines, buffers, render loop, resize, dispose.
apps/dashboard/src/lib/observatory/types.ts- CPU-side
ObservatoryNode,ObservatoryEdge,DemoMode,DemoClock, buffer layout constants.
- CPU-side
apps/dashboard/src/lib/observatory/demo-clock.ts- Deterministic fixed-step clock, seeded PRNG, loop-frame math.
apps/dashboard/src/lib/observatory/graph-upload.ts- Convert
GraphNode[]/GraphEdge[]to stable node index map, typed arrays, path steps fromplanCinemaPath.
- Convert
apps/dashboard/src/lib/observatory/overlays/TelemetryStrip.svelteapps/dashboard/src/lib/observatory/overlays/CommandSurface.svelteapps/dashboard/src/lib/observatory/overlays/TimelineSpine.svelteapps/dashboard/src/lib/observatory/overlays/InspectorPanel.svelteapps/dashboard/src/lib/observatory/shaders/simulate.wgsl.tsapps/dashboard/src/lib/observatory/shaders/render-nodes.wgsl.tsapps/dashboard/src/lib/observatory/shaders/render-edges.wgsl.tsapps/dashboard/src/lib/observatory/shaders/render-path.wgsl.tsapps/dashboard/src/lib/observatory/shaders/post-ripple-shock.wgsl.tsapps/dashboard/src/lib/observatory/__tests__/demo-clock.test.tsapps/dashboard/src/lib/observatory/__tests__/graph-upload.test.ts
Extend, do not rewrite:
apps/dashboard/src/lib/graph/cinema/pathfinder.ts- Export or reuse
planCinemaPathas-is. If extra metadata is needed, add small pure helperpathToIndexSteps(path, nodeIndexById)in observatory layer, not inside Cinema.
- Export or reuse
apps/dashboard/src/lib/graph/nodes.ts- Reuse
getNodeColor,getMemoryState,AHAGRAPH_COLORS. No renderer logic added here.
- Reuse
apps/dashboard/src/lib/graph/effects.ts- Do not move existing Graph3D effects. Use it as visual reference only.
apps/dashboard/src/lib/graph/force-sim.ts- Leave CPU simulation intact for Graph3D. Add no WebGPU imports here. Observatory gets its own GPU sim module.
Do not change:
apps/dashboard/src/routes/(app)/graph/+page.svelteprotectedMemoryCinemablock.apps/dashboard/src/lib/components/MemoryCinema.svelteunless Sam explicitly asks.crates/,Cargo.toml, backend APIs.
3. ?demo=recall-path priority build instructions
3.1 Route/component structure
- Create
routes/(app)/observatory/+page.svelte. - On mount:
- Parse
demo = new URLSearchParams(window.location.search).get('demo') ?? 'recall-path'. - Parse
seed = ...get('seed') ?? 'vestige-observatory-v1'. - Fetch graph with
api.graph({ max_nodes: 300, depth: 3, sort: 'recent' })for MVP. - Pass
nodes,edges,centerId,demo,seedinto<ObservatoryCanvas />.
- Parse
- Render route as a full viewport stage:
- parent
class="fixed inset-0 overflow-hidden bg-[#03040a]". - canvas layer absolute inset-0.
- overlay layer absolute inset-0 pointer-events-none.
- specific interactive controls use
pointer-events-auto.
- parent
3.2 Deterministic demo clock
Create demo-clock.ts:
const FPS = 60.const LOOP_SECONDS = 12for recall-path;LOOP_FRAMES = 720.frameis an integer, not derived fromDate.now().- In normal interactive mode, rAF accumulates elapsed time and advances fixed steps. In demo mode, each rendered frame advances exactly one fixed frame, or uses
?frame=Nfor capture. phase = (frame % LOOP_FRAMES) / LOOP_FRAMES.- Use a tiny deterministic PRNG (
xmur3seed hash +mulberry32) in local code. Do not add a dependency for seedrandom unless needed. - Ban
Math.random()andperformance.now()from simulation state.performance.now()may schedule frames only; it must not decide positions/colors/path.
Source rationale:
- Fixed timestep pattern: https://gafferongames.com/post/fix_your_timestep/
- URL query parsing: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
- Svelte lifecycle: https://svelte.dev/docs/svelte/lifecycle-hooks
3.3 Graph upload
Create graph-upload.ts:
- Stable-sort nodes for deterministic indices:
- center node first if
id === centerId. - then descending
updatedAt || createdAt. - tie-break by
id.localeCompare.
- center node first if
- Build
nodeIndexById: Map<string, number>. - Create
Float32Array nodeDatawith 16 floats per node for alignment and future-proofing:- 0..3: x, y, z, radius
- 4..7: vx, vy, vz, retention
- 8..11: r, g, b, flags
- 12..15: pathIntensity, birthPhase, ripplePhase, shockPhase
- Initial positions:
- Reuse the golden-angle sphere logic from
NodeManager.createNodes, but replace randomness with deterministic seed. - Radius scales from node count:
baseR = clamp(24 + sqrt(n) * 1.2, 35, 140).
- Reuse the golden-angle sphere logic from
- Create
Uint32Array edgeDatawith 2 u32 per edge, skipping edges whose node IDs are absent. - Call
planCinemaPath(nodes, edges, centerId, 8)and convertflowEdgestoPathStep[]. - Path timing:
- beat 0 at frame 60.
- each next beat +60 frames.
- final 180 frames reserved for afterglow and loop reset.
3.4 WebGPU engine setup
Create engine.ts:
- Feature detect:
if (!navigator.gpu) throw new Error('WebGPU unavailable')and render a DOM fallback panel.
- Request adapter/device.
- Configure canvas:
format = navigator.gpu.getPreferredCanvasFormat().- On resize, set
canvas.width = Math.min(clientWidth * devicePixelRatio, maxTextureDimension2D)and same for height. - Reconfigure context and recreate depth/scene textures.
- Create buffers:
nodeA,nodeB:STORAGE | VERTEX | COPY_DST.edges:STORAGE | COPY_DST.pathSteps:STORAGE | COPY_DST.params:UNIFORM | COPY_DST.- sprite vertex buffer for a 2-triangle quad or 3-vertex triangle impostor.
- Create pipelines:
simulatePipelinewith read nodeA/write nodeB.renderEdgesPipelinereads node current + edges.renderNodesPipelineuses instance index to fetch node state and draw billboard.renderPathPipelinedraws path edges + additive halos.postPipelineoptional in increment 5.
- Frame loop command order for recall-path MVP:
- write params uniform for integer frame.
- compute simulate: nodeA -> nodeB.
- render scene using nodeB.
- swap nodeA/nodeB.
3.5 WGSL simulation for recall path
MVP simulate.wgsl.ts:
- Inputs: params, previous nodes, next nodes, edges, path steps.
- For each node:
- start with previous
pos,vel. - apply low-cost centering and edge spring only after the first MVP render works; first render may use static positions.
- compute path intensity by scanning
pathSteps. - compute horizon drift from retention.
- write next node state.
- start with previous
Pseudo-WGSL shape for Qwen:
struct NodeState {
pos_radius: vec4f,
vel_retention: vec4f,
color_flags: vec4f,
demo: vec4f,
};
struct Params {
frame: u32,
loopFrames: u32,
nodeCount: u32,
edgeCount: u32,
pathCount: u32,
dt: f32,
time: f32,
_pad: f32,
};
struct PathStep { source: u32, target: u32, beatFrame: u32, kind: u32 };
@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var<storage, read> prevNodes: array<NodeState>;
@group(0) @binding(2) var<storage, read_write> nextNodes: array<NodeState>;
@group(0) @binding(3) var<storage, read> path: array<PathStep>;
fn saturate(x: f32) -> f32 { return clamp(x, 0.0, 1.0); }
fn smoothPulse(frame: f32, beat: f32, attack: f32, release: f32) -> f32 {
let a = smoothstep(beat - attack, beat, frame);
let r = 1.0 - smoothstep(beat, beat + release, frame);
return a * r;
}
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3u) {
let i = gid.x;
if (i >= params.nodeCount) { return; }
var node = prevNodes[i];
let f = f32(params.frame % params.loopFrames);
var recall = 0.0;
for (var p = 0u; p < params.pathCount; p = p + 1u) {
let step = path[p];
if (step.source == i || step.target == i) {
recall = max(recall, smoothPulse(f, f32(step.beatFrame), 18.0, 150.0));
}
}
node.demo.x = recall;
nextNodes[i] = node;
}
3.6 Render approach
Nodes:
- Use instanced billboards. Vertex shader fetches
NodeStateby@builtin(instance_index). - Billboard size =
radius * (1.0 + recallIntensity * 2.0). - Fragment shader radial alpha:
alpha = smoothstep(1.0, 0.0, length(localUv)). - Color =
mix(baseColor, vec3(0.8, 0.95, 1.0), recallIntensity). - Additive glow pass can be same shader with bigger billboard and low alpha.
Edges:
- MVP: draw edges as line-list if available; better: instanced thin quads from source to target in screen space.
- For path edges, draw a separate additive path pass from
PathSteponly, with wavefront alpha. - GraphWaGu source pattern fetches nodes in vertex shader by edge endpoint index; use that for WebGPU: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/edge_vert.wgsl
Path lighting:
- At beat frame, target node blooms first.
- During frames
beatFrame..beatFrame+45, edge from source to target gets a traveling pulse:edgeT = saturate((frame - beatFrame) / 45).- Fragment local coordinate along edge
u; alpha peak atabs(u - edgeT) < 0.08.
- Previous path nodes keep afterglow for 2-3 seconds.
3.7 DOM overlay
Overlay zones:
- Top telemetry strip: demo mode, seed, node/edge count, frame, fps estimate.
- Left command surface: demo selector buttons (
recall-path,birth,ripple,drift,firewall) and copy demo URL. - Bottom timeline spine: 720-frame loop with beat tick marks and current frame cursor.
- Right inspector: selected beat/node summary.
CSS pattern:
- Root stage:
position: fixed; inset: 0; isolation: isolate; overflow: hidden;. - Canvas:
position:absolute; inset:0; width:100%; height:100%; display:block; z-index:0;. - Overlay:
position:absolute; inset:0; z-index:10; pointer-events:none;. - Interactive overlay children:
pointer-events:auto;. - Avoid KPI cards. These are instruments: thin glass, monospaced labels, tick marks, command rail.
Sources:
- MDN
pointer-events: https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events - WebGPU canvas resizing: https://webgpufundamentals.org/webgpu/lessons/webgpu-resizing-the-canvas.html
- Svelte lifecycle hooks: https://svelte.dev/docs/svelte/lifecycle-hooks
4. Build task-list with verification increments
Increment 1 — route shell and deterministic URL contract
Build:
- Add
routes/(app)/observatory/+page.svelte. - Add minimal overlay shell and fetch graph data.
- Parse
?demo=recall-path&seed=...and display them.
Verify:
- Run
pnpm --filter @vestige/dashboard check. - Open
/observatory?demo=recall-path&seed=test. - See full-bleed dark stage, top telemetry strip, no KPI cards, no console errors.
Increment 2 — deterministic clock tests
Build:
- Add
demo-clock.tsand tests. - Fixed 60fps loop, 720-frame period, seeded PRNG.
Verify:
pnpm --filter @vestige/dashboard test -- demo-clock- Same seed produces identical first 100 random values and frame phases.
- Different seed changes generated positions.
Increment 3 — WebGPU canvas boots and clears
Build:
- Add
ObservatoryCanvas.svelteandengine.tsminimal WebGPU init. - Canvas clears to near-black and resizes with DPR clamp.
Verify:
/observatory?demo=recall-pathdisplays WebGPU canvas.- Resize browser; canvas remains sharp and fills screen.
- If WebGPU unavailable, route shows a readable fallback instead of crashing.
Increment 4 — upload graph and render static nodes
Build:
- Add
graph-upload.tsand tests. - Convert graph nodes into stable indexed
Float32Array. - Render node billboards from storage buffer; no simulation yet.
Verify:
- Test stable node ordering and edge filtering.
- Browser shows 100-300 nodes in deterministic positions.
- Reload same seed: identical node positions.
Increment 5 — recall path buffer + node glow
Build:
- Reuse
planCinemaPathto create path steps. - Add compute pass that writes
demo.xrecall intensity. - Node shader blooms active path nodes.
Verify:
/observatory?demo=recall-path&seed=Aloops every 12s.- The same sequence lights the same nodes after reload.
?seed=Bchanges layout but path timing remains stable.
Increment 6 — path edge wavefront
Build:
- Add render-path pipeline drawing additive edge wave traveling source -> target.
- Timeline spine shows beat ticks matching wave arrival.
Verify:
- Wavefront visibly travels along each path edge.
- At beat arrival, the target node blooms.
- Loop reset is seamless: final afterglow fades before frame 0.
Increment 7 — low-cost GPU force sim
Build:
- Add static edge attraction + centering + damping in compute.
- Keep repulsion disabled or low-count O(N^2) only for <=500 nodes.
Verify:
- Nodes subtly settle without CPU
ForceSimulation. - Performance remains smooth at 300 nodes.
- No CPU per-frame node position loops.
Increment 8 — lifecycle effects one by one
Build/verify separately:
- Born convergence particles: trigger in demo and verify deterministic convergence.
- Backward ripple post pass: verify radial distortion travels effect -> cause.
- Horizon drift/fog: silent/unavailable nodes visibly drift/fade toward horizon.
- Firewall shockwave/membrane: crimson ring expands and fades without breaking path demo.
Increment 9 — capture mode
Build:
- Add
?capture=1behavior: hide cursor, freeze random seed, optional?frame=Nexact-frame render. - Add overlay copy button for canonical demo URL.
Verify:
- Same URL + frame renders identical screenshot.
- 12-second screen recording loops without discontinuity.
Increment 10 — performance guardrails
Build:
- Add telemetry: CPU frame ms estimate, GPU timestamp query only if supported, node count, buffer mode.
- Cap default demo to 300 real nodes; support synthetic 10k only behind
?stress=10000.
Verify:
- No
mapAsyncin the frame loop except delayed optional profiling. - 300-node demo holds 60fps on Sam's M1 Max.
- Stress mode degrades gracefully and can be disabled by removing query param.
5. Source index
WebGPU core patterns:
- WebGPU Samples compute boids demo: https://webgpu.github.io/webgpu-samples/samples/computeBoids/
- compute-boids main TS: https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/main.ts
- compute-boids update WGSL: https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/updateSprites.wgsl
- compute-boids sprite WGSL: https://raw.githubusercontent.com/webgpu/webgpu-samples/main/sample/computeBoids/sprite.wgsl
- WebGPU storage buffers: https://webgpufundamentals.org/webgpu/lessons/webgpu-storage-buffers.html
- WebGPU compute shaders: https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html
- WebGPU canvas resize: https://webgpufundamentals.org/webgpu/lessons/webgpu-resizing-the-canvas.html
- MDN WebGPU API: https://developer.mozilla.org/en-US/docs/Web/API/WebGPU_API
Particle/lifecycle shader references:
- Particle Life WebGPU article: https://lisyarus.github.io/blog/posts/particle-life-simulation-in-browser-using-webgpu.html
- Particle Life demo: https://lisyarus.github.io/webgpu/particle-life.html
- compute.toys: https://compute.toys/
- compute.toys repo: https://github.com/compute-toys/compute.toys
- Codrops WebGPU fluids: https://tympanus.net/codrops/2025/02/26/webgpu-fluid-simulations-high-performance-real-time-rendering/
- Codrops WebGPU particles/fluids: https://tympanus.net/codrops/2025/01/29/particles-progress-and-perseverance-a-journey-into-webgpu-fluids/
- Codrops Shader.se WebGPU transitions: https://tympanus.net/codrops/2026/05/19/80s-business-tech-seamless-scene-transitions-inside-shader-ses-scroll-driven-webgpu-pipeline/
- Codrops False Earth WebGPU world: https://tympanus.net/codrops/2026/04/21/false-earth-from-webgl-limits-to-a-webgpu-driven-world/
GPU graph layout:
- GraphWaGu repo: https://github.com/harp-lab/GraphWaGu
- GraphWaGu force-directed TS: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/webgpu/force_directed.ts
- GraphWaGu exact forces: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/compute_forces.wgsl
- GraphWaGu Barnes-Hut forces: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/compute_forcesBH.wgsl
- GraphWaGu apply forces: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/apply_forces.wgsl
- GraphWaGu tree build: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/create_tree.wgsl
- GraphWaGu radix sort: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/radix_sort.wgsl
- GraphWaGu node vertex: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/node_vert.wgsl
- GraphWaGu edge vertex: https://raw.githubusercontent.com/harp-lab/GraphWaGu/main/src/wgsl/edge_vert.wgsl
- cosmos.gl repo: https://github.com/cosmograph-org/cosmos
- cosmos.gl README: https://raw.githubusercontent.com/cosmograph-org/cosmos/main/README.md
- Cosmograph product: https://cosmograph.app/
Svelte/DOM/determinism:
- Svelte lifecycle hooks: https://svelte.dev/docs/svelte/lifecycle-hooks
- MDN URLSearchParams: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
- MDN pointer-events: https://developer.mozilla.org/en-US/docs/Web/CSS/pointer-events
- Fixed timestep: https://gafferongames.com/post/fix_your_timestep/
- seedrandom reference if a dependency is later preferred over local PRNG: https://github.com/davidbau/seedrandom
6. Non-negotiable implementation constraints
- New Observatory route only. Do not mutate the protected Memory Cinema block in
graph/+page.svelte. - Keep existing Three/WebGL
Graph3Dintact. Observatory is a parallel WebGPU renderer, not a replacement in v1. - Do not add dependencies for v1. Use browser WebGPU, Svelte, TypeScript, and existing
threeonly if needed for matrix math; prefer small local math helpers. - No
Math.random()in demo-visible state. NoDate.now()/performance.now()deciding simulation state. - No GPU readback in the frame loop. Profiling readback must be delayed and optional.
- Every increment must render something verifiable before moving to the next effect.
7. VISUAL DNA — "from another dimension" (MANDATORY aesthetic contract)
Sam's directive (Jul 3 2026): the Observatory must be mind-boggling and look like it
came from another dimension — the same grammar as the WebGPU waitlist hero and
docs/launch/causal-brain-demo.html. This is NOT a dashboard skin. It is a living
tissue. Every increment below inherits this section. Generic = rejected.
7.1 The signature palette (fuse two systems — do not invent a third)
TWO real palettes already exist in-repo. The Observatory FUSES them:
- Meaning layer — FSRS state (from
lib/graph/nodes.ts, use verbatim):- active
#10b981emerald · dormant#f59e0bamber · silent#8b5cf6violet · unavailable#6b7280slate - aha
#FFD700· confusion/failure#EF4444· guardrail#9CA3AF
- active
- Transcendence layer — iridescent thin-film (from
causal-brain-demo.htmlspectral(w)): a catmull blend around 4 anchors, w∈[0,1]:- indigo
vec3(0.20,0.28,0.95)→ cyan-tealvec3(0.20,0.85,0.90)→ mintvec3(0.45,1.00,0.72)→ magenta rimvec3(0.85,0.45,1.00) - CORE reads indigo/teal (living tissue); MOTION shifts toward cyan/magenta; NEVER muddy orange.
- indigo
- Void: background
#05060a(near-black,color-scheme: dark). No grey dashboard chrome. - Rule: a node's BASE hue = its FSRS state color. Its ACTIVATION glow (recall, birth, ignite) rides the thin-film spectral band. So resting graph = meaningful; active graph = otherworldly.
Port spectral(w) to WGSL exactly (4-anchor catmull-ish blend). This is the single most
important visual primitive — it is what makes it look alien instead of corporate.
7.2 Glow / bloom language (copy the demo's exact values, then push further in WebGPU)
- Additive radial glow blobs (
glow(x,y,rad,col,alpha)in the demo) → in WebGPU, a real additive bloom pass. Node halos are0 0 24pxsoft; ignite events drop-shadow at0 0 26px rgba(110,240,220,.45)energy. - Text/instrument overlay glow: captions
text-shadow:0 0 24px rgba(30,180,255,.35); verdict headlinelinear-gradient(90deg,#7fe6c0,#6ef0e6,#a6dcff)clipped to text withdrop-shadow(0 0 26px rgba(110,240,220,.45)); wordmarkletter-spacing:.28em,#5dcaa5. - Breath: a global
pulse = 0.5 + 0.5*sin(t*0.002)(~0.32Hz) modulates halo intensity so the whole field breathes even at rest. Living, never static.
7.3 DOM = instrument overlay ONLY (already the spec rule — here is the exact style)
Copy the demo's overlay contract: .layer { position:fixed; inset:0; pointer-events:none }
over a full-bleed inset:0 canvas. Instruments (TelemetryStrip, CommandSurface,
TimelineSpine, InspectorPanel) are the ONLY DOM, styled like the demo's .cap.mono
(SF Mono, #cfe9ff), .verdict (radial-gradient vignette card), .wordmark. NO KPI cards.
NO solid panels. Instruments float, glow faintly, and never block the canvas
(pointer-events:auto only on the specific interactive control).
7.4 The reference moment already exists in 2D — port it, don't reinvent
causal-brain-demo.html IS Moment C (salience-rescue / backward-trace) as a 2D canvas
proof: brain point cloud (two lobes, center-dense), a failure flare on the deep right, a
vector-search stall on confounders, then a BACKWARD arrow tracing to the dormant cause on
the deep-left which ignites, then a verdict card ("Vestige 60% · vector search 0%"),
then the VESTIGE wordmark. Beat clock: brain forms 0-1.5s, fail fades in ~0.3s, vector
stalls 4-6.5s, backward trace fires 6.5-10.5s, verdict 10.5-13s, signature 13-15s, loop.
The Observatory's ?demo=salience-rescue must reproduce this exact narrative in real
WebGPU with real memory nodes. Study its spectral, seedBrain, glow, and beat
schedule before writing the WGSL.
7.5 Non-negotiable "another dimension" checklist (verifier MUST confirm each)
- Field breathes at rest (global pulse), never a static image.
- Node base hue = FSRS state; activation = thin-film spectral. Both visible.
- Real additive bloom on ignite/recall (not just opacity).
- Void
#05060a, zero grey dashboard chrome, zero KPI cards. - Instruments are glowing SF-Mono overlays with
pointer-events:noneexcept controls. ?demo=salience-rescuereproduces the causal-brain backward-trace narrative.- It looks like living tissue from another dimension, not a SaaS dashboard. If a reviewer would call it "clean" or "professional," it FAILED. The word is "alive."