feat(dashboard): Organ 3 — Contradictions "Immune Synapse Arena"

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 <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-07-09 08:46:56 -07:00
parent b5150f0b90
commit 722d0f8738
3 changed files with 1063 additions and 29 deletions

View file

@ -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<uniform> params: Params;
@group(0) @binding(1) var<storage, read> pairs: array<PairCell>;
const QUAD = array<vec2f, 6>(
vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0),
vec2f(-1.0, -1.0), vec2f(1.0, 1.0), vec2f(-1.0, 1.0)
);
struct VSOut {
@builtin(position) clip: vec4f,
@location(0) uv: vec2f,
@location(1) @interpolate(flat) misc: vec4f,
};
@vertex
fn vs_splat(@builtin(vertex_index) vi: u32, @builtin(instance_index) ii: u32) -> VSOut {
var out: VSOut;
let 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<uniform> params: Params;
@group(0) @binding(2) var field_sampler: sampler;
@group(0) @binding(3) 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(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<f32>;
@group(0) @binding(2) var<uniform> blur_dir: BlurDir;
const QUAD = array<vec2f, 6>(
vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0),
vec2f(-1.0, -1.0), vec2f(1.0, 1.0), vec2f(-1.0, 1.0)
);
struct VSOut { @builtin(position) clip: vec4f, @location(0) uv: vec2f };
@vertex
fn vs_fullscreen(@builtin(vertex_index) vi: u32) -> VSOut {
var out: VSOut;
let p = QUAD[vi];
out.clip = vec4f(p, 0.0, 1.0);
out.uv = p * 0.5 + vec2f(0.5);
return out;
}
@fragment
fn fs_blur(in: VSOut) -> @location(0) vec4f {
let dims = vec2f(textureDimensions(blur_src, 0));
let 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)];
}

View file

@ -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<string, unknown>;
};
}
export interface ContradictionsScene extends RouteSceneModel {
organ: 'contradictions';
pairs: ImmuneSynapsePair[];
raw: {
apiPairs: unknown[];
deepReferenceEvents: VestigeEvent[];
};
}
function record(v: unknown): Record<string, unknown> {
return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : {};
}
function text(v: unknown, fallback = ''): string {
return typeof v === 'string' ? v : v == null ? fallback : String(v);
}
function num(v: unknown, fallback = 0): number {
return typeof v === 'number' && Number.isFinite(v) ? v : fallback;
}
function clamp01(v: number): number {
return Math.max(0, Math.min(1, v));
}
function confidence01(v: unknown): number {
const n = num(v, 0);
return clamp01(n > 1 ? n / 100 : n);
}
function 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<string, unknown>, 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<string, ImmuneSynapsePair>();
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<string, number>();
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
};
}

View file

@ -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<string | null>(null);
let selectedSynapsePair = $state<ImmuneSynapsePair | null>(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<ContradictionsScene>(() =>
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;
}
</script>
<div class="relative isolate min-h-full">
<div class="absolute inset-0 -z-10 overflow-hidden" aria-hidden="true">
<AmbientField
count={Math.max(120, uniqueMemoryCount(contradictions) * 6)}
fracture={fractureFrac}
endangered={0.15}
accent={[0.95, 0.32, 0.42]}
opacity={0.32}
/>
</div>
<div class="min-h-full p-6 space-y-6">
<RouteStage
organ="contradictions"
seed={`immune-synapse-arena:${totalDetected}:${memoriesAnalyzed}`}
scene={contradictionsScene}
passes={createContradictionsPasses}
loading={loading}
error={error}
emptyLabel="CALM IMMUNE FIELD — NO STANDING CONTRADICTIONS"
onpick={handleRoutePick}
/>
<div class="relative z-10 min-h-full p-6 space-y-6 pointer-events-none">
<div class="pointer-events-auto">
<!-- Header -->
<PageHeader
icon="contradictions"
title="Contradiction Constellation"
subtitle="Where your memory disagrees with itself"
title="Immune Synapse Arena"
subtitle="Contradictory memories face each other across a scarlet trust-weighted seam. Click a fracture for the receipt."
accent="warning"
>
<span class="text-dim text-sm tabular-nums inline-flex items-center gap-1.5">
<AnimatedNumber value={filtered.length} /> in view
</span>
</PageHeader>
</div>
{#if error}
<div class="glass-panel flex flex-col items-center gap-3 rounded-2xl p-10 text-center">
<div 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 load contradictions</div>
<div class="max-w-md text-xs text-muted">{error}</div>
<button
@ -178,17 +218,17 @@
</button>
</div>
{:else if loading}
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 pointer-events-auto">
{#each Array(4) as _}
<div class="glass-subtle shimmer h-20 rounded-xl"></div>
{/each}
</div>
<div class="grid grid-cols-1 lg:grid-cols-[1fr_340px] gap-4">
<div class="grid grid-cols-1 lg:grid-cols-[1fr_340px] gap-4 pointer-events-auto">
<div class="glass-subtle shimmer min-h-[520px] rounded-2xl"></div>
<div class="glass-subtle shimmer h-[520px] rounded-2xl"></div>
</div>
{:else if contradictions.length === 0}
<div class="glass-panel enter flex flex-col items-center gap-3 rounded-2xl p-12 text-center">
<div 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"
>
@ -203,7 +243,7 @@
</div>
{:else}
<!-- Stats bar -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 pointer-events-auto">
<div use:reveal={{ delay: 0, y: 12 }} class="p-4 glass rounded-xl lift">
<div class="text-2xl text-bright font-bold tabular-nums">
<AnimatedNumber value={totalDetected} />
@ -238,7 +278,7 @@
</div>
<!-- Filter bar -->
<div class="flex flex-wrap gap-3 items-end enter">
<div class="flex flex-wrap gap-3 items-end enter pointer-events-auto">
<Dropdown
options={filterOptions}
value={filter}
@ -257,7 +297,10 @@
{/if}
{#if focusedPairIndex !== null}
<button
onclick={() => (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"
>
<Icon name="close" size={13} />
@ -267,7 +310,7 @@
</div>
<!-- Main view: constellation + sidebar -->
<div class="grid grid-cols-1 lg:grid-cols-[1fr_340px] gap-4">
<div class="grid grid-cols-1 lg:grid-cols-[minmax(0,0.9fr)_380px] gap-4 pointer-events-auto">
<!-- Constellation. NOTE: no `use:reveal` on this wrapper or the
ContradictionArcs SVG container — a transform/opacity entrance here
would interfere with the constellation's own layout. -->
@ -370,6 +413,72 @@
{/each}
</aside>
</div>
{#if selectedSynapsePair}
<section class="glass-panel pointer-events-auto rounded-2xl p-5 border-[#FF3B30]/35 shadow-[0_0_40px_rgba(255,59,48,0.12)]">
<div class="flex flex-wrap items-start justify-between gap-3 border-b border-[#FF3B30]/20 pb-3">
<div>
<div class="font-mono text-[10px] uppercase tracking-[0.22em] text-[#FF3B30]">Contradiction receipt</div>
<h2 class="mt-1 text-lg font-semibold text-bright">{selectedSynapsePair.topic}</h2>
</div>
<button
type="button"
onclick={() => {
selectedSynapsePair = null;
focusedPairIndex = null;
}}
class="rounded-lg border border-subtle/30 px-3 py-1.5 text-xs text-muted transition hover:border-[#FF3B30]/40 hover:text-[#FF3B30]"
>
Close
</button>
</div>
<div class="mt-4 grid gap-4 lg:grid-cols-2">
<div class="rounded-xl border border-[#F4F1D0]/18 bg-[#020307]/70 p-4">
<div class="mb-2 flex items-center justify-between gap-2">
<span class="font-mono text-[10px] uppercase tracking-wider text-[#F4F1D0]">Higher-trust membrane</span>
<span class="font-mono text-xs text-[#F4F1D0]">{(selectedSynapsePair.stronger.trust * 100).toFixed(0)}%</span>
</div>
<div class="text-xs text-muted break-all">{selectedSynapsePair.stronger.id}</div>
<p class="mt-3 text-sm text-text">{selectedSynapsePair.stronger.preview}</p>
{#if selectedSynapsePair.stronger.date}
<div class="mt-3 text-[11px] text-dim">{selectedSynapsePair.stronger.date}</div>
{/if}
</div>
<div class="rounded-xl border border-[#FF3B30]/24 bg-[#160407]/70 p-4">
<div class="mb-2 flex items-center justify-between gap-2">
<span class="font-mono text-[10px] uppercase tracking-wider text-[#FF3B30]">Opposing evidence</span>
<span class="font-mono text-xs text-[#FF3B30]">{(selectedSynapsePair.weaker.trust * 100).toFixed(0)}%</span>
</div>
<div class="text-xs text-muted break-all">{selectedSynapsePair.weaker.id}</div>
<p class="mt-3 text-sm text-text">{selectedSynapsePair.weaker.preview}</p>
{#if selectedSynapsePair.weaker.date}
<div class="mt-3 text-[11px] text-dim">{selectedSynapsePair.weaker.date}</div>
{/if}
</div>
</div>
<div class="mt-4 grid gap-3 md:grid-cols-4">
<div class="rounded-xl bg-white/[0.03] p-3">
<div class="text-[10px] uppercase tracking-wider text-muted">topic overlap</div>
<div class="mt-1 font-mono text-lg text-[#FF3B30]">{(selectedSynapsePair.topic_overlap * 100).toFixed(0)}%</div>
</div>
<div class="rounded-xl bg-white/[0.03] p-3">
<div class="text-[10px] uppercase tracking-wider text-muted">trust delta</div>
<div class="mt-1 font-mono text-lg text-[#F4F1D0]">{selectedSynapsePair.trust_delta.toFixed(2)}</div>
</div>
<div class="rounded-xl bg-white/[0.03] p-3">
<div class="text-[10px] uppercase tracking-wider text-muted">status</div>
<div class="mt-1 font-mono text-lg {selectedSynapsePair.resolved ? 'text-recall' : 'text-[#FF3B30]'}">
{selectedSynapsePair.resolved ? 'resolved' : 'unresolved'}
</div>
</div>
<div class="rounded-xl bg-white/[0.03] p-3">
<div class="text-[10px] uppercase tracking-wider text-muted">provenance</div>
<div class="mt-1 truncate font-mono text-xs text-dim" title={selectedSynapsePair.provenance.id}>{selectedSynapsePair.provenance.kind}:{selectedSynapsePair.provenance.id}</div>
</div>
</div>
</section>
{/if}
{/if}
</div>
</div>