feat(observatory): integrate into the main dashboard graph

The Observatory is no longer a separate URL you have to know about. It
is now a first-class part of the main graph page, following the proven
Memory Cinema pattern (which remains untouched per the protected-block
rule):

- The full experience is factored into a reusable ObservatoryStage
  component (engine, plans, overlays moved verbatim from the route).
- The graph control bar gains an Observatory button beside Dream and
  Memory Cinema; clicking it takes over the screen full-bleed (z-50).
- Inside the stage: an on-screen switcher for the 5 cognitive moments
  (RECALL / BIRTH / RESCUE / HORIZON / FIREWALL) and an EXIT control
  (Esc works too) that returns to the graph exactly as it was. Both are
  hidden in capture mode so recordings stay chrome-free.
- {#key demo} remounts the stage per moment: fresh engine, plans, and
  clock, deterministic from frame 0.
- The /observatory route stays as a thin wrapper so deep links and the
  ?frame=N / ?capture=1 recording contract are unchanged; switching a
  moment there rewrites the URL so links stay shareable.

Verified live in the running dashboard: button renders between Dream
and Cinema, click boots the WebGPU takeover (148 real nodes, 2865
edges), RESCUE chip switches the moment (telemetry: SALIENCE-RESCUE),
EXIT unmounts cleanly back to the graph (canvas count 2 -> 1).
svelte-check 940 files 0 errors 0 warnings; 96 observatory tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-07-08 11:54:03 -07:00
parent e886d4e5e7
commit 7da5f4aa97
3 changed files with 439 additions and 278 deletions

View file

@ -0,0 +1,363 @@
<script lang="ts">
/**
* ObservatoryStage — the full Cognitive Observatory experience as a
* reusable, props-driven component so it can live in TWO places:
*
* 1. The MAIN dashboard graph page (primary integration): launched from
* the graph control bar next to Dream / Memory Cinema, full-bleed
* overlay, with the on-screen demo switcher + exit control.
* 2. The /observatory route (thin wrapper): deep-linking + ?frame=N
* capture workflow for recordings stays byte-compatible.
*
* All engine/plan/overlay wiring moved verbatim from the route page —
* behavior is identical; only the input source changed (props, not URL).
*/
import { onMount } from 'svelte';
import { api } from '$stores/api';
import type { GraphResponse } from '$types';
import TelemetryStrip from '$lib/observatory/overlays/TelemetryStrip.svelte';
import TimelineSpine from '$lib/observatory/overlays/TimelineSpine.svelte';
import RescueVerdict from '$lib/observatory/overlays/RescueVerdict.svelte';
import ObservatoryCanvas from '$lib/components/ObservatoryCanvas.svelte';
import { DEMO_MODES, type DemoMode } from '$lib/observatory/types';
import type { ObservatoryEngine } from '$lib/observatory/engine';
import { NodeRenderer } from '$lib/observatory/node-renderer';
import { BirthRenderer } from '$lib/observatory/birth-renderer';
import { RescueRenderer } from '$lib/observatory/rescue-renderer';
import { buildRescuePlan, type RescuePlan } from '$lib/observatory/rescue-plan';
import { ForgettingRenderer } from '$lib/observatory/forgetting-renderer';
import { buildForgettingPlan } from '$lib/observatory/forgetting-plan';
import { FirewallRenderer } from '$lib/observatory/firewall-renderer';
import { buildFirewallPlan, type FirewallPlan } from '$lib/observatory/firewall-plan';
import type { PathStepMeta } from '$lib/observatory/path-builder';
interface Props {
demo: DemoMode;
seed?: string;
/** ?frame=N capture freeze — null runs the live loop. */
freezeFrame?: number | null;
/** Recording mode: hides EVERY DOM instrument (pure canvas). */
capture?: boolean;
/** Show the on-screen demo switcher (the 5 moments). */
showSwitcher?: boolean;
/** Called when the user picks another demo moment. */
ondemochange?: (demo: DemoMode) => void;
/** When provided, renders the exit control (× back to the dashboard). */
onexit?: () => void;
}
let {
demo,
seed = 'vestige-observatory-v1',
freezeFrame = null,
capture = false,
showSwitcher = true,
ondemochange,
onexit
}: Props = $props();
// Human labels for the switcher chips — short, mono, uppercase (visual DNA §7.3).
const DEMO_LABELS: Record<DemoMode, string> = {
'recall-path': 'RECALL',
'engram-birth': 'BIRTH',
'salience-rescue': 'RESCUE',
'forgetting-horizon': 'HORIZON',
firewall: 'FIREWALL'
};
// Intentional initial-value capture: `capture` is fixed for the lifetime of
// a mount ({#key} remounts on change) and the H key mutates showHud freely
// afterward — a $derived would fight the toggle.
// svelte-ignore state_referenced_locally
let showHud = $state(!capture);
function onKeydown(e: KeyboardEvent) {
if (e.key === 'h' || e.key === 'H') showHud = !showHud;
if (e.key === 'Escape' && onexit) onexit();
}
let graphData: GraphResponse | null = $state(null);
let loading = $state(true);
let error = $state('');
// Telemetry state — frames come from the engine's deterministic DemoClock.
let frameCount = $state(0);
let fpsEstimate = $state(0);
let nodeCount = $state(0);
let edgeCount = $state(0);
let centerId = $state('');
// Engine + renderer handles (upload happens once both are ready).
let engine = $state<ObservatoryEngine | null>(null);
let renderer: NodeRenderer | null = null;
let birthRenderer: BirthRenderer | null = null;
let rescueRenderer: RescueRenderer | null = null;
let rescuePlan = $state<RescuePlan | null>(null);
let forgettingRenderer: ForgettingRenderer | null = null;
let firewallRenderer: FirewallRenderer | null = null;
let firewallPlan = $state<FirewallPlan | null>(null);
let uploaded = false;
let pathSteps = $state<PathStepMeta[]>([]);
async function loadGraph() {
loading = true;
error = '';
try {
// Pull the DENSE real subgraph (the well-connected hotspot), not the
// newest-memory neighborhood — 'recent' centers on a lonely fresh node
// (~12 nodes), while 'connected' surfaces the populous, edge-rich field
// (~150 real memories, thousands of edges) so the Observatory reads as
// a living brain doing real work, not a sparse placeholder.
const data = await api.graph({
max_nodes: 200,
depth: 3,
sort: 'connected'
});
graphData = data;
nodeCount = data.nodeCount;
edgeCount = data.edgeCount;
centerId = data.center_id;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load graph data';
} finally {
loading = false;
}
}
function handleFrame(frame: number, fps: number) {
frameCount = frame;
fpsEstimate = fps;
}
function handleReady(e: ObservatoryEngine) {
// A fresh engine (first boot or HMR re-create) always needs an upload.
uploaded = false;
engine = e;
renderer = new NodeRenderer(e);
}
// Upload the memory field once the engine AND the graph are both ready.
$effect(() => {
if (engine && renderer && graphData && !uploaded) {
uploaded = true;
const isBirth = demo === 'engram-birth';
const isRescue = demo === 'salience-rescue';
const isHorizon = demo === 'forgetting-horizon';
const isFirewall = demo === 'firewall';
renderer.upload(graphData, seed, {
recallPath: !isBirth && !isRescue && !isHorizon && !isFirewall
});
if (isBirth) {
// Moment B: violet dust converges into a newborn memory.
birthRenderer = new BirthRenderer({ engine, nodeRenderer: renderer, seed });
birthRenderer.upload(seed);
// B6: the engrave steps ride the proven recall wavefront system —
// pulses travel outward from the newborn, neighbors bloom on landing.
const engrave = birthRenderer.engraveSteps;
const engraveMetas = [];
for (let i = 0; i < engrave.length / 4; i++) {
engraveMetas.push({
sourceIndex: engrave[i * 4],
targetIndex: engrave[i * 4 + 1],
beatFrame: engrave[i * 4 + 2],
kind: engrave[i * 4 + 3],
beatKind: 'engrave',
nodeId: `engrave-${i}`,
label: 'edge engraved'
});
}
renderer.setPathSteps(engrave, engraveMetas);
// Spine shows the birth beats (converge → flash → engrave).
pathSteps = birthRenderer.timeline.map((b, i) => ({
sourceIndex: 0,
targetIndex: 0,
beatFrame: b.startFrame,
kind: 0,
beatKind: 'birth',
nodeId: `birth-${i}`,
label: b.label
}));
} else if (isRescue) {
// Moment C: Retroactive Salience Backfill — vector search fails on
// camera, then Vestige reaches backward through time and ignites
// the true cause. All choreography is a pure CPU plan; the
// RescueRenderer overwrites the demo lanes AFTER recall_sim
// (construction order = pass order — load-bearing for the seam).
const plan = buildRescuePlan(graphData, renderer.graph!, seed);
rescuePlan = plan;
if (plan.viable) {
rescueRenderer = new RescueRenderer({ engine, nodeRenderer: renderer, plan });
rescueRenderer.upload();
// Probe beams + backward wave tree + causal arc ride the proven
// path-ribbon machinery. Metas are 1:1 with the GPU steps
// (setPathSteps sets params[4] + draw count from META length).
renderer.setPathSteps(plan.pathData, plan.pathMetas);
}
// Spine shows the curated story beats (unique beatFrames), never
// the raw GPU steps.
pathSteps = plan.spineBeats;
} else if (isHorizon) {
// Moment D: the forgetting horizon — FSRS as a visible living
// system. The lowest-retention memories dim and fall; three are
// recalled back on camera; the rest sink to a 6% floor (never
// deleted). Pure CPU plan; the ForgettingRenderer overwrites the
// demo lanes AFTER recall_sim (construction order = pass order).
const plan = buildForgettingPlan(renderer.graph!);
if (plan.viable) {
forgettingRenderer = new ForgettingRenderer({ engine, nodeRenderer: renderer, plan });
forgettingRenderer.upload();
// The 3 rescue ribbons ride the proven path-ribbon machinery.
renderer.setPathSteps(plan.pathData, plan.pathMetas);
}
// No verdict overlay for this moment — the spine narrates.
pathSteps = plan.spineBeats;
} else if (isFirewall) {
// Moment E: the immune response — a suspicious memory flares,
// a crimson shockwave crosses the field, its edges sever one by
// one, a membrane rings it, the verdict card lands. Pure CPU
// plan; the FirewallRenderer overwrites the demo lanes AFTER
// recall_sim (construction order = pass order).
const plan = buildFirewallPlan(renderer.graph!, seed);
firewallPlan = plan;
if (plan.viable) {
firewallRenderer = new FirewallRenderer({ engine, nodeRenderer: renderer, plan });
firewallRenderer.upload();
// The sever probe-beams ride the proven path-ribbon machinery.
renderer.setPathSteps(plan.pathData, plan.pathMetas);
}
pathSteps = plan.spineBeats;
} else {
pathSteps = renderer.pathSteps;
}
// Start the story at frame 0 now that the field exists — where the
// loop begins must never depend on how long the API call took.
engine.demoClock.reset();
}
});
onMount(() => {
loadGraph();
});
</script>
<svelte:window onkeydown={onKeydown} />
<!-- Full-bleed void stage: #05060a -->
<div class="fixed inset-0 overflow-hidden bg-[#05060a]" class:cursor-none={capture}>
<!-- Canvas layer (z-index 0) — the living memory field -->
<div class="absolute inset-0 z-0">
<ObservatoryCanvas
{demo}
{seed}
{freezeFrame}
onframe={handleFrame}
onready={handleReady}
/>
</div>
<!-- DOM overlay layer (pointer-events:none) — instruments only.
capture hides it all (pure canvas for recording); H toggles. -->
{#if showHud}
<div class="absolute inset-0 z-10 pointer-events-none">
<!-- Top telemetry strip -->
<TelemetryStrip
demoMode={demo}
{seed}
{nodeCount}
{edgeCount}
{centerId}
{frameCount}
{fpsEstimate}
{freezeFrame}
{loading}
{error}
/>
<!-- Exit control — back to the dashboard graph. Esc works too. -->
{#if onexit}
<button
onclick={onexit}
class="absolute top-10 right-4 pointer-events-auto font-mono text-xs tracking-widest
text-[#5dcaa5]/70 hover:text-[#5dcaa5] border border-[#5dcaa5]/25 hover:border-[#5dcaa5]/60
bg-[#05060a]/70 rounded px-3 py-1.5 transition-colors"
title="Exit Observatory (Esc)"
>
× EXIT
</button>
{/if}
<!-- Demo moment switcher — the 5 cognitive moments, one click each. -->
{#if showSwitcher}
<div class="absolute top-10 left-4 pointer-events-auto flex flex-col gap-1.5">
{#each DEMO_MODES as mode (mode)}
<button
onclick={() => ondemochange?.(mode)}
class="font-mono text-[11px] tracking-widest text-left rounded px-3 py-1.5 border transition-colors
{mode === demo
? 'text-[#05060a] bg-[#5dcaa5] border-[#5dcaa5]'
: 'text-[#5dcaa5]/60 hover:text-[#5dcaa5] bg-[#05060a]/70 border-[#5dcaa5]/20 hover:border-[#5dcaa5]/50'}"
title="Play the {DEMO_LABELS[mode]} moment"
>
{DEMO_LABELS[mode]}
</button>
{/each}
</div>
{/if}
<!-- Loading state -->
{#if loading}
<div class="absolute inset-0 flex items-center justify-center pointer-events-auto">
<div class="text-[#5dcaa5] font-mono text-sm tracking-widest animate-pulse">
LOADING MEMORY FIELD...
</div>
</div>
{/if}
<!-- Error state -->
{#if error && !loading}
<div class="absolute inset-0 flex items-center justify-center pointer-events-auto">
<div class="text-red-400 font-mono text-sm border border-red-900/50 bg-red-950/30 px-4 py-2 rounded">
{error}
</div>
</div>
{/if}
<!-- Timeline spine: beat ticks + playhead riding the loop -->
<TimelineSpine steps={pathSteps} frame={frameCount} />
<!-- Salience-rescue verdict card (frame-driven opacity) -->
{#if demo === 'salience-rescue' && rescuePlan?.viable}
<RescueVerdict frame={frameCount} verdict={rescuePlan.verdict} />
{/if}
<!-- Firewall quarantine verdict card (frames 480-620, crimson tone) -->
{#if demo === 'firewall' && firewallPlan?.viable}
<RescueVerdict
frame={frameCount}
tone="quarantine"
fadeWindow={[480, 495, 605, 620]}
verdict={{
headline: firewallPlan.verdict.headline,
causeLabel: firewallPlan.verdict.intruderLabel,
receipt: firewallPlan.verdict.receipt
}}
/>
{/if}
<!-- Empty graph state -->
{#if !loading && graphData && graphData.nodeCount === 0}
<div class="absolute inset-0 flex items-center justify-center pointer-events-auto">
<div class="text-[#5dcaa5] font-mono text-sm tracking-widest">
NO MEMORIES IN FIELD
</div>
</div>
{/if}
</div>
{/if}
</div>
<!-- VISUAL DNA §7: void #05060a is the base (bg utility above) — no exceptions -->

View file

@ -20,6 +20,10 @@
import type { GraphMutation } from '$lib/graph/events';
import { AHAGRAPH_COLORS, AHAGRAPH_DESCRIPTIONS, type ColorMode } from '$lib/graph/nodes';
import { filterByDate } from '$lib/graph/temporal';
// Cognitive Observatory — the WebGPU cinematic moments, launched from THIS
// page (its primary home) as a full-bleed takeover, like Memory Cinema.
import ObservatoryStage from '$lib/observatory/ObservatoryStage.svelte';
import type { DemoMode } from '$lib/observatory/types';
let graphData: GraphResponse | null = $state(null);
let selectedMemory: Memory | null = $state(null);
@ -54,6 +58,12 @@
let liveNodeCount = $state(0);
let liveEdgeCount = $state(0);
// Observatory takeover state — opens full-bleed over the graph; Esc or
// × EXIT returns here. {#key obsDemo} remounts the stage per moment so
// every demo starts deterministically from frame 0.
let showObservatory = $state(false);
let obsDemo = $state<DemoMode>('recall-path');
// Filtered graph data based on temporal mode
let displayNodes = $derived.by((): GraphNode[] => {
if (!graphData) return [];
@ -389,6 +399,18 @@ disown</code>
{isDreaming ? 'Dreaming…' : 'Dream'}
</button>
<!-- Observatory — the WebGPU cognitive moments, full-bleed takeover.
Lives HERE on the main graph, beside Dream and Memory Cinema. -->
<button
onclick={() => (showObservatory = true)}
class="shrink-0 inline-flex items-center gap-2 min-h-10 px-4 py-2 rounded-xl bg-[#5dcaa5]/15 border border-[#5dcaa5]/40 text-[#7fe6c0] text-sm
hover:bg-[#5dcaa5]/25 transition-all backdrop-blur-sm"
title="Open the Cognitive Observatory"
>
<Icon name="sparkle" size={16} />
Observatory
</button>
<!-- ═══════════════════════════════════════════════════════════
MEMORY CINEMA — PROTECTED · DO NOT MODIFY
The AI-narrated flythrough. Its trigger + props are flawless
@ -544,3 +566,21 @@ disown</code>
</div>
{/if}
</div>
<!-- ═══════════════════════════════════════════════════════════════════════
COGNITIVE OBSERVATORY — full-bleed takeover, part of the MAIN graph.
z-50 lifts it above all app chrome; the {#key} remounts the stage per
moment so every demo runs deterministically from frame 0. × EXIT / Esc
returns to this graph exactly as it was.
═══════════════════════════════════════════════════════════════════════ -->
{#if showObservatory}
<div class="fixed inset-0 z-50">
{#key obsDemo}
<ObservatoryStage
demo={obsDemo}
ondemochange={(d) => (obsDemo = d)}
onexit={() => (showObservatory = false)}
/>
{/key}
</div>
{/if}

View file

@ -1,291 +1,49 @@
<script lang="ts">
import { onMount } from 'svelte';
import { api } from '$stores/api';
import type { GraphResponse } from '$types';
import TelemetryStrip from '$lib/observatory/overlays/TelemetryStrip.svelte';
import TimelineSpine from '$lib/observatory/overlays/TimelineSpine.svelte';
import RescueVerdict from '$lib/observatory/overlays/RescueVerdict.svelte';
import ObservatoryCanvas from '$lib/components/ObservatoryCanvas.svelte';
/**
* /observatory route — thin wrapper over ObservatoryStage.
*
* The full experience lives in $lib/observatory/ObservatoryStage.svelte so
* the MAIN dashboard graph page can host it too (its primary home). This
* route keeps the deep-link + recording contract byte-compatible:
* ?demo=recall-path&seed=vestige-observatory-v1 — pick the moment
* ?frame=N — deterministic freeze
* ?capture=1 (&hud=1) — chrome-free recording
*/
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import ObservatoryStage from '$lib/observatory/ObservatoryStage.svelte';
import { isDemoMode, type DemoMode } from '$lib/observatory/types';
import type { ObservatoryEngine } from '$lib/observatory/engine';
import { NodeRenderer } from '$lib/observatory/node-renderer';
import { BirthRenderer } from '$lib/observatory/birth-renderer';
import { RescueRenderer } from '$lib/observatory/rescue-renderer';
import { buildRescuePlan, type RescuePlan } from '$lib/observatory/rescue-plan';
import { ForgettingRenderer } from '$lib/observatory/forgetting-renderer';
import { buildForgettingPlan } from '$lib/observatory/forgetting-plan';
import { FirewallRenderer } from '$lib/observatory/firewall-renderer';
import { buildFirewallPlan, type FirewallPlan } from '$lib/observatory/firewall-plan';
import type { PathStepMeta } from '$lib/observatory/path-builder';
// URL contract: ?demo=recall-path&seed=vestige-observatory-v1
const params = new URLSearchParams(window.location.search);
const demoParam = params.get('demo') ?? 'recall-path';
const demoMode: DemoMode = isDemoMode(demoParam) ? demoParam : 'recall-path';
let demo = $state<DemoMode>(isDemoMode(demoParam) ? demoParam : 'recall-path');
const seedValue = params.get('seed') ?? 'vestige-observatory-v1';
// Capture mode: ?frame=N freezes the sim at one loop frame (identical pixels).
const frameParam = params.get('frame');
const freezeFrame = frameParam !== null && frameParam !== '' ? Number(frameParam) : null;
// Recording mode: ?capture=1 hides EVERY DOM instrument — pure canvas for
// clips/stills. &hud=1 keeps the instruments; H toggles at runtime.
let showHud = $state(!(params.get('capture') === '1' && params.get('hud') !== '1'));
const isCapture = params.get('capture') === '1';
// Recording mode: ?capture=1 hides EVERY DOM instrument — pure canvas.
const isCapture = params.get('capture') === '1' && params.get('hud') !== '1';
function onKeydown(e: KeyboardEvent) {
if (e.key === 'h' || e.key === 'H') showHud = !showHud;
function switchDemo(next: DemoMode) {
if (next === demo) return;
demo = next;
// Keep the URL shareable — same contract as arriving via deep link.
const url = new URL(window.location.href);
url.searchParams.set('demo', next);
history.replaceState(history.state, '', url);
}
let graphData: GraphResponse | null = $state(null);
let loading = $state(true);
let error = $state('');
// Telemetry state — frames come from the engine's deterministic DemoClock.
let frameCount = $state(0);
let fpsEstimate = $state(0);
let nodeCount = $state(0);
let edgeCount = $state(0);
let centerId = $state('');
// Engine + renderer handles (upload happens once both are ready).
let engine = $state<ObservatoryEngine | null>(null);
let renderer: NodeRenderer | null = null;
let birthRenderer: BirthRenderer | null = null;
let rescueRenderer: RescueRenderer | null = null;
let rescuePlan = $state<RescuePlan | null>(null);
let forgettingRenderer: ForgettingRenderer | null = null;
let firewallRenderer: FirewallRenderer | null = null;
let firewallPlan = $state<FirewallPlan | null>(null);
let uploaded = false;
let pathSteps = $state<PathStepMeta[]>([]);
async function loadGraph() {
loading = true;
error = '';
try {
// Pull the DENSE real subgraph (the well-connected hotspot), not the
// newest-memory neighborhood — 'recent' centers on a lonely fresh node
// (~12 nodes), while 'connected' surfaces the populous, edge-rich field
// (~150 real memories, thousands of edges) so the Observatory reads as
// a living brain doing real work, not a sparse placeholder.
const data = await api.graph({
max_nodes: 200,
depth: 3,
sort: 'connected'
});
graphData = data;
nodeCount = data.nodeCount;
edgeCount = data.edgeCount;
centerId = data.center_id;
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to load graph data';
} finally {
loading = false;
}
}
function handleFrame(frame: number, fps: number) {
frameCount = frame;
fpsEstimate = fps;
}
function handleReady(e: ObservatoryEngine) {
// A fresh engine (first boot or HMR re-create) always needs an upload.
uploaded = false;
engine = e;
renderer = new NodeRenderer(e);
}
// Upload the memory field once the engine AND the graph are both ready.
$effect(() => {
if (engine && renderer && graphData && !uploaded) {
uploaded = true;
const isBirth = demoMode === 'engram-birth';
const isRescue = demoMode === 'salience-rescue';
const isHorizon = demoMode === 'forgetting-horizon';
const isFirewall = demoMode === 'firewall';
renderer.upload(graphData, seedValue, {
recallPath: !isBirth && !isRescue && !isHorizon && !isFirewall
});
if (isBirth) {
// Moment B: violet dust converges into a newborn memory.
birthRenderer = new BirthRenderer({ engine, nodeRenderer: renderer, seed: seedValue });
birthRenderer.upload(seedValue);
// B6: the engrave steps ride the proven recall wavefront system —
// pulses travel outward from the newborn, neighbors bloom on landing.
const engrave = birthRenderer.engraveSteps;
const engraveMetas = [];
for (let i = 0; i < engrave.length / 4; i++) {
engraveMetas.push({
sourceIndex: engrave[i * 4],
targetIndex: engrave[i * 4 + 1],
beatFrame: engrave[i * 4 + 2],
kind: engrave[i * 4 + 3],
beatKind: 'engrave',
nodeId: `engrave-${i}`,
label: 'edge engraved'
});
}
renderer.setPathSteps(engrave, engraveMetas);
// Spine shows the birth beats (converge → flash → engrave).
pathSteps = birthRenderer.timeline.map((b, i) => ({
sourceIndex: 0,
targetIndex: 0,
beatFrame: b.startFrame,
kind: 0,
beatKind: 'birth',
nodeId: `birth-${i}`,
label: b.label
}));
} else if (isRescue) {
// Moment C: Retroactive Salience Backfill — vector search fails on
// camera, then Vestige reaches backward through time and ignites
// the true cause. All choreography is a pure CPU plan; the
// RescueRenderer overwrites the demo lanes AFTER recall_sim
// (construction order = pass order — load-bearing for the seam).
const plan = buildRescuePlan(graphData, renderer.graph!, seedValue);
rescuePlan = plan;
if (plan.viable) {
rescueRenderer = new RescueRenderer({ engine, nodeRenderer: renderer, plan });
rescueRenderer.upload();
// Probe beams + backward wave tree + causal arc ride the proven
// path-ribbon machinery. Metas are 1:1 with the GPU steps
// (setPathSteps sets params[4] + draw count from META length).
renderer.setPathSteps(plan.pathData, plan.pathMetas);
}
// Spine shows the curated story beats (unique beatFrames), never
// the raw GPU steps.
pathSteps = plan.spineBeats;
} else if (isHorizon) {
// Moment D: the forgetting horizon — FSRS as a visible living
// system. The lowest-retention memories dim and fall; three are
// recalled back on camera; the rest sink to a 6% floor (never
// deleted). Pure CPU plan; the ForgettingRenderer overwrites the
// demo lanes AFTER recall_sim (construction order = pass order).
const plan = buildForgettingPlan(renderer.graph!);
if (plan.viable) {
forgettingRenderer = new ForgettingRenderer({ engine, nodeRenderer: renderer, plan });
forgettingRenderer.upload();
// The 3 rescue ribbons ride the proven path-ribbon machinery.
renderer.setPathSteps(plan.pathData, plan.pathMetas);
}
// No verdict overlay for this moment — the spine narrates.
pathSteps = plan.spineBeats;
} else if (isFirewall) {
// Moment E: the immune response — a suspicious memory flares,
// a crimson shockwave crosses the field, its edges sever one by
// one, a membrane rings it, the verdict card lands. Pure CPU
// plan; the FirewallRenderer overwrites the demo lanes AFTER
// recall_sim (construction order = pass order).
const plan = buildFirewallPlan(renderer.graph!, seedValue);
firewallPlan = plan;
if (plan.viable) {
firewallRenderer = new FirewallRenderer({ engine, nodeRenderer: renderer, plan });
firewallRenderer.upload();
// The sever probe-beams ride the proven path-ribbon machinery.
renderer.setPathSteps(plan.pathData, plan.pathMetas);
}
pathSteps = plan.spineBeats;
} else {
pathSteps = renderer.pathSteps;
}
// Start the story at frame 0 now that the field exists — where the
// loop begins must never depend on how long the API call took.
engine.demoClock.reset();
}
});
onMount(() => {
loadGraph();
});
</script>
<svelte:window onkeydown={onKeydown} />
<!-- Full-bleed void stage: #05060a -->
<div class="fixed inset-0 overflow-hidden bg-[#05060a]" class:cursor-none={isCapture}>
<!-- Canvas layer (z-index 0) — the living memory field -->
<div class="absolute inset-0 z-0">
<ObservatoryCanvas
demo={demoMode}
seed={seedValue}
{freezeFrame}
onframe={handleFrame}
onready={handleReady}
/>
</div>
<!-- DOM overlay layer (pointer-events:none) — instruments only.
?capture=1 hides it all (pure canvas for recording); H toggles. -->
{#if showHud}
<div class="absolute inset-0 z-10 pointer-events-none">
<!-- Top telemetry strip -->
<TelemetryStrip
demoMode={demoMode}
seed={seedValue}
nodeCount={nodeCount}
edgeCount={edgeCount}
centerId={centerId}
frameCount={frameCount}
fpsEstimate={fpsEstimate}
{freezeFrame}
loading={loading}
error={error}
/>
<!-- Loading state -->
{#if loading}
<div class="absolute inset-0 flex items-center justify-center pointer-events-auto">
<div class="text-[#5dcaa5] font-mono text-sm tracking-widest animate-pulse">
LOADING MEMORY FIELD...
</div>
</div>
{/if}
<!-- Error state -->
{#if error && !loading}
<div class="absolute inset-0 flex items-center justify-center pointer-events-auto">
<div class="text-red-400 font-mono text-sm border border-red-900/50 bg-red-950/30 px-4 py-2 rounded">
{error}
</div>
</div>
{/if}
<!-- Timeline spine: beat ticks + playhead riding the loop -->
<TimelineSpine steps={pathSteps} frame={frameCount} />
<!-- Salience-rescue verdict card (frames 600-660, frame-driven opacity) -->
{#if demoMode === 'salience-rescue' && rescuePlan?.viable}
<RescueVerdict frame={frameCount} verdict={rescuePlan.verdict} />
{/if}
<!-- Firewall quarantine verdict card (frames 480-620, crimson tone) -->
{#if demoMode === 'firewall' && firewallPlan?.viable}
<RescueVerdict
frame={frameCount}
tone="quarantine"
fadeWindow={[480, 495, 605, 620]}
verdict={{
headline: firewallPlan.verdict.headline,
causeLabel: firewallPlan.verdict.intruderLabel,
receipt: firewallPlan.verdict.receipt
}}
/>
{/if}
<!-- Empty graph state -->
{#if !loading && graphData && graphData.nodeCount === 0}
<div class="absolute inset-0 flex items-center justify-center pointer-events-auto">
<div class="text-[#5dcaa5] font-mono text-sm tracking-widest">
NO MEMORIES IN FIELD
</div>
</div>
{/if}
</div>
{/if}
</div>
<!-- VISUAL DNA §7: void #05060a is the base (bg utility above) — no exceptions -->
<!-- {#key} forces a full remount on demo switch: fresh engine, plans, clock —
deterministic from frame 0, no partial-state carryover. -->
{#key demo}
<ObservatoryStage
{demo}
seed={seedValue}
{freezeFrame}
capture={isCapture}
showSwitcher={freezeFrame === null}
ondemochange={switchDemo}
onexit={() => goto(`${base}/graph`)}
/>
{/key}