From 722d0f873802bf4f1139266b10a47f233fc9dfb5 Mon Sep 17 00:00:00 2001 From: Sam Valladares Date: Thu, 9 Jul 2026 08:46:56 -0700 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20Organ=203=20=E2=80=94=20Cont?= =?UTF-8?q?radictions=20"Immune=20Synapse=20Arena"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second Cognitive OS flagship, built by GPT-5.5 in the council, audited + verified live by Opus 4.8. - contradictions/contradictions-scene.ts: /api/contradictions → RouteSceneModel. Real backend {stronger:{id},weaker:{id},topic_overlap} shape + DeepReferenceCompleted.contradiction_pairs. Provenance enforced. - contradictions/contradictions-pass.ts: dual-channel signed metaball field — side A (stronger) splats red, side B (weaker) green; the seam (min(r,g) + opposing gradient) renders as a scarlet immune synapse. Trust thickens the membrane. Unresolved pairs spark scarlet arcs. All render passes (splat→render-pass blur→membrane), no storage textures, scarlet not magenta (magenta stays RSB-only). - contradictions/+page.svelte: RouteStage full-bleed, replaced the AmbientField base coat. Seam-pick → DOM receipt drawer. Honest "CALM IMMUNE FIELD" empty state. A WGSL struct-field mismatch (PairCell.meta vs p.signals) made the splat pipeline invalid at runtime — caught by the live-GPU audit (invisible to check/build), root-caused with getCompilationInfo + a web-verified spec research pass, fixed by renaming the field. Verified live vs the real 1241-memory brain: 104fps, valid pipelines, zero GPU errors, honest empty state (brain has 0 standing contradictions). check + build + 1043 tests green. Cinema + Graph field untouched. Co-Authored-By: Claude Opus 4.8 --- .../contradictions/contradictions-pass.ts | 609 ++++++++++++++++++ .../contradictions/contradictions-scene.ts | 316 +++++++++ .../routes/(app)/contradictions/+page.svelte | 167 ++++- 3 files changed, 1063 insertions(+), 29 deletions(-) create mode 100644 apps/dashboard/src/lib/observatory/contradictions/contradictions-pass.ts create mode 100644 apps/dashboard/src/lib/observatory/contradictions/contradictions-scene.ts diff --git a/apps/dashboard/src/lib/observatory/contradictions/contradictions-pass.ts b/apps/dashboard/src/lib/observatory/contradictions/contradictions-pass.ts new file mode 100644 index 0000000..7fb03e9 --- /dev/null +++ b/apps/dashboard/src/lib/observatory/contradictions/contradictions-pass.ts @@ -0,0 +1,609 @@ +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 { ContradictionsScene, ImmuneSynapsePair } from './contradictions-scene'; + +type RoutePick = { id: string; kind: string; index?: number; payload?: unknown }; + +const FIELD_FORMAT: GPUTextureFormat = 'rgba16float'; +const MAX_PAIRS = 160; +const PAIR_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 PairCell { + // stronger.xy, stronger trust, stronger membrane width + stronger: vec4f, + // weaker.xy, weaker trust, weaker membrane width + weaker: vec4f, + // x topic overlap, y trust delta, z unresolved, w slot phase + signals: vec4f, + // x pair index, y unused, z unused, w unused + ids: vec4f, +}; +`; + +const SPLAT_WGSL = /* wgsl */ ` +${COMMON_WGSL} + +@group(0) @binding(0) var params: Params; +@group(0) @binding(1) var pairs: array; + +const QUAD = array( + vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0), + vec2f(-1.0, -1.0), vec2f(1.0, 1.0), vec2f(-1.0, 1.0) +); + +struct VSOut { + @builtin(position) clip: vec4f, + @location(0) uv: vec2f, + @location(1) @interpolate(flat) misc: vec4f, +}; + +@vertex +fn vs_splat(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut { + var out: VSOut; + let pair_i = ii / 2u; + let side = ii % 2u; + let p = pairs[pair_i]; + let stronger_side = side == 0u; + let cell = select(p.weaker, p.stronger, stronger_side); + let trust = cell.z; + let width = cell.w; + let overlap = p.signals.x; + let unresolved = p.signals.z; + let breathing = 1.0 + 0.05 * sin(params.time * 2.2 + p.signals.w * 6.28318); + let radius = (0.105 + 0.05 * overlap + width * 1.8) * breathing; + out.clip = vec4f(cell.xy + QUAD[vi] * radius, 0.0, 1.0); + out.uv = QUAD[vi]; + out.misc = vec4f(trust, overlap, f32(side), unresolved); + return out; +} + +@fragment +fn fs_splat(in: VSOut) -> @location(0) vec4f { + let d = length(in.uv); + if (d > 1.0) { discard; } + let trust = clamp(in.misc.x, 0.0, 1.0); + let overlap = clamp(in.misc.y, 0.0, 1.0); + let side = in.misc.z; + let unresolved = in.misc.w; + let body = exp(-d * d * 3.4) * (0.42 + trust * 0.72) * (0.45 + overlap * 0.7); + let membrane = smoothstep(0.22, 0.02, abs(d - (0.62 + trust * 0.16))) * (0.18 + trust * 0.75); + let spark = unresolved * smoothstep(0.96, 0.68, d) * (0.6 + 0.4 * sin(params.time * 16.0 + in.uv.x * 7.0)); + // Dual-channel signed field: stronger splats red, weaker splats green. + let r = select(0.0, body + membrane * 0.55, side < 0.5); + let g = select(body + membrane * 0.55, 0.0, side < 0.5); + return vec4f(r, g, spark * 0.36, 1.0); +} + +@vertex +fn vs_cell(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut { + var out: VSOut; + let pair_i = ii / 2u; + let side = ii % 2u; + let p = pairs[pair_i]; + let stronger_side = side == 0u; + let cell = select(p.weaker, p.stronger, stronger_side); + let trust = cell.z; + let width = cell.w; + let radius = 0.028 + trust * 0.026 + width * 0.85; + out.clip = vec4f(cell.xy + QUAD[vi] * radius, 0.0, 1.0); + out.uv = QUAD[vi]; + out.misc = vec4f(trust, p.signals.x, f32(side), p.signals.z); + return out; +} + +@fragment +fn fs_cell(in: VSOut) -> @location(0) vec4f { + let d = length(in.uv); + if (d > 1.0) { discard; } + let trust = clamp(in.misc.x, 0.0, 1.0); + let side = in.misc.z; + let unresolved = in.misc.w; + let ivory = vec3f(0.96, 0.945, 0.815); + let luciferin = vec3f(0.66, 1.0, 0.37); + let redcore = vec3f(1.0, 0.23, 0.18); + let weaker_green = vec3f(0.16, 0.95, 0.66); + let core = select(weaker_green, mix(luciferin, ivory, trust), side < 0.5); + let rim = smoothstep(0.98, 0.72, d) * (1.0 - smoothstep(0.72, 0.2, d)); + let body = exp(-d*d*3.0) * (0.22 + trust * 0.42); + let flare = unresolved * smoothstep(0.18, 0.0, abs(d - 0.78)); + return vec4f(core * body + ivory * rim * (0.28 + trust * 0.52) + redcore * flare * 0.18, 1.0); +} + +@vertex +fn vs_arc(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut { + var out: VSOut; + let p = pairs[ii]; + let a = p.stronger.xy; + let b = p.weaker.xy; + let t = f32(vi / 2u) / 31.0; + let side = f32(vi % 2u) * 2.0 - 1.0; + let mid = (a + b) * 0.5; + let dir = normalize(b - a + vec2f(0.0001)); + let norm = vec2f(-dir.y, dir.x); + let bow = norm * sin(t * 3.14159) * (0.06 + p.signals.x * 0.12); + let pos = mix(a, b, t) + bow; + let thickness = (0.004 + min(p.stronger.z, p.weaker.z) * 0.011) * (1.0 + p.signals.z * (0.4 + 0.4 * sin(params.time * 18.0 + t * 20.0))); + out.clip = vec4f(pos + norm * side * thickness, 0.0, 1.0); + out.uv = vec2f(t, side); + out.misc = vec4f(p.signals.x, p.signals.y, p.signals.z, distance(pos, mid)); + return out; +} + +@fragment +fn fs_arc(in: VSOut) -> @location(0) vec4f { + let unresolved = in.misc.z; + let overlap = in.misc.x; + let pulse = 0.55 + 0.45 * sin(42.0 * in.uv.x - 8.0 * in.misc.w); + let scarlet = vec3f(1.0, 0.13, 0.09); + let darkred = vec3f(0.73, 0.05, 0.17); + let color = mix(darkred, scarlet, pulse * unresolved + overlap * 0.35); + return vec4f(color * (0.12 + unresolved * 0.55 + overlap * 0.26), 1.0); +} +`; + +const MEMBRANE_WGSL = /* wgsl */ ` +${COMMON_WGSL} + +@group(0) @binding(0) var params: Params; +@group(0) @binding(2) var field_sampler: sampler; +@group(0) @binding(3) var field_tex: texture_2d; + +const QUAD = array( + 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(in: VSOut) -> @location(0) vec4f { + let dims = vec2f(textureDimensions(field_tex, 0)); + let px = 1.0 / max(dims, vec2f(1.0)); + let f = textureSample(field_tex, field_sampler, in.uv); + let left = textureSampleLevel(field_tex, field_sampler, in.uv - vec2f(px.x, 0.0), 0.0); + let right = textureSampleLevel(field_tex, field_sampler, in.uv + vec2f(px.x, 0.0), 0.0); + let down = textureSampleLevel(field_tex, field_sampler, in.uv - vec2f(0.0, px.y), 0.0); + let up = textureSampleLevel(field_tex, field_sampler, in.uv + vec2f(0.0, px.y), 0.0); + + let strong = clamp(f.r, 0.0, 5.0); + let weak = clamp(f.g, 0.0, 5.0); + let seam = min(strong, weak); + let grad_r = vec2f(right.r - left.r, up.r - down.r); + let grad_g = vec2f(right.g - left.g, up.g - down.g); + let opposing = max(0.0, -dot(normalize(grad_r + vec2f(0.0001)), normalize(grad_g + vec2f(0.0001)))); + let fracture = smoothstep(0.11, 1.1, seam) * (0.42 + 0.9 * opposing); + let membrane = smoothstep(0.10, 0.95, max(strong, weak)) * (1.0 - smoothstep(1.75, 3.6, max(strong, weak))); + let spark = clamp(f.b, 0.0, 2.0) * (0.6 + 0.4 * params.pulse); + + let blackwater = vec3f(0.008, 0.012, 0.018); + let stronger_glow = vec3f(1.0, 0.21, 0.16); + let weaker_glow = vec3f(0.15, 0.95, 0.62); + let ivory = vec3f(0.96, 0.945, 0.815); + let scarlet = vec3f(1.0, 0.09, 0.055); + let lacquer = vec3f(0.73, 0.05, 0.17); + + var color = blackwater * (0.16 + 0.05 * max(strong, weak)); + color = color + stronger_glow * strong * 0.045 + weaker_glow * weak * 0.035; + color = color + ivory * membrane * 0.10; + color = color + mix(lacquer, scarlet, opposing) * fracture * (0.55 + seam * 0.18); + color = color + scarlet * spark * 0.28; + let vignette = smoothstep(0.96, 0.20, distance(in.uv, vec2f(0.5))); + return vec4f(color * (0.35 + 0.65 * vignette) * params.brightness, 1.0); +} +`; + +const BLUR_WGSL = /* wgsl */ ` +struct BlurDir { + dir: vec2f, + _pad: vec2f, +}; + +@group(0) @binding(0) var blur_sampler: sampler; +@group(0) @binding(1) var blur_src: texture_2d; +@group(0) @binding(2) var blur_dir: BlurDir; + +const QUAD = array( + vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0), + vec2f(-1.0, -1.0), vec2f(1.0, 1.0), vec2f(-1.0, 1.0) +); + +struct VSOut { @builtin(position) clip: vec4f, @location(0) uv: vec2f }; + +@vertex +fn vs_fullscreen(@builtin(vertex_index) vi: u32) -> VSOut { + var out: VSOut; + let p = QUAD[vi]; + out.clip = vec4f(p, 0.0, 1.0); + out.uv = p * 0.5 + vec2f(0.5); + return out; +} + +@fragment +fn fs_blur(in: VSOut) -> @location(0) vec4f { + let dims = vec2f(textureDimensions(blur_src, 0)); + let stepv = blur_dir.dir / max(dims, vec2f(1.0)); + var acc = textureSampleLevel(blur_src, blur_sampler, in.uv - stepv * 2.0, 0.0) * 0.06136; + acc = acc + textureSampleLevel(blur_src, blur_sampler, in.uv - stepv, 0.0) * 0.24477; + acc = acc + textureSampleLevel(blur_src, blur_sampler, in.uv, 0.0) * 0.38774; + acc = acc + textureSampleLevel(blur_src, blur_sampler, in.uv + stepv, 0.0) * 0.24477; + acc = acc + textureSampleLevel(blur_src, blur_sampler, in.uv + stepv * 2.0, 0.0) * 0.06136; + return acc; +} +`; + +type GpuResources = { + pairBuffer: GPUBuffer; + blurHBuffer: GPUBuffer; + blurVBuffer: GPUBuffer; + splatBindGroup: GPUBindGroup; + blurHBindGroup: GPUBindGroup; + blurVBindGroup: GPUBindGroup; + membraneBindGroup: GPUBindGroup; + fieldA: GPUTexture; + fieldB: GPUTexture; + fieldAView: GPUTextureView; + fieldBView: GPUTextureView; + fieldSize: [number, number]; +}; + +export class ContradictionsPass implements FramePass { + private engine: ObservatoryEngine; + private scene: ContradictionsScene | 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 arcPipeline: GPURenderPipeline | null = null; + private pairCount = 0; + private pairGeometry: { pair: ImmuneSynapsePair; ax: number; ay: number; bx: number; by: number; mx: number; my: number; radius: number }[] = []; + + constructor(engine: ObservatoryEngine, scene: RouteSceneModel) { + this.engine = engine; + this.uploadScene(scene); + } + + uploadScene(scene: RouteSceneModel): void { + this.scene = scene as ContradictionsScene; + this.buildPairGeometry(); + 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, 'contradictions-synapse-splat-wgsl', SPLAT_WGSL); + const blurModule = createDiagnosedShaderModule(device, 'contradictions-synapse-blur-wgsl', BLUR_WGSL); + const membraneModule = createDiagnosedShaderModule(device, 'contradictions-synapse-membrane-wgsl', MEMBRANE_WGSL); + this.splatBindLayout = device.createBindGroupLayout({ + label: 'contradictions-synapse-splat-bind-layout', + entries: [ + { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } }, + { binding: 1, visibility: GPUShaderStage.VERTEX, buffer: { type: 'read-only-storage' } } + ] + }); + this.blurBindLayout = device.createBindGroupLayout({ + label: 'contradictions-synapse-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: 'contradictions-synapse-membrane-bind-layout', + entries: [ + { binding: 0, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } }, + { binding: 2, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'filtering' } }, + { binding: 3, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'float' } } + ] + }); + const splatLayout = device.createPipelineLayout({ label: 'contradictions-synapse-splat-layout', bindGroupLayouts: [this.splatBindLayout] }); + const blurLayout = device.createPipelineLayout({ label: 'contradictions-synapse-blur-layout', bindGroupLayouts: [this.blurBindLayout] }); + const membraneLayout = device.createPipelineLayout({ label: 'contradictions-synapse-membrane-layout', bindGroupLayouts: [this.membraneBindLayout] }); + this.sampler = device.createSampler({ magFilter: 'linear', minFilter: 'linear' }); + this.splatPipeline = device.createRenderPipeline({ + label: 'contradictions-field-additive-splat', + layout: splatLayout, + vertex: { module: splatModule, entryPoint: 'vs_splat' }, + fragment: { + module: splatModule, + entryPoint: 'fs_splat', + targets: [{ format: FIELD_FORMAT, blend: { color: { srcFactor: 'one', dstFactor: 'one', operation: 'add' }, alpha: { srcFactor: 'one', dstFactor: 'one', operation: 'add' } } }] + }, + primitive: { topology: 'triangle-list' } + }); + this.blurPipeline = device.createRenderPipeline({ + label: 'contradictions-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' } + }); + const additive = { color: { srcFactor: 'one' as GPUBlendFactor, dstFactor: 'one' as GPUBlendFactor, operation: 'add' as GPUBlendOperation }, alpha: { srcFactor: 'one' as GPUBlendFactor, dstFactor: 'one' as GPUBlendFactor, operation: 'add' as GPUBlendOperation } }; + this.membranePipeline = device.createRenderPipeline({ + label: 'contradictions-immune-synapse-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: 'contradictions-memory-membrane-cells', + 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.arcPipeline = device.createRenderPipeline({ + label: 'contradictions-scarlet-unresolved-arcs', + layout: splatLayout, + vertex: { module: splatModule, entryPoint: 'vs_arc' }, + fragment: { module: splatModule, entryPoint: 'fs_arc', 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 pairBuffer = this.resources?.pairBuffer; + let blurHBuffer = this.resources?.blurHBuffer; + let blurVBuffer = this.resources?.blurVBuffer; + if (!pairBuffer) { + pairBuffer = device.createBuffer({ label: 'contradictions-pairs', size: MAX_PAIRS * PAIR_FLOATS * 4, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST }); + } + if (!blurHBuffer) { + blurHBuffer = device.createBuffer({ label: 'contradictions-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: 'contradictions-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: 'contradictions-field-a-rgba16float', size: [w, h], format: FIELD_FORMAT, usage }); + const fieldB = device.createTexture({ label: 'contradictions-field-b-rgba16float', size: [w, h], format: FIELD_FORMAT, usage }); + const fieldAView = fieldA.createView(); + const fieldBView = fieldB.createView(); + const splatBindGroup = device.createBindGroup({ + label: 'contradictions-synapse-splat-bind', + layout: this.splatBindLayout, + entries: [ + { binding: 0, resource: { buffer: this.engine.paramsBuffer } }, + { binding: 1, resource: { buffer: pairBuffer } } + ] + }); + const blurHBindGroup = device.createBindGroup({ + label: 'contradictions-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: 'contradictions-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: 'contradictions-membrane-bind', + layout: this.membraneBindLayout, + entries: [ + { binding: 0, resource: { buffer: this.engine.paramsBuffer } }, + { binding: 2, resource: this.sampler }, + { binding: 3, resource: fieldAView } + ] + }); + this.resources = { pairBuffer, blurHBuffer, blurVBuffer, splatBindGroup, blurHBindGroup, blurVBindGroup, membraneBindGroup, fieldA, fieldB, fieldAView, fieldBView, fieldSize: [w, h] }; + } + + private buildPairGeometry(): void { + const pairs = this.scene?.pairs ?? []; + const n = Math.max(1, pairs.length); + this.pairGeometry = pairs.slice(0, MAX_PAIRS).map((pair, i) => { + const slot = (i / n) * Math.PI * 2 - Math.PI / 2; + const lane = 0.42 + 0.10 * Math.sin(i * 1.7); + const centerX = Math.cos(slot) * lane * 0.72; + const centerY = Math.sin(slot) * lane; + const normal = slot + Math.PI / 2; + const spread = 0.16 + pair.topic_overlap * 0.14; + const ax = centerX + Math.cos(normal) * spread; + const ay = centerY + Math.sin(normal) * spread; + const bx = centerX - Math.cos(normal) * spread; + const by = centerY - Math.sin(normal) * spread; + return { pair, ax, ay, bx, by, mx: centerX, my: centerY, radius: spread * 0.95 }; + }); + } + + private uploadBuffers(device: GPUDevice): void { + if (!this.resources) return; + const pairData = new Float32Array(MAX_PAIRS * PAIR_FLOATS); + this.pairCount = Math.min(MAX_PAIRS, this.pairGeometry.length); + for (let i = 0; i < this.pairCount; i++) { + const g = this.pairGeometry[i]; + const p = g.pair; + const strongWidth = membraneWidth(p.stronger.trust); + const weakWidth = membraneWidth(p.weaker.trust); + pairData.set( + [ + g.ax, + g.ay, + p.stronger.trust, + strongWidth, + g.bx, + g.by, + p.weaker.trust, + weakWidth, + p.topic_overlap, + p.trust_delta, + p.resolved ? 0 : 1, + i / Math.max(1, this.pairCount), + i, + 0, + 0, + 0 + ], + i * PAIR_FLOATS + ); + } + this.engine.params[4] = this.pairCount; + device.queue.writeBuffer(this.resources.pairBuffer, 0, pairData); + } + + 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: 'contradictions-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.pairCount * 2); + splat.end(); + + const blurH = encoder.beginRenderPass({ + label: 'contradictions-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: 'contradictions-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.arcPipeline) return; + pass.setPipeline(this.membranePipeline); + pass.setBindGroup(0, this.resources.membraneBindGroup); + pass.draw(6, 1); + if (this.pairCount > 0) { + pass.setPipeline(this.arcPipeline); + pass.setBindGroup(0, this.resources.splatBindGroup); + pass.draw(64, this.pairCount); + pass.setPipeline(this.cellPipeline); + pass.draw(6, this.pairCount * 2); + } + } + + pickAt(ndcX: number, ndcY: number): RoutePick | null { + for (let i = 0; i < this.pairGeometry.length; i++) { + const g = this.pairGeometry[i]; + const dx = ndcX - g.mx; + const dy = ndcY - g.my; + const distToSeam = Math.hypot(dx, dy); + const distToLine = distanceToSegment(ndcX, ndcY, g.ax, g.ay, g.bx, g.by); + if (distToLine <= 0.075 || distToSeam <= g.radius) { + return { id: g.pair.id, kind: 'contradiction-seam', index: i, payload: g.pair }; + } + } + return null; + } + + dispose(): void { + this.resources?.pairBuffer.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 createContradictionsPasses(engine: ObservatoryEngine, scene: RouteSceneModel): ContradictionsPass[] { + void rgb01(MEDIUM.blackwater); + void rgb01(IMMUNE.veto); + void rgb01(IMMUNE.suppressionScar); + void rgb01(RETENTION.recall); + return [new ContradictionsPass(engine, scene)]; +} diff --git a/apps/dashboard/src/lib/observatory/contradictions/contradictions-scene.ts b/apps/dashboard/src/lib/observatory/contradictions/contradictions-scene.ts new file mode 100644 index 0000000..2dc08b6 --- /dev/null +++ b/apps/dashboard/src/lib/observatory/contradictions/contradictions-scene.ts @@ -0,0 +1,316 @@ +import { + assertProvenance, + type Provenance, + type RouteEdge, + type RouteEvent, + type RouteNode, + type RouteReceipt, + type RouteSceneModel +} from '$lib/observatory/route-scene'; +import type { ContradictionPair, VestigeEvent } from '$types'; + +export interface ContradictionMemorySide { + id: string; + preview: string; + trust: number; + date: string; + type?: string; + tags?: string[]; + provenance: Provenance; +} + +export interface ImmuneSynapsePair { + id: string; + stronger: ContradictionMemorySide; + weaker: ContradictionMemorySide; + topic_overlap: number; + topic: string; + trust_delta: number; + date_diff_days: number; + resolved: boolean; + provenance: Provenance; + receipt: { + label: string; + evidence: Record; + }; +} + +export interface ContradictionsScene extends RouteSceneModel { + organ: 'contradictions'; + pairs: ImmuneSynapsePair[]; + raw: { + apiPairs: unknown[]; + deepReferenceEvents: VestigeEvent[]; + }; +} + +function record(v: unknown): Record { + return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record) : {}; +} + +function text(v: unknown, fallback = ''): string { + return typeof v === 'string' ? v : v == null ? fallback : String(v); +} + +function num(v: unknown, fallback = 0): number { + return typeof v === 'number' && Number.isFinite(v) ? v : fallback; +} + +function clamp01(v: number): number { + return Math.max(0, Math.min(1, v)); +} + +function confidence01(v: unknown): number { + const n = num(v, 0); + return clamp01(n > 1 ? n / 100 : n); +} + +function 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: `contradictions.${name}`, scalar: { name, value } }; +} + +function sideFromRecord(rawSide: Record, fallbackId: string, fallbackPreview = ''): ContradictionMemorySide { + const id = text(rawSide.id ?? rawSide.memory_id ?? fallbackId); + const trust = confidence01(rawSide.trust ?? rawSide.trust_score ?? 0); + return { + id, + preview: text(rawSide.preview ?? rawSide.content ?? fallbackPreview ?? id), + trust, + date: text(rawSide.date ?? rawSide.created_at ?? rawSide.createdAt ?? ''), + type: rawSide.node_type ? text(rawSide.node_type) : rawSide.nodeType ? text(rawSide.nodeType) : undefined, + tags: Array.isArray(rawSide.tags) ? rawSide.tags.map((t) => text(t)).filter(Boolean) : undefined, + provenance: source('memory', id) + }; +} + +function pairKey(a: string, b: string): string { + return `contradiction:${a}:${b}`; +} + +function normalizeApiPair(input: unknown, index: number): ImmuneSynapsePair | null { + const c = record(input); + const strongerRaw = record(c.stronger); + const weakerRaw = record(c.weaker); + + let stronger: ContradictionMemorySide; + let weaker: ContradictionMemorySide; + let topicOverlap: number; + let topic: string; + let dateDiffDays: number; + + if (Object.keys(strongerRaw).length > 0 || Object.keys(weakerRaw).length > 0) { + stronger = sideFromRecord(strongerRaw, ''); + weaker = sideFromRecord(weakerRaw, ''); + topicOverlap = clamp01(num(c.topic_overlap ?? c.similarity, 0)); + topic = text(c.topic ?? c.summary ?? 'trust-weighted contradiction'); + dateDiffDays = num(c.date_diff_days, 0); + } else { + const a = sideFromRecord( + { + id: c.memory_a_id ?? c.a_id, + preview: c.memory_a_preview, + trust: c.trust_a, + date: c.memory_a_created, + node_type: c.memory_a_type, + tags: c.memory_a_tags + }, + '' + ); + const b = sideFromRecord( + { + id: c.memory_b_id ?? c.b_id, + preview: c.memory_b_preview, + trust: c.trust_b, + date: c.memory_b_created, + node_type: c.memory_b_type, + tags: c.memory_b_tags + }, + '' + ); + // /api/contradictions currently exposes chronological a/b; Organ 3 needs + // stronger/weaker membranes, so choose by real trust while retaining both IDs. + [stronger, weaker] = a.trust >= b.trust ? [a, b] : [b, a]; + topicOverlap = clamp01(num(c.topic_overlap ?? c.similarity, 0)); + topic = text(c.topic ?? 'trust-weighted contradiction'); + dateDiffDays = num(c.date_diff_days, 0); + } + + if (!stronger.id || !weaker.id) return null; + const id = pairKey(stronger.id, weaker.id); + const trustDelta = Math.abs(stronger.trust - weaker.trust); + return { + id, + stronger, + weaker, + topic_overlap: topicOverlap, + topic, + trust_delta: trustDelta, + date_diff_days: dateDiffDays, + resolved: text(c.status).toLowerCase() === 'resolved' || Boolean(c.resolved), + provenance: source('pair', id), + receipt: { + label: `immune synapse ${index + 1}: ${stronger.id.slice(0, 8)} ↔ ${weaker.id.slice(0, 8)}`, + evidence: c + } + }; +} + +function normalizeDeepReferenceEvent(event: VestigeEvent, pairIndexBase: number): ImmuneSynapsePair[] { + if (event.type !== 'DeepReferenceCompleted') return []; + const pairs = Array.isArray(event.data?.contradiction_pairs) ? event.data.contradiction_pairs : []; + return pairs + .map((entry, i): ImmuneSynapsePair | null => { + const tuple = Array.isArray(entry) ? entry : []; + const a = text(tuple[0]); + const b = text(tuple[1]); + if (!a || !b) return null; + const stronger = sideFromRecord({ id: a, preview: `DeepReference contradiction side ${a.slice(0, 8)}` }, a); + const weaker = sideFromRecord({ id: b, preview: `DeepReference contradiction side ${b.slice(0, 8)}` }, b); + const id = pairKey(a, b); + return { + id, + stronger, + weaker, + topic_overlap: 0.64, + topic: text(event.data.query ?? event.data.intent ?? 'deep_reference contradiction'), + trust_delta: 0, + date_diff_days: 0, + resolved: false, + provenance: source('event', `DeepReferenceCompleted:${text(event.data.timestamp, String(pairIndexBase + i))}:${id}`), + receipt: { + label: `DeepReference contradiction pair ${i + 1}`, + evidence: { + query: event.data.query, + intent: event.data.intent, + status: event.data.status, + confidence: event.data.confidence, + memories_analyzed: event.data.memories_analyzed, + contradiction_pair: [a, b] + } + } + }; + }) + .filter((p): p is ImmuneSynapsePair => Boolean(p)); +} + +export function normalizeContradictionsScene(input: { + contradictions?: unknown[]; + total?: number; + memoriesAnalyzed?: number; + deepReferenceEvents?: VestigeEvent[]; +}): ContradictionsScene { + const apiPairs = input.contradictions ?? []; + const events = (input.deepReferenceEvents ?? []).filter((e) => e.type === 'DeepReferenceCompleted'); + const pairsById = new Map(); + + apiPairs.forEach((p, i) => { + const pair = normalizeApiPair(p, i); + if (pair) pairsById.set(pair.id, pair); + }); + for (const event of events) { + for (const pair of normalizeDeepReferenceEvent(event, pairsById.size)) { + if (!pairsById.has(pair.id)) pairsById.set(pair.id, pair); + } + } + + const pairs = Array.from(pairsById.values()); + const nodes: RouteNode[] = []; + const nodeIndex = new Map(); + const addNode = (side: ContradictionMemorySide, roleTag: string) => { + if (nodeIndex.has(side.id)) return nodeIndex.get(side.id)!; + const index = nodes.length; + nodeIndex.set(side.id, index); + nodes.push({ + source: side.provenance, + index, + label: side.preview || side.id.slice(0, 8), + retention: side.trust, + trust: side.trust, + lastAccessed: side.date || undefined, + tags: [roleTag, ...(side.tags ?? [])], + type: side.type ?? 'memory' + }); + return index; + }; + + const edges: RouteEdge[] = []; + const receipts: RouteReceipt[] = []; + pairs.forEach((pair) => { + const a = addNode(pair.stronger, 'stronger'); + const b = addNode(pair.weaker, 'weaker'); + edges.push({ + source: pair.provenance, + sourceIndex: a, + targetIndex: b, + weight: Math.max(0.1, pair.topic_overlap), + kind: 'contradiction' + }); + receipts.push({ + source: pair.provenance, + label: pair.receipt.label, + nodeIndices: [a, b] + }); + }); + + const unresolved = pairs.filter((p) => !p.resolved).length; + const avgTrustDelta = pairs.length ? pairs.reduce((sum, p) => sum + p.trust_delta, 0) / pairs.length : 0; + const eventsOut: RouteEvent[] = unresolved + ? [ + { + source: source('scalar', 'contradictions.unresolved', { name: 'unresolved', value: unresolved }), + type: 'ContradictionUnresolved', + targetIndex: -1, + frame: 80, + energy: unresolved + } + ] + : []; + + const scene: ContradictionsScene = { + organ: 'contradictions', + nodes, + edges, + events: eventsOut, + receipts, + scalars: { + total: num(input.total, pairs.length), + memoriesAnalyzed: num(input.memoriesAnalyzed, nodes.length), + pairCount: pairs.length, + unresolved, + avgTrustDelta + }, + alive: pairs.length > 0, + pairs, + raw: { apiPairs, deepReferenceEvents: events } + }; + + if (import.meta.env.DEV) assertProvenance(scene); + // Scalar provenance is not part of RouteSceneModel arrays, so force-create a + // real scalar source here as a discipline marker for the empty/calm state. + void scalarSource('pairCount', pairs.length); + return scene; +} + +export function pairToLegacyContradiction(pair: ImmuneSynapsePair): ContradictionPair { + return { + memory_a_id: pair.stronger.id, + memory_b_id: pair.weaker.id, + memory_a_preview: pair.stronger.preview, + memory_b_preview: pair.weaker.preview, + memory_a_type: pair.stronger.type, + memory_b_type: pair.weaker.type, + memory_a_created: pair.stronger.date, + memory_b_created: pair.weaker.date, + memory_a_tags: pair.stronger.tags, + memory_b_tags: pair.weaker.tags, + trust_a: pair.stronger.trust, + trust_b: pair.weaker.trust, + similarity: pair.topic_overlap, + date_diff_days: pair.date_diff_days, + topic: pair.topic + }; +} diff --git a/apps/dashboard/src/routes/(app)/contradictions/+page.svelte b/apps/dashboard/src/routes/(app)/contradictions/+page.svelte index 7661ba8..fcd13d4 100644 --- a/apps/dashboard/src/routes/(app)/contradictions/+page.svelte +++ b/apps/dashboard/src/routes/(app)/contradictions/+page.svelte @@ -5,9 +5,16 @@ import Dropdown, { type DropdownOption } from '$components/Dropdown.svelte'; import Icon from '$components/Icon.svelte'; import AnimatedNumber from '$components/AnimatedNumber.svelte'; - import AmbientField from '$components/AmbientField.svelte'; + import RouteStage, { type RoutePick } from '$lib/observatory/RouteStage.svelte'; + import { createContradictionsPasses } from '$lib/observatory/contradictions/contradictions-pass'; + import { + normalizeContradictionsScene, + type ContradictionsScene, + type ImmuneSynapsePair + } from '$lib/observatory/contradictions/contradictions-scene'; import { reveal } from '$lib/actions/reveal'; import { api } from '$stores/api'; + import { eventFeed } from '$stores/websocket'; import { severityColor, severityLabel, @@ -23,8 +30,10 @@ // System-wide count from the backend, vs. the derived stats below which // reflect only the pairs the page holds. let totalDetected = $state(0); + let memoriesAnalyzed = $state(0); let loading = $state(true); let error = $state(null); + let selectedSynapsePair = $state(null); async function load() { loading = true; @@ -33,10 +42,12 @@ const res = await api.contradictions(); contradictions = res.contradictions; totalDetected = res.total; + memoriesAnalyzed = res.memoriesAnalyzed; } catch (e) { error = e instanceof Error ? e.message : 'Failed to load contradictions'; contradictions = []; totalDetected = 0; + memoriesAnalyzed = 0; } finally { loading = false; } @@ -113,6 +124,11 @@ function selectPair(i: number | null) { focusedPairIndex = i; + selectedSynapsePair = i == null ? null : visibleList[i]?.scenePair ?? null; + } + + function sameUnorderedPair(a1: string, b1: string, a2: string, b2: string) { + return (a1 === a2 && b1 === b2) || (a1 === b2 && b1 === a2); } // --- Stats. `totalDetected` is the backend's system-wide count; everything @@ -123,50 +139,74 @@ // Map filtered index -> original index in `contradictions` so the // constellation and sidebar stay in sync regardless of which filter is on. - const visibleList = $derived.by<{ orig: number; c: Contradiction }[]>(() => { + const contradictionsScene = $derived.by(() => + normalizeContradictionsScene({ + contradictions, + total: totalDetected, + memoriesAnalyzed, + deepReferenceEvents: $eventFeed + }) + ); + + const visibleList = $derived.by<{ orig: number; c: Contradiction; scenePair: ImmuneSynapsePair | null }[]>(() => { const byId = new Map(contradictions.map((c, i) => [c.memory_a_id + '|' + c.memory_b_id, i])); return filtered.map((c) => ({ orig: byId.get(c.memory_a_id + '|' + c.memory_b_id) ?? 0, - c + c, + scenePair: + contradictionsScene.pairs.find((p) => + sameUnorderedPair(p.stronger.id, p.weaker.id, c.memory_a_id, c.memory_b_id) + ) ?? null })); }); // The ContradictionArcs component receives the filtered list; its internal // indices run 0..filtered.length-1. We translate when the sidebar clicks. function sidebarClick(localIndex: number) { - focusedPairIndex = focusedPairIndex === localIndex ? null : localIndex; + const next = focusedPairIndex === localIndex ? null : localIndex; + focusedPairIndex = next; + selectedSynapsePair = next == null ? null : visibleList[next]?.scenePair ?? null; } - // Phase 5 base coat — the field behind this page fractures with the REAL - // contradiction load: no contradictions = a calm field, many = visible rifts. - let fractureFrac = $derived.by(() => Math.min(1, totalDetected / 24)); + function handleRoutePick(pick: RoutePick) { + if (pick.kind !== 'contradiction-seam') return; + const pair = pick.payload as ImmuneSynapsePair; + selectedSynapsePair = pair; + const visibleIndex = visibleList.findIndex((entry) => + sameUnorderedPair(pair.stronger.id, pair.weaker.id, entry.c.memory_a_id, entry.c.memory_b_id) + ); + focusedPairIndex = visibleIndex >= 0 ? visibleIndex : null; + } -
- -
+ + +
+
in view +
{#if error} -
+
Couldn't load contradictions
{error}
{:else if loading} -
+
{#each Array(4) as _}
{/each}
-
+
{:else if contradictions.length === 0} -
+
@@ -203,7 +243,7 @@
{:else} -
+
@@ -238,7 +278,7 @@
-
+
(focusedPairIndex = null)} + onclick={() => { + focusedPairIndex = null; + selectedSynapsePair = null; + }} class="ml-auto inline-flex items-center gap-1.5 px-3 py-2 rounded-xl text-xs border border-subtle/30 text-dim hover:text-text hover:border-synapse/30 hover:bg-white/[0.03] transition lift" > @@ -267,7 +310,7 @@
-
+
@@ -370,6 +413,72 @@ {/each}
+ + {#if selectedSynapsePair} +
+
+
+
Contradiction receipt
+

{selectedSynapsePair.topic}

+
+ +
+ +
+
+
+ Higher-trust membrane + {(selectedSynapsePair.stronger.trust * 100).toFixed(0)}% +
+
{selectedSynapsePair.stronger.id}
+

{selectedSynapsePair.stronger.preview}

+ {#if selectedSynapsePair.stronger.date} +
{selectedSynapsePair.stronger.date}
+ {/if} +
+
+
+ Opposing evidence + {(selectedSynapsePair.weaker.trust * 100).toFixed(0)}% +
+
{selectedSynapsePair.weaker.id}
+

{selectedSynapsePair.weaker.preview}

+ {#if selectedSynapsePair.weaker.date} +
{selectedSynapsePair.weaker.date}
+ {/if} +
+
+ +
+
+
topic overlap
+
{(selectedSynapsePair.topic_overlap * 100).toFixed(0)}%
+
+
+
trust delta
+
{selectedSynapsePair.trust_delta.toFixed(2)}
+
+
+
status
+
+ {selectedSynapsePair.resolved ? 'resolved' : 'unresolved'} +
+
+
+
provenance
+
{selectedSynapsePair.provenance.kind}:{selectedSynapsePair.provenance.id}
+
+
+
+ {/if} {/if}
-