mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-24 23:41:01 +02:00
feat(dashboard): Organ Blackbox — Agent Flight Recorder Nerve Trace
Built by GPT-5.5 swarm worker, audited + fixed live by Opus 4.8. An agent run rendered as a living nervous system: tool calls / retrievals / writes / suppressions / vetoes as impulses, receipts as beads, real TraceEvent order. - blackbox/blackbox-scene.ts + blackbox-pass.ts (metaball nerve field, all render passes, no storage textures) + wired route. - Live-GPU audit caught a WGSL 'meta' reserved-keyword struct field (invalid module, 600 pipeline errors/frame, gates green) — renamed to 'beat'. Added meta to the brief's reserved-word list. Verified live vs the real brain: real agent runs list, real event log (Tool Call smart_ingest, Wrote <memory-id>), real event producers (mcp.call/memory.write/retrieve/suppress), honest empty states (no contradiction/dream/veto this run), 115fps. 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
722d0f8738
commit
f167be2663
7 changed files with 1890 additions and 7 deletions
124
.council/ORGAN-BUILD-BRIEF.md
Normal file
124
.council/ORGAN-BUILD-BRIEF.md
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
# ORGAN BUILD BRIEF — swarm worker cards read this FIRST
|
||||
|
||||
You are a GPT-5.5 worker building ONE Cognitive OS dashboard organ. Branch
|
||||
`feat/dashboard-live-max` at `/Users/entity002/vestige` (cd there; the kanban
|
||||
scratch workspace is just your home — do the real work in the repo).
|
||||
|
||||
## 🔴 THIS IS A BUILD TASK. YOU MUST WRITE + SAVE REAL FILES. (read twice)
|
||||
A "design", "plan", "recommendation", "handoff", or "reconnaissance" is a
|
||||
FAILURE. You are NOT done until three NEW files exist on disk and the gate is
|
||||
green. If you finish without writing files, you have failed the card.
|
||||
|
||||
MANDATORY DEFINITION OF DONE (all must be TRUE, verify each yourself):
|
||||
1. `apps/dashboard/src/lib/observatory/<organ>/<organ>-scene.ts` EXISTS on disk
|
||||
(you wrote it — `ls` it to confirm).
|
||||
2. `apps/dashboard/src/lib/observatory/<organ>/<organ>-pass.ts` EXISTS on disk
|
||||
(you wrote the FramePass + WGSL — `ls` it to confirm).
|
||||
3. `apps/dashboard/src/routes/(app)/<organ>/+page.svelte` is MODIFIED to mount
|
||||
`<RouteStage organ="<organ>" passes={...} scene={...} .../>` full-bleed
|
||||
(`git diff` it to confirm your edit is there).
|
||||
4. `pnpm --filter @vestige/dashboard check` prints `0 ERRORS`. You RAN it and
|
||||
pasted the real output.
|
||||
5. `pnpm --filter @vestige/dashboard build` succeeded. You RAN it.
|
||||
In your completion summary you MUST paste: the `ls -la` of your organ dir, the
|
||||
`git diff --stat` showing your changed files, and the tail of the real check +
|
||||
build output. NO files + no gate output = the verifier bounces you as RED.
|
||||
|
||||
## HOW TO BUILD IT (do the work, don't just plan it)
|
||||
1. FIRST copy the working reference to steal its exact structure:
|
||||
`cp apps/dashboard/src/lib/observatory/reasoning/reasoning-theater-pass.ts
|
||||
apps/dashboard/src/lib/observatory/<organ>/<organ>-pass.ts` then rewrite it
|
||||
for your organ's hero + real data. Same for the scene adapter (copy
|
||||
reasoning-scene.ts). This guarantees the WebGPU-safe pipeline (all render
|
||||
passes, no storage textures, split modules, no reserved words, real struct
|
||||
fields) instead of reinventing it and re-hitting the traps.
|
||||
2. You MAY use `delegate_task` to PARALLELIZE the WRITING — but each subagent
|
||||
must WRITE ITS FILE, not return prose. E.g. delegate: "write the full
|
||||
contents of <organ>-scene.ts to disk and confirm with ls" — not "recommend a
|
||||
design." If a subagent returns a design instead of a written file, YOU write
|
||||
the file from its design. The card is not done until the files are on disk.
|
||||
3. Then YOU integrate, wire the route, and RUN the gate. Paste the output.
|
||||
The bar is "people lose their minds" AND it renders — but step 0 is the files
|
||||
must exist. A brilliant design with no files is a RED card. GO WRITE CODE.
|
||||
|
||||
READ FIRST, in order:
|
||||
1. `/Users/entity002/vestige/.council/ROUND3-FINAL-PLAN.md` — the full plan +
|
||||
§6b traps T1-T6 (BINDING).
|
||||
2. `/Users/entity002/vestige/.council/gpt55-round2-research.md` — the WGSL
|
||||
recipes (metaball field D1, spline advection D2, retrograde axon D3).
|
||||
3. WORKING REFERENCE CODE — copy the proven patterns, don't reinvent:
|
||||
- `apps/dashboard/src/lib/observatory/reasoning/reasoning-theater-pass.ts`
|
||||
(the metaball field: additive splat -> render-pass blur -> membrane, all
|
||||
render passes, no storage textures; compute spline advection).
|
||||
- `apps/dashboard/src/lib/observatory/contradictions/contradictions-pass.ts`
|
||||
(dual-channel signed field, scarlet seam).
|
||||
- `apps/dashboard/src/lib/observatory/RouteStage.svelte` (the organ shell).
|
||||
- `apps/dashboard/src/lib/observatory/route-scene.ts` (RouteSceneModel +
|
||||
assertProvenance) and `cognitive-palette.ts` (the color language).
|
||||
|
||||
## HARD RULES (a card is RED if it violates any)
|
||||
- NEVER touch: `MemoryCinema.svelte`, `src/lib/graph/cinema/*`,
|
||||
`observatory/types.ts` NodeState (64 bytes), the 8 existing observatory
|
||||
shaders, the Graph field. REUSE the engine, don't fork it.
|
||||
- Every visual primitive carries real `Provenance` (memory/event/receipt/pair/
|
||||
trace/pr/pattern/scalar). Run `assertProvenance(scene)` in dev. NO
|
||||
Math.random() as a semantic input. The Math.random() discipline test governs.
|
||||
- Reuse `cognitive-palette.ts`. magenta `#FF2DF7` = RSB retrograde causality
|
||||
ONLY. indigo `#7C6CFF` = bitemporal ONLY. scarlet/immune reds for
|
||||
contradiction/veto/suppression. blackwater `#020307` base, never purple.
|
||||
|
||||
## WEBGPU TRAPS (all found live in Organ 1/3 — DO NOT repeat)
|
||||
- T3: field ping-pong textures are `rgba16float` with usage `RENDER_ATTACHMENT
|
||||
| TEXTURE_BINDING` (NO STORAGE_BINDING). Do separable blur as FULLSCREEN
|
||||
RENDER PASSES, never `texture_storage_2d<...,write>` + textureStore (not
|
||||
portable). The whole field pipeline = render passes: splat(additive) ->
|
||||
blur-H -> blur-V -> membrane. Copy reasoning-theater-pass.ts.
|
||||
- T6: split WGSL into per-pipeline modules. A render pipeline's module must NOT
|
||||
declare `var<storage, read_write>` or write storage textures (compute-only).
|
||||
Render-stage storage buffers are `var<storage, read>` only.
|
||||
- WGSL reserved words: NEVER name a var OR STRUCT FIELD `active`, `meta`,
|
||||
`filter`, `common`, `sample`, `override`, `enable`, `const`, `handle`,
|
||||
`input`, `output`, `texture`, `binding`, `access`, `layout`. (`active` broke
|
||||
Organ 1, `meta` broke Blackbox — both found live.) The WGSL reserved list is
|
||||
long — pick descriptive safe names like `beat`/`info`/`data2`.
|
||||
- STRUCT-FIELD MATCH: every `struct.field` you reference in WGSL must EXIST in
|
||||
the struct (Organ 3's bug was `p.signals` vs a struct declaring `meta`). TS
|
||||
build does NOT catch this.
|
||||
- FramePass sequencing: the engine calls ALL `compute(encoder, frame)` then
|
||||
opens ONE main HDR scene pass and calls ALL `render(pass, frame)`. So a field
|
||||
pass encodes its own offscreen splat + blur render passes INSIDE
|
||||
`compute(encoder)` (it gets a `GPUCommandEncoder`), then draws the membrane in
|
||||
`render()`. Route field textures need their own `ensure(w,h)` on resize.
|
||||
- Pipeline-layout scoping (web-verified vs W3C spec): a pipeline's explicit
|
||||
layout need only cover the bindings the SELECTED entry point statically
|
||||
reaches — a shared multi-entry-point module is fine. Layout must be a SUPERSET
|
||||
of the used bindings. Watch texture sampleType `float` vs a filtering sampler
|
||||
(common creation error).
|
||||
- One-shot backend events (e.g. DeepReferenceCompleted) become LABELED
|
||||
deterministic replays, never fake streaming. A stage/element lights only if
|
||||
it has real output.
|
||||
|
||||
## LIFECYCLE CONTRACT (every organ ships all of it)
|
||||
loading / ready / stale / empty / error / reduced-motion / paused. WebGPU
|
||||
absent -> DOM/SVG fallback, NEVER a black canvas. Adapter-null -> honest metric
|
||||
snapshot. RouteStage already inherits pause + reduced-motion — reuse it.
|
||||
|
||||
## PER-CARD DELIVERABLE
|
||||
1. `<organ>/<organ>-scene.ts` — adapter: real API/event -> RouteSceneModel,
|
||||
run assertProvenance. Handle the honest empty state (scene.alive=false).
|
||||
2. `<organ>/<organ>-pass.ts` — the FramePass(es) implementing the hero (see the
|
||||
organ's spec in ROUND3-FINAL-PLAN §3/§4).
|
||||
3. Wire `src/routes/(app)/<organ>/+page.svelte`: mount RouteStage full-bleed
|
||||
with the pass; keep DOM detail/list panels for accessibility ON TOP; wire the
|
||||
primary click to a real receipt/action.
|
||||
|
||||
## GATE (do NOT self-assess — RUN these; the verifier will re-run them)
|
||||
- `pnpm --filter @vestige/dashboard check` -> 0 errors
|
||||
- `pnpm --filter @vestige/dashboard build` -> success
|
||||
Both must pass before you mark the card done. Note in your completion metadata:
|
||||
the files you created, and confirm you did NOT touch protected files.
|
||||
|
||||
## NOTE ON LIVE-GPU BUGS
|
||||
check+build CANNOT catch invalid-pipeline runtime errors. After building, if you
|
||||
can, sanity-check your WGSL modules with `getCompilationInfo()`. The human
|
||||
conductor (Claude) will do the final live-GPU audit against the running app.
|
||||
497
apps/dashboard/src/lib/observatory/blackbox/blackbox-pass.ts
Normal file
497
apps/dashboard/src/lib/observatory/blackbox/blackbox-pass.ts
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
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 { BlackboxScene, BlackboxTraceImpulse } from './blackbox-scene';
|
||||
|
||||
type RoutePick = { id: string; kind: string; index?: number; payload?: unknown };
|
||||
|
||||
const MAX_EVENTS = 512;
|
||||
const EVENT_FLOATS = 12;
|
||||
const RECEIPT_FLOATS = 8;
|
||||
const MAX_RECEIPTS = 128;
|
||||
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 TraceEventCell {
|
||||
// x order 0..1, y lane 0..6, z confidence, w event kind code
|
||||
order_lane_conf_kind: vec4f,
|
||||
// x frame start, y visible gate, z selected, w receipt flag
|
||||
timing_flags: vec4f,
|
||||
// x retrieved ids count, y suppress/write/veto strength, z run duration fraction, w spare
|
||||
metric: vec4f,
|
||||
};
|
||||
|
||||
struct ReceiptBead {
|
||||
// xy NDC, z intensity, w event index
|
||||
pos_energy: vec4f,
|
||||
// x node-count, y receipt ordinal, zw spare
|
||||
beat: vec4f,
|
||||
};
|
||||
`;
|
||||
|
||||
const TRACE_WGSL = /* wgsl */ `
|
||||
${COMMON_WGSL}
|
||||
|
||||
@group(0) @binding(0) var<uniform> params: Params;
|
||||
@group(0) @binding(1) var<storage, read> trace_events: array<TraceEventCell>;
|
||||
@group(0) @binding(2) var<storage, read> receipt_beads: array<ReceiptBead>;
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
fn lane_y(lane: f32) -> f32 {
|
||||
return mix(0.66, -0.62, lane / 6.0);
|
||||
}
|
||||
|
||||
fn event_x(order: f32) -> f32 {
|
||||
return mix(-0.84, 0.84, clamp(order, 0.0, 1.0));
|
||||
}
|
||||
|
||||
fn lane_color(kind: f32, lane: f32, conf: f32) -> vec3f {
|
||||
let luciferin = vec3f(0.66, 1.0, 0.37);
|
||||
let cyan = vec3f(0.08, 0.78, 0.92);
|
||||
let green = vec3f(0.12, 0.95, 0.56);
|
||||
let scarlet = vec3f(1.0, 0.18, 0.12);
|
||||
let amber = vec3f(1.0, 0.64, 0.06);
|
||||
let violet = vec3f(0.45, 0.42, 1.0);
|
||||
let bone = vec3f(0.88, 1.0, 0.70);
|
||||
var col = cyan;
|
||||
if (kind < 0.5) { col = bone; }
|
||||
else if (kind < 1.5) { col = green; }
|
||||
else if (kind < 2.5) { col = scarlet; }
|
||||
else if (kind < 3.5) { col = luciferin; }
|
||||
else if (kind < 4.5) { col = amber; }
|
||||
else if (kind < 5.5) { col = scarlet; }
|
||||
else { col = violet; }
|
||||
return mix(col * 0.62, col, clamp(conf, 0.0, 1.0)) * (0.88 + lane * 0.025);
|
||||
}
|
||||
|
||||
struct VSOut {
|
||||
@builtin(position) clip: vec4f,
|
||||
@location(0) local_uv: vec2f,
|
||||
@location(1) @interpolate(flat) misc: vec4f,
|
||||
@location(2) @interpolate(flat) color_energy: vec4f,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_impulse(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
|
||||
let cell = trace_events[ii];
|
||||
let corner = QUAD[vi];
|
||||
let order = cell.order_lane_conf_kind.x;
|
||||
let lane = cell.order_lane_conf_kind.y;
|
||||
let conf = cell.order_lane_conf_kind.z;
|
||||
let kind = cell.order_lane_conf_kind.w;
|
||||
let visible = cell.timing_flags.y;
|
||||
let selected = cell.timing_flags.z;
|
||||
let t = params.frame - cell.timing_flags.x;
|
||||
let pulse = smoothstep(0.0, 18.0, t) * (1.0 - smoothstep(92.0, 180.0, t));
|
||||
let center = vec2f(event_x(order), lane_y(lane));
|
||||
let radius = vec2f(0.028 + 0.026 * conf + 0.012 * selected, 0.026 + 0.018 * conf + 0.010 * pulse);
|
||||
var out: VSOut;
|
||||
out.clip = vec4f(center + corner * radius, 0.0, 1.0);
|
||||
out.local_uv = corner;
|
||||
out.misc = vec4f(kind, lane, selected, visible);
|
||||
out.color_energy = vec4f(lane_color(kind, lane, conf), visible * (0.35 + conf * 0.75 + pulse * 0.38));
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_impulse(frag: VSOut) -> @location(0) vec4f {
|
||||
let d = length(frag.local_uv);
|
||||
if (d > 1.0 || frag.misc.w < 0.5) { discard; }
|
||||
let core = exp(-d * d * 3.4) * frag.color_energy.a;
|
||||
let ring = smoothstep(0.88, 0.62, abs(d - 0.64)) * (0.18 + frag.misc.z * 0.62);
|
||||
return vec4f(frag.color_energy.rgb * (core + ring), 1.0);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_lane(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
|
||||
let corner = QUAD[vi];
|
||||
let lane = f32(ii);
|
||||
let center = vec2f(0.0, lane_y(lane));
|
||||
let size = vec2f(0.93, 0.012);
|
||||
var out: VSOut;
|
||||
out.clip = vec4f(center + corner * size, 0.0, 1.0);
|
||||
out.local_uv = corner;
|
||||
out.misc = vec4f(0.0, lane, 0.0, 1.0);
|
||||
out.color_energy = vec4f(lane_color(lane, lane, 0.4), 0.12 + 0.03 * params.pulse);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_lane(frag: VSOut) -> @location(0) vec4f {
|
||||
let fade = smoothstep(1.0, 0.08, abs(frag.local_uv.x)) * smoothstep(1.0, 0.0, abs(frag.local_uv.y));
|
||||
return vec4f(frag.color_energy.rgb * frag.color_energy.a * fade, 1.0);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_receipt(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
|
||||
let bead = receipt_beads[ii];
|
||||
let corner = QUAD[vi];
|
||||
let event_i = bead.pos_energy.w;
|
||||
let linked = trace_events[u32(max(0.0, event_i))];
|
||||
let center = vec2f(event_x(linked.order_lane_conf_kind.x), lane_y(linked.order_lane_conf_kind.y) - 0.05 - 0.012 * bead.beat.y);
|
||||
let radius = 0.017 + 0.005 * min(5.0, bead.beat.x);
|
||||
var out: VSOut;
|
||||
out.clip = vec4f(center + corner * radius, 0.0, 1.0);
|
||||
out.local_uv = corner;
|
||||
out.misc = vec4f(0.0, linked.order_lane_conf_kind.y, 0.0, linked.timing_flags.y);
|
||||
out.color_energy = vec4f(vec3f(0.90, 1.0, 0.70), bead.pos_energy.z * linked.timing_flags.y);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_receipt(frag: VSOut) -> @location(0) vec4f {
|
||||
let d = length(frag.local_uv);
|
||||
if (d > 1.0 || frag.misc.w < 0.5) { discard; }
|
||||
let bead = smoothstep(1.0, 0.0, d) + smoothstep(0.70, 0.58, abs(d - 0.64));
|
||||
return vec4f(frag.color_energy.rgb * frag.color_energy.a * bead, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const MEMBRANE_WGSL = /* wgsl */ `
|
||||
${COMMON_WGSL}
|
||||
|
||||
@group(0) @binding(0) var<uniform> params: Params;
|
||||
@group(0) @binding(3) var field_sampler: sampler;
|
||||
@group(0) @binding(4) 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,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_fullscreen(@builtin(vertex_index) vi: u32) -> VSOut {
|
||||
let p = QUAD[vi];
|
||||
var out: VSOut;
|
||||
out.clip = vec4f(p, 0.0, 1.0);
|
||||
out.uv = p * 0.5 + vec2f(0.5);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_membrane(frag: VSOut) -> @location(0) vec4f {
|
||||
let f = textureSample(field_tex, field_sampler, frag.uv);
|
||||
let density = clamp(f.r, 0.0, 4.0);
|
||||
let recall = clamp(f.g, 0.0, 4.0);
|
||||
let immune = clamp(f.b, 0.0, 4.0);
|
||||
let write_glow = clamp(f.a, 0.0, 4.0);
|
||||
let centerline = smoothstep(0.44, 0.02, abs(frag.uv.y - 0.5));
|
||||
let blackwater = vec3f(0.006, 0.014, 0.016);
|
||||
var color = blackwater * (0.24 + density * 0.11);
|
||||
color = color + vec3f(0.62, 1.0, 0.35) * recall * 0.10;
|
||||
color = color + vec3f(1.0, 0.18, 0.12) * immune * 0.16;
|
||||
color = color + vec3f(0.90, 1.0, 0.70) * write_glow * 0.10;
|
||||
color = color + vec3f(0.08, 0.70, 0.80) * centerline * (0.03 + 0.02 * params.pulse);
|
||||
let vignette = smoothstep(0.92, 0.22, distance(frag.uv, vec2f(0.5)));
|
||||
return vec4f(color * (0.45 + 0.55 * 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 {
|
||||
let p = QUAD[vi];
|
||||
var out: VSOut;
|
||||
out.clip = vec4f(p, 0.0, 1.0);
|
||||
out.uv = p * 0.5 + vec2f(0.5);
|
||||
return out;
|
||||
}
|
||||
@fragment
|
||||
fn fs_blur(frag: 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, frag.uv - step * 2.0, 0.0) * 0.06136;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, frag.uv - step, 0.0) * 0.24477;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, frag.uv, 0.0) * 0.38774;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, frag.uv + step, 0.0) * 0.24477;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, frag.uv + step * 2.0, 0.0) * 0.06136;
|
||||
return acc;
|
||||
}
|
||||
`;
|
||||
|
||||
type GpuResources = {
|
||||
eventBuffer: GPUBuffer;
|
||||
receiptBuffer: GPUBuffer;
|
||||
blurHBuffer: GPUBuffer;
|
||||
blurVBuffer: GPUBuffer;
|
||||
traceBindGroup: GPUBindGroup;
|
||||
membraneBindGroup: GPUBindGroup;
|
||||
blurHBindGroup: GPUBindGroup;
|
||||
blurVBindGroup: GPUBindGroup;
|
||||
fieldA: GPUTexture;
|
||||
fieldB: GPUTexture;
|
||||
fieldAView: GPUTextureView;
|
||||
fieldBView: GPUTextureView;
|
||||
fieldSize: [number, number];
|
||||
};
|
||||
|
||||
function eventKindCode(type: BlackboxTraceImpulse['type']): number {
|
||||
switch (type) {
|
||||
case 'mcp.call': return 0;
|
||||
case 'memory.retrieve': return 1;
|
||||
case 'memory.suppress': return 2;
|
||||
case 'memory.write': return 3;
|
||||
case 'sanhedrin.veto': return 4;
|
||||
case 'contradiction.detected': return 5;
|
||||
case 'dream.patch': return 6;
|
||||
}
|
||||
}
|
||||
|
||||
function laneCode(lane: BlackboxTraceImpulse['lane']): number {
|
||||
return ['tool', 'retrieve', 'suppress', 'write', 'veto', 'contradiction', 'dream'].indexOf(lane);
|
||||
}
|
||||
|
||||
export class BlackboxPass implements FramePass {
|
||||
private engine: ObservatoryEngine;
|
||||
private scene: BlackboxScene | null = null;
|
||||
private resources: GpuResources | null = null;
|
||||
private sampler: GPUSampler | null = null;
|
||||
private traceBindLayout: GPUBindGroupLayout | null = null;
|
||||
private membraneBindLayout: GPUBindGroupLayout | null = null;
|
||||
private blurBindLayout: GPUBindGroupLayout | null = null;
|
||||
private impulsePipeline: GPURenderPipeline | null = null;
|
||||
private lanePipeline: GPURenderPipeline | null = null;
|
||||
private receiptPipeline: GPURenderPipeline | null = null;
|
||||
private blurPipeline: GPURenderPipeline | null = null;
|
||||
private membranePipeline: GPURenderPipeline | null = null;
|
||||
private eventCount = 0;
|
||||
private receiptCount = 0;
|
||||
private hitRects: { event: BlackboxTraceImpulse; 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 BlackboxScene;
|
||||
this.buildHitRects();
|
||||
const device = this.engine.gpuDevice;
|
||||
if (!device) return;
|
||||
this.ensurePipelines(device);
|
||||
this.ensureResources(device);
|
||||
this.uploadBuffers(device);
|
||||
}
|
||||
|
||||
private ensurePipelines(device: GPUDevice): void {
|
||||
if (this.impulsePipeline || !this.engine.paramsBuffer) return;
|
||||
const traceModule = device.createShaderModule({ label: 'blackbox-trace-wgsl', code: TRACE_WGSL });
|
||||
const membraneModule = device.createShaderModule({ label: 'blackbox-membrane-wgsl', code: MEMBRANE_WGSL });
|
||||
const blurModule = device.createShaderModule({ label: 'blackbox-blur-wgsl', code: BLUR_WGSL });
|
||||
this.traceBindLayout = device.createBindGroupLayout({
|
||||
label: 'blackbox-trace-bind-layout',
|
||||
entries: [
|
||||
{ binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
||||
{ binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } },
|
||||
{ binding: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }
|
||||
]
|
||||
});
|
||||
this.membraneBindLayout = device.createBindGroupLayout({
|
||||
label: 'blackbox-membrane-bind-layout',
|
||||
entries: [
|
||||
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
||||
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
||||
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } }
|
||||
]
|
||||
});
|
||||
this.blurBindLayout = device.createBindGroupLayout({
|
||||
label: 'blackbox-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 traceLayout = device.createPipelineLayout({ label: 'blackbox-trace-layout', bindGroupLayouts: [this.traceBindLayout] });
|
||||
const membraneLayout = device.createPipelineLayout({ label: 'blackbox-membrane-layout', bindGroupLayouts: [this.membraneBindLayout] });
|
||||
const blurLayout = device.createPipelineLayout({ label: 'blackbox-blur-layout', bindGroupLayouts: [this.blurBindLayout] });
|
||||
this.sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear' });
|
||||
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.impulsePipeline = device.createRenderPipeline({ label: 'blackbox-impulses', layout: traceLayout, vertex: { module: traceModule, entryPoint: 'vs_impulse' }, fragment: { module: traceModule, entryPoint: 'fs_impulse', targets: [{ format: FIELD_FORMAT, blend: additive }] }, primitive: { topology: 'triangle-list' } });
|
||||
this.lanePipeline = device.createRenderPipeline({ label: 'blackbox-lanes', layout: traceLayout, vertex: { module: traceModule, entryPoint: 'vs_lane' }, fragment: { module: traceModule, entryPoint: 'fs_lane', targets: [{ format: FIELD_FORMAT, blend: additive }] }, primitive: { topology: 'triangle-list' } });
|
||||
this.receiptPipeline = device.createRenderPipeline({ label: 'blackbox-receipt-beads', layout: traceLayout, vertex: { module: traceModule, entryPoint: 'vs_receipt' }, fragment: { module: traceModule, entryPoint: 'fs_receipt', targets: [{ format: FIELD_FORMAT, blend: additive }] }, primitive: { topology: 'triangle-list' } });
|
||||
this.blurPipeline = device.createRenderPipeline({ label: 'blackbox-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.membranePipeline = device.createRenderPipeline({ label: 'blackbox-field-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' } });
|
||||
}
|
||||
|
||||
private ensureResources(device: GPUDevice): void {
|
||||
if (!this.traceBindLayout || !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 eventBuffer = this.resources?.eventBuffer;
|
||||
let receiptBuffer = this.resources?.receiptBuffer;
|
||||
let blurHBuffer = this.resources?.blurHBuffer;
|
||||
let blurVBuffer = this.resources?.blurVBuffer;
|
||||
if (!eventBuffer) eventBuffer = device.createBuffer({ label: 'blackbox-event-cells', size: MAX_EVENTS * EVENT_FLOATS * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
||||
if (!receiptBuffer) receiptBuffer = device.createBuffer({ label: 'blackbox-receipt-beads', size: MAX_RECEIPTS * RECEIPT_FLOATS * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
||||
if (!blurHBuffer) {
|
||||
blurHBuffer = device.createBuffer({ label: 'blackbox-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: 'blackbox-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: 'blackbox-field-a-rgba16float', size: [w, h], format: FIELD_FORMAT, usage });
|
||||
const fieldB = device.createTexture({ label: 'blackbox-field-b-rgba16float', size: [w, h], format: FIELD_FORMAT, usage });
|
||||
const fieldAView = fieldA.createView();
|
||||
const fieldBView = fieldB.createView();
|
||||
const traceBindGroup = device.createBindGroup({ label: 'blackbox-trace-bind', layout: this.traceBindLayout, entries: [{ binding: 0, resource: { buffer: this.engine.paramsBuffer } }, { binding: 1, resource: { buffer: eventBuffer } }, { binding: 2, resource: { buffer: receiptBuffer } }] });
|
||||
const membraneBindGroup = device.createBindGroup({ label: 'blackbox-membrane-bind', layout: this.membraneBindLayout, entries: [{ binding: 0, resource: { buffer: this.engine.paramsBuffer } }, { binding: 3, resource: this.sampler }, { binding: 4, resource: fieldAView }] });
|
||||
const blurHBindGroup = device.createBindGroup({ label: 'blackbox-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: 'blackbox-blur-v-bind', layout: this.blurBindLayout, entries: [{ binding: 0, resource: this.sampler }, { binding: 1, resource: fieldBView }, { binding: 2, resource: { buffer: blurVBuffer } }] });
|
||||
this.resources = { eventBuffer, receiptBuffer, blurHBuffer, blurVBuffer, traceBindGroup, membraneBindGroup, blurHBindGroup, blurVBindGroup, fieldA, fieldB, fieldAView, fieldBView, fieldSize: [w, h] };
|
||||
}
|
||||
|
||||
private uploadBuffers(device: GPUDevice): void {
|
||||
if (!this.resources || !this.scene) return;
|
||||
const visible = Math.min(MAX_EVENTS, this.scene.visibleEventCount || this.scene.traceEvents.length);
|
||||
const total = Math.max(1, this.scene.traceEvents.length - 1);
|
||||
const events = new Float32Array(MAX_EVENTS * EVENT_FLOATS);
|
||||
this.eventCount = Math.min(MAX_EVENTS, this.scene.traceEvents.length);
|
||||
for (let i = 0; i < this.eventCount; i++) {
|
||||
const ev = this.scene.traceEvents[i];
|
||||
const visibleGate = i < visible ? 1 : 0;
|
||||
const selected = i === this.scene.selectedIndex ? 1 : 0;
|
||||
const lane = laneCode(ev.lane);
|
||||
const kind = eventKindCode(ev.type);
|
||||
const memoryCount = ev.memoryIds.length;
|
||||
const strength = ev.type === 'memory.suppress' || ev.type === 'sanhedrin.veto' || ev.type === 'contradiction.detected' ? 1 : ev.type === 'memory.write' ? 0.75 : 0.35;
|
||||
events.set([i / total, lane, ev.confidence, kind, i * 34 + 18, visibleGate, selected, 0, memoryCount, strength, total ? i / total : 0, 0], i * EVENT_FLOATS);
|
||||
}
|
||||
device.queue.writeBuffer(this.resources.eventBuffer, 0, events);
|
||||
const receipts = new Float32Array(MAX_RECEIPTS * RECEIPT_FLOATS);
|
||||
this.receiptCount = Math.min(MAX_RECEIPTS, this.scene.receipts.length);
|
||||
for (let i = 0; i < this.receiptCount; i++) {
|
||||
const linkedEventIndex = Math.min(Math.max(0, visible - 1), this.eventCount - 1);
|
||||
receipts.set([0, 0, 0.65, linkedEventIndex, this.scene.receipts[i].nodeIndices.length, i, 0, 0], i * RECEIPT_FLOATS);
|
||||
}
|
||||
device.queue.writeBuffer(this.resources.receiptBuffer, 0, receipts);
|
||||
this.engine.params[4] = this.eventCount;
|
||||
}
|
||||
|
||||
private buildHitRects(): void {
|
||||
const events = this.scene?.traceEvents ?? [];
|
||||
const total = Math.max(1, events.length - 1);
|
||||
this.hitRects = events.map((ev, i) => ({ event: ev, x: -0.84 + 1.68 * (i / total), y: 0.66 - 1.28 * (laneCode(ev.lane) / 6), w: 0.055, h: 0.065 }));
|
||||
}
|
||||
|
||||
compute(encoder: GPUCommandEncoder): void {
|
||||
const device = this.engine.gpuDevice;
|
||||
if (!device || !this.resources || !this.impulsePipeline || !this.lanePipeline || !this.receiptPipeline || !this.blurPipeline) return;
|
||||
this.ensureResources(device);
|
||||
const res = this.resources;
|
||||
const splat = encoder.beginRenderPass({ label: 'blackbox-field-splat-pass', colorAttachments: [{ view: res.fieldAView, clearValue: { r: 0, g: 0, b: 0, a: 0 }, loadOp: 'clear', storeOp: 'store' }] });
|
||||
splat.setBindGroup(0, res.traceBindGroup);
|
||||
splat.setPipeline(this.lanePipeline);
|
||||
splat.draw(6, 7);
|
||||
if (this.eventCount > 0) {
|
||||
splat.setPipeline(this.impulsePipeline);
|
||||
splat.draw(6, this.eventCount);
|
||||
}
|
||||
if (this.receiptCount > 0 && this.eventCount > 0) {
|
||||
splat.setPipeline(this.receiptPipeline);
|
||||
splat.draw(6, this.receiptCount);
|
||||
}
|
||||
splat.end();
|
||||
const blurH = encoder.beginRenderPass({ label: 'blackbox-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: 'blackbox-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();
|
||||
}
|
||||
|
||||
render(pass: GPURenderPassEncoder): void {
|
||||
if (!this.resources || !this.membranePipeline || !this.impulsePipeline || !this.lanePipeline || !this.receiptPipeline) return;
|
||||
pass.setPipeline(this.membranePipeline);
|
||||
pass.setBindGroup(0, this.resources.membraneBindGroup);
|
||||
pass.draw(6, 1);
|
||||
pass.setBindGroup(0, this.resources.traceBindGroup);
|
||||
pass.setPipeline(this.lanePipeline);
|
||||
pass.draw(6, 7);
|
||||
if (this.eventCount > 0) {
|
||||
pass.setPipeline(this.impulsePipeline);
|
||||
pass.draw(6, this.eventCount);
|
||||
}
|
||||
if (this.receiptCount > 0 && this.eventCount > 0) {
|
||||
pass.setPipeline(this.receiptPipeline);
|
||||
pass.draw(6, this.receiptCount);
|
||||
}
|
||||
}
|
||||
|
||||
pickAt(ndcX: number, ndcY: number): RoutePick | null {
|
||||
for (const rect of this.hitRects) {
|
||||
if (Math.abs(ndcX - rect.x) <= rect.w && Math.abs(ndcY - rect.y) <= rect.h) {
|
||||
return { id: rect.event.id, kind: 'trace-event', index: rect.event.index, payload: rect.event };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.resources?.eventBuffer.destroy();
|
||||
this.resources?.receiptBuffer.destroy();
|
||||
this.resources?.blurHBuffer.destroy();
|
||||
this.resources?.blurVBuffer.destroy();
|
||||
this.resources?.fieldA.destroy();
|
||||
this.resources?.fieldB.destroy();
|
||||
this.resources = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function createBlackboxPasses(engine: ObservatoryEngine, scene: RouteSceneModel): BlackboxPass[] {
|
||||
void rgb01(RETENTION.healthy);
|
||||
void rgb01(IMMUNE.veto);
|
||||
void rgb01(CAUSAL.forward);
|
||||
return [new BlackboxPass(engine, scene)];
|
||||
}
|
||||
260
apps/dashboard/src/lib/observatory/blackbox/blackbox-scene.ts
Normal file
260
apps/dashboard/src/lib/observatory/blackbox/blackbox-scene.ts
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
import {
|
||||
assertProvenance,
|
||||
type Provenance,
|
||||
type RouteEvent,
|
||||
type RouteNode,
|
||||
type RouteReceipt,
|
||||
type RouteSceneModel
|
||||
} from '$lib/observatory/route-scene';
|
||||
import type { Receipt, TraceDetail, TraceEvent } from '$lib/stores/api';
|
||||
|
||||
export type BlackboxLane = 'tool' | 'retrieve' | 'suppress' | 'write' | 'veto' | 'contradiction' | 'dream';
|
||||
|
||||
export interface ActivationPair {
|
||||
id: string;
|
||||
activation: number;
|
||||
}
|
||||
|
||||
export interface BlackboxTraceImpulse {
|
||||
index: number;
|
||||
id: string;
|
||||
type: TraceEvent['type'];
|
||||
lane: BlackboxLane;
|
||||
runId: string;
|
||||
at: number;
|
||||
label: string;
|
||||
summary: string;
|
||||
memoryIds: string[];
|
||||
activationPairs: ActivationPair[];
|
||||
confidence: number;
|
||||
provenance: Provenance;
|
||||
raw: TraceEvent;
|
||||
}
|
||||
|
||||
export interface BlackboxScene extends RouteSceneModel {
|
||||
organ: 'blackbox';
|
||||
runId: string | null;
|
||||
traceEvents: BlackboxTraceImpulse[];
|
||||
visibleEventCount: number;
|
||||
selectedIndex: number;
|
||||
startedAt: number;
|
||||
lastAt: number;
|
||||
durationMs: number;
|
||||
receiptRows: Receipt[];
|
||||
}
|
||||
|
||||
type TraceInput = TraceDetail | (TraceDetail & { summary?: Record<string, unknown> | null }) | null | undefined;
|
||||
|
||||
function clamp01(v: number): number {
|
||||
return Math.max(0, Math.min(1, Number.isFinite(v) ? v : 0));
|
||||
}
|
||||
|
||||
function num(v: unknown, fallback = 0): number {
|
||||
return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
|
||||
}
|
||||
|
||||
function text(v: unknown, fallback = ''): string {
|
||||
return typeof v === 'string' ? v : v == null ? fallback : String(v);
|
||||
}
|
||||
|
||||
function traceSource(runId: string | null, index: number, eventType: string): Provenance {
|
||||
return { kind: 'trace', id: `${runId ?? 'trace:none'}:event:${index}:${eventType}` };
|
||||
}
|
||||
|
||||
function scalarSource(name: string, value: number): Provenance {
|
||||
return { kind: 'scalar', id: `blackbox.${name}`, scalar: { name, value } };
|
||||
}
|
||||
|
||||
function eventSource(runId: string | null, index: number, eventType: string): Provenance {
|
||||
return { kind: 'event', id: `${runId ?? 'trace:none'}:event:${index}:${eventType}` };
|
||||
}
|
||||
|
||||
function memorySource(id: string): Provenance {
|
||||
return { kind: 'memory', id };
|
||||
}
|
||||
|
||||
function laneFor(type: TraceEvent['type']): BlackboxLane {
|
||||
switch (type) {
|
||||
case 'mcp.call': return 'tool';
|
||||
case 'memory.retrieve': return 'retrieve';
|
||||
case 'memory.suppress': return 'suppress';
|
||||
case 'memory.write': return 'write';
|
||||
case 'sanhedrin.veto': return 'veto';
|
||||
case 'contradiction.detected': return 'contradiction';
|
||||
case 'dream.patch': return 'dream';
|
||||
}
|
||||
}
|
||||
|
||||
function eventMemoryIds(ev: TraceEvent): string[] {
|
||||
switch (ev.type) {
|
||||
case 'memory.retrieve': return ev.ids;
|
||||
case 'memory.suppress': return [ev.id];
|
||||
case 'memory.write': return [ev.id];
|
||||
case 'contradiction.detected': return ev.ids;
|
||||
case 'sanhedrin.veto': return ev.evidenceIds;
|
||||
case 'dream.patch': return ev.proposalIds;
|
||||
case 'mcp.call': return [];
|
||||
}
|
||||
}
|
||||
|
||||
function eventLabel(ev: TraceEvent): string {
|
||||
switch (ev.type) {
|
||||
case 'mcp.call': return ev.tool;
|
||||
case 'memory.retrieve': return `${ev.ids.length} memories retrieved`;
|
||||
case 'memory.suppress': return `suppressed ${ev.id.slice(0, 8)}`;
|
||||
case 'memory.write': return `wrote ${ev.id.slice(0, 8)}`;
|
||||
case 'contradiction.detected': return `contradiction ${ev.ids.join(' ↔ ')}`;
|
||||
case 'sanhedrin.veto': return 'Sanhedrin veto';
|
||||
case 'dream.patch': return `${ev.proposalIds.length} dream proposals`;
|
||||
}
|
||||
}
|
||||
|
||||
function eventSummary(ev: TraceEvent): string {
|
||||
switch (ev.type) {
|
||||
case 'mcp.call': return `MCP tool ${ev.tool} called; args hash ${ev.argsHash.slice(0, 12)}`;
|
||||
case 'memory.retrieve': return `Retrieved ${ev.ids.length} memories with activation map`;
|
||||
case 'memory.suppress': return `Suppressed ${ev.id}: ${ev.reason}`;
|
||||
case 'memory.write': return `Memory write ${ev.id} from ${ev.source}`;
|
||||
case 'contradiction.detected': return ev.detail;
|
||||
case 'sanhedrin.veto': return `${Math.round(clamp01(ev.confidence) * 100)}% veto confidence: ${ev.claim}`;
|
||||
case 'dream.patch': return `Dream patch proposals: ${ev.proposalIds.join(', ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
function activationPairs(ev: TraceEvent): ActivationPair[] {
|
||||
if (ev.type !== 'memory.retrieve') return [];
|
||||
return Object.entries(ev.activation ?? {})
|
||||
.map(([id, activation]) => ({ id, activation: clamp01(activation) }))
|
||||
.sort((a, b) => b.activation - a.activation);
|
||||
}
|
||||
|
||||
function confidenceFor(ev: TraceEvent): number {
|
||||
if (ev.type === 'sanhedrin.veto') return clamp01(ev.confidence);
|
||||
if (ev.type === 'memory.retrieve') {
|
||||
const values = Object.values(ev.activation ?? {});
|
||||
return values.length ? clamp01(values.reduce((a, b) => a + b, 0) / values.length) : 0.45;
|
||||
}
|
||||
if (ev.type === 'memory.suppress') return 0.78;
|
||||
if (ev.type === 'memory.write') return 0.72;
|
||||
if (ev.type === 'contradiction.detected') return 0.82;
|
||||
if (ev.type === 'dream.patch') return 0.52;
|
||||
return 0.62;
|
||||
}
|
||||
|
||||
export function normalizeBlackboxScene(input: TraceInput, receipts: Receipt[] = [], selectedIndex?: number): BlackboxScene {
|
||||
const runId = input?.runId ?? null;
|
||||
const eventsRaw = input?.events ?? [];
|
||||
const summary = input?.summary ?? null;
|
||||
const startedAt = num(summary?.startedAt, eventsRaw[0]?.at ?? 0);
|
||||
const lastAt = num(summary?.lastAt, eventsRaw[eventsRaw.length - 1]?.at ?? startedAt);
|
||||
const maxVisible = eventsRaw.length ? eventsRaw.length - 1 : 0;
|
||||
const visibleTo = Math.max(0, Math.min(maxVisible, selectedIndex ?? maxVisible));
|
||||
const visibleEventCount = eventsRaw.length ? visibleTo + 1 : 0;
|
||||
|
||||
const traceEvents: BlackboxTraceImpulse[] = eventsRaw.map((ev, index) => ({
|
||||
index,
|
||||
id: `${runId ?? ev.runId}:event:${index}:${ev.type}`,
|
||||
type: ev.type,
|
||||
lane: laneFor(ev.type),
|
||||
runId: ev.runId,
|
||||
at: ev.at,
|
||||
label: eventLabel(ev),
|
||||
summary: eventSummary(ev),
|
||||
memoryIds: eventMemoryIds(ev),
|
||||
activationPairs: activationPairs(ev),
|
||||
confidence: confidenceFor(ev),
|
||||
provenance: traceSource(runId, index, ev.type),
|
||||
raw: ev
|
||||
}));
|
||||
|
||||
const memoryIndex = new Map<string, RouteNode>();
|
||||
for (const impulse of traceEvents) {
|
||||
for (const id of impulse.memoryIds) {
|
||||
if (!id || memoryIndex.has(id)) continue;
|
||||
const activation = impulse.activationPairs.find((p) => p.id === id)?.activation ?? 0;
|
||||
memoryIndex.set(id, {
|
||||
source: memorySource(id),
|
||||
index: memoryIndex.size,
|
||||
label: id.slice(0, 12),
|
||||
retention: Math.max(0.25, activation),
|
||||
activation,
|
||||
trust: impulse.type === 'memory.suppress' ? 0.2 : Math.max(0.35, impulse.confidence),
|
||||
suppression: impulse.type === 'memory.suppress' ? 1 : 0,
|
||||
tags: [impulse.lane, impulse.type],
|
||||
type: 'trace-memory'
|
||||
});
|
||||
}
|
||||
}
|
||||
const nodes = [...memoryIndex.values()];
|
||||
|
||||
const routeEvents: RouteEvent[] = traceEvents.slice(0, visibleEventCount).map((ev) => ({
|
||||
source: eventSource(runId, ev.index, ev.type),
|
||||
type: ev.type,
|
||||
targetIndex: ev.memoryIds.length ? (memoryIndex.get(ev.memoryIds[0])?.index ?? -1) : -1,
|
||||
frame: ev.index * 34 + 18,
|
||||
energy: Math.max(0.18, ev.confidence)
|
||||
}));
|
||||
|
||||
const edges = traceEvents.slice(0, visibleEventCount).flatMap((ev) => {
|
||||
const ids = ev.memoryIds.filter((id) => memoryIndex.has(id));
|
||||
if (ids.length < 2) return [];
|
||||
return ids.slice(1).map((id, i) => ({
|
||||
source: { kind: 'pair' as const, id: `${ev.id}:pair:${ids[0]}:${id}` },
|
||||
sourceIndex: memoryIndex.get(ids[0])!.index,
|
||||
targetIndex: memoryIndex.get(id)!.index,
|
||||
weight: ev.activationPairs[i + 1]?.activation ?? ev.confidence,
|
||||
kind: ev.type
|
||||
}));
|
||||
});
|
||||
|
||||
const receiptRows = receipts ?? [];
|
||||
const routeReceipts: RouteReceipt[] = receiptRows.map((receipt) => {
|
||||
const ids = [...new Set([...(receipt.retrieved ?? []), ...(receipt.activation_path ?? []), ...(receipt.mutations ?? []).map((m) => m.id)])];
|
||||
return {
|
||||
source: { kind: 'receipt', id: receipt.receipt_id },
|
||||
label: `receipt ${receipt.receipt_id.slice(0, 10)}`,
|
||||
nodeIndices: ids.map((id) => memoryIndex.get(id)?.index).filter((i): i is number => typeof i === 'number')
|
||||
};
|
||||
});
|
||||
|
||||
const scene: BlackboxScene = {
|
||||
organ: 'blackbox',
|
||||
nodes,
|
||||
edges,
|
||||
events: routeEvents,
|
||||
receipts: routeReceipts,
|
||||
scalars: {
|
||||
eventCount: eventsRaw.length,
|
||||
visibleEventCount,
|
||||
retrievedCount: num(summary?.retrievedCount, traceEvents.filter((e) => e.type === 'memory.retrieve').length),
|
||||
suppressedCount: num(summary?.suppressedCount, traceEvents.filter((e) => e.type === 'memory.suppress').length),
|
||||
writeCount: num(summary?.writeCount, traceEvents.filter((e) => e.type === 'memory.write').length),
|
||||
vetoCount: num(summary?.vetoCount, traceEvents.filter((e) => e.type === 'sanhedrin.veto').length),
|
||||
durationMs: Math.max(0, lastAt - startedAt),
|
||||
receiptCount: routeReceipts.length
|
||||
},
|
||||
alive: eventsRaw.length > 0,
|
||||
runId,
|
||||
traceEvents,
|
||||
visibleEventCount,
|
||||
selectedIndex: visibleTo,
|
||||
startedAt,
|
||||
lastAt,
|
||||
durationMs: Math.max(0, lastAt - startedAt),
|
||||
receiptRows
|
||||
};
|
||||
|
||||
if (!scene.alive) {
|
||||
scene.receipts = receiptRows.map((receipt) => ({
|
||||
source: { kind: 'receipt', id: receipt.receipt_id },
|
||||
label: `receipt ${receipt.receipt_id.slice(0, 10)}`,
|
||||
nodeIndices: []
|
||||
}));
|
||||
scene.scalars.eventCount = 0;
|
||||
scene.scalars.visibleEventCount = 0;
|
||||
void scalarSource('empty', 0);
|
||||
}
|
||||
|
||||
if (import.meta.env.DEV) assertProvenance(scene);
|
||||
return scene;
|
||||
}
|
||||
716
apps/dashboard/src/lib/observatory/duplicates/duplicates-pass.ts
Normal file
716
apps/dashboard/src/lib/observatory/duplicates/duplicates-pass.ts
Normal file
|
|
@ -0,0 +1,716 @@
|
|||
import type { ObservatoryEngine, FramePass } from '$lib/observatory/engine';
|
||||
import { IMMUNE, MEDIUM, RETENTION, membraneWidth, rgb01 } from '$lib/observatory/cognitive-palette';
|
||||
import type { RouteSceneModel } from '$lib/observatory/route-scene';
|
||||
import type { DuplicateFusionCluster, DuplicatesScene } from './duplicates-scene';
|
||||
|
||||
type RoutePick = { id: string; kind: string; index?: number; payload?: unknown };
|
||||
|
||||
const FIELD_FORMAT: GPUTextureFormat = 'rgba16float';
|
||||
const MAX_CELLS = 512;
|
||||
const MAX_NECKS = 512;
|
||||
const CELL_FLOATS = 16;
|
||||
const NECK_FLOATS = 16;
|
||||
|
||||
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 FusionCell {
|
||||
// x/y position in NDC, z retention, w winner flag
|
||||
pos_retention: vec4f,
|
||||
// x similarity, y threshold, z member slot, w cluster slot
|
||||
cluster_meta: vec4f,
|
||||
// x mismatch intensity, y merge flag, z radius, w member count
|
||||
visual_meta: vec4f,
|
||||
// x cell index, y cluster index, z/w spare
|
||||
ids: vec4f,
|
||||
};
|
||||
|
||||
struct FusionNeck {
|
||||
// x/y winner position, z winner retention, w winner radius
|
||||
a: vec4f,
|
||||
// x/y candidate position, z candidate retention, w candidate radius
|
||||
b: vec4f,
|
||||
// x similarity, y threshold, z mismatch intensity, w merge flag
|
||||
signals: vec4f,
|
||||
// x neck index, y cluster index, z/w spare
|
||||
ids: vec4f,
|
||||
};
|
||||
`;
|
||||
|
||||
const SPLAT_WGSL = /* wgsl */ `
|
||||
${COMMON_WGSL}
|
||||
|
||||
@group(0) @binding(0) var<uniform> params: Params;
|
||||
@group(0) @binding(1) var<storage, read> cells: array<FusionCell>;
|
||||
@group(0) @binding(2) var<storage, read> necks: array<FusionNeck>;
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
fn similarity_neck(similarity: f32) -> f32 {
|
||||
return smoothstep(0.78, 0.98, similarity);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_splat(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
|
||||
var out: VSOut;
|
||||
let corner = QUAD[vi];
|
||||
let cell_count = u32(params.node_count);
|
||||
if (ii < cell_count) {
|
||||
let c = cells[ii];
|
||||
let merge_gate = c.visual_meta.y;
|
||||
let radius = c.visual_meta.z * (1.0 + 0.045 * sin(params.time * 2.0 + c.cluster_meta.w * 6.28318));
|
||||
out.clip = vec4f(c.pos_retention.xy + corner * radius, 0.0, 1.0);
|
||||
out.uv = corner;
|
||||
out.misc = vec4f(c.pos_retention.z, c.cluster_meta.x, c.visual_meta.x, merge_gate);
|
||||
} else {
|
||||
let n = necks[ii - cell_count];
|
||||
let a = n.a.xy;
|
||||
let b = n.b.xy;
|
||||
let center = (a + b) * 0.5;
|
||||
let dir = normalize(b - a + vec2f(0.0001, 0.0001));
|
||||
let normal = vec2f(-dir.y, dir.x);
|
||||
let fused = similarity_neck(n.signals.x);
|
||||
let length_half = distance(a, b) * 0.5;
|
||||
let thickness = 0.035 + fused * 0.085 + n.signals.z * 0.025;
|
||||
let pos = center + dir * corner.x * length_half + normal * corner.y * thickness;
|
||||
out.clip = vec4f(pos, 0.0, 1.0);
|
||||
out.uv = vec2f(corner.x, corner.y / max(0.001, thickness));
|
||||
out.misc = vec4f(n.signals.x, fused, n.signals.z, n.signals.w);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_splat(frag: VSOut) -> @location(0) vec4f {
|
||||
let d = length(frag.uv);
|
||||
let is_neck = f32(abs(frag.uv.y) > 1.0);
|
||||
if (is_neck < 0.5 && d > 1.0) { discard; }
|
||||
let retention = clamp(frag.misc.x, 0.0, 1.0);
|
||||
let similarity = clamp(frag.misc.y, 0.0, 1.0);
|
||||
let mismatch = clamp(frag.misc.z, 0.0, 1.0);
|
||||
let merge_gate = frag.misc.w;
|
||||
let cell_body = exp(-d * d * 3.15) * (0.38 + retention * 0.62) * (0.5 + similarity * 0.58);
|
||||
let cell_rim = smoothstep(0.24, 0.02, abs(d - (0.58 + retention * 0.16))) * (0.2 + similarity * 0.55);
|
||||
let neck_body = exp(-frag.uv.y * frag.uv.y * 4.0) * smoothstep(1.05, 0.82, abs(frag.uv.x)) * (0.35 + similarity * 0.9);
|
||||
let density = max(cell_body + cell_rim, neck_body * (0.4 + similarity));
|
||||
// r=density, g=retention/luciferin, b=mismatch amber, a reserved. No storage textures.
|
||||
return vec4f(density, density * (0.35 + retention * 0.65), mismatch * (0.18 + merge_gate * 0.12), 1.0);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_cell(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
|
||||
var out: VSOut;
|
||||
let c = cells[ii];
|
||||
let corner = QUAD[vi];
|
||||
let winner = c.pos_retention.w;
|
||||
let radius = c.visual_meta.z * (0.46 + winner * 0.18);
|
||||
out.clip = vec4f(c.pos_retention.xy + corner * radius, 0.0, 1.0);
|
||||
out.uv = corner;
|
||||
out.misc = vec4f(c.pos_retention.z, c.cluster_meta.x, c.visual_meta.x, winner);
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_cell(frag: VSOut) -> @location(0) vec4f {
|
||||
let d = length(frag.uv);
|
||||
if (d > 1.0) { discard; }
|
||||
let retention = clamp(frag.misc.x, 0.0, 1.0);
|
||||
let similarity = clamp(frag.misc.y, 0.0, 1.0);
|
||||
let mismatch = clamp(frag.misc.z, 0.0, 1.0);
|
||||
let winner = frag.misc.w;
|
||||
let sediment = vec3f(0.54, 0.29, 0.09);
|
||||
let recall = vec3f(0.16, 0.95, 0.66);
|
||||
let luciferin = vec3f(0.91, 1.0, 0.72);
|
||||
let ivory = vec3f(0.96, 0.945, 0.815);
|
||||
let amber = vec3f(1.0, 0.69, 0.08);
|
||||
let core = mix(sediment, mix(recall, luciferin, retention), retention);
|
||||
let rim = smoothstep(0.98, 0.72, d) * (1.0 - smoothstep(0.72, 0.22, d));
|
||||
let body = exp(-d*d*3.2) * (0.20 + retention * 0.44 + winner * 0.16);
|
||||
let mismatch_ring = smoothstep(0.16, 0.0, abs(d - 0.80)) * mismatch;
|
||||
return vec4f(core * body + ivory * rim * (0.16 + similarity * 0.52) + amber * mismatch_ring * 0.34, 1.0);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_neck(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
|
||||
var out: VSOut;
|
||||
let n = necks[ii];
|
||||
let a = n.a.xy;
|
||||
let b = n.b.xy;
|
||||
let t = f32(vi / 2u) / 31.0;
|
||||
let side = f32(vi % 2u) * 2.0 - 1.0;
|
||||
let dir = normalize(b - a + vec2f(0.0001, 0.0001));
|
||||
let normal = vec2f(-dir.y, dir.x);
|
||||
let midpoint = (a + b) * 0.5;
|
||||
let fused = similarity_neck(n.signals.x);
|
||||
let threshold_pull = clamp(n.signals.x - n.signals.y + 0.22, 0.0, 1.0);
|
||||
let bow = normal * sin(t * 3.14159) * (0.030 + n.signals.z * 0.050) * (1.0 - fused * 0.35);
|
||||
let pos = mix(a, b, t) + bow;
|
||||
let thickness = 0.005 + fused * 0.025 + threshold_pull * 0.010;
|
||||
out.clip = vec4f(pos + normal * side * thickness, 0.0, 1.0);
|
||||
out.uv = vec2f(t, side);
|
||||
out.misc = vec4f(n.signals.x, n.signals.y, n.signals.z, distance(pos, midpoint));
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_neck(frag: VSOut) -> @location(0) vec4f {
|
||||
let similarity = clamp(frag.misc.x, 0.0, 1.0);
|
||||
let threshold = clamp(frag.misc.y, 0.0, 1.0);
|
||||
let mismatch = clamp(frag.misc.z, 0.0, 1.0);
|
||||
let pulse = 0.55 + 0.45 * sin(36.0 * frag.uv.x - 8.0 * frag.misc.w);
|
||||
let bridge = vec3f(0.10, 0.82, 0.92);
|
||||
let luciferin = vec3f(0.91, 1.0, 0.72);
|
||||
let amber = vec3f(1.0, 0.69, 0.08);
|
||||
let pull = smoothstep(-0.08, 0.20, similarity - threshold);
|
||||
let color = mix(bridge, luciferin, pull) + amber * mismatch * pulse * 0.34;
|
||||
return vec4f(color * (0.14 + similarity * 0.55 + mismatch * 0.18), 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const MEMBRANE_WGSL = /* wgsl */ `
|
||||
${COMMON_WGSL}
|
||||
|
||||
@group(0) @binding(0) var<uniform> params: Params;
|
||||
@group(0) @binding(3) var field_sampler: sampler;
|
||||
@group(0) @binding(4) 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 };
|
||||
|
||||
@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_membrane(frag: VSOut) -> @location(0) vec4f {
|
||||
let f = textureSample(field_tex, field_sampler, frag.uv);
|
||||
let density = clamp(f.r, 0.0, 5.0);
|
||||
let retention = clamp(f.g, 0.0, 5.0);
|
||||
let mismatch = clamp(f.b, 0.0, 3.0);
|
||||
let membrane = smoothstep(0.13, 0.88, density) * (1.0 - smoothstep(1.9, 3.8, density));
|
||||
let blackwater = vec3f(0.008, 0.012, 0.018);
|
||||
let bridge = vec3f(0.10, 0.82, 0.92);
|
||||
let luciferin = vec3f(0.66, 1.0, 0.37);
|
||||
let ivory = vec3f(0.96, 0.945, 0.815);
|
||||
let amber = vec3f(1.0, 0.69, 0.08);
|
||||
var color = blackwater * (0.18 + density * 0.055);
|
||||
color = color + bridge * density * 0.055 + luciferin * retention * 0.080;
|
||||
color = color + ivory * membrane * 0.22 + amber * mismatch * (0.20 + 0.08 * params.pulse);
|
||||
let vignette = smoothstep(0.96, 0.18, distance(frag.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(frag: VSOut) -> @location(0) vec4f {
|
||||
let dims = vec2f(textureDimensions(blur_src, 0));
|
||||
let stepv = blur_dir.dir / max(dims, vec2f(1.0));
|
||||
var acc = textureSampleLevel(blur_src, blur_sampler, frag.uv - stepv * 2.0, 0.0) * 0.06136;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, frag.uv - stepv, 0.0) * 0.24477;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, frag.uv, 0.0) * 0.38774;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, frag.uv + stepv, 0.0) * 0.24477;
|
||||
acc = acc + textureSampleLevel(blur_src, blur_sampler, frag.uv + stepv * 2.0, 0.0) * 0.06136;
|
||||
return acc;
|
||||
}
|
||||
`;
|
||||
|
||||
type GpuResources = {
|
||||
cellBuffer: GPUBuffer;
|
||||
neckBuffer: GPUBuffer;
|
||||
blurHBuffer: GPUBuffer;
|
||||
blurVBuffer: GPUBuffer;
|
||||
splatBindGroup: GPUBindGroup;
|
||||
blurHBindGroup: GPUBindGroup;
|
||||
blurVBindGroup: GPUBindGroup;
|
||||
membraneBindGroup: GPUBindGroup;
|
||||
fieldA: GPUTexture;
|
||||
fieldB: GPUTexture;
|
||||
fieldAView: GPUTextureView;
|
||||
fieldBView: GPUTextureView;
|
||||
fieldSize: [number, number];
|
||||
};
|
||||
|
||||
type CellGeometry = {
|
||||
cluster: DuplicateFusionCluster;
|
||||
memoryId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
retention: number;
|
||||
winner: boolean;
|
||||
mismatch: number;
|
||||
radius: number;
|
||||
memberSlot: number;
|
||||
memberCount: number;
|
||||
};
|
||||
|
||||
type NeckGeometry = {
|
||||
cluster: DuplicateFusionCluster;
|
||||
winnerId: string;
|
||||
candidateId: string;
|
||||
ax: number;
|
||||
ay: number;
|
||||
bx: number;
|
||||
by: number;
|
||||
winnerRetention: number;
|
||||
candidateRetention: number;
|
||||
winnerRadius: number;
|
||||
candidateRadius: number;
|
||||
mismatch: number;
|
||||
};
|
||||
|
||||
export class DuplicatesPass implements FramePass {
|
||||
private engine: ObservatoryEngine;
|
||||
private scene: DuplicatesScene | null = null;
|
||||
private resources: GpuResources | null = null;
|
||||
private sampler: GPUSampler | 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 membranePipeline: GPURenderPipeline | null = null;
|
||||
private cellPipeline: GPURenderPipeline | null = null;
|
||||
private neckPipeline: GPURenderPipeline | null = null;
|
||||
private cellCount = 0;
|
||||
private neckCount = 0;
|
||||
private cellGeometry: CellGeometry[] = [];
|
||||
private neckGeometry: NeckGeometry[] = [];
|
||||
|
||||
constructor(engine: ObservatoryEngine, scene: RouteSceneModel) {
|
||||
this.engine = engine;
|
||||
this.uploadScene(scene);
|
||||
}
|
||||
|
||||
uploadScene(scene: RouteSceneModel): void {
|
||||
this.scene = scene as DuplicatesScene;
|
||||
this.buildGeometry();
|
||||
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 splatModule = createDiagnosedShaderModule(device, 'duplicates-fusion-splat-wgsl', SPLAT_WGSL);
|
||||
const blurModule = createDiagnosedShaderModule(device, 'duplicates-fusion-blur-wgsl', BLUR_WGSL);
|
||||
const membraneModule = createDiagnosedShaderModule(device, 'duplicates-fusion-membrane-wgsl', MEMBRANE_WGSL);
|
||||
this.splatBindLayout = device.createBindGroupLayout({
|
||||
label: 'duplicates-fusion-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: 2, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } }
|
||||
]
|
||||
});
|
||||
this.blurBindLayout = device.createBindGroupLayout({
|
||||
label: 'duplicates-fusion-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' } }
|
||||
]
|
||||
});
|
||||
this.membraneBindLayout = device.createBindGroupLayout({
|
||||
label: 'duplicates-fusion-membrane-bind-layout',
|
||||
entries: [
|
||||
{ binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
|
||||
{ binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } },
|
||||
{ binding: 4, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } }
|
||||
]
|
||||
});
|
||||
const splatLayout = device.createPipelineLayout({ label: 'duplicates-fusion-splat-layout', bindGroupLayouts: [this.splatBindLayout] });
|
||||
const blurLayout = device.createPipelineLayout({ label: 'duplicates-fusion-blur-layout', bindGroupLayouts: [this.blurBindLayout] });
|
||||
const membraneLayout = device.createPipelineLayout({ label: 'duplicates-fusion-membrane-layout', bindGroupLayouts: [this.membraneBindLayout] });
|
||||
this.sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear' });
|
||||
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.splatPipeline = device.createRenderPipeline({
|
||||
label: 'duplicates-field-additive-splat',
|
||||
layout: splatLayout,
|
||||
vertex: { module: splatModule, entryPoint: 'vs_splat' },
|
||||
fragment: { module: splatModule, entryPoint: 'fs_splat', targets: [{ format: FIELD_FORMAT, blend: additive }] },
|
||||
primitive: { topology: 'triangle-list' }
|
||||
});
|
||||
this.blurPipeline = device.createRenderPipeline({
|
||||
label: 'duplicates-field-blur-render-pass',
|
||||
layout: blurLayout,
|
||||
vertex: { module: blurModule, entryPoint: 'vs_fullscreen' },
|
||||
fragment: { module: blurModule, entryPoint: 'fs_blur', targets: [{ format: FIELD_FORMAT }] },
|
||||
primitive: { topology: 'triangle-list' }
|
||||
});
|
||||
this.membranePipeline = device.createRenderPipeline({
|
||||
label: 'duplicates-synaptic-fusion-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.cellPipeline = device.createRenderPipeline({
|
||||
label: 'duplicates-memory-nuclei',
|
||||
layout: splatLayout,
|
||||
vertex: { module: splatModule, entryPoint: 'vs_cell' },
|
||||
fragment: { module: splatModule, entryPoint: 'fs_cell', targets: [{ format: this.engine.sceneFormat, blend: additive }] },
|
||||
primitive: { topology: 'triangle-list' }
|
||||
});
|
||||
this.neckPipeline = device.createRenderPipeline({
|
||||
label: 'duplicates-mismatch-filaments',
|
||||
layout: splatLayout,
|
||||
vertex: { module: splatModule, entryPoint: 'vs_neck' },
|
||||
fragment: { module: splatModule, entryPoint: 'fs_neck', targets: [{ format: this.engine.sceneFormat, blend: additive }] },
|
||||
primitive: { topology: 'triangle-strip' }
|
||||
});
|
||||
}
|
||||
|
||||
private ensureResources(device: GPUDevice): void {
|
||||
if (!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 cellBuffer = this.resources?.cellBuffer;
|
||||
let neckBuffer = this.resources?.neckBuffer;
|
||||
let blurHBuffer = this.resources?.blurHBuffer;
|
||||
let blurVBuffer = this.resources?.blurVBuffer;
|
||||
if (!cellBuffer) cellBuffer = device.createBuffer({ label: 'duplicates-cells', size: MAX_CELLS * CELL_FLOATS * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
||||
if (!neckBuffer) neckBuffer = device.createBuffer({ label: 'duplicates-necks', size: MAX_NECKS * NECK_FLOATS * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
|
||||
if (!blurHBuffer) {
|
||||
blurHBuffer = device.createBuffer({ label: 'duplicates-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: 'duplicates-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: 'duplicates-field-a-rgba16float', size: [w, h], format: FIELD_FORMAT, usage });
|
||||
const fieldB = device.createTexture({ label: 'duplicates-field-b-rgba16float', size: [w, h], format: FIELD_FORMAT, usage });
|
||||
const fieldAView = fieldA.createView();
|
||||
const fieldBView = fieldB.createView();
|
||||
const splatBindGroup = device.createBindGroup({
|
||||
label: 'duplicates-fusion-splat-bind',
|
||||
layout: this.splatBindLayout,
|
||||
entries: [
|
||||
{ binding: 0, resource: { buffer: this.engine.paramsBuffer } },
|
||||
{ binding: 1, resource: { buffer: cellBuffer } },
|
||||
{ binding: 2, resource: { buffer: neckBuffer } }
|
||||
]
|
||||
});
|
||||
const blurHBindGroup = device.createBindGroup({
|
||||
label: 'duplicates-field-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: 'duplicates-field-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: 'duplicates-membrane-bind',
|
||||
layout: this.membraneBindLayout,
|
||||
entries: [
|
||||
{ binding: 0, resource: { buffer: this.engine.paramsBuffer } },
|
||||
{ binding: 3, resource: this.sampler },
|
||||
{ binding: 4, resource: fieldAView }
|
||||
]
|
||||
});
|
||||
this.resources = { cellBuffer, neckBuffer, blurHBuffer, blurVBuffer, splatBindGroup, blurHBindGroup, blurVBindGroup, membraneBindGroup, fieldA, fieldB, fieldAView, fieldBView, fieldSize: [w, h] };
|
||||
}
|
||||
|
||||
private buildGeometry(): void {
|
||||
const clusters = this.scene?.clusters ?? [];
|
||||
const n = Math.max(1, clusters.length);
|
||||
const cells: CellGeometry[] = [];
|
||||
const necks: NeckGeometry[] = [];
|
||||
for (let i = 0; i < clusters.length; i++) {
|
||||
const cluster = clusters[i];
|
||||
const angle = (i / n) * Math.PI * 2 - Math.PI / 2;
|
||||
const lane = 0.18 + 0.58 * Math.sqrt((i + 0.5) / n);
|
||||
const centerX = Math.cos(angle) * lane * 0.86;
|
||||
const centerY = Math.sin(angle) * lane;
|
||||
const memberCount = Math.max(1, cluster.memories.length);
|
||||
const pull = Math.max(0.04, 0.25 - Math.max(0, cluster.similarity - cluster.threshold) * 0.55);
|
||||
const winnerMemory = cluster.memories.find((m) => m.id === cluster.winnerId) ?? cluster.memories[0];
|
||||
const cellById = new Map<string, CellGeometry>();
|
||||
for (let j = 0; j < cluster.memories.length && cells.length < MAX_CELLS; j++) {
|
||||
const memory = cluster.memories[j];
|
||||
const memberAngle = angle + (j / memberCount) * Math.PI * 2 + (memberCount % 2 ? 0 : Math.PI / memberCount);
|
||||
const winner = memory.id === cluster.winnerId;
|
||||
const spread = winner ? pull * 0.18 : pull + 0.025 * (j % 3);
|
||||
const mismatch = Math.min(1, (memory.mismatchTokens?.length ?? 0) / 8);
|
||||
const radius = 0.085 + Math.min(0.045, membraneWidth(memory.retention) * 2.1) + (winner ? 0.012 : 0);
|
||||
const cell = {
|
||||
cluster,
|
||||
memoryId: memory.id,
|
||||
x: centerX + Math.cos(memberAngle) * spread,
|
||||
y: centerY + Math.sin(memberAngle) * spread,
|
||||
retention: Math.max(0, Math.min(1, memory.retention || 0)),
|
||||
winner,
|
||||
mismatch,
|
||||
radius,
|
||||
memberSlot: j,
|
||||
memberCount
|
||||
};
|
||||
cellById.set(memory.id, cell);
|
||||
cells.push(cell);
|
||||
}
|
||||
const winnerCell = cellById.get(winnerMemory.id);
|
||||
if (!winnerCell) continue;
|
||||
for (const memory of cluster.memories) {
|
||||
if (necks.length >= MAX_NECKS || memory.id === winnerMemory.id) continue;
|
||||
const candidate = cellById.get(memory.id);
|
||||
if (!candidate) continue;
|
||||
necks.push({
|
||||
cluster,
|
||||
winnerId: winnerMemory.id,
|
||||
candidateId: memory.id,
|
||||
ax: winnerCell.x,
|
||||
ay: winnerCell.y,
|
||||
bx: candidate.x,
|
||||
by: candidate.y,
|
||||
winnerRetention: winnerCell.retention,
|
||||
candidateRetention: candidate.retention,
|
||||
winnerRadius: winnerCell.radius,
|
||||
candidateRadius: candidate.radius,
|
||||
mismatch: Math.max(candidate.mismatch, Math.min(1, cluster.mismatchTokens.length / 12))
|
||||
});
|
||||
}
|
||||
}
|
||||
this.cellGeometry = cells;
|
||||
this.neckGeometry = necks;
|
||||
}
|
||||
|
||||
private uploadBuffers(device: GPUDevice): void {
|
||||
if (!this.resources) return;
|
||||
const cellData = new Float32Array(MAX_CELLS * CELL_FLOATS);
|
||||
const neckData = new Float32Array(MAX_NECKS * NECK_FLOATS);
|
||||
this.cellCount = Math.min(MAX_CELLS, this.cellGeometry.length);
|
||||
this.neckCount = Math.min(MAX_NECKS, this.neckGeometry.length);
|
||||
for (let i = 0; i < this.cellCount; i++) {
|
||||
const c = this.cellGeometry[i];
|
||||
cellData.set([
|
||||
c.x,
|
||||
c.y,
|
||||
c.retention,
|
||||
c.winner ? 1 : 0,
|
||||
c.cluster.similarity,
|
||||
c.cluster.threshold,
|
||||
c.memberSlot,
|
||||
c.cluster.index,
|
||||
c.mismatch,
|
||||
c.cluster.suggestedAction === 'merge' ? 1 : 0,
|
||||
c.radius,
|
||||
c.memberCount,
|
||||
i,
|
||||
c.cluster.index,
|
||||
0,
|
||||
0
|
||||
], i * CELL_FLOATS);
|
||||
}
|
||||
for (let i = 0; i < this.neckCount; i++) {
|
||||
const n = this.neckGeometry[i];
|
||||
neckData.set([
|
||||
n.ax,
|
||||
n.ay,
|
||||
n.winnerRetention,
|
||||
n.winnerRadius,
|
||||
n.bx,
|
||||
n.by,
|
||||
n.candidateRetention,
|
||||
n.candidateRadius,
|
||||
n.cluster.similarity,
|
||||
n.cluster.threshold,
|
||||
n.mismatch,
|
||||
n.cluster.suggestedAction === 'merge' ? 1 : 0,
|
||||
i,
|
||||
n.cluster.index,
|
||||
0,
|
||||
0
|
||||
], i * NECK_FLOATS);
|
||||
}
|
||||
this.engine.params[2] = this.cellCount;
|
||||
this.engine.params[3] = this.neckCount;
|
||||
this.engine.params[4] = this.neckCount;
|
||||
device.queue.writeBuffer(this.resources.cellBuffer, 0, cellData);
|
||||
device.queue.writeBuffer(this.resources.neckBuffer, 0, neckData);
|
||||
}
|
||||
|
||||
compute(encoder: GPUCommandEncoder): void {
|
||||
const device = this.engine.gpuDevice;
|
||||
if (!device || !this.resources || !this.splatPipeline || !this.blurPipeline) return;
|
||||
this.ensureResources(device);
|
||||
const res = this.resources;
|
||||
const splat = encoder.beginRenderPass({
|
||||
label: 'duplicates-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, this.cellCount + this.neckCount);
|
||||
splat.end();
|
||||
|
||||
const blurH = encoder.beginRenderPass({
|
||||
label: 'duplicates-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: 'duplicates-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();
|
||||
}
|
||||
|
||||
render(pass: GPURenderPassEncoder): void {
|
||||
if (!this.resources || !this.membranePipeline || !this.cellPipeline || !this.neckPipeline) return;
|
||||
pass.setPipeline(this.membranePipeline);
|
||||
pass.setBindGroup(0, this.resources.membraneBindGroup);
|
||||
pass.draw(6, 1);
|
||||
if (this.neckCount > 0) {
|
||||
pass.setPipeline(this.neckPipeline);
|
||||
pass.setBindGroup(0, this.resources.splatBindGroup);
|
||||
pass.draw(64, this.neckCount);
|
||||
}
|
||||
if (this.cellCount > 0) {
|
||||
pass.setPipeline(this.cellPipeline);
|
||||
pass.setBindGroup(0, this.resources.splatBindGroup);
|
||||
pass.draw(6, this.cellCount);
|
||||
}
|
||||
}
|
||||
|
||||
pickAt(ndcX: number, ndcY: number): RoutePick | null {
|
||||
for (let i = 0; i < this.neckGeometry.length; i++) {
|
||||
const g = this.neckGeometry[i];
|
||||
const distToLine = distanceToSegment(ndcX, ndcY, g.ax, g.ay, g.bx, g.by);
|
||||
const midX = (g.ax + g.bx) * 0.5;
|
||||
const midY = (g.ay + g.by) * 0.5;
|
||||
const fusedRadius = 0.055 + Math.max(0, g.cluster.similarity - g.cluster.threshold) * 0.45;
|
||||
if (distToLine <= fusedRadius || Math.hypot(ndcX - midX, ndcY - midY) <= fusedRadius) {
|
||||
return { id: g.cluster.id, kind: 'duplicate-neck', index: i, payload: g.cluster };
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < this.cellGeometry.length; i++) {
|
||||
const c = this.cellGeometry[i];
|
||||
if (Math.hypot(ndcX - c.x, ndcY - c.y) <= c.radius * 0.8) {
|
||||
return { id: c.memoryId, kind: 'duplicate-memory', index: i, payload: c.cluster };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.resources?.cellBuffer.destroy();
|
||||
this.resources?.neckBuffer.destroy();
|
||||
this.resources?.blurHBuffer.destroy();
|
||||
this.resources?.blurVBuffer.destroy();
|
||||
this.resources?.fieldA.destroy();
|
||||
this.resources?.fieldB.destroy();
|
||||
this.resources = null;
|
||||
}
|
||||
}
|
||||
|
||||
function distanceToSegment(px: number, py: number, ax: number, ay: number, bx: number, by: number): number {
|
||||
const vx = bx - ax;
|
||||
const vy = by - ay;
|
||||
const wx = px - ax;
|
||||
const wy = py - ay;
|
||||
const c1 = vx * wx + vy * wy;
|
||||
if (c1 <= 0) return Math.hypot(px - ax, py - ay);
|
||||
const c2 = vx * vx + vy * vy;
|
||||
if (c2 <= c1) return Math.hypot(px - bx, py - by);
|
||||
const t = c1 / c2;
|
||||
return Math.hypot(px - (ax + t * vx), py - (ay + t * vy));
|
||||
}
|
||||
|
||||
function createDiagnosedShaderModule(device: GPUDevice, label: string, code: string): GPUShaderModule {
|
||||
device.pushErrorScope('validation');
|
||||
const module = device.createShaderModule({ label, code });
|
||||
void module.getCompilationInfo().then((info) => {
|
||||
for (const message of info.messages) {
|
||||
console.error(`[observatory] ${label} WGSL ${message.type} ${message.lineNum}:${message.linePos} ${message.message}`);
|
||||
}
|
||||
});
|
||||
void device.popErrorScope().then((error) => {
|
||||
if (error) console.error(`[observatory] ${label} shader module validation: ${error.message}`);
|
||||
});
|
||||
return module;
|
||||
}
|
||||
|
||||
export function createDuplicatesPasses(engine: ObservatoryEngine, scene: RouteSceneModel): DuplicatesPass[] {
|
||||
void rgb01(MEDIUM.blackwater);
|
||||
void rgb01(RETENTION.recall);
|
||||
void rgb01(RETENTION.luciferin);
|
||||
void rgb01(IMMUNE.trustMembrane);
|
||||
return [new DuplicatesPass(engine, scene)];
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
import { clusterKey, pickWinner } from '$components/duplicates-helpers';
|
||||
import {
|
||||
assertProvenance,
|
||||
type Provenance,
|
||||
type RouteEdge,
|
||||
type RouteEvent,
|
||||
type RouteNode,
|
||||
type RouteSceneModel
|
||||
} from '$lib/observatory/route-scene';
|
||||
import type { DuplicateClusterGroup, DuplicateClusterMemory, DuplicatesResponse } from '$types';
|
||||
|
||||
export interface DuplicateFusionMemory extends DuplicateClusterMemory {
|
||||
index: number;
|
||||
preview: string;
|
||||
winner: boolean;
|
||||
mismatchTokens: string[];
|
||||
}
|
||||
|
||||
export interface DuplicateFusionCluster {
|
||||
id: string;
|
||||
index: number;
|
||||
similarity: number;
|
||||
threshold: number;
|
||||
suggestedAction: 'merge' | 'review';
|
||||
winnerId: string;
|
||||
memories: DuplicateFusionMemory[];
|
||||
mismatchTokens: string[];
|
||||
source: Provenance;
|
||||
}
|
||||
|
||||
export interface DuplicatesScene extends RouteSceneModel {
|
||||
organ: 'duplicates';
|
||||
threshold: number;
|
||||
total: number;
|
||||
clusters: DuplicateFusionCluster[];
|
||||
raw: DuplicatesResponse;
|
||||
}
|
||||
|
||||
function clamp01(v: number): number {
|
||||
return Math.max(0, Math.min(1, Number.isFinite(v) ? v : 0));
|
||||
}
|
||||
|
||||
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: `duplicates.${name}`, scalar: { name, value } };
|
||||
}
|
||||
|
||||
function preview(content: string, max = 84): string {
|
||||
const text = (content || '').trim().replace(/\s+/g, ' ');
|
||||
return text.length <= max ? text : `${text.slice(0, max)}…`;
|
||||
}
|
||||
|
||||
function tokenize(content: string): string[] {
|
||||
return (content || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_\s-]/g, ' ')
|
||||
.split(/\s+/)
|
||||
.filter((token) => token.length >= 4)
|
||||
.slice(0, 80);
|
||||
}
|
||||
|
||||
function mismatchTokens(memories: DuplicateClusterMemory[]): string[] {
|
||||
if (memories.length < 2) return [];
|
||||
const tokenSets = memories.map((m) => new Set(tokenize(m.content)));
|
||||
const counts = new Map<string, number>();
|
||||
for (const tokenSet of tokenSets) {
|
||||
for (const token of tokenSet) counts.set(token, (counts.get(token) ?? 0) + 1);
|
||||
}
|
||||
return Array.from(counts.entries())
|
||||
.filter(([, count]) => count > 0 && count < memories.length)
|
||||
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
.slice(0, 12)
|
||||
.map(([token]) => token);
|
||||
}
|
||||
|
||||
function normalizeCluster(
|
||||
cluster: DuplicateClusterGroup,
|
||||
index: number,
|
||||
threshold: number
|
||||
): DuplicateFusionCluster | null {
|
||||
const members = Array.isArray(cluster.memories) ? cluster.memories.filter((m) => m.id) : [];
|
||||
if (members.length < 2) return null;
|
||||
const id = clusterKey(members);
|
||||
const winner = pickWinner(members);
|
||||
const clusterMismatch = mismatchTokens(members);
|
||||
return {
|
||||
id,
|
||||
index,
|
||||
similarity: clamp01(cluster.similarity),
|
||||
threshold: clamp01(threshold),
|
||||
suggestedAction: cluster.suggestedAction === 'merge' ? 'merge' : 'review',
|
||||
winnerId: winner?.id ?? members[0].id,
|
||||
memories: members.map((memory, memoryIndex) => ({
|
||||
...memory,
|
||||
index: memoryIndex,
|
||||
preview: preview(memory.content),
|
||||
winner: memory.id === (winner?.id ?? members[0].id),
|
||||
mismatchTokens: clusterMismatch.filter((token) => tokenize(memory.content).includes(token)).slice(0, 8)
|
||||
})),
|
||||
mismatchTokens: clusterMismatch,
|
||||
source: source('pair', id)
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeDuplicatesScene(rawInput: DuplicatesResponse): DuplicatesScene {
|
||||
const threshold = clamp01(rawInput.threshold ?? 0.8);
|
||||
const rawClusters = Array.isArray(rawInput.clusters) ? rawInput.clusters : [];
|
||||
const clusters = rawClusters
|
||||
.map((cluster, index) => normalizeCluster(cluster, index, threshold))
|
||||
.filter((cluster): cluster is DuplicateFusionCluster => cluster !== null);
|
||||
|
||||
let nodeIndex = 0;
|
||||
const nodes: RouteNode[] = [];
|
||||
const nodeIndexByMemoryId = new Map<string, number>();
|
||||
for (const cluster of clusters) {
|
||||
for (const memory of cluster.memories) {
|
||||
if (nodeIndexByMemoryId.has(memory.id)) continue;
|
||||
const currentIndex = nodeIndex++;
|
||||
nodeIndexByMemoryId.set(memory.id, currentIndex);
|
||||
nodes.push({
|
||||
source: source('memory', memory.id),
|
||||
index: currentIndex,
|
||||
label: memory.preview || memory.id.slice(0, 8),
|
||||
retention: clamp01(memory.retention),
|
||||
trust: clamp01(cluster.similarity),
|
||||
lastAccessed: memory.createdAt,
|
||||
tags: [memory.nodeType, ...memory.tags, memory.winner ? 'winner' : 'candidate'].filter(Boolean),
|
||||
type: memory.nodeType || 'memory'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const edges: RouteEdge[] = [];
|
||||
for (const cluster of clusters) {
|
||||
const winnerIndex = nodeIndexByMemoryId.get(cluster.winnerId);
|
||||
if (winnerIndex == null) continue;
|
||||
for (const memory of cluster.memories) {
|
||||
const targetIndex = nodeIndexByMemoryId.get(memory.id);
|
||||
if (targetIndex == null || targetIndex === winnerIndex) continue;
|
||||
edges.push({
|
||||
source: source('pair', `${cluster.id}:${cluster.winnerId}:${memory.id}`),
|
||||
sourceIndex: winnerIndex,
|
||||
targetIndex,
|
||||
weight: Math.max(0.05, cluster.similarity),
|
||||
kind: cluster.suggestedAction === 'merge' ? 'fusion-candidate' : 'review-candidate'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const events: RouteEvent[] = clusters.map((cluster, index) => ({
|
||||
source: source('event', `duplicates.cluster.${cluster.id}`),
|
||||
type: cluster.suggestedAction === 'merge' ? 'DuplicateMergeCandidate' : 'DuplicateReviewCandidate',
|
||||
targetIndex: -1,
|
||||
frame: 20 + index * 14,
|
||||
energy: Math.max(0.1, cluster.similarity - threshold + 0.1)
|
||||
}));
|
||||
|
||||
const total = Number.isFinite(rawInput.total) ? rawInput.total : clusters.length;
|
||||
const memoryCount = nodes.length;
|
||||
const maxSimilarity = clusters.reduce((max, cluster) => Math.max(max, cluster.similarity), 0);
|
||||
const mergeCandidates = clusters.filter((cluster) => cluster.suggestedAction === 'merge').length;
|
||||
const reviewCandidates = clusters.length - mergeCandidates;
|
||||
|
||||
const scene: DuplicatesScene = {
|
||||
organ: 'duplicates',
|
||||
nodes,
|
||||
edges,
|
||||
events,
|
||||
receipts: [],
|
||||
scalars: {
|
||||
threshold: scalarSource('threshold', threshold).scalar?.value ?? threshold,
|
||||
clusterCount: clusters.length,
|
||||
memoryCount,
|
||||
maxSimilarity,
|
||||
mergeCandidates,
|
||||
reviewCandidates,
|
||||
total
|
||||
},
|
||||
alive: clusters.length > 0,
|
||||
threshold,
|
||||
total,
|
||||
clusters,
|
||||
raw: rawInput
|
||||
};
|
||||
|
||||
if (import.meta.env.DEV) assertProvenance(scene);
|
||||
return scene;
|
||||
}
|
||||
|
|
@ -36,6 +36,13 @@
|
|||
formatAt,
|
||||
relativeMs
|
||||
} from '$components/blackbox-helpers';
|
||||
import RouteStage, { type RoutePick } from '$lib/observatory/RouteStage.svelte';
|
||||
import { createBlackboxPasses } from '$lib/observatory/blackbox/blackbox-pass';
|
||||
import {
|
||||
normalizeBlackboxScene,
|
||||
type BlackboxScene,
|
||||
type BlackboxTraceImpulse
|
||||
} from '$lib/observatory/blackbox/blackbox-scene';
|
||||
|
||||
// ---- state ----------------------------------------------------------
|
||||
let runs = $state<TraceRunSummary[]>([]);
|
||||
|
|
@ -46,6 +53,7 @@
|
|||
let scrubIndex = $state(0); // index into detail.events
|
||||
let proofMode = $state(false);
|
||||
let receipts = $state<Receipt[]>([]);
|
||||
let selectedImpulse = $state<BlackboxTraceImpulse | null>(null);
|
||||
|
||||
// The events up to and including the scrubber position — what the agent had
|
||||
// "experienced" at that moment in the run.
|
||||
|
|
@ -68,6 +76,14 @@
|
|||
const hasContradiction = $derived(
|
||||
detail?.events.some((e) => e.type === 'contradiction.detected') ?? false
|
||||
);
|
||||
const blackboxScene = $derived<BlackboxScene>(normalizeBlackboxScene(detail, receipts, scrubIndex));
|
||||
|
||||
function handleRoutePick(pick: RoutePick) {
|
||||
if (pick.kind !== 'trace-event') return;
|
||||
const impulse = pick.payload as BlackboxTraceImpulse;
|
||||
selectedImpulse = impulse;
|
||||
scrubIndex = Math.max(0, Math.min(detail?.events.length ? detail.events.length - 1 : 0, impulse.index));
|
||||
}
|
||||
|
||||
async function loadRuns() {
|
||||
try {
|
||||
|
|
@ -86,6 +102,7 @@
|
|||
try {
|
||||
detail = await api.traces.get(runId);
|
||||
scrubIndex = Math.max(0, (detail.events.length || 1) - 1);
|
||||
selectedImpulse = null;
|
||||
// Receipts are the proof behind THIS run's retrievals — scoped to
|
||||
// the selected run (B5), not the global latest.
|
||||
receipts = (await api.receipts.listForRun(runId, 8)).receipts;
|
||||
|
|
@ -123,7 +140,18 @@
|
|||
onMount(loadRuns);
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-6xl px-5 py-6">
|
||||
<RouteStage
|
||||
organ="blackbox"
|
||||
seed={`agent-flight-recorder:${selectedRunId ?? 'empty'}:${blackboxScene.visibleEventCount}`}
|
||||
scene={blackboxScene}
|
||||
passes={createBlackboxPasses}
|
||||
loading={loading}
|
||||
error={error}
|
||||
emptyLabel="NO AGENT TRACE SELECTED — RECORDER ARMED"
|
||||
onpick={handleRoutePick}
|
||||
/>
|
||||
|
||||
<div class="relative z-10 mx-auto max-w-6xl px-5 py-6">
|
||||
<PageHeader
|
||||
icon="blackbox"
|
||||
title="Agent Black Box"
|
||||
|
|
@ -178,6 +206,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{#if selectedImpulse}
|
||||
<div class="selected-impulse glass" use:reveal>
|
||||
<span class="selected-kicker">GPU pick</span>
|
||||
<strong>{selectedImpulse.label}</strong>
|
||||
<span>{selectedImpulse.summary}</span>
|
||||
<code>{selectedImpulse.provenance.id}</code>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !proofMode}
|
||||
<div class="layout">
|
||||
<!-- ░░ RUN PICKER ░░ -->
|
||||
|
|
|
|||
|
|
@ -12,6 +12,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 { createDuplicatesPasses } from '$lib/observatory/duplicates/duplicates-pass';
|
||||
import {
|
||||
normalizeDuplicatesScene,
|
||||
type DuplicateFusionCluster,
|
||||
type DuplicatesScene
|
||||
} from '$lib/observatory/duplicates/duplicates-scene';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import { spotlight } from '$lib/actions/interactions';
|
||||
import { api } from '$stores/api';
|
||||
|
|
@ -25,6 +32,7 @@
|
|||
let dismissed = $state(new Set<string>());
|
||||
let loading = $state(true);
|
||||
let error: string | null = $state(null);
|
||||
let selectedCluster: DuplicateFusionCluster | null = $state(null);
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
async function detect() {
|
||||
|
|
@ -83,11 +91,35 @@
|
|||
overflowed ? visibleClusters.slice(0, CLUSTER_RENDER_CAP) : visibleClusters
|
||||
);
|
||||
|
||||
const duplicatesScene = $derived.by<DuplicatesScene>(() =>
|
||||
normalizeDuplicatesScene({
|
||||
threshold,
|
||||
total: visibleClusters.length,
|
||||
clusters: visibleClusters.map(({ c }) => c)
|
||||
})
|
||||
);
|
||||
|
||||
function handleRoutePick(pick: RoutePick) {
|
||||
if (pick.kind !== 'duplicate-neck' && pick.kind !== 'duplicate-memory') return;
|
||||
selectedCluster = pick.payload as DuplicateFusionCluster;
|
||||
}
|
||||
|
||||
onMount(() => detect());
|
||||
onDestroy(() => clearTimeout(debounceTimer));
|
||||
</script>
|
||||
|
||||
<div class="relative mx-auto max-w-5xl space-y-6 p-6">
|
||||
<RouteStage
|
||||
organ="duplicates"
|
||||
seed={`synaptic-fusion:${threshold}:${visibleClusters.length}:${totalDuplicates}`}
|
||||
scene={duplicatesScene}
|
||||
passes={createDuplicatesPasses}
|
||||
loading={loading}
|
||||
error={error}
|
||||
emptyLabel={`NO DUPLICATES ABOVE ${(threshold * 100).toFixed(0)}% SIMILARITY`}
|
||||
onpick={handleRoutePick}
|
||||
/>
|
||||
|
||||
<div class="relative z-10 mx-auto max-w-5xl space-y-6 p-6 pointer-events-none">
|
||||
<!-- Header -->
|
||||
<PageHeader
|
||||
icon="duplicates"
|
||||
|
|
@ -105,7 +137,7 @@
|
|||
</PageHeader>
|
||||
|
||||
<!-- Controls panel -->
|
||||
<div class="glass-panel flex flex-wrap items-center gap-5 rounded-2xl p-4">
|
||||
<div class="glass-panel pointer-events-auto flex flex-wrap items-center gap-5 rounded-2xl p-4">
|
||||
<!-- Threshold slider -->
|
||||
<label class="flex flex-1 min-w-64 items-center gap-3 text-xs text-dim">
|
||||
<span class="whitespace-nowrap">Similarity threshold</span>
|
||||
|
|
@ -158,10 +190,36 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Field pick inspector -->
|
||||
{#if selectedCluster}
|
||||
<div class="glass-panel pointer-events-auto rounded-2xl border border-synapse/25 bg-black/30 p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div class="font-mono text-[11px] uppercase tracking-[0.18em] text-synapse-glow">
|
||||
Synaptic neck selected
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-bright">
|
||||
{selectedCluster.memories.length} memories · {(selectedCluster.similarity * 100).toFixed(1)}% similar · winner {selectedCluster.winnerId.slice(0, 8)}
|
||||
</div>
|
||||
<div class="mt-1 max-w-2xl text-xs text-muted">
|
||||
Real pair key: {selectedCluster.id}. Mismatch filaments: {selectedCluster.mismatchTokens.length ? selectedCluster.mismatchTokens.join(', ') : 'none exposed'}.
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (selectedCluster = null)}
|
||||
class="rounded-lg bg-white/[0.04] px-3 py-1.5 text-xs text-dim transition hover:bg-white/[0.08] hover:text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-synapse/60"
|
||||
>
|
||||
Clear field focus
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Results -->
|
||||
{#if error}
|
||||
<div
|
||||
class="glass-panel flex flex-col items-center gap-3 rounded-2xl p-10 text-center"
|
||||
class="glass-panel pointer-events-auto flex flex-col items-center gap-3 rounded-2xl p-10 text-center"
|
||||
>
|
||||
<div class="text-sm text-decay">Couldn't detect duplicates</div>
|
||||
<div class="max-w-md text-xs text-muted">{error}</div>
|
||||
|
|
@ -174,14 +232,14 @@
|
|||
</button>
|
||||
</div>
|
||||
{:else if loading}
|
||||
<div class="space-y-3">
|
||||
<div class="pointer-events-auto space-y-3">
|
||||
{#each Array(3) as _}
|
||||
<div class="glass-subtle shimmer h-40 rounded-2xl"></div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if visibleClusters.length === 0}
|
||||
<div
|
||||
class="glass-panel enter flex flex-col items-center gap-3 rounded-2xl p-12 text-center"
|
||||
class="glass-panel pointer-events-auto enter flex flex-col items-center gap-3 rounded-2xl p-12 text-center"
|
||||
>
|
||||
<div
|
||||
class="flex h-14 w-14 items-center justify-center rounded-2xl border border-recall/25 bg-recall/10 text-recall"
|
||||
|
|
@ -197,7 +255,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
<div class="pointer-events-auto space-y-4">
|
||||
{#if overflowed}
|
||||
<div
|
||||
class="glass-subtle rounded-xl border border-warning/30 bg-warning/5 px-4 py-2 text-xs text-dim"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue