Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,64 @@ A base scan (`src/lib/analysisEngine.js`) is enriched by `runLayerRouter`
| **L46 — Firewall Receipt** | Deterministic content/result/soliton hashes for a reproducible audit trail. |
| **L103 — 39 Hz Soliton Field** | Gamma-band synchrony + leapfrogging ionic-soliton model (below). |

### How good are the numbers?

The scores used to be asserted. They are now measured against a labelled
corpus of 18 content archetypes (`src/lib/calibrationCorpus.js`), reported as
**rank agreement** rather than invented precision, and guarded in CI so a
scoring change cannot silently regress:

| Dimension | Spearman ρ | Pairs ordered wrongly |
| --- | --- | --- |
| trust | 0.700 | 16 / 107 |
| urgency | 0.641 | 14 / 111 |
| manipulation risk | 0.631 | 21 / 117 |
| viral pull | 0.366 | 32 / 107 |

**81% of 442 labelled comparisons ranked correctly** (mean ρ 0.585).

Calibration found a real defect: trust was originally **anti-correlated**
(ρ −0.505), scoring outrage bait as more trustworthy than a sincere apology,
because it counted trust *vocabulary* rather than evidence. Adding specificity
and stated-limitation signals — and discounting specifics that sit inside
urgency phrasing, so a fake deadline cannot buy credibility — turned it
positive.

Two honesty notes, both enforced by tests rather than left to good intentions:

- The scores are **0–100 indices, not probabilities**. Each one is shown with
its percentile against the corpus, because "86th percentile of known
archetypes" is a true statement where "86%" is not.
- Labels are **ordinal**. We can defend that phishing carries more pressure
than an understated luxury line; we cannot defend a claim that it scores
exactly 84. See `docs/ANNOTATION_RUBRIC.md`, which also requires a
Krippendorff's alpha (`src/lib/agreement.js`) before any corpus release
claims reliability.

### The brain, and a real spiking network

The 7-region model (`src/features/brain3d/brainModel.js`) is deterministic and
seeded, so the same content always produces the same run — which is what makes
brain readouts scoreable, shareable and verifiable. It exposes interventions
(lesion a region, cut a pathway, inject current) and derived measurements:
firing rates in Hz, PFC/AMY control ratio, hijack index, gain around the
THL→CTX→AMY→BG⊣THL control loop, E/I balance, spike correlation, settling
time, plasticity and net STDP flux.

That model is a *rate* model. `src/lib/snn/` adds the real thing: a leaky
integrate-and-fire network in the Brunel (2000) formulation — Dale's law,
sparse random connectivity, transmission delays, Poisson drive — with the
standard measurements (CV of ISI, Fano factor, population spectrum). Its
regime behaviour is checked against the published analysis in
`brunelValidation.test.js`, so **gamma-band power is measured rather than
asserted**.

**Claim boundary.** Every neural readout ships with it: these are simulated
dynamics of a model driven by lexical features of the text, not a measurement
of any human brain, and not a clinical or predictive claim. The simulation is
downstream of the same lexical scores, so it adds structure — not new
information about the text.

The full catalog of 103 layers lives in `src/lib/layerCatalog.js`; the Research view has a
searchable Layer Explorer.

Expand Down
3 changes: 3 additions & 0 deletions brainsnn-r3f-app/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ coverage/
*.log
.env*
!.env.example

# Externally licensed evaluation corpora are fetched locally, never vendored.
datasets/
83 changes: 83 additions & 0 deletions brainsnn-r3f-app/scripts/eval-corpus.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env node
// Evaluate the scoring engine against an externally labelled corpus.
//
// node scripts/eval-corpus.mjs datasets/persuasion.jsonl --dimension manipulationRisk
//
// Input is JSONL, one object per line:
// { "id": "...", "text": "...", "labels": { "manipulationRisk": 1, "trust": 0 } }
// where each label is 0/1 (or true/false).
//
// Public corpora are NOT vendored into this repo — they carry their own
// licences. Fetch them into datasets/ (gitignored) and point this script at
// the converted JSONL. See docs/ANNOTATION_RUBRIC.md for the mapping from
// published technique taxonomies onto our dimensions.
import { readFileSync } from 'node:fs';
import { pathToFileURL } from 'node:url';

// The engine reads a localStorage shim in a couple of places.
globalThis.window = globalThis.window || {
localStorage: { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {} },
};
globalThis.localStorage = globalThis.window.localStorage;

const [, , file, ...rest] = process.argv;
if (!file) {
console.error('usage: node scripts/eval-corpus.mjs <corpus.jsonl> [--dimension <name>] [--bins <n>]');
process.exit(2);
}

function flag(name, fallback) {
const index = rest.indexOf(`--${name}`);
return index >= 0 && rest[index + 1] ? rest[index + 1] : fallback;
}

const base = pathToFileURL(`${process.cwd()}/`);
const { scoreCorpusItem } = await import(new URL('src/lib/calibration.js', base));
const { evaluateBinary } = await import(new URL('src/lib/evalMetrics.js', base));

const rows = readFileSync(file, 'utf8')
.split('\n')
.map((line) => line.trim())
.filter(Boolean)
.map((line, index) => {
try {
return JSON.parse(line);
} catch (error) {
console.error(`skipping unparseable line ${index + 1}: ${error.message}`);
return null;
}
})
.filter((row) => row && typeof row.text === 'string' && row.labels);

if (!rows.length) {
console.error('no usable rows found');
process.exit(1);
}

const requested = flag('dimension', null);
const dimensions = requested
? [requested]
: [...new Set(rows.flatMap((row) => Object.keys(row.labels)))];

const scored = rows.map((row) => ({ row, metrics: scoreCorpusItem({ content: row.text }) }));

const report = { corpus: file, items: rows.length, dimensions: {} };
for (const dimension of dimensions) {
const usable = scored.filter(({ row }) => row.labels[dimension] != null);
if (usable.length < 4) {
report.dimensions[dimension] = { skipped: `only ${usable.length} labelled items` };
continue;
}
const scores = usable.map(({ metrics }) => metrics[dimension] ?? 0);
const labels = usable.map(({ row }) => (row.labels[dimension] ? 1 : 0));
const result = evaluateBinary(scores, labels, { bins: Number(flag('bins', 10)) });
// Keep the console summary readable; the table is in the JSON.
report.dimensions[dimension] = result;
console.log(
`${dimension.padEnd(20)} n=${String(result.n).padStart(5)} pos=${String(result.positives).padStart(5)} `
+ `AUC=${result.auc.toFixed(3)} Brier=${result.brier.toFixed(3)} ECE=${result.ece.toFixed(3)} `
+ `bestF1=${result.bestF1.f1.toFixed(3)}@${result.bestF1.threshold}`,
);
}

if (rest.includes('--json')) console.log(JSON.stringify(report, null, 2));
6 changes: 3 additions & 3 deletions brainsnn-r3f-app/src/app/GaugeGapLanding.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
<Button variant="secondary" onClick={() => scrollTo('clients')}>Build for your audience <ArrowRight size={17} /></Button>
</div>
<div className="gg-hero-proof" aria-label="Product capabilities">
<div><strong>13 live</strong><span>12 arcade + flagship lab</span></div>
<div><strong>16 live</strong><span>15 arcade + flagship lab</span></div>
<div><strong>4 paths</strong><span>Clear first steps</span></div>
<div><strong>Client</strong><span>Custom pilot pathway</span></div>
<div><strong>Open</strong><span>Models and limits</span></div>
Expand Down Expand Up @@ -176,7 +176,7 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {
</section>

<VisitorRoutes onResearch={openResearch} />
<ExperimentArcade />
<ExperimentArcade onOpenScanner={onStart} />

<section className="gg-loop" aria-labelledby="gg-loop-heading">
<div className="gg-section-heading"><p className="gg-kicker"><Share2 size={16} /> The engagement engine</p><h2 id="gg-loop-heading">A useful interaction should continue after the first click.</h2><p>The visual earns attention. The model earns understanding. The shared state brings the next person into the same system.</p></div>
Expand All @@ -195,7 +195,7 @@ export function GaugeGapLanding({ onStart, onNavigate, onOpenReconstruct }) {

<section className="gg-depth" aria-labelledby="gg-depth-heading">
<div className="gg-depth-visual" aria-hidden="true"><div className="gg-depth-ring gg-depth-ring-one" /><div className="gg-depth-ring gg-depth-ring-two" /><div className="gg-depth-core"><Layers3 size={38} /></div><span className="gg-depth-node gg-depth-node-one">Play</span><span className="gg-depth-node gg-depth-node-two">Model</span><span className="gg-depth-node gg-depth-node-three">Verify</span><span className="gg-depth-node gg-depth-node-four">Publish</span></div>
<div className="gg-depth-copy"><p className="gg-kicker"><Layers3 size={16} /> One platform, two visible products</p><h2 id="gg-depth-heading">GaugeGap attracts attention. BrainSNN turns the result into a decision.</h2><p>The public experience should be simple. The engine underneath preserves the model, analyzes the message and supports a more responsible publishing workflow.</p><ul><li><CheckCircle2 size={17} /> GaugeGap handles interaction, missions and shareable states.</li><li><CheckCircle2 size={17} /> BrainSNN handles content analysis, revision and publishing decisions.</li><li><CheckCircle2 size={17} /> Research mode exposes equations, diagnostics and limitations.</li></ul><div className="gg-depth-actions"><Button variant="primary" onClick={openResearch}>Open research mode <Microscope size={16} /></Button><Button variant="ghost" onClick={() => onStart?.(CONTENT_SAMPLE)}>Test BrainSNN <WandSparkles size={16} /></Button></div></div>
<div className="gg-depth-copy"><p className="gg-kicker"><Layers3 size={16} /> One platform, two visible products</p><h2 id="gg-depth-heading">GaugeGap attracts attention. BrainSNN turns the result into a decision.</h2><p>The public experience should be simple. The engine underneath preserves the model, analyzes the message and supports a more responsible publishing workflow.</p><ul><li><CheckCircle2 size={17} /> GaugeGap handles interaction, missions and shareable states.</li><li><CheckCircle2 size={17} /> BrainSNN handles content analysis, revision and publishing decisions.</li><li><CheckCircle2 size={17} /> Research mode exposes equations, diagnostics and limitations.</li></ul><div className="gg-depth-actions"><Button variant="primary" onClick={openResearch}>Open research mode <Microscope size={16} /></Button><Button variant="ghost" onClick={() => onStart?.(CONTENT_SAMPLE)}>Test BrainSNN <WandSparkles size={16} /></Button></div><p className="gg-depth-foundry"><a href="https://xioaisolutions.github.io/gaugegap-foundry/foundry-experience/" target="_blank" rel="noreferrer">Open the Deep Foundry — the verification-first research playground with proofpack export <ArrowRight size={14} /></a></p></div>
</section>

<TrustLadder onResearch={openResearch} />
Expand Down
165 changes: 165 additions & 0 deletions brainsnn-r3f-app/src/features/brain3d/brainMetrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// Derived measurements over a brain trial.
//
// Every quantity here is a deterministic function of the trace produced by
// runBrainTrial, so the same content and seed always yield the same numbers.
//
// CLAIM BOUNDARY: these describe the dynamics of a 7-region model driven by
// lexical features of the input text. They are not measurements of any human
// brain, and carry no clinical or predictive meaning.
import { BRAIN_REGIONS, PATHWAYS } from './brainRegions.js';

// The one closed loop in the graph: attention -> meaning -> salience -> action
// gate -> (inhibits) attention. Its gain is the model's runaway-vs-controlled
// quantity, and the reason the game has a fail state.
export const CONTROL_LOOP = ['THL-CTX', 'CTX-AMY', 'AMY-BG', 'BG-THL'];

function mean(values) {
if (!values.length) return 0;
return values.reduce((sum, value) => sum + value, 0) / values.length;
}

function clamp01(value) {
return Math.max(0, Math.min(1, value));
}

function round(value, places = 4) {
const factor = 10 ** places;
return Math.round((Number(value) || 0) * factor) / factor;
}

// Pearson correlation; returns 0 when either train is constant (undefined r).
function correlation(a, b) {
const n = Math.min(a.length, b.length);
if (n < 2) return 0;
const meanA = mean(a);
const meanB = mean(b);
let num = 0;
let devA = 0;
let devB = 0;
for (let i = 0; i < n; i += 1) {
const da = a[i] - meanA;
const db = b[i] - meanB;
num += da * db;
devA += da * da;
devB += db * db;
}
if (devA <= 0 || devB <= 0) return 0;
return num / Math.sqrt(devA * devB);
}

function activitySeries(trace, code) {
return trace.map((frame) => frame.activities[code] ?? 0);
}

function spikeSeries(trace, code) {
return trace.map((frame) => (frame.spikes[code] ? 1 : 0));
}

export function computeBrainMetrics(trial) {
const { trace = [], finalState, params, targets } = trial || {};
if (!trace.length || !finalState) return null;

const seconds = (trace.length * params.tickMs) / 1000;
const codes = BRAIN_REGIONS.map((region) => region.code);

const firingRateHz = {};
const meanActivity = {};
const eiBalance = {};
for (const code of codes) {
const spikes = spikeSeries(trace, code);
firingRateHz[code] = round(spikes.reduce((sum, value) => sum + value, 0) / seconds, 3);
meanActivity[code] = round(mean(activitySeries(trace, code)), 4);
const excitatory = mean(trace.map((frame) => frame.drive[code]?.excitatory ?? 0));
const inhibitory = mean(trace.map((frame) => frame.drive[code]?.inhibitory ?? 0));
const total = excitatory + inhibitory;
eiBalance[code] = round(total > 0 ? (excitatory - inhibitory) / total : 0, 4);
}

// Judgment vs threat: the model's core semantic contrast. Capped because a
// silenced amygdala drives the denominator to ~0, and any ratio past 10 says
// the same thing: threat is not competing.
const CONTROL_RATIO_CAP = 10;
const pfc = meanActivity.PFC;
const amy = meanActivity.AMY;
const controlRatio = round(
amy > 0 ? Math.min(CONTROL_RATIO_CAP, pfc / amy) : pfc > 0 ? CONTROL_RATIO_CAP : 1,
4,
);

// How far threat + action pressure ran ahead of reflective control, 0-100.
const hijackRaw = mean(trace.map((frame) => (
((frame.activities.AMY ?? 0) + (frame.activities.BG ?? 0)) / 2 - (frame.activities.PFC ?? 0)
)));
const hijackIndex = Math.round(clamp01(hijackRaw + 0.5) * 100);

const finalWeights = finalState.weights;
const loopGain = round(CONTROL_LOOP.reduce((product, id) => product * (finalWeights[id] ?? 0), 1), 6);
const gateIntegrity = round((finalWeights['BG-THL'] ?? 0) * meanActivity.BG, 4);

// Mean pairwise correlation of the region spike trains.
const trains = codes.map((code) => spikeSeries(trace, code));
const pairs = [];
for (let i = 0; i < trains.length; i += 1) {
for (let j = i + 1; j < trains.length; j += 1) pairs.push(correlation(trains[i], trains[j]));
}
const synchronyIndex = round(mean(pairs), 4);

// Time to reach the driven equilibrium, in ticks (null if it never settles).
let settlingTicks = null;
if (targets) {
const tolerance = 0.08 * codes.length;
let consecutive = 0;
for (let index = 0; index < trace.length; index += 1) {
const distance = codes.reduce((sum, code) => (
targets[code] == null ? sum : sum + Math.abs((trace[index].activities[code] ?? 0) - targets[code])
), 0);
consecutive = distance < tolerance ? consecutive + 1 : 0;
if (consecutive >= 5) {
settlingTicks = index - 4;
break;
}
}
}

const weightDrift = round(PATHWAYS.reduce((sum, pathway) => (
sum + Math.abs((finalWeights[pathway.id] ?? 0) - pathway.initialWeight)
), 0), 4);

const stdpFlux = round(trace.reduce((sum, frame) => (
sum + Object.values(frame.stdpDelta || {}).reduce((inner, value) => inner + value, 0)
), 0), 4);

return {
seconds: round(seconds, 3),
ticks: trace.length,
firingRateHz,
meanActivity,
eiBalance,
controlRatio,
hijackIndex,
loopGain,
gateIntegrity,
synchronyIndex,
settlingTicks,
plasticity: round(finalState.plasticity, 4),
weightDrift,
stdpFlux,
meanFiring: round(mean(trace.map((frame) => frame.meanFiring)), 4),
};
}

// Presentation metadata: label, units and which direction is "healthy", so the
// UI never has to hardcode interpretation.
export const METRIC_DESCRIPTORS = Object.freeze([
{ id: 'controlRatio', label: 'Control ratio', unit: 'PFC ÷ AMY', direction: 'higher-good', explanation: 'Reflective control relative to threat response.' },
{ id: 'hijackIndex', label: 'Hijack index', unit: '0–100', direction: 'lower-good', explanation: 'How far threat and action pressure ran ahead of judgment.' },
{ id: 'loopGain', label: 'Loop gain', unit: 'product of 4 weights', direction: 'lower-good', explanation: 'Gain around the attention → salience → gate loop.' },
{ id: 'gateIntegrity', label: 'Gate integrity', unit: '0–1', direction: 'higher-good', explanation: 'Strength of the inhibitory brake on incoming attention.' },
{ id: 'synchronyIndex', label: 'Synchrony', unit: 'mean pairwise r', direction: 'contextual', explanation: 'Correlation between region spike trains.' },
{ id: 'plasticity', label: 'Plasticity', unit: 'mean weight', direction: 'contextual', explanation: 'Average synaptic strength after learning.' },
{ id: 'weightDrift', label: 'Weight drift', unit: 'Σ|Δw|', direction: 'contextual', explanation: 'Total learning away from the initial wiring.' },
{ id: 'stdpFlux', label: 'STDP flux', unit: 'Σ potentiation − depression', direction: 'contextual', explanation: 'Net direction of spike-timing learning.' },
]);

export const BRAIN_CLAIM_BOUNDARY = 'Simulated dynamics of a 7-region model driven by lexical features of the text. '
+ 'Not a measurement of any human brain, and not a clinical or predictive claim.';
Loading
Loading