vestige/apps/dashboard/OBSERVATORY-SPEC.md
Sam Valladares a046c620c8 feat(observatory): raw-WebGPU living cognitive field with 5 deterministic demos
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>
2026-07-08 10:49:18 -07:00

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.svelte currently 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.ts is a 101-line CPU simulation with O(N^2) repulsion, edge attraction, damping, centering, and frame-count cooldown.
  • apps/dashboard/src/lib/graph/scene.ts is Three/WebGL WebGLRenderer + EffectComposer, not WebGPU.
  • apps/dashboard/src/lib/graph/particles.ts and effects.ts still use CPU-updated THREE.BufferGeometry attributes and Math.random() bursts.
  • apps/dashboard/src/lib/graph/cinema/pathfinder.ts is already deterministic and produces story beats/flow edges. Reuse this for recall paths.
  • apps/dashboard/src/lib/graph/cinema/director.ts is renderer-agnostic camera choreography. Reuse its path semantics, not its renderer.
  • apps/dashboard/src/routes/(app)/graph/+page.svelte has protected MemoryCinema; 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:

Concrete pattern to use:

  • NodeState storage buffer, 16-byte aligned lanes:
    • pos_radius: vec4f = xyz + visual radius
    • vel_retention: vec4f = xyz velocity + FSRS retention
    • color_flags: vec4f = rgb + packed visual flags as f32 for MVP; move to u32 later if needed
    • demo: vec4f = path phase, birth phase, ripple phase, shock phase
  • Two ping-pong GPUBuffers for simulation: previous read-only, next read-write.
  • One static EdgeIndex storage buffer: array<vec2u> source/target indices.
  • One PathStep storage 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 and flowEdges.
  • Convert each beat/edge to node indices and upload to PathStep.
  • Compute pass recallPathPass runs 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 PathEdgeInstance buffer is optional for MVP; simpler first build draws only path edges as instanced line quads using the PathStep source/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:

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 BirthParticle storage 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:

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 = -1 and radius contracts from the failure node toward the cause node, while PathStep beats are ordered effect -> cause.

Sources:

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 lifecycle scalar 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:

Moment E: crimson SHOCKWAVE + membrane — firewall / immune response

Chosen technique: expanding instanced ring/membrane mesh + fullscreen chromatic shock pass.

Implementation:

  • Keep an EventPulse uniform/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:

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:

  1. clearForcesPass: one invocation per node, zero force.xyz.
  2. 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 to EdgeForce and reducing per node in pass 3; advanced path uses atomic fixed-point accumulation.
  3. 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.
  4. integratePass: one invocation per node; apply centering, damping, max velocity, retention/lifecycle drift, and ping-pong write to next node buffer.
  5. boundsPass optional: 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 + GPU NodeState buffers.
  • velocities: Map<string, THREE.Vector3> becomes vel_retention.xyz in NodeState.
  • repulsionStrength, attractionStrength, dampening, alpha, maxSteps become uniform fields.
  • tick(edges) becomes command encoding of the compute passes above.
  • addNode/removeNode/reset become buffer rebuilds for v1; optimize incremental updates later.

Sources:

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.svelte or MemoryCinema wiring.
    • Fetch graph data via existing api.graph pattern from graph route.
    • Parse ?demo= and ?seed= with URLSearchParams.
    • Render full-bleed ObservatoryCanvas plus DOM overlay.
  • apps/dashboard/src/lib/components/ObservatoryCanvas.svelte
    • Svelte wrapper with onMount/onDestroy, canvas bind, ResizeObserver, WebGPU lifecycle.
  • 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.
  • 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 from planCinemaPath.
  • apps/dashboard/src/lib/observatory/overlays/TelemetryStrip.svelte
  • apps/dashboard/src/lib/observatory/overlays/CommandSurface.svelte
  • apps/dashboard/src/lib/observatory/overlays/TimelineSpine.svelte
  • apps/dashboard/src/lib/observatory/overlays/InspectorPanel.svelte
  • apps/dashboard/src/lib/observatory/shaders/simulate.wgsl.ts
  • apps/dashboard/src/lib/observatory/shaders/render-nodes.wgsl.ts
  • apps/dashboard/src/lib/observatory/shaders/render-edges.wgsl.ts
  • apps/dashboard/src/lib/observatory/shaders/render-path.wgsl.ts
  • apps/dashboard/src/lib/observatory/shaders/post-ripple-shock.wgsl.ts
  • apps/dashboard/src/lib/observatory/__tests__/demo-clock.test.ts
  • apps/dashboard/src/lib/observatory/__tests__/graph-upload.test.ts

Extend, do not rewrite:

  • apps/dashboard/src/lib/graph/cinema/pathfinder.ts
    • Export or reuse planCinemaPath as-is. If extra metadata is needed, add small pure helper pathToIndexSteps(path, nodeIndexById) in observatory layer, not inside Cinema.
  • apps/dashboard/src/lib/graph/nodes.ts
    • Reuse getNodeColor, getMemoryState, AHAGRAPH_COLORS. No renderer logic added here.
  • 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.svelte protected MemoryCinema block.
  • apps/dashboard/src/lib/components/MemoryCinema.svelte unless Sam explicitly asks.
  • crates/, Cargo.toml, backend APIs.

3. ?demo=recall-path priority build instructions

3.1 Route/component structure

  1. Create routes/(app)/observatory/+page.svelte.
  2. 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, seed into <ObservatoryCanvas />.
  3. 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.

3.2 Deterministic demo clock

Create demo-clock.ts:

  • const FPS = 60.
  • const LOOP_SECONDS = 12 for recall-path; LOOP_FRAMES = 720.
  • frame is an integer, not derived from Date.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=N for capture.
  • phase = (frame % LOOP_FRAMES) / LOOP_FRAMES.
  • Use a tiny deterministic PRNG (xmur3 seed hash + mulberry32) in local code. Do not add a dependency for seedrandom unless needed.
  • Ban Math.random() and performance.now() from simulation state. performance.now() may schedule frames only; it must not decide positions/colors/path.

Source rationale:

3.3 Graph upload

Create graph-upload.ts:

  1. Stable-sort nodes for deterministic indices:
    • center node first if id === centerId.
    • then descending updatedAt || createdAt.
    • tie-break by id.localeCompare.
  2. Build nodeIndexById: Map<string, number>.
  3. Create Float32Array nodeData with 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
  4. 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).
  5. Create Uint32Array edgeData with 2 u32 per edge, skipping edges whose node IDs are absent.
  6. Call planCinemaPath(nodes, edges, centerId, 8) and convert flowEdges to PathStep[].
  7. 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:

  1. Feature detect:
    • if (!navigator.gpu) throw new Error('WebGPU unavailable') and render a DOM fallback panel.
  2. Request adapter/device.
  3. 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.
  4. 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.
  5. Create pipelines:
    • simulatePipeline with read nodeA/write nodeB.
    • renderEdgesPipeline reads node current + edges.
    • renderNodesPipeline uses instance index to fetch node state and draw billboard.
    • renderPathPipeline draws path edges + additive halos.
    • postPipeline optional in increment 5.
  6. 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.

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 NodeState by @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:

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 at abs(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:

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.ts and 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.svelte and engine.ts minimal WebGPU init.
  • Canvas clears to near-black and resizes with DPR clamp.

Verify:

  • /observatory?demo=recall-path displays 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.ts and 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 planCinemaPath to create path steps.
  • Add compute pass that writes demo.x recall intensity.
  • Node shader blooms active path nodes.

Verify:

  • /observatory?demo=recall-path&seed=A loops every 12s.
  • The same sequence lights the same nodes after reload.
  • ?seed=B changes 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=1 behavior: hide cursor, freeze random seed, optional ?frame=N exact-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 mapAsync in 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:

Particle/lifecycle shader references:

GPU graph layout:

Svelte/DOM/determinism:

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 Graph3D intact. Observatory is a parallel WebGPU renderer, not a replacement in v1.
  • Do not add dependencies for v1. Use browser WebGPU, Svelte, TypeScript, and existing three only if needed for matrix math; prefer small local math helpers.
  • No Math.random() in demo-visible state. No Date.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 #10b981 emerald · dormant #f59e0b amber · silent #8b5cf6 violet · unavailable #6b7280 slate
    • aha #FFD700 · confusion/failure #EF4444 · guardrail #9CA3AF
  • Transcendence layer — iridescent thin-film (from causal-brain-demo.html spectral(w)): a catmull blend around 4 anchors, w∈[0,1]:
    • indigo vec3(0.20,0.28,0.95) → cyan-teal vec3(0.20,0.85,0.90) → mint vec3(0.45,1.00,0.72) → magenta rim vec3(0.85,0.45,1.00)
    • CORE reads indigo/teal (living tissue); MOTION shifts toward cyan/magenta; NEVER muddy orange.
  • 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 are 0 0 24px soft; ignite events drop-shadow at 0 0 26px rgba(110,240,220,.45) energy.
  • Text/instrument overlay glow: captions text-shadow:0 0 24px rgba(30,180,255,.35); verdict headline linear-gradient(90deg,#7fe6c0,#6ef0e6,#a6dcff) clipped to text with drop-shadow(0 0 26px rgba(110,240,220,.45)); wordmark letter-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:none except controls.
  • ?demo=salience-rescue reproduces 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."