mirror of
https://github.com/samvallad33/vestige.git
synced 2026-07-24 23:41:01 +02:00
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:
parent
b5150f0b90
commit
722d0f8738
3 changed files with 1063 additions and 29 deletions
|
|
@ -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)];
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue