mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-24 23:41:01 +02:00
feat(dashboard): Organ 1 — Reasoning Theater (Cognitive OS de-risk prototype)
The first full-bleed WebGPU organ of the Cognitive Bioluminescent Cortex,
built by GPT-5.5 in the design council, audited + fixed live by Opus 4.8.
- RouteStage.svelte: reusable organ shell (engine/canvas lifecycle, WebGPU
fallback, reduced-motion auto-pause, persistent pause control) — the
lifecycle-only reuse of ObservatoryStage, NOT its Graph data path.
- reasoning/reasoning-scene.ts: /deep_reference → RouteSceneModel adapter.
Honest 8-stage derivation from real fields (label unexposed internals),
real backend contradiction {stronger/weaker} + superseded {superseded_by}
shapes, provenance enforced.
- reasoning/reasoning-theater-pass.ts: the 8-chamber organ. Metaball field
(additive splat -> render-pass separable blur -> membrane, all render
passes, no storage textures), compute Bezier spline packet advection,
labeled deterministic replay of the one-shot receipt. Palette-driven;
magenta stays RSB-only; contradictions scarlet.
- reasoning/+page.svelte: query -> /deep_reference -> scene -> full-bleed
organ behind the DOM answer/evidence panels.
Three WebGPU runtime traps caught by the live-GPU audit (invisible to
check/build) and fixed: (T6) no read_write storage in a shared compute+
render module; (T3) no rgba16float write-storage blur -> use render-pass
blur; and the 'active' reserved-keyword WGSL parse error.
Verified LIVE vs the real 1241-memory brain: query "How does FSRS-6 trust
scoring work?" lit the chambers with a REAL memory (trust 51%), 84%
confidence, 26 analyzed, contradiction stage correctly dark (none real),
120fps. check + build + 1043 tests green. Cinema + Graph field untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9017d731dc
commit
b5150f0b90
5 changed files with 1347 additions and 4 deletions
|
|
@ -245,9 +245,15 @@ with legacy fallback:
|
|||
closest real receipt id = `composition_event_id`. There is NO `receipt` field.
|
||||
|
||||
**T3 — WebGPU format + FramePass traps.**
|
||||
- Field ping-pong / storage textures: use `rgba16float`, NOT `rg16float` write-
|
||||
only storage (narrower support; portability trap). Logical channels in `.rg`,
|
||||
`.ba` reserved. OR do separable blur as fullscreen render-pass draws.
|
||||
- HARD RULE (verified live on M-series: `rgba16float-renderable`=false, rgba16float
|
||||
is NOT a writable storage-texture format here): NEVER use `texture_storage_2d<...,
|
||||
write>` + `textureStore` for the blur. Do the separable blur as FULLSCREEN
|
||||
RENDER-PASS draws (H then V) into RENDER_ATTACHMENT textures, sampling the
|
||||
source via `textureSampleLevel`. Field textures usage = `RENDER_ATTACHMENT |
|
||||
TEXTURE_BINDING` (NO `STORAGE_BINDING`). The whole field pipeline is render
|
||||
passes: splat (additive) → blur-H → blur-V → membrane. No storage textures
|
||||
anywhere. `rgba16float` is fully valid as a RENDER TARGET.
|
||||
- Logical channels in `.rg`, `.ba` reserved.
|
||||
- FramePass sequencing: the engine calls ALL `compute()` then opens ONE main HDR
|
||||
scene pass and calls ALL `render()`. So a field pass does splat+blur INSIDE its
|
||||
`compute(encoder)` (it receives a `GPUCommandEncoder`, not a compute-pass
|
||||
|
|
@ -270,6 +276,19 @@ shared Graph params. Run `assertProvenance(scene)` in dev before upload.
|
|||
contradiction seam — WRONG. Contradictions use scarlet/immune reds. Magenta
|
||||
`#FF2DF7` appears ONLY in the retrograde causal axon.
|
||||
|
||||
**T6 — NEVER share one WGSL module between compute and render pipelines when it
|
||||
declares a `read_write` storage buffer or a `write` storage texture.** WebGPU
|
||||
forbids read_write storage buffers AND write storage textures in vertex/fragment
|
||||
stages (compute-only). A render pipeline whose module/layout exposes such a
|
||||
binding is INVALID at runtime (check+build stay green — only live-GPU validation
|
||||
catches it). SPLIT WGSL into per-pipeline modules: compute module (advect/blur)
|
||||
= read_write storage + write storage texture OK; render modules (splat/membrane)
|
||||
= storage buffers as `var<storage, read>` only. Each pipeline gets its own
|
||||
explicit bind group layout matching exactly. (Found live in Organ 1's field
|
||||
splat pass — this is the D1 metaball pattern's #1 trap.)
|
||||
This is why Claude's LIVE-GPU verify per organ is non-optional: the swarm
|
||||
verifier's check/build cannot catch invalid-pipeline runtime errors.
|
||||
|
||||
## 7. THE PROTOTYPE ACCEPTANCE TEST (Organ 1, de-risks all)
|
||||
1. User enters a query on `/reasoning`.
|
||||
2. Backend returns `/deep_reference`.
|
||||
|
|
|
|||
199
apps/dashboard/src/lib/observatory/RouteStage.svelte
Normal file
199
apps/dashboard/src/lib/observatory/RouteStage.svelte
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import ObservatoryCanvas from '$lib/components/ObservatoryCanvas.svelte';
|
||||
import type { ObservatoryEngine, FramePass } from '$lib/observatory/engine';
|
||||
import { type DemoMode } from '$lib/observatory/types';
|
||||
import {
|
||||
assertProvenance,
|
||||
emptyScene,
|
||||
type RouteOrgan,
|
||||
type RouteSceneModel
|
||||
} from '$lib/observatory/route-scene';
|
||||
|
||||
export interface RoutePick {
|
||||
id: string;
|
||||
kind: string;
|
||||
index?: number;
|
||||
payload?: unknown;
|
||||
}
|
||||
|
||||
export interface RouteFramePass extends FramePass {
|
||||
uploadScene?: (scene: RouteSceneModel) => void;
|
||||
pickAt?: (ndcX: number, ndcY: number) => Promise<RoutePick | null> | RoutePick | null;
|
||||
dispose?: () => void;
|
||||
}
|
||||
|
||||
export type RoutePassFactory = (
|
||||
engine: ObservatoryEngine,
|
||||
scene: RouteSceneModel
|
||||
) => RouteFramePass[];
|
||||
|
||||
interface Props {
|
||||
organ: RouteOrgan;
|
||||
seed?: string;
|
||||
scene?: RouteSceneModel | null;
|
||||
passes?: RoutePassFactory | RouteFramePass[];
|
||||
embedded?: boolean;
|
||||
onpick?: (pick: RoutePick) => void;
|
||||
/** Existing ObservatoryEngine needs an existing DemoMode; route id stays pass-local. */
|
||||
demo?: DemoMode;
|
||||
loading?: boolean;
|
||||
error?: string | null;
|
||||
emptyLabel?: string;
|
||||
}
|
||||
|
||||
let {
|
||||
organ,
|
||||
seed = 'vestige-route-organ-v1',
|
||||
scene = null,
|
||||
passes,
|
||||
embedded = false,
|
||||
onpick,
|
||||
demo = 'recall-path',
|
||||
loading = false,
|
||||
error = null,
|
||||
emptyLabel = 'NO ROUTE DATA IN FIELD'
|
||||
}: Props = $props();
|
||||
|
||||
let currentScene = $derived(scene ?? emptyScene(organ));
|
||||
let canvasLayerEl: HTMLDivElement | null = $state(null);
|
||||
let engine = $state<ObservatoryEngine | null>(null);
|
||||
let routePasses: RouteFramePass[] = [];
|
||||
let frameCount = $state(0);
|
||||
let fpsEstimate = $state(0);
|
||||
let paused = $state(false);
|
||||
let userSetPause = $state(false);
|
||||
let ready = $state(false);
|
||||
|
||||
function initReducedMotion() {
|
||||
if (typeof window === 'undefined') return;
|
||||
const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
|
||||
if (mq.matches && !userSetPause) paused = true;
|
||||
const onChange = (e: MediaQueryListEvent) => {
|
||||
if (!userSetPause) paused = e.matches;
|
||||
};
|
||||
mq.addEventListener('change', onChange);
|
||||
return () => mq.removeEventListener('change', onChange);
|
||||
}
|
||||
|
||||
function togglePause() {
|
||||
userSetPause = true;
|
||||
paused = !paused;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
engine?.setPaused(paused);
|
||||
});
|
||||
|
||||
function upload(sceneToUpload: RouteSceneModel) {
|
||||
if (!engine) return;
|
||||
if (import.meta.env.DEV) assertProvenance(sceneToUpload);
|
||||
for (const pass of routePasses) pass.uploadScene?.(sceneToUpload);
|
||||
engine.demoClock.reset();
|
||||
}
|
||||
|
||||
function handleFrame(frame: number, fps: number) {
|
||||
frameCount = frame;
|
||||
fpsEstimate = fps;
|
||||
}
|
||||
|
||||
function handleReady(e: ObservatoryEngine) {
|
||||
ready = true;
|
||||
engine = e;
|
||||
for (const pass of routePasses) pass.dispose?.();
|
||||
routePasses = typeof passes === 'function' ? passes(e, currentScene) : (passes ?? []);
|
||||
for (const pass of routePasses) e.addPass(pass);
|
||||
upload(currentScene);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!engine || !ready) return;
|
||||
upload(currentScene);
|
||||
});
|
||||
|
||||
async function handleFieldClick(e: MouseEvent) {
|
||||
if (!onpick || !canvasLayerEl) return;
|
||||
const rect = canvasLayerEl.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) return;
|
||||
const ndcX = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||
const ndcY = -(((e.clientY - rect.top) / rect.height) * 2 - 1);
|
||||
for (const pass of routePasses) {
|
||||
const hit = await pass.pickAt?.(ndcX, ndcY);
|
||||
if (hit) {
|
||||
onpick(hit);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
return () => {
|
||||
for (const pass of routePasses) pass.dispose?.();
|
||||
routePasses = [];
|
||||
};
|
||||
});
|
||||
|
||||
onMount(() => initReducedMotion());
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
||||
<div class="{embedded ? 'absolute' : 'fixed'} inset-0 overflow-hidden route-stage">
|
||||
<div
|
||||
bind:this={canvasLayerEl}
|
||||
class="absolute inset-0 z-0"
|
||||
class:cursor-crosshair={!!onpick}
|
||||
onclick={handleFieldClick}
|
||||
>
|
||||
<ObservatoryCanvas {demo} {seed} onframe={handleFrame} onready={handleReady} />
|
||||
</div>
|
||||
|
||||
<div class="absolute inset-0 z-10 pointer-events-none">
|
||||
<slot name="chrome" {frameCount} {fpsEstimate} {paused} {ready} />
|
||||
|
||||
<button
|
||||
onclick={togglePause}
|
||||
class="absolute bottom-4 right-4 pointer-events-auto flex items-center gap-2 px-3 py-1.5
|
||||
rounded-xl border border-[#22C7DE]/25 bg-[#020307]/80 backdrop-blur-sm
|
||||
font-mono text-[11px] tracking-wide text-[#22C7DE]/80 hover:text-[#22C7DE]
|
||||
hover:border-[#22C7DE]/50 transition-colors"
|
||||
title={paused ? 'Resume route organ motion' : 'Pause route organ motion'}
|
||||
aria-pressed={paused}
|
||||
>
|
||||
{paused ? '▶ RESUME' : '❚❚ PAUSE'}
|
||||
</button>
|
||||
|
||||
<div class="absolute top-4 right-4 font-mono text-[10px] tracking-[0.18em] text-[#7fe6c0]/60 uppercase">
|
||||
{organ} · {frameCount}f · {fpsEstimate}fps
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-auto">
|
||||
<div class="text-[#A8FF5E] font-mono text-sm tracking-widest animate-pulse">
|
||||
REPLAYING COGNITIVE RECEIPT...
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if error && !loading}
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-auto">
|
||||
<div class="text-[#ff8a8a] font-mono text-sm border border-[#FF3B30]/50 bg-[#1a0508]/70 px-4 py-2 rounded">
|
||||
{error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !loading && !error && !currentScene.alive}
|
||||
<div class="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div class="text-[#5dcaa5]/70 font-mono text-xs tracking-widest border border-[#5dcaa5]/15 bg-[#020307]/45 px-4 py-2 rounded-xl backdrop-blur-sm">
|
||||
{emptyLabel}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.route-stage {
|
||||
background: #020307;
|
||||
}
|
||||
</style>
|
||||
370
apps/dashboard/src/lib/observatory/reasoning/reasoning-scene.ts
Normal file
370
apps/dashboard/src/lib/observatory/reasoning/reasoning-scene.ts
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
import {
|
||||
assertProvenance,
|
||||
type Provenance,
|
||||
type RouteEvent,
|
||||
type RouteNode,
|
||||
type RouteReceipt,
|
||||
type RouteSceneModel
|
||||
} from '$lib/observatory/route-scene';
|
||||
|
||||
export type ReasoningStageKind =
|
||||
| 'intent'
|
||||
| 'retrieve'
|
||||
| 'activate'
|
||||
| 'evidence'
|
||||
| 'contradiction'
|
||||
| 'synthesis'
|
||||
| 'recommendation'
|
||||
| 'receipt';
|
||||
|
||||
export interface ReasoningStageReceipt {
|
||||
index: number;
|
||||
kind: ReasoningStageKind;
|
||||
label: string;
|
||||
count: number;
|
||||
confidence: number;
|
||||
lit: boolean;
|
||||
provenance: Provenance;
|
||||
exposed: Record<string, unknown>;
|
||||
not_exposed_by_backend: string[];
|
||||
interrupt: 'none' | 'contradiction' | 'supersession';
|
||||
}
|
||||
|
||||
export interface NormalizedEvidence {
|
||||
id: string;
|
||||
trust: number;
|
||||
date: string;
|
||||
role: 'primary' | 'supporting' | 'contradicting' | 'superseded';
|
||||
preview: string;
|
||||
nodeType?: string;
|
||||
}
|
||||
|
||||
export interface NormalizedContradiction {
|
||||
stronger: NormalizedEvidence;
|
||||
weaker: NormalizedEvidence;
|
||||
topic_overlap: number;
|
||||
summary: string;
|
||||
}
|
||||
|
||||
export interface NormalizedSupersession {
|
||||
id: string;
|
||||
preview: string;
|
||||
trust: number;
|
||||
date: string;
|
||||
superseded_by: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export interface NormalizedRecommended {
|
||||
answer_preview: string;
|
||||
memory_id: string;
|
||||
trust_score: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface ReasoningScene extends RouteSceneModel {
|
||||
organ: 'reasoning';
|
||||
stages: ReasoningStageReceipt[];
|
||||
evidence: NormalizedEvidence[];
|
||||
contradictions: NormalizedContradiction[];
|
||||
superseded: NormalizedSupersession[];
|
||||
recommended: NormalizedRecommended | null;
|
||||
raw: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const STAGE_LABELS: Record<ReasoningStageKind, string> = {
|
||||
intent: 'Intent classification',
|
||||
retrieve: 'Memory retrieval',
|
||||
activate: 'Activation expansion',
|
||||
evidence: 'Evidence grounding',
|
||||
contradiction: 'Contradiction check',
|
||||
synthesis: 'Synthesis',
|
||||
recommendation: 'Recommendation',
|
||||
receipt: 'Composition receipt'
|
||||
};
|
||||
|
||||
const STAGE_KINDS: ReasoningStageKind[] = [
|
||||
'intent',
|
||||
'retrieve',
|
||||
'activate',
|
||||
'evidence',
|
||||
'contradiction',
|
||||
'synthesis',
|
||||
'recommendation',
|
||||
'receipt'
|
||||
];
|
||||
|
||||
function record(v: unknown): Record<string, unknown> {
|
||||
return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
function text(v: unknown, fallback = ''): string {
|
||||
return typeof v === 'string' ? v : v == null ? fallback : String(v);
|
||||
}
|
||||
|
||||
function num(v: unknown, fallback = 0): number {
|
||||
return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
|
||||
}
|
||||
|
||||
function clamp01(v: number): number {
|
||||
return Math.max(0, Math.min(1, v));
|
||||
}
|
||||
|
||||
function confidence01(v: unknown): number {
|
||||
const n = num(v, 0);
|
||||
return clamp01(n > 1 ? n / 100 : n);
|
||||
}
|
||||
|
||||
function role(v: unknown): NormalizedEvidence['role'] {
|
||||
return v === 'primary' || v === 'supporting' || v === 'contradicting' || v === 'superseded'
|
||||
? v
|
||||
: 'supporting';
|
||||
}
|
||||
|
||||
function evidenceFromRecord(e: Record<string, unknown>, fallbackId = ''): NormalizedEvidence {
|
||||
const trust = confidence01(e.trust ?? e.trust_score ?? 0);
|
||||
return {
|
||||
id: text(e.id ?? e.memory_id ?? fallbackId),
|
||||
trust,
|
||||
date: text(e.date ?? e.created_at ?? ''),
|
||||
role: role(e.role),
|
||||
preview: text(e.preview ?? e.answer_preview ?? e.content ?? ''),
|
||||
nodeType: e.node_type ? text(e.node_type) : e.nodeType ? text(e.nodeType) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
function source(kind: Provenance['kind'], id: string, scalar?: Provenance['scalar']): Provenance {
|
||||
return scalar ? { kind, id, scalar } : { kind, id: id || `${kind}:unknown` };
|
||||
}
|
||||
|
||||
function scalarSource(name: string, value: number): Provenance {
|
||||
return { kind: 'scalar', id: `deep_reference.${name}`, scalar: { name, value } };
|
||||
}
|
||||
|
||||
export function normalizeDeepReferenceResponse(rawInput: Record<string, unknown>): ReasoningScene {
|
||||
const raw = record(rawInput);
|
||||
const evidenceRaw = Array.isArray(raw.evidence) ? raw.evidence.map(record) : [];
|
||||
const evidence = evidenceRaw
|
||||
.map((e) => evidenceFromRecord(e))
|
||||
.filter((e) => e.id.length > 0);
|
||||
|
||||
const rec = record(raw.recommended);
|
||||
const recommended: NormalizedRecommended | null =
|
||||
Object.keys(rec).length > 0 || evidence.length > 0
|
||||
? {
|
||||
answer_preview: text(rec.answer_preview ?? evidence[0]?.preview ?? ''),
|
||||
memory_id: text(rec.memory_id ?? evidence[0]?.id ?? ''),
|
||||
trust_score: confidence01(rec.trust_score ?? evidence[0]?.trust ?? 0),
|
||||
date: text(rec.date ?? evidence[0]?.date ?? '')
|
||||
}
|
||||
: null;
|
||||
|
||||
const contradictionsRaw = Array.isArray(raw.contradictions)
|
||||
? raw.contradictions.map(record)
|
||||
: Array.isArray(raw.claim_conflicts)
|
||||
? raw.claim_conflicts.map(record)
|
||||
: [];
|
||||
const contradictions: NormalizedContradiction[] = contradictionsRaw
|
||||
.map((c) => {
|
||||
// T2 current shape first: { stronger:{id,...}, weaker:{id,...}, topic_overlap }
|
||||
const strongerRaw = record(c.stronger);
|
||||
const weakerRaw = record(c.weaker);
|
||||
if (Object.keys(strongerRaw).length > 0 || Object.keys(weakerRaw).length > 0) {
|
||||
const stronger = evidenceFromRecord(strongerRaw);
|
||||
const weaker = evidenceFromRecord(weakerRaw);
|
||||
return {
|
||||
stronger,
|
||||
weaker,
|
||||
topic_overlap: clamp01(num(c.topic_overlap, 0)),
|
||||
summary: text(
|
||||
c.summary,
|
||||
`Trust-weighted conflict: ${stronger.id.slice(0, 8)} over ${weaker.id.slice(0, 8)}`
|
||||
)
|
||||
};
|
||||
}
|
||||
// Legacy fallback: { a_id, b_id, summary }
|
||||
const a = evidenceFromRecord({ id: c.a_id, role: 'contradicting' });
|
||||
const b = evidenceFromRecord({ id: c.b_id, role: 'contradicting' });
|
||||
return {
|
||||
stronger: a,
|
||||
weaker: b,
|
||||
topic_overlap: clamp01(num(c.topic_overlap, 0)),
|
||||
summary: text(c.summary ?? c.reason, 'Trust-weighted conflict between high-FSRS memories.')
|
||||
};
|
||||
})
|
||||
.filter((c) => c.stronger.id || c.weaker.id);
|
||||
|
||||
const supersededRaw = Array.isArray(raw.superseded) ? raw.superseded.map(record) : [];
|
||||
const superseded: NormalizedSupersession[] = supersededRaw
|
||||
.map((s) => {
|
||||
// T2 current shape first: { id, preview, trust, date, superseded_by }
|
||||
if (s.id || s.superseded_by) {
|
||||
return {
|
||||
id: text(s.id),
|
||||
preview: text(s.preview ?? ''),
|
||||
trust: confidence01(s.trust ?? 0),
|
||||
date: text(s.date ?? ''),
|
||||
superseded_by: text(s.superseded_by ?? recommended?.memory_id ?? ''),
|
||||
reason: text(s.reason ?? 'Superseded by newer memory with higher trust.')
|
||||
};
|
||||
}
|
||||
// Legacy fallback: { old_id, new_id, reason }
|
||||
return {
|
||||
id: text(s.old_id),
|
||||
preview: text(s.preview ?? ''),
|
||||
trust: confidence01(s.trust ?? 0),
|
||||
date: text(s.date ?? ''),
|
||||
superseded_by: text(s.new_id ?? recommended?.memory_id ?? ''),
|
||||
reason: text(s.reason ?? 'Superseded by newer memory with higher trust.')
|
||||
};
|
||||
})
|
||||
.filter((s) => s.id || s.superseded_by);
|
||||
|
||||
const confidence = confidence01(raw.confidence);
|
||||
const memoriesAnalyzed = num(raw.memoriesAnalyzed ?? raw.memories_analyzed, evidence.length);
|
||||
const activationExpanded = num(raw.activationExpanded ?? raw.activation_expanded, 0);
|
||||
const intent = text(raw.intent ?? '');
|
||||
const reasoning = text(raw.reasoning ?? raw.guidance ?? '');
|
||||
const guidance = text(raw.guidance ?? '');
|
||||
const compositionEventId = text(raw.composition_event_id ?? '');
|
||||
const compositionWriteStatus = text(raw.compositionWriteStatus ?? raw.composition_write_status ?? '');
|
||||
const receiptValue = compositionEventId || compositionWriteStatus;
|
||||
const receiptIsPersisted = compositionEventId.length > 0;
|
||||
|
||||
const nodes: RouteNode[] = evidence.map((e, index) => ({
|
||||
source: source('memory', e.id),
|
||||
index,
|
||||
label: e.preview || e.id.slice(0, 8),
|
||||
retention: clamp01(e.trust),
|
||||
trust: clamp01(e.trust),
|
||||
lastAccessed: e.date || undefined,
|
||||
tags: [e.role, ...(e.nodeType ? [e.nodeType] : [])],
|
||||
type: e.nodeType ?? 'memory'
|
||||
}));
|
||||
|
||||
const indexById = new Map(nodes.map((n) => [n.source.id, n.index]));
|
||||
const edges = contradictions.flatMap((c, i) => {
|
||||
const a = indexById.get(c.stronger.id);
|
||||
const b = indexById.get(c.weaker.id);
|
||||
if (a == null || b == null) return [];
|
||||
return [
|
||||
{
|
||||
source: source('pair', `contradiction:${c.stronger.id}:${c.weaker.id}`),
|
||||
sourceIndex: a,
|
||||
targetIndex: b,
|
||||
weight: Math.max(0.2, c.topic_overlap || 0.5),
|
||||
kind: 'contradiction'
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
const events: RouteEvent[] = [];
|
||||
if (contradictions.length > 0) {
|
||||
events.push({
|
||||
source: source('event', `deep_reference.contradictions.${contradictions.length}`),
|
||||
type: 'ReasoningContradictionInterrupt',
|
||||
targetIndex: -1,
|
||||
frame: 250,
|
||||
energy: contradictions.length
|
||||
});
|
||||
}
|
||||
if (superseded.length > 0) {
|
||||
events.push({
|
||||
source: source('event', `deep_reference.superseded.${superseded.length}`),
|
||||
type: 'ReasoningSupersessionInterrupt',
|
||||
targetIndex: -1,
|
||||
frame: 330,
|
||||
energy: superseded.length
|
||||
});
|
||||
}
|
||||
|
||||
const receipts: RouteReceipt[] = [];
|
||||
if (receiptValue) {
|
||||
receipts.push({
|
||||
source: receiptIsPersisted
|
||||
? source('receipt', compositionEventId)
|
||||
: scalarSource('compositionWriteStatus', compositionWriteStatus === 'persisted' ? 1 : 0),
|
||||
label: receiptIsPersisted ? `receipt ${compositionEventId.slice(0, 8)}` : compositionWriteStatus,
|
||||
nodeIndices: nodes.map((n) => n.index)
|
||||
});
|
||||
}
|
||||
|
||||
const mkStage = (
|
||||
index: number,
|
||||
kind: ReasoningStageKind,
|
||||
count: number,
|
||||
stageConfidence: number,
|
||||
exposed: Record<string, unknown>,
|
||||
provenance: Provenance,
|
||||
missing: string[],
|
||||
interrupt: ReasoningStageReceipt['interrupt'] = 'none'
|
||||
): ReasoningStageReceipt => ({
|
||||
index,
|
||||
kind,
|
||||
label: STAGE_LABELS[kind],
|
||||
count,
|
||||
confidence: clamp01(stageConfidence),
|
||||
lit: count > 0 || stageConfidence > 0,
|
||||
provenance,
|
||||
exposed,
|
||||
not_exposed_by_backend: missing,
|
||||
interrupt
|
||||
});
|
||||
|
||||
const stages: ReasoningStageReceipt[] = STAGE_KINDS.map((kind, index) => {
|
||||
switch (kind) {
|
||||
case 'intent':
|
||||
return mkStage(index, kind, intent ? 1 : 0, intent ? confidence : 0, { intent }, scalarSource('intent_present', intent ? 1 : 0), ['raw classifier trace']);
|
||||
case 'retrieve':
|
||||
return mkStage(index, kind, memoriesAnalyzed || evidence.length, confidence, { memoriesAnalyzed, evidence_count: evidence.length }, scalarSource('memoriesAnalyzed', memoriesAnalyzed), ['candidate ids discarded before evidence']);
|
||||
case 'activate':
|
||||
return mkStage(index, kind, activationExpanded, activationExpanded > 0 ? confidence : 0, { activationExpanded }, scalarSource('activationExpanded', activationExpanded), ['activation path/map']);
|
||||
case 'evidence':
|
||||
return mkStage(index, kind, evidence.length, evidence.length > 0 ? confidence : 0, { evidence }, scalarSource('evidence_count', evidence.length), ['reranker discarded candidates']);
|
||||
case 'contradiction':
|
||||
return mkStage(index, kind, contradictions.length, contradictions.length > 0 ? confidence : 0, { contradictions, claim_conflicts: raw.claim_conflicts ?? [] }, scalarSource('contradiction_count', contradictions.length), ['full claim graph'], contradictions.length > 0 ? 'contradiction' : 'none');
|
||||
case 'synthesis':
|
||||
return mkStage(index, kind, reasoning || guidance ? 1 : 0, reasoning || guidance ? confidence : 0, { reasoning, guidance }, scalarSource('synthesis_present', reasoning || guidance ? 1 : 0), ['token-level chain internals']);
|
||||
case 'recommendation':
|
||||
return mkStage(index, kind, recommended?.memory_id ? 1 : 0, recommended?.trust_score ?? 0, { recommended }, recommended?.memory_id ? source('memory', recommended.memory_id) : scalarSource('recommended_present', 0), ['alternative recommendations']);
|
||||
case 'receipt':
|
||||
return mkStage(index, kind, receiptValue ? 1 : 0, receiptValue ? confidence : 0, { composition_event_id: compositionEventId, compositionWriteStatus }, receiptIsPersisted ? source('receipt', compositionEventId) : scalarSource('compositionWriteStatus', compositionWriteStatus === 'persisted' ? 1 : 0), ['separate receipt field'], superseded.length > 0 ? 'supersession' : 'none');
|
||||
}
|
||||
});
|
||||
|
||||
const scene: ReasoningScene = {
|
||||
organ: 'reasoning',
|
||||
nodes,
|
||||
edges,
|
||||
events,
|
||||
receipts,
|
||||
scalars: {
|
||||
confidence,
|
||||
memoriesAnalyzed,
|
||||
activationExpanded,
|
||||
evidenceCount: evidence.length,
|
||||
contradictionCount: contradictions.length,
|
||||
supersededCount: superseded.length,
|
||||
compositionPersisted: receiptIsPersisted ? 1 : 0
|
||||
},
|
||||
alive: !!(
|
||||
intent ||
|
||||
reasoning ||
|
||||
guidance ||
|
||||
evidence.length ||
|
||||
contradictions.length ||
|
||||
superseded.length ||
|
||||
recommended?.memory_id ||
|
||||
receiptValue
|
||||
),
|
||||
stages,
|
||||
evidence,
|
||||
contradictions,
|
||||
superseded,
|
||||
recommended,
|
||||
raw
|
||||
};
|
||||
|
||||
if (import.meta.env.DEV) assertProvenance(scene);
|
||||
return scene;
|
||||
}
|
||||
|
|
@ -0,0 +1,673 @@
|
|||
import type { ObservatoryEngine, FramePass } from '$lib/observatory/engine';
|
||||
import { CAUSAL, IMMUNE, RETENTION, rgb01 } from '$lib/observatory/cognitive-palette';
|
||||
import type { RouteSceneModel } from '$lib/observatory/route-scene';
|
||||
import type { ReasoningScene, ReasoningStageReceipt } from './reasoning-scene';
|
||||
|
||||
type RoutePick = { id: string; kind: string; index?: number; payload?: unknown };
|
||||
|
||||
const STAGE_COUNT = 8;
|
||||
const STAGE_FLOATS = 8;
|
||||
const PACKET_FLOATS = 8;
|
||||
const PACKET_OUT_FLOATS = 8;
|
||||
const MAX_PACKETS = 256;
|
||||
const FIELD_FORMAT: GPUTextureFormat = 'rgba16float';
|
||||
|
||||
const COMMON_WGSL = /* wgsl */ `
|
||||
struct Params {
|
||||
frame: f32,
|
||||
loop_phase: f32,
|
||||
node_count: f32,
|
||||
edge_count: f32,
|
||||
path_count: f32,
|
||||
pulse: f32,
|
||||
viewport_w: f32,
|
||||
viewport_h: f32,
|
||||
brightness: f32,
|
||||
demo_id: f32,
|
||||
time: f32,
|
||||
capture_mode: f32,
|
||||
live_kind: f32,
|
||||
live_frame: f32,
|
||||
live_energy: f32,
|
||||
projection_days: f32,
|
||||
};
|
||||
|
||||
struct Stage {
|
||||
// xy center in NDC, z count, w confidence
|
||||
pos_count_conf: vec4f,
|
||||
// x start_frame, y interrupt_kind, z lit, w reserved
|
||||
timing: vec4f,
|
||||
};
|
||||
|
||||
struct PacketIn {
|
||||
// x src stage, y dst stage, z evidence/node index, w flags
|
||||
route: vec4f,
|
||||
// x start_frame, y duration, z energy, w interrupt_kind
|
||||
timing: vec4f,
|
||||
};
|
||||
|
||||
struct PacketOut {
|
||||
pos_energy: vec4f,
|
||||
tangent_flags: vec4f,
|
||||
};
|
||||
`;
|
||||
|
||||
const COMPUTE_WGSL = /* wgsl */ `
|
||||
${COMMON_WGSL}
|
||||
|
||||
@group(0) @binding(0) var<uniform> params: Params;
|
||||
@group(0) @binding(1) var<storage, read> stages: array<Stage>;
|
||||
@group(0) @binding(2) var<storage, read> packets_in: array<PacketIn>;
|
||||
@group(0) @binding(3) var<storage, read_write> packets_out: array<PacketOut>;
|
||||
|
||||
fn stage_center(i: u32) -> vec2f {
|
||||
return stages[i].pos_count_conf.xy;
|
||||
}
|
||||
|
||||
fn cubic(a: vec2f, b: vec2f, t_in: f32) -> vec2f {
|
||||
let t = clamp(t_in, 0.0, 1.0);
|
||||
let u = 1.0 - t;
|
||||
let bend = 0.18 * sin(f32(i32(a.y * 10.0) + i32(b.y * 10.0)));
|
||||
let p0 = a;
|
||||
let p1 = vec2f(a.x + bend, mix(a.y, b.y, 0.32));
|
||||
let p2 = vec2f(b.x - bend, mix(a.y, b.y, 0.68));
|
||||
let p3 = b;
|
||||
return u*u*u*p0 + 3.0*u*u*t*p1 + 3.0*u*t*t*p2 + t*t*t*p3;
|
||||
}
|
||||
|
||||
fn cubic_tangent(a: vec2f, b: vec2f, t_in: f32) -> vec2f {
|
||||
let t = clamp(t_in, 0.0, 1.0);
|
||||
let e = 0.01;
|
||||
return normalize(cubic(a, b, min(1.0, t + e)) - cubic(a, b, max(0.0, t - e)) + vec2f(0.0001, 0.0001));
|
||||
}
|
||||
|
||||
@compute @workgroup_size(64)
|
||||
fn advect_packets(@builtin(global_invocation_id) gid: vec3u) {
|
||||
let i = gid.x;
|
||||
if (i >= u32(params.path_count)) { return; }
|
||||
let p = packets_in[i];
|
||||
let src = u32(p.route.x);
|
||||
let dst = u32(p.route.y);
|
||||
if (src >= 8u || dst >= 8u) { return; }
|
||||
let start = p.timing.x;
|
||||
let dur = max(1.0, p.timing.y);
|
||||
var t = clamp((params.frame - start) / dur, 0.0, 1.0);
|
||||
let tt = t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
|
||||
let a = stage_center(src);
|
||||
let b = stage_center(dst);
|
||||
let pos = cubic(a, b, tt);
|
||||
let tan = cubic_tangent(a, b, tt);
|
||||
let alive_f = f32(params.frame >= start) * f32(params.frame <= start + dur + 90.0);
|
||||
let age = max(0.0, params.frame - start - dur);
|
||||
let tail = 1.0 - smoothstep(0.0, 90.0, age);
|
||||
packets_out[i] = PacketOut(vec4f(pos, p.timing.z * alive_f * max(0.18, tail), 1.0), vec4f(tan, p.timing.w, p.route.z));
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
const SPLAT_WGSL = /* wgsl */ `
|
||||
${COMMON_WGSL}
|
||||
|
||||
@group(0) @binding(0) var<uniform> params: Params;
|
||||
@group(0) @binding(1) var<storage, read> stages: array<Stage>;
|
||||
@group(0) @binding(3) var<storage, read> packets_out: array<PacketOut>;
|
||||
|
||||
const QUAD = array<vec2f, 6>(
|
||||
vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0),
|
||||
vec2f(-1.0, -1.0), vec2f(1.0, 1.0), vec2f(-1.0, 1.0)
|
||||
);
|
||||
|
||||
struct VSOut {
|
||||
@builtin(position) clip: vec4f,
|
||||
@location(0) uv: vec2f,
|
||||
@location(1) @interpolate(flat) misc: vec4f,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_splat(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
|
||||
var out: VSOut;
|
||||
let corner = QUAD[vi];
|
||||
let is_stage = ii < 8u;
|
||||
var center = vec2f(0.0);
|
||||
var radius = 0.045;
|
||||
var energy = 0.0;
|
||||
var interrupt = 0.0;
|
||||
if (is_stage) {
|
||||
let s = stages[ii];
|
||||
center = s.pos_count_conf.xy;
|
||||
let age = params.frame - s.timing.x;
|
||||
let gate = s.timing.z * smoothstep(0.0, 22.0, age);
|
||||
radius = 0.095 + 0.02 * s.pos_count_conf.w;
|
||||
energy = gate * (0.18 + 0.82 * s.pos_count_conf.w) * (0.7 + 0.3 * params.pulse);
|
||||
interrupt = s.timing.y;
|
||||
} else {
|
||||
let pi = ii - 8u;
|
||||
let p = packets_out[pi];
|
||||
center = p.pos_energy.xy;
|
||||
radius = 0.026 + 0.014 * p.pos_energy.z;
|
||||
energy = p.pos_energy.z;
|
||||
interrupt = p.tangent_flags.z;
|
||||
}
|
||||
out.clip = vec4f(center + corner * radius, 0.0, 1.0);
|
||||
out.uv = corner;
|
||||
out.misc = vec4f(energy, interrupt, f32(is_stage), radius);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_splat(in: VSOut) -> @location(0) vec4f {
|
||||
let d = length(in.uv);
|
||||
if (d > 1.0) { discard; }
|
||||
let body = exp(-d * d * 3.2) * in.misc.x;
|
||||
let scar = f32(in.misc.y > 0.5) * smoothstep(0.75, 0.2, abs(d - 0.62)) * in.misc.x;
|
||||
// logical .r = living density, .g = trust/flow, .b = immune interrupt, .a reserved
|
||||
return vec4f(body, body * (0.45 + 0.35 * params.pulse), scar, 1.0);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_chamber(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
|
||||
var out: VSOut;
|
||||
let s = stages[ii];
|
||||
let corner = QUAD[vi];
|
||||
let age = params.frame - s.timing.x;
|
||||
let gate = s.timing.z * smoothstep(0.0, 24.0, age);
|
||||
let size = vec2f(0.28 + 0.025 * s.pos_count_conf.w, 0.043 + 0.018 * gate);
|
||||
out.clip = vec4f(s.pos_count_conf.xy + corner * size, 0.0, 1.0);
|
||||
out.uv = corner;
|
||||
out.misc = vec4f(gate, s.pos_count_conf.w, s.timing.y, s.pos_count_conf.z);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_chamber(in: VSOut) -> @location(0) vec4f {
|
||||
let q = abs(in.uv);
|
||||
let edge = smoothstep(0.98, 0.80, max(q.x, q.y));
|
||||
let inner = smoothstep(0.92, 0.15, max(q.x, q.y));
|
||||
let lit = in.misc.x;
|
||||
let trust = in.misc.y;
|
||||
let interrupt = in.misc.z;
|
||||
let count = in.misc.w;
|
||||
let green = vec3f(0.64, 1.0, 0.36);
|
||||
let cyan = vec3f(0.10, 0.84, 0.98);
|
||||
let scarlet = vec3f(1.0, 0.20, 0.16);
|
||||
let amber = vec3f(1.0, 0.69, 0.08);
|
||||
var color = mix(cyan, green, trust) * (0.05 + lit * 0.34) * inner;
|
||||
let rim_color = select(mix(cyan, green, trust), select(amber, scarlet, interrupt < 1.5), interrupt > 0.5);
|
||||
color = color + rim_color * edge * (0.05 + lit * (0.32 + 0.08 * log2(count + 1.0)));
|
||||
return vec4f(color, 1.0);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_packet(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
|
||||
var out: VSOut;
|
||||
let p = packets_out[ii];
|
||||
let corner = QUAD[vi];
|
||||
let tangent = normalize(p.tangent_flags.xy + vec2f(0.0001));
|
||||
let normal = vec2f(-tangent.y, tangent.x);
|
||||
let size = vec2f(0.038 + 0.018 * p.pos_energy.z, 0.010 + 0.006 * p.pos_energy.z);
|
||||
let pos = p.pos_energy.xy + tangent * corner.x * size.x + normal * corner.y * size.y;
|
||||
out.clip = vec4f(pos, 0.0, 1.0);
|
||||
out.uv = corner;
|
||||
out.misc = vec4f(p.pos_energy.z, p.tangent_flags.z, p.tangent_flags.w, 0.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_packet(in: VSOut) -> @location(0) vec4f {
|
||||
let d = length(in.uv);
|
||||
if (d > 1.0) { discard; }
|
||||
let e = in.misc.x;
|
||||
let interrupt = in.misc.y;
|
||||
let body = exp(-d*d*2.2) * e;
|
||||
let luciferin = vec3f(0.91, 1.0, 0.72);
|
||||
let recall = vec3f(0.16, 0.95, 0.66);
|
||||
let scarlet = vec3f(1.0, 0.23, 0.19);
|
||||
let amber = vec3f(1.0, 0.69, 0.08);
|
||||
var color = mix(recall, luciferin, clamp(e, 0.0, 1.0));
|
||||
if (interrupt > 0.5) { color = select(amber, scarlet, interrupt < 1.5); }
|
||||
return vec4f(color * body * (1.2 + 0.4 * params.pulse), 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const MEMBRANE_WGSL = /* wgsl */ `
|
||||
${COMMON_WGSL}
|
||||
|
||||
@group(0) @binding(0) var<uniform> params: Params;
|
||||
@group(0) @binding(4) var field_sampler: sampler;
|
||||
@group(0) @binding(5) var field_tex: texture_2d<f32>;
|
||||
|
||||
const QUAD = array<vec2f, 6>(
|
||||
vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0),
|
||||
vec2f(-1.0, -1.0), vec2f(1.0, 1.0), vec2f(-1.0, 1.0)
|
||||
);
|
||||
|
||||
struct VSOut {
|
||||
@builtin(position) clip: vec4f,
|
||||
@location(0) uv: vec2f,
|
||||
@location(1) @interpolate(flat) misc: vec4f,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_fullscreen(@builtin(vertex_index) vi: u32) -> VSOut {
|
||||
var out: VSOut;
|
||||
let p = QUAD[vi];
|
||||
out.clip = vec4f(p, 0.0, 1.0);
|
||||
out.uv = p * 0.5 + vec2f(0.5);
|
||||
out.misc = vec4f(0.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_membrane(in: VSOut) -> @location(0) vec4f {
|
||||
let f = textureSample(field_tex, field_sampler, in.uv);
|
||||
let density = clamp(f.r, 0.0, 4.0);
|
||||
let flow = clamp(f.g, 0.0, 4.0);
|
||||
let immune = clamp(f.b, 0.0, 3.0);
|
||||
let membrane = smoothstep(0.14, 0.8, density) * (1.0 - smoothstep(1.6, 3.2, density));
|
||||
let blackwater = vec3f(0.006, 0.014, 0.014);
|
||||
let luciferin = vec3f(0.65, 1.0, 0.36);
|
||||
let bridge = vec3f(0.10, 0.82, 0.92);
|
||||
let scarlet = vec3f(1.0, 0.15, 0.10);
|
||||
var color = blackwater * (0.16 + density * 0.08);
|
||||
color = color + luciferin * density * 0.11 + bridge * flow * 0.08;
|
||||
color = color + vec3f(0.90, 1.0, 0.72) * membrane * 0.26;
|
||||
color = color + scarlet * immune * 0.26;
|
||||
let vignette = smoothstep(0.92, 0.22, distance(in.uv, vec2f(0.5)));
|
||||
return vec4f(color * (0.35 + 0.65 * vignette) * params.brightness, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const BLUR_WGSL = /* wgsl */ `
|
||||
struct BlurDir {
|
||||
dir: vec2f,
|
||||
_pad: vec2f,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var blur_sampler: sampler;
|
||||
@group(0) @binding(1) var blur_src: texture_2d<f32>;
|
||||
@group(0) @binding(2) var<uniform> blur_dir: BlurDir;
|
||||
|
||||
const QUAD = array<vec2f, 6>(
|
||||
vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0),
|
||||
vec2f(-1.0, -1.0), vec2f(1.0, 1.0), vec2f(-1.0, 1.0)
|
||||
);
|
||||
|
||||
struct VSOut {
|
||||
@builtin(position) clip: vec4f,
|
||||
@location(0) uv: vec2f,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_fullscreen(@builtin(vertex_index) vi: u32) -> VSOut {
|
||||
var out: VSOut;
|
||||
let p = QUAD[vi];
|
||||
out.clip = vec4f(p, 0.0, 1.0);
|
||||
out.uv = p * 0.5 + vec2f(0.5);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_blur(in: VSOut) -> @location(0) vec4f {
|
||||
let dims = vec2f(textureDimensions(blur_src, 0));
|
||||
let step = blur_dir.dir / max(dims, vec2f(1.0));
|
||||
var acc = textureSampleLevel(blur_src, blur_sampler, in.uv - step * 2.0, 0.0) * 0.06136;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, in.uv - step, 0.0) * 0.24477;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, in.uv, 0.0) * 0.38774;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, in.uv + step, 0.0) * 0.24477;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, in.uv + step * 2.0, 0.0) * 0.06136;
|
||||
return acc;
|
||||
}
|
||||
`;
|
||||
|
||||
type GpuResources = {
|
||||
stageBuffer: GPUBuffer;
|
||||
packetBuffer: GPUBuffer;
|
||||
packetOutBuffer: GPUBuffer;
|
||||
blurHBuffer: GPUBuffer;
|
||||
blurVBuffer: GPUBuffer;
|
||||
computeBindGroup: GPUBindGroup;
|
||||
splatBindGroup: GPUBindGroup;
|
||||
blurHBindGroup: GPUBindGroup;
|
||||
blurVBindGroup: GPUBindGroup;
|
||||
membraneBindGroup: GPUBindGroup;
|
||||
fieldA: GPUTexture;
|
||||
fieldB: GPUTexture;
|
||||
fieldAView: GPUTextureView;
|
||||
fieldBView: GPUTextureView;
|
||||
fieldSize: [number, number];
|
||||
};
|
||||
|
||||
export class ReasoningTheaterPass implements FramePass {
|
||||
private engine: ObservatoryEngine;
|
||||
private scene: ReasoningScene | null = null;
|
||||
private resources: GpuResources | null = null;
|
||||
private sampler: GPUSampler | null = null;
|
||||
private computeBindLayout: GPUBindGroupLayout | null = null;
|
||||
private splatBindLayout: GPUBindGroupLayout | null = null;
|
||||
private blurBindLayout: GPUBindGroupLayout | null = null;
|
||||
private membraneBindLayout: GPUBindGroupLayout | null = null;
|
||||
private splatPipeline: GPURenderPipeline | null = null;
|
||||
private blurPipeline: GPURenderPipeline | null = null;
|
||||
private packetPipeline: GPUComputePipeline | null = null;
|
||||
private membranePipeline: GPURenderPipeline | null = null;
|
||||
private chamberPipeline: GPURenderPipeline | null = null;
|
||||
private packetRenderPipeline: GPURenderPipeline | null = null;
|
||||
private packetCount = 0;
|
||||
private stageRects: { stage: ReasoningStageReceipt; x: number; y: number; w: number; h: number }[] = [];
|
||||
|
||||
constructor(engine: ObservatoryEngine, scene: RouteSceneModel) {
|
||||
this.engine = engine;
|
||||
this.uploadScene(scene);
|
||||
}
|
||||
|
||||
uploadScene(scene: RouteSceneModel): void {
|
||||
this.scene = scene as ReasoningScene;
|
||||
this.buildStageRects();
|
||||
const device = this.engine.gpuDevice;
|
||||
if (!device) return;
|
||||
this.ensurePipelines(device);
|
||||
this.ensureResources(device);
|
||||
this.uploadBuffers(device);
|
||||
}
|
||||
|
||||
private ensurePipelines(device: GPUDevice): void {
|
||||
if (this.splatPipeline || !this.engine.paramsBuffer) return;
|
||||
const computeModule = device.createShaderModule({ label: 'reasoning-theater-compute-wgsl', code: COMPUTE_WGSL });
|
||||
const splatModule = device.createShaderModule({ label: 'reasoning-theater-splat-wgsl', code: SPLAT_WGSL });
|
||||
const blurModule = device.createShaderModule({ label: 'reasoning-theater-blur-wgsl', code: BLUR_WGSL });
|
||||
const membraneModule = device.createShaderModule({ label: 'reasoning-theater-membrane-wgsl', code: MEMBRANE_WGSL });
|
||||
this.computeBindLayout = device.createBindGroupLayout({
|
||||
label: 'reasoning-theater-compute-bind-layout',
|
||||
entries: [
|
||||
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'uniform' } },
|
||||
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
||||
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
||||
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } }
|
||||
]
|
||||
});
|
||||
this.splatBindLayout = device.createBindGroupLayout({
|
||||
label: 'reasoning-theater-splat-bind-layout',
|
||||
entries: [
|
||||
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
||||
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
||||
{ binding: 3, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }
|
||||
]
|
||||
});
|
||||
this.membraneBindLayout = device.createBindGroupLayout({
|
||||
label: 'reasoning-theater-membrane-bind-layout',
|
||||
entries: [
|
||||
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
||||
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
||||
{ binding: 5, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } }
|
||||
]
|
||||
});
|
||||
this.blurBindLayout = device.createBindGroupLayout({
|
||||
label: 'reasoning-theater-blur-bind-layout',
|
||||
entries: [
|
||||
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
||||
{ binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } },
|
||||
{ binding: 2, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } }
|
||||
]
|
||||
});
|
||||
const computeLayout = device.createPipelineLayout({ label: 'reasoning-theater-compute-layout', bindGroupLayouts: [this.computeBindLayout] });
|
||||
const splatLayout = device.createPipelineLayout({ label: 'reasoning-theater-splat-layout', bindGroupLayouts: [this.splatBindLayout] });
|
||||
const blurLayout = device.createPipelineLayout({ label: 'reasoning-theater-blur-layout', bindGroupLayouts: [this.blurBindLayout] });
|
||||
const membraneLayout = device.createPipelineLayout({ label: 'reasoning-theater-membrane-layout', bindGroupLayouts: [this.membraneBindLayout] });
|
||||
this.sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear' });
|
||||
this.splatPipeline = device.createRenderPipeline({
|
||||
label: 'reasoning-field-splat',
|
||||
layout: splatLayout,
|
||||
vertex: { module: splatModule, entryPoint: 'vs_splat' },
|
||||
fragment: {
|
||||
module: splatModule,
|
||||
entryPoint: 'fs_splat',
|
||||
targets: [{ format: FIELD_FORMAT, blend: { color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' }, alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' } } }]
|
||||
},
|
||||
primitive: { topology: 'triangle-list' }
|
||||
});
|
||||
this.blurPipeline = device.createRenderPipeline({
|
||||
label: 'reasoning-field-blur',
|
||||
layout: blurLayout,
|
||||
vertex: { module: blurModule, entryPoint: 'vs_fullscreen' },
|
||||
fragment: { module: blurModule, entryPoint: 'fs_blur', targets: [{ format: FIELD_FORMAT }] },
|
||||
primitive: { topology: 'triangle-list' }
|
||||
});
|
||||
this.packetPipeline = device.createComputePipeline({ label: 'reasoning-packet-advect', layout: computeLayout, compute: { module: computeModule, entryPoint: 'advect_packets' } });
|
||||
const additive = { color: { srcFactor: 'one' as GPUBlendFactor, dstFactor: 'one' as GPUBlendFactor, operation: 'add' as GPUBlendOperation }, alpha: { srcFactor: 'one' as GPUBlendFactor, dstFactor: 'one' as GPUBlendFactor, operation: 'add' as GPUBlendOperation } };
|
||||
this.membranePipeline = device.createRenderPipeline({
|
||||
label: 'reasoning-membrane',
|
||||
layout: membraneLayout,
|
||||
vertex: { module: membraneModule, entryPoint: 'vs_fullscreen' },
|
||||
fragment: { module: membraneModule, entryPoint: 'fs_membrane', targets: [{ format: this.engine.sceneFormat, blend: additive }] },
|
||||
primitive: { topology: 'triangle-list' }
|
||||
});
|
||||
this.chamberPipeline = device.createRenderPipeline({
|
||||
label: 'reasoning-chambers',
|
||||
layout: splatLayout,
|
||||
vertex: { module: splatModule, entryPoint: 'vs_chamber' },
|
||||
fragment: { module: splatModule, entryPoint: 'fs_chamber', targets: [{ format: this.engine.sceneFormat, blend: additive }] },
|
||||
primitive: { topology: 'triangle-list' }
|
||||
});
|
||||
this.packetRenderPipeline = device.createRenderPipeline({
|
||||
label: 'reasoning-packets',
|
||||
layout: splatLayout,
|
||||
vertex: { module: splatModule, entryPoint: 'vs_packet' },
|
||||
fragment: { module: splatModule, entryPoint: 'fs_packet', targets: [{ format: this.engine.sceneFormat, blend: additive }] },
|
||||
primitive: { topology: 'triangle-list' }
|
||||
});
|
||||
}
|
||||
|
||||
private ensureResources(device: GPUDevice): void {
|
||||
if (!this.computeBindLayout || !this.splatBindLayout || !this.blurBindLayout || !this.membraneBindLayout || !this.engine.paramsBuffer || !this.sampler) return;
|
||||
const w = Math.max(16, Math.floor((this.engine.params[6] || 1280) / 2));
|
||||
const h = Math.max(16, Math.floor((this.engine.params[7] || 720) / 2));
|
||||
const needsTextures = !this.resources || this.resources.fieldSize[0] !== w || this.resources.fieldSize[1] !== h;
|
||||
let stageBuffer = this.resources?.stageBuffer;
|
||||
let packetBuffer = this.resources?.packetBuffer;
|
||||
let packetOutBuffer = this.resources?.packetOutBuffer;
|
||||
let blurHBuffer = this.resources?.blurHBuffer;
|
||||
let blurVBuffer = this.resources?.blurVBuffer;
|
||||
if (!stageBuffer) {
|
||||
stageBuffer = device.createBuffer({ label: 'reasoning-stages', size: STAGE_COUNT * STAGE_FLOATS * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
||||
}
|
||||
if (!packetBuffer) {
|
||||
packetBuffer = device.createBuffer({ label: 'reasoning-packets-in', size: MAX_PACKETS * PACKET_FLOATS * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
||||
}
|
||||
if (!packetOutBuffer) {
|
||||
packetOutBuffer = device.createBuffer({ label: 'reasoning-packets-out', size: MAX_PACKETS * PACKET_OUT_FLOATS * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
||||
}
|
||||
if (!blurHBuffer) {
|
||||
blurHBuffer = device.createBuffer({ label: 'reasoning-blur-h-dir', size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
||||
device.queue.writeBuffer(blurHBuffer, 0, new Float32Array([1, 0, 0, 0]));
|
||||
}
|
||||
if (!blurVBuffer) {
|
||||
blurVBuffer = device.createBuffer({ label: 'reasoning-blur-v-dir', size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
||||
device.queue.writeBuffer(blurVBuffer, 0, new Float32Array([0, 1, 0, 0]));
|
||||
}
|
||||
if (!needsTextures && this.resources) return;
|
||||
this.resources?.fieldA.destroy();
|
||||
this.resources?.fieldB.destroy();
|
||||
const usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
|
||||
const fieldA = device.createTexture({ label: 'reasoning-field-a-rgba16float', size: [w, h], format: FIELD_FORMAT, usage });
|
||||
const fieldB = device.createTexture({ label: 'reasoning-field-b-rgba16float', size: [w, h], format: FIELD_FORMAT, usage });
|
||||
const fieldAView = fieldA.createView();
|
||||
const fieldBView = fieldB.createView();
|
||||
const computeBindGroup = device.createBindGroup({
|
||||
label: 'reasoning-theater-compute-bind',
|
||||
layout: this.computeBindLayout,
|
||||
entries: [
|
||||
{ binding: 0, resource: { buffer: this.engine.paramsBuffer } },
|
||||
{ binding: 1, resource: { buffer: stageBuffer } },
|
||||
{ binding: 2, resource: { buffer: packetBuffer } },
|
||||
{ binding: 3, resource: { buffer: packetOutBuffer } }
|
||||
]
|
||||
});
|
||||
const splatBindGroup = device.createBindGroup({
|
||||
label: 'reasoning-theater-splat-bind',
|
||||
layout: this.splatBindLayout,
|
||||
entries: [
|
||||
{ binding: 0, resource: { buffer: this.engine.paramsBuffer } },
|
||||
{ binding: 1, resource: { buffer: stageBuffer } },
|
||||
{ binding: 3, resource: { buffer: packetOutBuffer } }
|
||||
]
|
||||
});
|
||||
const blurHBindGroup = device.createBindGroup({
|
||||
label: 'reasoning-theater-blur-h-bind',
|
||||
layout: this.blurBindLayout,
|
||||
entries: [
|
||||
{ binding: 0, resource: this.sampler },
|
||||
{ binding: 1, resource: fieldAView },
|
||||
{ binding: 2, resource: { buffer: blurHBuffer } }
|
||||
]
|
||||
});
|
||||
const blurVBindGroup = device.createBindGroup({
|
||||
label: 'reasoning-theater-blur-v-bind',
|
||||
layout: this.blurBindLayout,
|
||||
entries: [
|
||||
{ binding: 0, resource: this.sampler },
|
||||
{ binding: 1, resource: fieldBView },
|
||||
{ binding: 2, resource: { buffer: blurVBuffer } }
|
||||
]
|
||||
});
|
||||
const membraneBindGroup = device.createBindGroup({
|
||||
label: 'reasoning-theater-membrane-bind',
|
||||
layout: this.membraneBindLayout,
|
||||
entries: [
|
||||
{ binding: 0, resource: { buffer: this.engine.paramsBuffer } },
|
||||
{ binding: 4, resource: this.sampler },
|
||||
{ binding: 5, resource: fieldAView }
|
||||
]
|
||||
});
|
||||
this.resources = { stageBuffer, packetBuffer, packetOutBuffer, blurHBuffer, blurVBuffer, computeBindGroup, splatBindGroup, blurHBindGroup, blurVBindGroup, membraneBindGroup, fieldA, fieldB, fieldAView, fieldBView, fieldSize: [w, h] };
|
||||
}
|
||||
|
||||
private buildStageRects(): void {
|
||||
const stages = this.scene?.stages ?? [];
|
||||
this.stageRects = stages.map((stage, i) => {
|
||||
const y = 0.76 - i * (1.52 / 7);
|
||||
const confidence = stage.confidence || 0;
|
||||
return { stage, x: 0, y, w: 0.28 + 0.025 * confidence, h: 0.07 };
|
||||
});
|
||||
}
|
||||
|
||||
private uploadBuffers(device: GPUDevice): void {
|
||||
if (!this.resources || !this.scene) return;
|
||||
const stages = this.scene.stages ?? [];
|
||||
const stageData = new Float32Array(STAGE_COUNT * STAGE_FLOATS);
|
||||
for (let i = 0; i < STAGE_COUNT; i++) {
|
||||
const s = stages[i];
|
||||
const y = 0.76 - i * (1.52 / 7);
|
||||
const lit = s?.lit ? 1 : 0;
|
||||
const interrupt = s?.interrupt === 'contradiction' ? 1 : s?.interrupt === 'supersession' ? 2 : 0;
|
||||
stageData.set([0, y, s?.count ?? 0, s?.confidence ?? 0, i * 46 + 18, interrupt, lit, 0], i * STAGE_FLOATS);
|
||||
}
|
||||
device.queue.writeBuffer(this.resources.stageBuffer, 0, stageData);
|
||||
|
||||
const packets = new Float32Array(MAX_PACKETS * PACKET_FLOATS);
|
||||
let p = 0;
|
||||
for (let i = 0; i < STAGE_COUNT - 1 && p < MAX_PACKETS; i++) {
|
||||
const a = stages[i];
|
||||
const b = stages[i + 1];
|
||||
if (!a?.lit || !b?.lit) continue;
|
||||
const energy = Math.max(0.12, Math.min(1, (a.confidence + b.confidence) / 2 || 0.25));
|
||||
const interrupt = b.interrupt === 'contradiction' ? 1 : b.interrupt === 'supersession' ? 2 : 0;
|
||||
packets.set([i, i + 1, -1, 0, i * 46 + 34, 38, energy, interrupt], p * PACKET_FLOATS);
|
||||
p++;
|
||||
}
|
||||
for (const c of this.scene.contradictions ?? []) {
|
||||
if (p >= MAX_PACKETS) break;
|
||||
packets.set([3, 4, -1, 0, 4 * 46 + 18, 24, Math.max(0.35, c.topic_overlap || 0.5), 1], p * PACKET_FLOATS);
|
||||
p++;
|
||||
}
|
||||
for (const _s of this.scene.superseded ?? []) {
|
||||
if (p >= MAX_PACKETS) break;
|
||||
packets.set([4, 7, -1, 0, 5 * 46, 52, 0.65, 2], p * PACKET_FLOATS);
|
||||
p++;
|
||||
}
|
||||
this.packetCount = p;
|
||||
this.engine.params[4] = p;
|
||||
device.queue.writeBuffer(this.resources.packetBuffer, 0, packets);
|
||||
}
|
||||
|
||||
compute(encoder: GPUCommandEncoder): void {
|
||||
const device = this.engine.gpuDevice;
|
||||
if (!device || !this.resources || !this.splatPipeline || !this.blurPipeline || !this.packetPipeline) return;
|
||||
this.ensureResources(device);
|
||||
const res = this.resources;
|
||||
const splat = encoder.beginRenderPass({
|
||||
label: 'reasoning-field-splat-pass',
|
||||
colorAttachments: [{ view: res.fieldAView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: 'clear', storeOp: 'store' }]
|
||||
});
|
||||
splat.setPipeline(this.splatPipeline);
|
||||
splat.setBindGroup(0, res.splatBindGroup);
|
||||
splat.draw(6, STAGE_COUNT + this.packetCount);
|
||||
splat.end();
|
||||
|
||||
const blurH = encoder.beginRenderPass({
|
||||
label: 'reasoning-field-blur-h-pass',
|
||||
colorAttachments: [{ view: res.fieldBView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: 'clear', storeOp: 'store' }]
|
||||
});
|
||||
blurH.setPipeline(this.blurPipeline);
|
||||
blurH.setBindGroup(0, res.blurHBindGroup);
|
||||
blurH.draw(6, 1);
|
||||
blurH.end();
|
||||
|
||||
const blurV = encoder.beginRenderPass({
|
||||
label: 'reasoning-field-blur-v-pass',
|
||||
colorAttachments: [{ view: res.fieldAView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: 'clear', storeOp: 'store' }]
|
||||
});
|
||||
blurV.setPipeline(this.blurPipeline);
|
||||
blurV.setBindGroup(0, res.blurVBindGroup);
|
||||
blurV.draw(6, 1);
|
||||
blurV.end();
|
||||
|
||||
if (this.packetCount > 0) {
|
||||
const advect = encoder.beginComputePass({ label: 'reasoning-packet-advect-pass' });
|
||||
advect.setPipeline(this.packetPipeline);
|
||||
advect.setBindGroup(0, res.computeBindGroup);
|
||||
advect.dispatchWorkgroups(Math.ceil(this.packetCount / 64));
|
||||
advect.end();
|
||||
}
|
||||
}
|
||||
|
||||
render(pass: GPURenderPassEncoder): void {
|
||||
if (!this.resources || !this.membranePipeline || !this.chamberPipeline || !this.packetRenderPipeline) return;
|
||||
pass.setPipeline(this.membranePipeline);
|
||||
pass.setBindGroup(0, this.resources.membraneBindGroup);
|
||||
pass.draw(6, 1);
|
||||
pass.setPipeline(this.chamberPipeline);
|
||||
pass.setBindGroup(0, this.resources.splatBindGroup);
|
||||
pass.draw(6, STAGE_COUNT);
|
||||
if (this.packetCount > 0) {
|
||||
pass.setPipeline(this.packetRenderPipeline);
|
||||
pass.draw(6, this.packetCount);
|
||||
}
|
||||
}
|
||||
|
||||
pickAt(ndcX: number, ndcY: number): RoutePick | null {
|
||||
for (const rect of this.stageRects) {
|
||||
if (Math.abs(ndcX - rect.x) <= rect.w && Math.abs(ndcY - rect.y) <= rect.h) {
|
||||
return { id: `reasoning-stage:${rect.stage.kind}`, kind: 'stage', index: rect.stage.index, payload: rect.stage };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.resources?.stageBuffer.destroy();
|
||||
this.resources?.packetBuffer.destroy();
|
||||
this.resources?.packetOutBuffer.destroy();
|
||||
this.resources?.blurHBuffer.destroy();
|
||||
this.resources?.blurVBuffer.destroy();
|
||||
this.resources?.fieldA.destroy();
|
||||
this.resources?.fieldB.destroy();
|
||||
this.resources = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function createReasoningTheaterPasses(engine: ObservatoryEngine, scene: RouteSceneModel): ReasoningTheaterPass[] {
|
||||
void rgb01(RETENTION.healthy);
|
||||
void rgb01(IMMUNE.veto);
|
||||
void rgb01(CAUSAL.forward);
|
||||
return [new ReasoningTheaterPass(engine, scene)];
|
||||
}
|
||||
|
|
@ -6,6 +6,13 @@
|
|||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import AnimatedNumber from '$lib/components/AnimatedNumber.svelte';
|
||||
import RouteStage, { type RoutePick } from '$lib/observatory/RouteStage.svelte';
|
||||
import { createReasoningTheaterPasses } from '$lib/observatory/reasoning/reasoning-theater-pass';
|
||||
import {
|
||||
normalizeDeepReferenceResponse,
|
||||
type ReasoningScene,
|
||||
type ReasoningStageReceipt
|
||||
} from '$lib/observatory/reasoning/reasoning-scene';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import { spotlight, magnetic } from '$lib/actions/interactions';
|
||||
import {
|
||||
|
|
@ -66,11 +73,14 @@
|
|||
memoriesAnalyzed: number;
|
||||
}
|
||||
|
||||
let latestReasoningScene: ReasoningScene | null = null;
|
||||
|
||||
// Real backend call — wraps the 8-stage deep_reference cognitive pipeline
|
||||
// via /api/deep_reference. The handler emits DeepReferenceCompleted on
|
||||
// the WebSocket so Graph3D can glide + pulse + arc in the 3D scene.
|
||||
async function deepReferenceFetch(query: string): Promise<DeepReferenceResponse> {
|
||||
const raw = (await api.deepReference(query, 20)) as Record<string, unknown>;
|
||||
latestReasoningScene = normalizeDeepReferenceResponse(raw);
|
||||
|
||||
const evidenceRaw = Array.isArray(raw.evidence) ? (raw.evidence as Record<string, unknown>[]) : [];
|
||||
const evidence: EvidenceEntry[] = evidenceRaw.map((e) => {
|
||||
|
|
@ -170,6 +180,8 @@
|
|||
let query = $state('');
|
||||
let loading = $state(false);
|
||||
let response: DeepReferenceResponse | null = $state(null);
|
||||
let reasoningScene: ReasoningScene | null = $state(null);
|
||||
let selectedStage: ReasoningStageReceipt | null = $state(null);
|
||||
let error: string | null = $state(null);
|
||||
let askInputEl: HTMLInputElement | null = $state(null);
|
||||
|
||||
|
|
@ -183,9 +195,12 @@
|
|||
loading = true;
|
||||
error = null;
|
||||
response = null;
|
||||
reasoningScene = null;
|
||||
selectedStage = null;
|
||||
arcs = [];
|
||||
try {
|
||||
response = await deepReferenceFetch(q);
|
||||
reasoningScene = latestReasoningScene;
|
||||
// After DOM paints the evidence cards, measure & draw arcs
|
||||
requestAnimationFrame(() => requestAnimationFrame(measureArcs));
|
||||
} catch (e) {
|
||||
|
|
@ -218,6 +233,12 @@
|
|||
arcs = next;
|
||||
}
|
||||
|
||||
function handleRoutePick(pick: RoutePick) {
|
||||
if (pick.kind === 'stage') {
|
||||
selectedStage = pick.payload as ReasoningStageReceipt;
|
||||
}
|
||||
}
|
||||
|
||||
function handleGlobalKey(e: KeyboardEvent) {
|
||||
// Cmd/Ctrl + K focuses the ask box
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
||||
|
|
@ -249,7 +270,18 @@
|
|||
<title>Reasoning Theater · Vestige</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="p-6 max-w-6xl mx-auto space-y-8 enter">
|
||||
<RouteStage
|
||||
organ="reasoning"
|
||||
seed={`reasoning-theater:${query || 'empty'}`}
|
||||
scene={reasoningScene}
|
||||
passes={createReasoningTheaterPasses}
|
||||
loading={loading}
|
||||
error={error}
|
||||
emptyLabel="ASK A QUERY TO LIGHT THE EIGHT-STAGE ORGAN"
|
||||
onpick={handleRoutePick}
|
||||
/>
|
||||
|
||||
<div class="relative z-10 p-6 max-w-6xl mx-auto space-y-8 enter">
|
||||
<!-- Header -->
|
||||
<PageHeader
|
||||
icon="reasoning"
|
||||
|
|
@ -610,6 +642,56 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if selectedStage}
|
||||
<aside
|
||||
class="fixed right-4 top-24 bottom-4 z-30 w-[min(26rem,calc(100vw-2rem))] overflow-auto rounded-2xl
|
||||
border border-[#A8FF5E]/25 bg-[#020307]/92 p-4 shadow-2xl backdrop-blur-xl font-mono"
|
||||
aria-label="Reasoning stage receipt"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3 border-b border-[#A8FF5E]/15 pb-3">
|
||||
<div>
|
||||
<div class="text-[10px] uppercase tracking-[0.22em] text-[#A8FF5E]/70">stage receipt</div>
|
||||
<h2 class="text-bright text-sm mt-1">{selectedStage.index + 1}. {selectedStage.label}</h2>
|
||||
</div>
|
||||
<button class="text-muted hover:text-bright" onclick={() => (selectedStage = null)} aria-label="Close stage receipt">×</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2 py-3 text-[11px]">
|
||||
<div class="rounded-lg border border-white/10 bg-white/[0.03] p-2">
|
||||
<div class="text-muted">count</div>
|
||||
<div class="text-[#E9FFB7]">{selectedStage.count}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-white/10 bg-white/[0.03] p-2">
|
||||
<div class="text-muted">confidence</div>
|
||||
<div class="text-[#E9FFB7]">{Math.round(selectedStage.confidence * 100)}%</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-white/10 bg-white/[0.03] p-2">
|
||||
<div class="text-muted">interrupt</div>
|
||||
<div class={selectedStage.interrupt === 'none' ? 'text-dim' : 'text-[#FF3B30]'}>{selectedStage.interrupt}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 text-[11px]">
|
||||
<div>
|
||||
<div class="uppercase tracking-[0.16em] text-muted mb-1">provenance</div>
|
||||
<pre class="whitespace-pre-wrap rounded-xl border border-[#22C7DE]/15 bg-[#07100D]/75 p-3 text-[#9DFFEB]">{JSON.stringify(selectedStage.provenance, null, 2)}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<div class="uppercase tracking-[0.16em] text-muted mb-1">exact exposed backend data</div>
|
||||
<pre class="whitespace-pre-wrap rounded-xl border border-[#A8FF5E]/15 bg-[#07100D]/75 p-3 text-[#E9FFB7]">{JSON.stringify(selectedStage.exposed, null, 2)}</pre>
|
||||
</div>
|
||||
<div>
|
||||
<div class="uppercase tracking-[0.16em] text-muted mb-1">not_exposed_by_backend</div>
|
||||
<ul class="rounded-xl border border-[#FFD166]/15 bg-[#11140A]/75 p-3 text-[#FFD166] space-y-1">
|
||||
{#each selectedStage.not_exposed_by_backend as item}
|
||||
<li>• {item}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.conf-number {
|
||||
animation: conf-pop 900ms cubic-bezier(0.22, 0.8, 0.3, 1) backwards;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue