diff --git a/brainsnn-r3f-app/src/App.jsx b/brainsnn-r3f-app/src/App.jsx
index afb3b3f..6f65555 100644
--- a/brainsnn-r3f-app/src/App.jsx
+++ b/brainsnn-r3f-app/src/App.jsx
@@ -24,6 +24,7 @@ import HeatmapTimeline from './components/HeatmapTimeline';
import KnowledgeBrainPanel from './components/KnowledgeBrainPanel';
import MCPBridgePanel from './components/MCPBridgePanel';
import CodeBrainPanel from './components/CodeBrainPanel';
+import GraphInsightsPanel from './components/GraphInsightsPanel';
import BrainStewardPanel from './components/BrainStewardPanel';
import ConversationBrainPanel from './components/ConversationBrainPanel';
import ImmunityPanel from './components/ImmunityPanel';
@@ -553,6 +554,21 @@ export default function App() {
/>
+
+ {
+ setState((prev) => ({
+ ...prev,
+ regions: { ...prev.regions, ...payload.regions },
+ scenario: payload.scenario,
+ tick: prev.tick + 1
+ }));
+ recordImmunity(IMMUNITY_EVENTS.CODE_ANALYZED, {});
+ toastSuccess('Graph insights mapped onto brain');
+ }}
+ />
+
+
diff --git a/brainsnn-r3f-app/src/components/GraphInsightsPanel.jsx b/brainsnn-r3f-app/src/components/GraphInsightsPanel.jsx
new file mode 100644
index 0000000..074b50b
--- /dev/null
+++ b/brainsnn-r3f-app/src/components/GraphInsightsPanel.jsx
@@ -0,0 +1,320 @@
+import React, { useMemo, useState } from 'react';
+import { parseFiles } from '../utils/codeParser';
+import { detectCommunities, detectCommunitiesLeiden } from '../utils/communities';
+import { analyzeGraph } from '../utils/graphAnalytics';
+import { downloadGraphWikiBundle } from '../utils/wikiGenerator';
+
+/**
+ * Layer 101 — Graph Insights
+ *
+ * Cannibalized from safishamsi/graphify. Takes the same code paste as
+ * Layer 20 and runs three structural analytics on top:
+ * - Edge confidence (EXTRACTED / INFERRED / AMBIGUOUS) provenance
+ * - God-node detection (cross-community hubs, artifact-filtered)
+ * - Surprising connections (cross-community + cross-filetype + asym)
+ * - Suggested questions (7 archetypes derived from graph structure)
+ *
+ * Toggle Leiden refinement to split disconnected communities. Export
+ * everything as a markdown wiki bundle.
+ */
+export default function GraphInsightsPanel({ onApplyToNetwork }) {
+ const [input, setInput] = useState(EXAMPLE_INPUT);
+ const [graph, setGraph] = useState(null);
+ const [algorithm, setAlgorithm] = useState('leiden'); // 'louvain' | 'leiden'
+ const [error, setError] = useState(null);
+
+ const result = useMemo(() => {
+ if (!graph) return null;
+ try {
+ const detect = algorithm === 'leiden' ? detectCommunitiesLeiden : detectCommunities;
+ const communities = detect({ nodes: graph.nodes, edges: graph.edges });
+ const analytics = analyzeGraph(graph, communities);
+ return { graph, communities, analytics };
+ } catch (err) {
+ setError(err.message);
+ return null;
+ }
+ }, [graph, algorithm]);
+
+ function handleAnalyze() {
+ setError(null);
+ const files = parseInput(input);
+ if (!files.length) {
+ setGraph(null);
+ return;
+ }
+ const parsed = parseFiles(files);
+ setGraph(parsed);
+ }
+
+ function handleExport() {
+ if (!result) return;
+ downloadGraphWikiBundle(result.graph, result.communities, result.analytics);
+ }
+
+ function applyToBrain() {
+ if (!result?.analytics) return;
+ const { godNodes, surprising, questions } = result.analytics;
+ // Map insight density onto cognitive regions:
+ // PFC = analytical load (questions)
+ // HPC = recall surface (god-nodes)
+ // CTX = breadth (community count)
+ // THL = signal routing (surprising connections)
+ // AMY = ambiguity / unease (AMBIGUOUS edges)
+ const ambig = (graph?.stats?.provenance?.AMBIGUOUS) || 0;
+ const regions = {
+ PFC: clamp(0.35 + questions.length * 0.04, 0.2, 0.95),
+ HPC: clamp(0.3 + godNodes.length * 0.05, 0.2, 0.9),
+ CTX: clamp(0.25 + (result.communities.communities.length || 0) * 0.03, 0.2, 0.9),
+ THL: clamp(0.25 + surprising.length * 0.04, 0.2, 0.9),
+ AMY: clamp(0.2 + ambig * 0.02, 0.15, 0.9),
+ BG: 0.35,
+ CBL: 0.3
+ };
+ onApplyToNetwork?.({
+ regions,
+ scenario: `Graph Insights · ${godNodes.length} god-nodes · ${surprising.length} surprising · ${questions.length} questions`
+ });
+ }
+
+ return (
+
+ Layer 101
+ Graph Insights
+
+ Cannibalized from safishamsi/graphify. Edge-confidence provenance,
+ god-node detection, surprising connections, and 7 graph-derived
+ question archetypes — turns Layer 20 from a viewer into an analyst.
+
+
+
+ Paste files using === path/to/file.ext === delimiters,
+ or one file raw:
+
+
+ );
+}
+
+function ProvenanceBreakdown({ stats }) {
+ const prov = stats?.provenance;
+ if (!prov || !Object.keys(prov).length) return null;
+ const total = Object.values(prov).reduce((a, b) => a + b, 0) || 1;
+ const tags = [
+ { id: 'EXTRACTED', color: '#5ee69a' },
+ { id: 'INFERRED', color: '#5ad4ff' },
+ { id: 'AMBIGUOUS', color: '#f59e0b' }
+ ];
+ return (
+
+
Edge confidence
+
+ {tags.map((t) => {
+ const count = prov[t.id] || 0;
+ if (!count) return null;
+ const pct = ((count / total) * 100).toFixed(0);
+ return (
+
+ {t.id}
+ {count} · {pct}%
+
+ );
+ })}
+
+
+ );
+}
+
+function Section({ title, hint, children }) {
+ return (
+
+
{title}
+ {hint &&
{hint}
}
+ {children}
+
+ );
+}
+
+function Empty({ children }) {
+ return {children}
;
+}
+
+function clamp(v, lo, hi) {
+ return Math.max(lo, Math.min(hi, v));
+}
+
+// ---------- input parsing (mirrors CodeBrainPanel) ----------
+
+function parseInput(text) {
+ if (!text?.trim()) return [];
+ const blockRe = /^===\s*(.+?)\s*===\s*$/gm;
+ if (!blockRe.test(text)) {
+ return [{ path: 'pasted.js', source: text }];
+ }
+ const files = [];
+ const lines = text.split('\n');
+ let currentPath = null;
+ let buffer = [];
+ const flush = () => {
+ if (currentPath) files.push({ path: currentPath, source: buffer.join('\n') });
+ buffer = [];
+ };
+ for (const line of lines) {
+ const m = line.match(/^===\s*(.+?)\s*===\s*$/);
+ if (m) {
+ flush();
+ currentPath = m[1];
+ } else if (currentPath) {
+ buffer.push(line);
+ }
+ }
+ flush();
+ return files;
+}
+
+const EXAMPLE_INPUT = `=== src/auth.js ===
+import bcrypt from 'bcrypt';
+import { logger } from './logger';
+export function login(user, password) {
+ return verifyCredentials(user, password);
+}
+export function logout(token) {
+ return invalidate(token);
+}
+class AuthService {
+ constructor(db) { this.db = db; }
+ authenticate(user) { return this.db.check(user); }
+}
+
+=== src/models/user.js ===
+import { db } from '../auth';
+export class User {
+ constructor(id, email) { this.id = id; this.email = email; }
+ save() { return db.insert(this); }
+}
+export function findUser(id) { return db.query(id); }
+
+=== src/api/routes.js ===
+import express from 'express';
+import { login, logout } from '../auth';
+import { User, findUser } from '../models/user';
+export function registerRoutes(app) {
+ app.post('/login', login);
+ app.post('/logout', logout);
+ app.get('/user/:id', (req) => findUser(req.params.id));
+}
+
+=== src/logger.js ===
+export const logger = {
+ info: (msg) => console.log(msg),
+ error: (msg) => console.error(msg)
+};
+
+=== src/utils/hash.py ===
+import hashlib
+def sha256(text):
+ return hashlib.sha256(text.encode()).hexdigest()
+class Hasher:
+ def hash(self, value): return sha256(value)
+`;
diff --git a/brainsnn-r3f-app/src/styles/global.css b/brainsnn-r3f-app/src/styles/global.css
index 27d340c..1616e10 100644
--- a/brainsnn-r3f-app/src/styles/global.css
+++ b/brainsnn-r3f-app/src/styles/global.css
@@ -2017,6 +2017,24 @@ h1,h2,h3{
}
.code-brain-name{font-family:'JetBrains Mono', ui-monospace, monospace}
.code-brain-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
+.code-brain-question-list{
+ list-style:none;padding:0;margin:0;
+ display:flex;flex-direction:column;gap:6px
+}
+.code-brain-question-list li{
+ display:flex;gap:10px;align-items:flex-start;
+ padding:8px 10px;
+ background:rgba(255,255,255,.02);
+ border:1px solid rgba(255,255,255,.05);
+ border-radius:6px;
+ font-size:.8rem
+}
+.code-brain-archetype{
+ font-family:'JetBrains Mono', ui-monospace, monospace;
+ font-size:.7rem;opacity:.7;
+ text-transform:uppercase;letter-spacing:.04em;
+ flex-shrink:0;min-width:110px
+}
/* ---------- Layer 21: Brain Steward ---------- */
diff --git a/brainsnn-r3f-app/src/utils/codeParser.js b/brainsnn-r3f-app/src/utils/codeParser.js
index c2f3861..90dffd2 100644
--- a/brainsnn-r3f-app/src/utils/codeParser.js
+++ b/brainsnn-r3f-app/src/utils/codeParser.js
@@ -6,8 +6,23 @@
* JS/TS/Python/Go/Rust function + class + import + export extraction.
* Returns symbols that can be fed into BM25 index + community detection
* to build a code-aware knowledge graph.
+ *
+ * Edges carry provenance tags borrowed from safishamsi/graphify so
+ * downstream consumers (Layer 101 Graph Insights) can reason about
+ * confidence:
+ * EXTRACTED — directly observable in source (file→symbol contains,
+ * resolved imports). Weight 1.0.
+ * INFERRED — derived from heuristics (sibling adjacency,
+ * basename-fuzzy import). Weight 0.8.
+ * AMBIGUOUS — uncertain (unresolved external imports). Weight 0.5.
*/
+export const PROVENANCE = {
+ EXTRACTED: { tag: 'EXTRACTED', confidence: 1.0 },
+ INFERRED: { tag: 'INFERRED', confidence: 0.8 },
+ AMBIGUOUS: { tag: 'AMBIGUOUS', confidence: 0.5 }
+};
+
const LANG_PATTERNS = {
js: {
ext: /\.(js|jsx|ts|tsx|mjs|cjs)$/i,
@@ -148,9 +163,10 @@ export function parseFiles(files) {
}
// Build edges:
- // - file → symbol (contains)
- // - file → file (via import resolution — best-effort by basename)
- // - symbol → symbol (sibling within file, weak)
+ // - file → symbol (contains) EXTRACTED, weight 1
+ // - file → file (import resolved) EXTRACTED, weight 2
+ // - file → external module (import unresolved) AMBIGUOUS, weight 0.5
+ // - symbol → symbol (sibling in file) INFERRED, weight 0.3
const edges = [];
const fileByBasename = new Map();
for (const f of parsed) {
@@ -158,24 +174,69 @@ export function parseFiles(files) {
fileByBasename.set(base, f.path);
}
+ const externalModules = new Map(); // name → node id
+
for (const f of parsed) {
const fileId = `file:${f.path}`;
for (const sym of f.symbols) {
- edges.push({ source: fileId, target: `${sym.kind}:${f.path}:${sym.name}`, weight: 1 });
+ edges.push({
+ source: fileId,
+ target: `${sym.kind}:${f.path}:${sym.name}`,
+ weight: 1,
+ kind: 'contains',
+ provenance: PROVENANCE.EXTRACTED.tag,
+ confidence: PROVENANCE.EXTRACTED.confidence
+ });
}
- // Imports (best-effort resolution)
+ // Imports — resolve to file when possible, otherwise tag AMBIGUOUS
for (const imp of f.imports) {
const base = imp.split('/').pop().replace(/\.[^.]+$/, '');
const resolved = fileByBasename.get(base);
if (resolved && resolved !== f.path) {
- edges.push({ source: fileId, target: `file:${resolved}`, weight: 2 });
+ edges.push({
+ source: fileId,
+ target: `file:${resolved}`,
+ weight: 2,
+ kind: 'imports',
+ provenance: PROVENANCE.EXTRACTED.tag,
+ confidence: PROVENANCE.EXTRACTED.confidence
+ });
+ } else {
+ // External / unresolved — surface as a placeholder node with an
+ // AMBIGUOUS edge so Layer 101 can ask "what is this?"
+ const extId = `external:${imp}`;
+ if (!externalModules.has(imp)) {
+ externalModules.set(imp, extId);
+ nodes.push({
+ id: extId,
+ kind: 'external',
+ label: imp,
+ external: true
+ });
+ nodeIds.add(extId);
+ }
+ edges.push({
+ source: fileId,
+ target: extId,
+ weight: 0.5,
+ kind: 'imports',
+ provenance: PROVENANCE.AMBIGUOUS.tag,
+ confidence: PROVENANCE.AMBIGUOUS.confidence
+ });
}
}
- // Sibling symbols within same file
+ // Sibling symbols within same file — proximity is a weak signal
for (let i = 0; i < f.symbols.length - 1; i++) {
const a = `${f.symbols[i].kind}:${f.path}:${f.symbols[i].name}`;
const b = `${f.symbols[i + 1].kind}:${f.path}:${f.symbols[i + 1].name}`;
- edges.push({ source: a, target: b, weight: 0.3 });
+ edges.push({
+ source: a,
+ target: b,
+ weight: 0.3,
+ kind: 'sibling',
+ provenance: PROVENANCE.INFERRED.tag,
+ confidence: PROVENANCE.INFERRED.confidence
+ });
}
}
@@ -186,9 +247,16 @@ export function parseFiles(files) {
meta: n
}));
+ const provenance = edges.reduce((acc, e) => {
+ const tag = e.provenance || 'EXTRACTED';
+ acc[tag] = (acc[tag] || 0) + 1;
+ return acc;
+ }, {});
+
const stats = {
totalFiles: parsed.length,
- totalSymbols: nodes.filter((n) => n.kind !== 'file').length,
+ totalSymbols: nodes.filter((n) => n.kind !== 'file' && n.kind !== 'external').length,
+ totalExternals: nodes.filter((n) => n.kind === 'external').length,
byKind: nodes.reduce((acc, n) => {
acc[n.kind] = (acc[n.kind] || 0) + 1;
return acc;
@@ -196,7 +264,8 @@ export function parseFiles(files) {
byLang: parsed.reduce((acc, f) => {
acc[f.lang] = (acc[f.lang] || 0) + 1;
return acc;
- }, {})
+ }, {}),
+ provenance
};
return { nodes, edges, docs, files: parsed, stats };
diff --git a/brainsnn-r3f-app/src/utils/communities.js b/brainsnn-r3f-app/src/utils/communities.js
index 6d6f5a2..a42fce2 100644
--- a/brainsnn-r3f-app/src/utils/communities.js
+++ b/brainsnn-r3f-app/src/utils/communities.js
@@ -1,12 +1,18 @@
/**
- * Layer 20 — Community detection (Louvain-lite)
+ * Layer 20 — Community detection (Louvain-lite + Leiden refinement)
*
* Cannibalized from GitNexus's Leiden clustering for code symbols.
* Pure JS Louvain-lite: greedy modularity optimization in one pass.
* Sufficient for small-to-medium knowledge graphs (<1k nodes).
*
+ * `detectCommunities` runs Louvain.
+ * `detectCommunitiesLeiden` runs Louvain + a Leiden-style refinement
+ * that splits any community whose induced subgraph is disconnected —
+ * the main guarantee Leiden adds over Louvain (Traag, Waltman, van Eck
+ * 2019). The full multi-level Leiden is overkill at this scale.
+ *
* Input: { nodes: [{id, label?}], edges: [{source, target, weight?}] }
- * Output: { communities: [{id, members, label}], modularity, assignments }
+ * Output: { communities: [{id, members, label, size}], modularity, assignments }
*/
export function detectCommunities(graph) {
@@ -136,3 +142,116 @@ export function detectCommunities(graph) {
return { communities, modularity: Q, assignments };
}
+
+/**
+ * Leiden-lite. Run Louvain, then for each community check connectedness
+ * inside its own subgraph. Any community that decomposes into multiple
+ * connected components gets split — each component becomes its own
+ * community. Modularity is recomputed after the split.
+ *
+ * This costs one extra BFS per community plus a modularity recompute
+ * (O(V + E) total). Cheap, deterministic, no new deps.
+ */
+export function detectCommunitiesLeiden(graph) {
+ const base = detectCommunities(graph);
+ if (!base.communities.length) return { ...base, refined: 0, algorithm: 'leiden-lite' };
+
+ const { nodes = [], edges = [] } = graph;
+ const adj = new Map(nodes.map((n) => [n.id, []]));
+ for (const e of edges) {
+ if (!adj.has(e.source) || !adj.has(e.target)) continue;
+ adj.get(e.source).push(e.target);
+ adj.get(e.target).push(e.source);
+ }
+
+ const refined = [];
+ let splitCount = 0;
+ for (const comm of base.communities) {
+ const memberSet = new Set(comm.members);
+ const seen = new Set();
+ const components = [];
+
+ for (const start of comm.members) {
+ if (seen.has(start)) continue;
+ const queue = [start];
+ const component = [];
+ seen.add(start);
+ while (queue.length) {
+ const cur = queue.shift();
+ component.push(cur);
+ for (const nb of adj.get(cur) || []) {
+ if (memberSet.has(nb) && !seen.has(nb)) {
+ seen.add(nb);
+ queue.push(nb);
+ }
+ }
+ }
+ components.push(component);
+ }
+
+ if (components.length === 1) {
+ refined.push(comm);
+ } else {
+ splitCount += components.length - 1;
+ components.forEach((members, i) => {
+ refined.push({
+ id: `${comm.id}_${i}`,
+ members,
+ label: comm.label + (i ? ` ·${i}` : ''),
+ size: members.length
+ });
+ });
+ }
+ }
+
+ refined.sort((a, b) => b.size - a.size);
+ refined.forEach((c, idx) => { c.id = `c${idx}`; });
+
+ // Recompute modularity over the split set
+ const degree = new Map(nodes.map((n) => [n.id, 0]));
+ let m2 = 0;
+ for (const e of edges) {
+ if (!degree.has(e.source) || !degree.has(e.target)) continue;
+ const w = e.weight ?? 1;
+ degree.set(e.source, degree.get(e.source) + w);
+ degree.set(e.target, degree.get(e.target) + w);
+ m2 += 2 * w;
+ }
+
+ const adjW = new Map(nodes.map((n) => [n.id, new Map()]));
+ for (const e of edges) {
+ if (!adjW.has(e.source) || !adjW.has(e.target)) continue;
+ const w = e.weight ?? 1;
+ adjW.get(e.source).set(e.target, (adjW.get(e.source).get(e.target) || 0) + w);
+ adjW.get(e.target).set(e.source, (adjW.get(e.target).get(e.source) || 0) + w);
+ }
+
+ let Q = 0;
+ if (m2 > 0) {
+ for (const c of refined) {
+ const memberSet = new Set(c.members);
+ let inWeight = 0;
+ let totDeg = 0;
+ for (const mid of c.members) {
+ totDeg += degree.get(mid) || 0;
+ for (const [neighbor, w] of adjW.get(mid)) {
+ if (memberSet.has(neighbor)) inWeight += w;
+ }
+ }
+ Q += inWeight / m2 - (totDeg / m2) ** 2;
+ }
+ }
+
+ const assignments = {};
+ refined.forEach((c) => {
+ c.members.forEach((m) => { assignments[m] = c.id; });
+ });
+
+ return {
+ communities: refined,
+ modularity: Q,
+ assignments,
+ refined: splitCount,
+ algorithm: 'leiden-lite'
+ };
+}
diff --git a/brainsnn-r3f-app/src/utils/graphAnalytics.js b/brainsnn-r3f-app/src/utils/graphAnalytics.js
new file mode 100644
index 0000000..0af88ca
--- /dev/null
+++ b/brainsnn-r3f-app/src/utils/graphAnalytics.js
@@ -0,0 +1,450 @@
+/**
+ * Layer 101 — Graph Insights
+ *
+ * Cannibalized from safishamsi/graphify's analyze.py. Three primitives
+ * that turn a code knowledge graph from a viewer into an analyst:
+ *
+ * 1. God-nodes — high-degree hubs that bridge multiple communities,
+ * with synthetic-artifact filtering (degree-1 stubs,
+ * container files, etc.) so the list is honest.
+ * 2. Surprising — connections that cross communities AND file types,
+ * weighted by peripheral→hub asymmetry. Optional
+ * semantic-similarity bonus when embeddings exist.
+ * 3. Suggested — graph-derived questions a reader should ask:
+ * unresolved AMBIGUOUS edges, high-betweenness
+ * bridges, isolated nodes, low-cohesion communities.
+ *
+ * Pure structural analysis — no LLM calls. Operates on the output of
+ * Layer 20's parseFiles + detectCommunities/detectCommunitiesLeiden.
+ */
+
+// ---------- helpers ----------
+
+function buildAdjacency(edges, nodes) {
+ const adj = new Map(nodes.map((n) => [n.id, new Map()]));
+ const degree = new Map(nodes.map((n) => [n.id, 0]));
+ for (const e of edges) {
+ if (!adj.has(e.source) || !adj.has(e.target)) continue;
+ const w = e.weight ?? 1;
+ adj.get(e.source).set(e.target, (adj.get(e.source).get(e.target) || 0) + w);
+ adj.get(e.target).set(e.source, (adj.get(e.target).get(e.source) || 0) + w);
+ degree.set(e.source, (degree.get(e.source) || 0) + w);
+ degree.set(e.target, (degree.get(e.target) || 0) + w);
+ }
+ return { adj, degree };
+}
+
+function fileTypeOf(node) {
+ if (!node) return 'unknown';
+ if (node.kind === 'external') return 'external';
+ if (node.kind === 'file') return node.lang || 'unknown';
+ // symbol — derive from path
+ const path = node.path || '';
+ const m = path.match(/\.([a-zA-Z0-9]+)$/);
+ return m ? m[1].toLowerCase() : (node.lang || 'unknown');
+}
+
+// ---------- god-node detection ----------
+
+/**
+ * Return nodes that act as cross-community hubs.
+ * A node is a god-node iff:
+ * - degree >= minDegree
+ * - touches >= minCommunities distinct communities
+ * - is NOT a synthetic artifact:
+ * * not a degree-≤1 module function
+ * * not a "file" node whose only edges are its own contained symbols
+ * * not an external module (those are AMBIGUOUS by construction)
+ */
+export function findGodNodes(graph, communities, opts = {}) {
+ const cfg = {
+ minDegree: 4,
+ minCommunities: 2,
+ excludeFileContainers: true,
+ excludeExternals: true,
+ topK: 10,
+ ...opts
+ };
+
+ const { nodes = [], edges = [] } = graph;
+ const { assignments = {} } = communities || {};
+ const { adj, degree } = buildAdjacency(edges, nodes);
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
+
+ const ranked = [];
+ for (const node of nodes) {
+ const deg = degree.get(node.id) || 0;
+ if (deg < cfg.minDegree) continue;
+
+ if (cfg.excludeExternals && node.kind === 'external') continue;
+
+ // Synthetic-artifact filter: a "file" whose neighborhood is exactly
+ // its contained symbols and nothing else is a structural container,
+ // not a real hub.
+ if (cfg.excludeFileContainers && node.kind === 'file') {
+ const neighbors = Array.from(adj.get(node.id)?.keys() || []);
+ const onlyOwnSymbols = neighbors.every((nb) => {
+ const m = nodeById.get(nb);
+ return m && m.path === node.path && m.kind !== 'file';
+ });
+ if (onlyOwnSymbols) continue;
+ }
+
+ // Count distinct communities touched
+ const commsTouched = new Set();
+ for (const nb of adj.get(node.id)?.keys() || []) {
+ const c = assignments[nb];
+ if (c) commsTouched.add(c);
+ }
+ if (commsTouched.size < cfg.minCommunities) continue;
+
+ ranked.push({
+ id: node.id,
+ label: node.label,
+ kind: node.kind,
+ path: node.path,
+ degree: deg,
+ communities: commsTouched.size,
+ score: deg * Math.log2(1 + commsTouched.size)
+ });
+ }
+
+ ranked.sort((a, b) => b.score - a.score);
+ return ranked.slice(0, cfg.topK);
+}
+
+// ---------- surprising connections ----------
+
+/**
+ * Score every edge for surprise. The composite score follows graphify's
+ * recipe (analyze.py):
+ * base = edge confidence (1.0 EXTRACTED, 0.8 INFERRED, 0.5 AMBIGUOUS)
+ * + cross-community bonus (+0.5 if endpoints are in different communities)
+ * + cross-filetype bonus (+0.3 if endpoints have different file types)
+ * + peripheral→hub bonus (+0.4 if degree asymmetry > 4×)
+ * × semantic similarity ×1.5 (when embeddings provided and cosine > 0.4)
+ *
+ * Returns top-K edges by surprise score, with a human-readable reason
+ * string per hit so the panel can show "why" without reverse-engineering.
+ */
+export function findSurprisingConnections(graph, communities, opts = {}) {
+ const cfg = {
+ topK: 10,
+ minConfidence: 0.5,
+ semanticBonus: 1.5,
+ semanticThreshold: 0.4,
+ embeddings: null, // optional Map
+ cosineFn: null, // optional (a, b) => number
+ ...opts
+ };
+
+ const { nodes = [], edges = [] } = graph;
+ const { assignments = {} } = communities || {};
+ const { degree } = buildAdjacency(edges, nodes);
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
+
+ const scored = [];
+ for (const e of edges) {
+ const a = nodeById.get(e.source);
+ const b = nodeById.get(e.target);
+ if (!a || !b) continue;
+
+ const conf = typeof e.confidence === 'number' ? e.confidence : 1;
+ if (conf < cfg.minConfidence) continue;
+
+ const reasons = [];
+ let score = conf;
+
+ const ca = assignments[a.id];
+ const cb = assignments[b.id];
+ if (ca && cb && ca !== cb) {
+ score += 0.5;
+ reasons.push('cross-community');
+ }
+
+ const fa = fileTypeOf(a);
+ const fb = fileTypeOf(b);
+ if (fa !== fb && fa !== 'unknown' && fb !== 'unknown') {
+ score += 0.3;
+ reasons.push(`cross-filetype (${fa}↔${fb})`);
+ }
+
+ const da = degree.get(a.id) || 0;
+ const db = degree.get(b.id) || 0;
+ const ratio = da && db ? Math.max(da, db) / Math.max(1, Math.min(da, db)) : 0;
+ if (ratio >= 4) {
+ score += 0.4;
+ reasons.push(`peripheral→hub (×${ratio.toFixed(1)})`);
+ }
+
+ if (cfg.embeddings && cfg.cosineFn) {
+ const va = cfg.embeddings.get(a.id);
+ const vb = cfg.embeddings.get(b.id);
+ if (va && vb) {
+ const sim = cfg.cosineFn(va, vb);
+ if (sim > cfg.semanticThreshold) {
+ score *= cfg.semanticBonus * sim;
+ reasons.push(`semantic ${sim.toFixed(2)}`);
+ }
+ }
+ }
+
+ if (reasons.length === 0) continue;
+
+ scored.push({
+ source: a.id,
+ target: b.id,
+ sourceLabel: a.label,
+ targetLabel: b.label,
+ kind: e.kind,
+ provenance: e.provenance,
+ score,
+ reasons
+ });
+ }
+
+ scored.sort((a, b) => b.score - a.score);
+ return scored.slice(0, cfg.topK);
+}
+
+// ---------- betweenness centrality (Brandes) ----------
+
+/**
+ * Brandes' algorithm, unweighted. Cost O(VE). Fine for <1k nodes.
+ * Returns Map.
+ */
+function betweennessCentrality(nodes, edges) {
+ const adj = new Map(nodes.map((n) => [n.id, []]));
+ for (const e of edges) {
+ if (!adj.has(e.source) || !adj.has(e.target)) continue;
+ adj.get(e.source).push(e.target);
+ adj.get(e.target).push(e.source);
+ }
+
+ const cb = new Map(nodes.map((n) => [n.id, 0]));
+
+ for (const s of nodes.map((n) => n.id)) {
+ const stack = [];
+ const pred = new Map(nodes.map((n) => [n.id, []]));
+ const sigma = new Map(nodes.map((n) => [n.id, 0]));
+ const dist = new Map(nodes.map((n) => [n.id, -1]));
+ sigma.set(s, 1);
+ dist.set(s, 0);
+ const queue = [s];
+
+ while (queue.length) {
+ const v = queue.shift();
+ stack.push(v);
+ for (const w of adj.get(v) || []) {
+ if (dist.get(w) < 0) {
+ queue.push(w);
+ dist.set(w, dist.get(v) + 1);
+ }
+ if (dist.get(w) === dist.get(v) + 1) {
+ sigma.set(w, sigma.get(w) + sigma.get(v));
+ pred.get(w).push(v);
+ }
+ }
+ }
+
+ const delta = new Map(nodes.map((n) => [n.id, 0]));
+ while (stack.length) {
+ const w = stack.pop();
+ for (const v of pred.get(w) || []) {
+ const ratio = (sigma.get(v) / sigma.get(w)) * (1 + delta.get(w));
+ delta.set(v, delta.get(v) + ratio);
+ }
+ if (w !== s) cb.set(w, cb.get(w) + delta.get(w));
+ }
+ }
+
+ // Normalize by 1/((n-1)(n-2)) for undirected
+ const n = nodes.length;
+ const norm = n > 2 ? 1 / ((n - 1) * (n - 2)) : 1;
+ for (const [k, v] of cb) cb.set(k, v * norm);
+ return cb;
+}
+
+// ---------- suggested questions ----------
+
+/**
+ * 7 question archetypes derived purely from graph structure.
+ * Each returned question is { archetype, prompt, target?, score? } so
+ * the UI can render them as actionable chips.
+ */
+export function generateSuggestedQuestions(graph, communities, opts = {}) {
+ const cfg = {
+ minIsolated: 1,
+ cohesionThreshold: 0.15,
+ bridgeTopK: 5,
+ maxQuestions: 12,
+ ...opts
+ };
+
+ const { nodes = [], edges = [] } = graph;
+ const { assignments = {}, communities: commList = [] } = communities || {};
+ const { adj, degree } = buildAdjacency(edges, nodes);
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
+ const questions = [];
+
+ // (1) Unresolved AMBIGUOUS edges
+ const ambiguous = edges.filter((e) => e.provenance === 'AMBIGUOUS');
+ if (ambiguous.length) {
+ const samples = ambiguous.slice(0, 3).map((e) => {
+ const a = nodeById.get(e.source);
+ const b = nodeById.get(e.target);
+ return `${a?.label || e.source} → ${b?.label || e.target}`;
+ });
+ questions.push({
+ archetype: 'ambiguous-edge',
+ prompt: `${ambiguous.length} import${ambiguous.length === 1 ? '' : 's'} couldn't be resolved (${samples.join(', ')}). Are these external libraries you actually use, or stale references?`,
+ score: ambiguous.length
+ });
+ }
+
+ // (2) High-betweenness bridges (only if graph is small enough)
+ if (nodes.length <= 600) {
+ const bc = betweennessCentrality(nodes, edges);
+ const bridges = Array.from(bc.entries())
+ .filter(([id]) => {
+ const n = nodeById.get(id);
+ return n && n.kind !== 'external';
+ })
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, cfg.bridgeTopK)
+ .filter(([, v]) => v > 0);
+ bridges.forEach(([id, score]) => {
+ const n = nodeById.get(id);
+ questions.push({
+ archetype: 'bridge',
+ prompt: `If you removed ${n.label}, the graph fractures. What guarantees does it actually carry?`,
+ target: id,
+ score
+ });
+ });
+ }
+
+ // (3) Isolated nodes (degree 0 or 1, non-external)
+ const isolated = nodes.filter((n) => {
+ const d = degree.get(n.id) || 0;
+ return n.kind !== 'external' && d <= cfg.minIsolated;
+ });
+ if (isolated.length) {
+ const samples = isolated.slice(0, 3).map((n) => n.label).join(', ');
+ questions.push({
+ archetype: 'isolated',
+ prompt: `${isolated.length} node${isolated.length === 1 ? '' : 's'} (${samples}) sit on the periphery with ≤1 connection. Dead code, or future hooks waiting to be wired up?`,
+ score: isolated.length
+ });
+ }
+
+ // (4) Low-cohesion communities
+ for (const c of commList) {
+ if (c.members.length < 3) continue;
+ const memberSet = new Set(c.members);
+ let internal = 0;
+ let external = 0;
+ for (const mid of c.members) {
+ for (const [nb, w] of adj.get(mid) || []) {
+ if (memberSet.has(nb)) internal += w;
+ else external += w;
+ }
+ }
+ const total = internal + external;
+ if (total === 0) continue;
+ const cohesion = internal / total;
+ if (cohesion < cfg.cohesionThreshold) {
+ questions.push({
+ archetype: 'low-cohesion',
+ prompt: `Community "${c.label}" has more outbound edges than internal ones (cohesion ${(cohesion * 100).toFixed(0)}%). Should it be split, or is it really a router/coordinator?`,
+ target: c.id,
+ score: 1 - cohesion
+ });
+ }
+ }
+
+ // (5) Cross-community heavyweight pairs — communities that are
+ // joined by an unusually large number of edges
+ const interCommEdges = new Map();
+ for (const e of edges) {
+ const ca = assignments[e.source];
+ const cb = assignments[e.target];
+ if (!ca || !cb || ca === cb) continue;
+ const key = [ca, cb].sort().join('|');
+ interCommEdges.set(key, (interCommEdges.get(key) || 0) + 1);
+ }
+ const heavyPairs = Array.from(interCommEdges.entries())
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 2);
+ for (const [key, count] of heavyPairs) {
+ if (count < 3) continue;
+ const [ca, cb] = key.split('|');
+ const lA = commList.find((c) => c.id === ca)?.label || ca;
+ const lB = commList.find((c) => c.id === cb)?.label || cb;
+ questions.push({
+ archetype: 'tight-coupling',
+ prompt: `"${lA}" and "${lB}" are joined by ${count} edges. Is this one concept the clusterer split, or genuinely two layers that talk a lot?`,
+ score: count
+ });
+ }
+
+ // (6) External-heavy files — files leaning on lots of unresolved imports
+ const externalLoad = new Map();
+ for (const e of edges) {
+ if (e.provenance !== 'AMBIGUOUS') continue;
+ const src = nodeById.get(e.source);
+ if (!src || src.kind !== 'file') continue;
+ externalLoad.set(src.id, (externalLoad.get(src.id) || 0) + 1);
+ }
+ const heaviest = Array.from(externalLoad.entries())
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 1);
+ for (const [fid, count] of heaviest) {
+ if (count < 3) continue;
+ const n = nodeById.get(fid);
+ questions.push({
+ archetype: 'external-load',
+ prompt: `${n.label} imports ${count} unresolved modules. Is its complexity coming from your code, or from third-party glue?`,
+ target: fid,
+ score: count
+ });
+ }
+
+ // (7) Singleton communities — likely orphans the clusterer couldn't
+ // attach. Worth a glance.
+ const singletons = commList.filter((c) => c.members.length === 1);
+ if (singletons.length >= 3) {
+ questions.push({
+ archetype: 'singletons',
+ prompt: `${singletons.length} singleton communities — nodes the clusterer couldn't fit anywhere. Add edges to attach them, or accept they're standalone?`,
+ score: singletons.length
+ });
+ }
+
+ questions.sort((a, b) => (b.score || 0) - (a.score || 0));
+ return questions.slice(0, cfg.maxQuestions);
+}
+
+// ---------- one-shot analyzer ----------
+
+/**
+ * Run all three analytics in one pass. Returns
+ * { godNodes, surprising, questions, summary }
+ */
+export function analyzeGraph(graph, communities, opts = {}) {
+ const godNodes = findGodNodes(graph, communities, opts.godNodes);
+ const surprising = findSurprisingConnections(graph, communities, opts.surprising);
+ const questions = generateSuggestedQuestions(graph, communities, opts.questions);
+
+ const summary = {
+ nodes: graph.nodes?.length || 0,
+ edges: graph.edges?.length || 0,
+ communities: communities?.communities?.length || 0,
+ modularity: communities?.modularity || 0,
+ godNodeCount: godNodes.length,
+ surprisingCount: surprising.length,
+ questionCount: questions.length
+ };
+
+ return { godNodes, surprising, questions, summary };
+}
diff --git a/brainsnn-r3f-app/src/utils/layerCatalog.js b/brainsnn-r3f-app/src/utils/layerCatalog.js
index 110bd86..7107f9a 100644
--- a/brainsnn-r3f-app/src/utils/layerCatalog.js
+++ b/brainsnn-r3f-app/src/utils/layerCatalog.js
@@ -107,6 +107,7 @@ export const LAYER_CATALOG = [
{ id: 98, name: 'Theme + A11y', group: 'view', blurb: 'Dark/light, high-contrast, reduced-motion, font scale.' },
{ id: 99, name: 'Federated Community Firewall', group: 'firewall', blurb: 'Weekly-rotated community rule pack.' },
{ id: 100, name: 'Milestone Dashboard', group: 'view', blurb: '100 layers shipped — synthesis + personal stats.' },
+ { id: 101, name: 'Graph Insights', group: 'data', blurb: 'God-nodes, surprising connections, suggested questions, edge-confidence (cannibalized from safishamsi/graphify).' },
];
export const LAYER_GROUPS = {
diff --git a/brainsnn-r3f-app/src/utils/wikiGenerator.js b/brainsnn-r3f-app/src/utils/wikiGenerator.js
index d7e8713..f8b6b04 100644
--- a/brainsnn-r3f-app/src/utils/wikiGenerator.js
+++ b/brainsnn-r3f-app/src/utils/wikiGenerator.js
@@ -176,3 +176,195 @@ function makeBar(value, width = 10) {
const filled = Math.round(value * width);
return '█'.repeat(filled) + '░'.repeat(width - filled);
}
+
+// ---------- Graph-Insights wiki (Layer 101) ----------
+
+/**
+ * Render a graphify-style wiki bundle from a parsed code graph + its
+ * detected communities + the analytics output of `analyzeGraph()`.
+ *
+ * Returns an array of { filename, content } objects:
+ * index.md — overview, community list, top god-nodes,
+ * surprising edges, suggested questions
+ * community-N.md — one article per community (members, internal
+ * edges, neighbor communities)
+ */
+export function generateGraphWikiBundle(graph, communities, analytics) {
+ const files = [];
+ const { nodes = [], edges = [], stats = {} } = graph || {};
+ const commList = communities?.communities || [];
+ const assignments = communities?.assignments || {};
+ const nodeById = new Map(nodes.map((n) => [n.id, n]));
+
+ // index.md ----------------------------------------------------------
+ const indexLines = [
+ '# Code Knowledge Graph',
+ '',
+ `> Auto-generated by Layer 101 (Graph Insights). ${new Date().toISOString().split('T')[0]}`,
+ '',
+ '## Summary',
+ '',
+ `- **Files:** ${stats.totalFiles ?? '—'}`,
+ `- **Symbols:** ${stats.totalSymbols ?? '—'}`,
+ `- **External modules:** ${stats.totalExternals ?? 0}`,
+ `- **Communities:** ${commList.length}`,
+ `- **Modularity:** ${(communities?.modularity ?? 0).toFixed(3)}`,
+ ''
+ ];
+
+ if (stats.provenance) {
+ indexLines.push('### Edge confidence', '');
+ indexLines.push('| Provenance | Edges |');
+ indexLines.push('|---|---|');
+ for (const [tag, count] of Object.entries(stats.provenance)) {
+ indexLines.push(`| ${tag} | ${count} |`);
+ }
+ indexLines.push('');
+ }
+
+ if (commList.length) {
+ indexLines.push('## Communities', '');
+ commList.forEach((c, i) => {
+ const slug = communitySlug(c, i);
+ indexLines.push(`- [${c.label}](./${slug}.md) — ${c.members.length} member${c.members.length === 1 ? '' : 's'}`);
+ });
+ indexLines.push('');
+ }
+
+ if (analytics?.godNodes?.length) {
+ indexLines.push('## God-nodes', '');
+ indexLines.push('Nodes with high degree that bridge multiple communities.', '');
+ for (const g of analytics.godNodes) {
+ indexLines.push(`- **${g.label}** (${g.kind}) — degree ${g.degree}, ${g.communities} communities${g.path ? ` · \`${g.path}\`` : ''}`);
+ }
+ indexLines.push('');
+ }
+
+ if (analytics?.surprising?.length) {
+ indexLines.push('## Surprising connections', '');
+ for (const s of analytics.surprising) {
+ indexLines.push(`- **${s.sourceLabel} ↔ ${s.targetLabel}** — score ${s.score.toFixed(2)} (${s.reasons.join(', ')})`);
+ }
+ indexLines.push('');
+ }
+
+ if (analytics?.questions?.length) {
+ indexLines.push('## Suggested questions', '');
+ for (const q of analytics.questions) {
+ indexLines.push(`- _(${q.archetype})_ ${q.prompt}`);
+ }
+ indexLines.push('');
+ }
+
+ indexLines.push('---', '*Generated by BrainSNN Layer 101 — Graph Insights*');
+ files.push({ filename: 'index.md', content: indexLines.join('\n') });
+
+ // community-N.md ---------------------------------------------------
+ commList.forEach((c, idx) => {
+ const slug = communitySlug(c, idx);
+ const memberSet = new Set(c.members);
+ const internal = [];
+ const outbound = new Map(); // otherCommunityId → count
+
+ for (const e of edges) {
+ const sIn = memberSet.has(e.source);
+ const tIn = memberSet.has(e.target);
+ if (sIn && tIn) {
+ internal.push(e);
+ } else if (sIn || tIn) {
+ const otherId = sIn ? e.target : e.source;
+ const otherComm = assignments[otherId];
+ if (otherComm) outbound.set(otherComm, (outbound.get(otherComm) || 0) + 1);
+ }
+ }
+
+ const lines = [
+ `# ${c.label}`,
+ '',
+ `> Community \`${c.id}\` · ${c.members.length} members · ${internal.length} internal edge${internal.length === 1 ? '' : 's'}`,
+ '',
+ '## Members',
+ ''
+ ];
+
+ const grouped = {};
+ for (const mid of c.members) {
+ const n = nodeById.get(mid);
+ if (!n) continue;
+ const k = n.kind || 'node';
+ (grouped[k] ||= []).push(n);
+ }
+ for (const [kind, members] of Object.entries(grouped)) {
+ lines.push(`### ${kind} (${members.length})`, '');
+ for (const n of members) {
+ const path = n.path ? ` — \`${n.path}\`${n.line ? `:${n.line}` : ''}` : '';
+ lines.push(`- ${n.label}${path}`);
+ }
+ lines.push('');
+ }
+
+ if (outbound.size) {
+ lines.push('## Connections to other communities', '');
+ const ranked = Array.from(outbound.entries())
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 10);
+ for (const [otherId, count] of ranked) {
+ const other = commList.find((x) => x.id === otherId);
+ if (!other) continue;
+ const otherSlug = communitySlug(other, commList.indexOf(other));
+ lines.push(`- [${other.label}](./${otherSlug}.md) — ${count} edge${count === 1 ? '' : 's'}`);
+ }
+ lines.push('');
+ }
+
+ if (analytics?.godNodes?.length) {
+ const touching = analytics.godNodes.filter((g) => {
+ const cset = new Set();
+ for (const e of edges) {
+ if (e.source !== g.id && e.target !== g.id) continue;
+ const other = e.source === g.id ? e.target : e.source;
+ if (assignments[other]) cset.add(assignments[other]);
+ }
+ return cset.has(c.id);
+ });
+ if (touching.length) {
+ lines.push('## God-nodes touching this community', '');
+ for (const g of touching) {
+ lines.push(`- **${g.label}** (degree ${g.degree}, spans ${g.communities} communities)`);
+ }
+ lines.push('');
+ }
+ }
+
+ lines.push('---', `[← Back to index](./index.md)`);
+ files.push({ filename: `${slug}.md`, content: lines.join('\n') });
+ });
+
+ return files;
+}
+
+function communitySlug(c, idx) {
+ const safe = (c.label || c.id || `community-${idx}`)
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '')
+ .slice(0, 40) || `community-${idx}`;
+ return `community-${idx + 1}-${safe}`;
+}
+
+/**
+ * Bundle the graph wiki as a single concatenated markdown blob and
+ * trigger a download. Mirrors `downloadWikiBundle` for the existing
+ * Knowledge Brain.
+ */
+export function downloadGraphWikiBundle(graph, communities, analytics) {
+ const bundle = generateGraphWikiBundle(graph, communities, analytics);
+ const combined = bundle.map((f) => `\n\n${f.content}\n\n`).join('\n---\n\n');
+ const blob = new Blob([combined], { type: 'text/markdown' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `graph-insights-wiki-${Date.now()}.md`;
+ a.click();
+ URL.revokeObjectURL(url);
+}