feat(intelligence): add Graph Cohesion#2978
Conversation
📝 WalkthroughWalkthroughAdds a deterministic graph-cohesion engine, a graphCohesion API facade, a stateful GraphCohesionTab and presentational GraphCohesionPanel, tests for computation and UI, Intelligence page wiring for a new "cohesion" tab, and i18n entries across locales. ChangesGraph Cohesion Feature
Sequence Diagram(s)sequenceDiagram
participant User
participant GraphCohesionTab
participant graphCohesionApi
participant memoryGraphQuery
participant computeGraphCohesion
participant GraphCohesionPanel
User->>GraphCohesionTab: Open Intelligence / select cohesion tab
GraphCohesionTab->>graphCohesionApi: loadCohesion(namespace)
graphCohesionApi->>memoryGraphQuery: memoryGraphQuery(namespace)
memoryGraphQuery-->>graphCohesionApi: GraphRelation[]
graphCohesionApi->>computeGraphCohesion: computeGraphCohesion(relations)
computeGraphCohesion-->>graphCohesionApi: CohesionResult
graphCohesionApi-->>GraphCohesionTab: CohesionResult
GraphCohesionTab->>GraphCohesionPanel: render(result, loading=false)
GraphCohesionPanel-->>User: display metrics, summary, brokers
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
3182427 to
42888ae
Compare
Rust Core Coverage failed in its post-step with "No space left on device" during cache save — a runner-disk infra flake unrelated to this TS-only PR (Rust is byte-identical to main; the same flake hit tinyhumansai#2978 previously). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A new read-only "Cohesion" tab for the Intelligence view: clustering- coefficient analysis of the memory knowledge graph. Treating the triples as an undirected simple graph, it surfaces a structural signal the centrality and frequency lenses cannot — BROKERS: entities whose neighbours are not connected to each other, i.e. the sole link holding otherwise-separate clusters together. Engine (pure, deterministic — no React/RPC/clock/RNG): - per-node local clustering coefficient C(v) = 2·triangles / (deg·(deg-1)), - triangleCount, averageClustering (mean over degree>=2 nodes), and transitivity (global clustering coefficient = 3·triangles / connected-triples), - findBrokers() ranks the loosest-neighbourhood entities (structural holes). Adds ZERO new core surface: composes the already-shipped memoryGraphQuery / memoryListNamespaces JSON-RPC wrappers and delegates all math to the engine. Container/presentational split with a monotonic request-token race guard for load-on-mount; i18n across all 13 locales. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Rust Core Coverage job failed in its post-run cache-save step with "No space left on device" — a runner-disk infra flake unrelated to this TS-only change (Rust is byte-identical to main; all other Rust jobs passed). Empty commit to re-run on a fresh runner. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rust Core Coverage failed on an unrelated, flaky upstream Rust unit test (openhuman::memory_tools::tools::put::tests::execute_defaults_unknown_priority_to_normal — a non-deterministic "namespace/key cannot contain personal identifiers" rule). This PR is TS-only; Rust is byte-identical to main and the same job passes on sibling PRs. Re-running on a fresh runner. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
42888ae to
71bc99a
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/src/lib/memory/graphCohesion.test.ts`:
- Around line 206-218: The inline comment in the test "sorts nodes by clustering
DESC, then degree DESC, then id ASC" is incorrect: update the note that
currently says "B,C also 1 here" to reflect that B and C have localClustering
2/3 (degree 3) and only A and D have clustering 1; edit the comment near the
computeGraphCohesion call or the rel(...) list so it accurately documents the
diamond shape and that the ones array (derived from r.nodes.filter(...).map(n =>
n.id)) should be ['A','D'].
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 074eeae0-b6d9-49cc-8b4e-49110efce73e
📒 Files selected for processing (23)
app/src/components/intelligence/GraphCohesionPanel.test.tsxapp/src/components/intelligence/GraphCohesionPanel.tsxapp/src/components/intelligence/GraphCohesionTab.test.tsxapp/src/components/intelligence/GraphCohesionTab.tsxapp/src/lib/i18n/chunks/ar-1.tsapp/src/lib/i18n/chunks/bn-1.tsapp/src/lib/i18n/chunks/de-1.tsapp/src/lib/i18n/chunks/en-1.tsapp/src/lib/i18n/chunks/es-1.tsapp/src/lib/i18n/chunks/fr-1.tsapp/src/lib/i18n/chunks/hi-1.tsapp/src/lib/i18n/chunks/id-1.tsapp/src/lib/i18n/chunks/it-1.tsapp/src/lib/i18n/chunks/ko-1.tsapp/src/lib/i18n/chunks/pt-1.tsapp/src/lib/i18n/chunks/ru-1.tsapp/src/lib/i18n/chunks/zh-CN-1.tsapp/src/lib/i18n/en.tsapp/src/lib/memory/graphCohesion.test.tsapp/src/lib/memory/graphCohesion.tsapp/src/pages/Intelligence.tsxapp/src/services/api/graphCohesionApi.test.tsapp/src/services/api/graphCohesionApi.ts
✅ Files skipped from review due to trivial changes (4)
- app/src/lib/i18n/chunks/it-1.ts
- app/src/lib/i18n/en.ts
- app/src/lib/i18n/chunks/fr-1.ts
- app/src/lib/i18n/chunks/es-1.ts
🚧 Files skipped from review as they are similar to previous changes (14)
- app/src/components/intelligence/GraphCohesionTab.test.tsx
- app/src/pages/Intelligence.tsx
- app/src/lib/i18n/chunks/bn-1.ts
- app/src/services/api/graphCohesionApi.ts
- app/src/lib/i18n/chunks/id-1.ts
- app/src/lib/i18n/chunks/ar-1.ts
- app/src/services/api/graphCohesionApi.test.ts
- app/src/lib/i18n/chunks/en-1.ts
- app/src/lib/i18n/chunks/ru-1.ts
- app/src/components/intelligence/GraphCohesionTab.tsx
- app/src/lib/i18n/chunks/zh-CN-1.ts
- app/src/components/intelligence/GraphCohesionPanel.test.tsx
- app/src/lib/memory/graphCohesion.ts
- app/src/components/intelligence/GraphCohesionPanel.tsx
| it('sorts nodes by clustering DESC, then degree DESC, then id ASC', () => { | ||
| const r = computeGraphCohesion([ | ||
| rel('A', 'B'), | ||
| rel('A', 'C'), | ||
| rel('B', 'C'), // triangle A-B-C (clustering 1 for A; B,C also 1 here) | ||
| rel('B', 'D'), | ||
| rel('C', 'D'), // diamond | ||
| ]); | ||
| // top entries are the clustering-1 nodes; A before D by id at equal degree. | ||
| expect(r.nodes[0].localClustering).toBe(1); | ||
| const ones = r.nodes.filter(n => n.localClustering === 1).map(n => n.id); | ||
| expect(ones).toEqual(['A', 'D']); | ||
| }); |
There was a problem hiding this comment.
Inline comment contradicts the assertion below it.
The comment claims B,C also 1 here, but in this diamond B and C have degree 3 and cluster at 2/3 — only A and D are at 1, which is exactly what the ones assertion verifies. The misleading note could prompt a future reader to "fix" a correct test.
✏️ Suggested comment correction
- rel('B', 'C'), // triangle A-B-C (clustering 1 for A; B,C also 1 here)
+ rel('B', 'C'), // triangle A-B-C; A & D cluster at 1, B & C at 2/3 (diamond spine)
rel('B', 'D'),
rel('C', 'D'), // diamond📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it('sorts nodes by clustering DESC, then degree DESC, then id ASC', () => { | |
| const r = computeGraphCohesion([ | |
| rel('A', 'B'), | |
| rel('A', 'C'), | |
| rel('B', 'C'), // triangle A-B-C (clustering 1 for A; B,C also 1 here) | |
| rel('B', 'D'), | |
| rel('C', 'D'), // diamond | |
| ]); | |
| // top entries are the clustering-1 nodes; A before D by id at equal degree. | |
| expect(r.nodes[0].localClustering).toBe(1); | |
| const ones = r.nodes.filter(n => n.localClustering === 1).map(n => n.id); | |
| expect(ones).toEqual(['A', 'D']); | |
| }); | |
| it('sorts nodes by clustering DESC, then degree DESC, then id ASC', () => { | |
| const r = computeGraphCohesion([ | |
| rel('A', 'B'), | |
| rel('A', 'C'), | |
| rel('B', 'C'), // triangle A-B-C; A & D cluster at 1, B & C at 2/3 (diamond spine) | |
| rel('B', 'D'), | |
| rel('C', 'D'), // diamond | |
| ]); | |
| // top entries are the clustering-1 nodes; A before D by id at equal degree. | |
| expect(r.nodes[0].localClustering).toBe(1); | |
| const ones = r.nodes.filter(n => n.localClustering === 1).map(n => n.id); | |
| expect(ones).toEqual(['A', 'D']); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/src/lib/memory/graphCohesion.test.ts` around lines 206 - 218, The inline
comment in the test "sorts nodes by clustering DESC, then degree DESC, then id
ASC" is incorrect: update the note that currently says "B,C also 1 here" to
reflect that B and C have localClustering 2/3 (degree 3) and only A and D have
clustering 1; edit the comment near the computeGraphCohesion call or the
rel(...) list so it accurately documents the diamond shape and that the ones
array (derived from r.nodes.filter(...).map(n => n.id)) should be ['A','D'].
Rust Core Coverage failed in its post-step with "No space left on device" during cache save — a runner-disk infra flake unrelated to this TS-only PR (Rust is byte-identical to main; the same flake hit tinyhumansai#2978 previously). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rmation) A new read-only "Specialisation" tab. The vocabulary lens (tinyhumansai#17 Predicate Diversity) tells you how varied the global predicate set is. The thickness lens (tinyhumansai#18 Predicate Bundles) tells you which entity pairs share many predicates. The frequency lens (tinyhumansai#7 Relationship Types) tells you which predicates dominate overall. None of them answers the question this lens does: how much does knowing the SUBJECT predict which predicate it will use? And which entities speak in a specialised vocabulary versus a generalist one? Engine (pure, deterministic — no React/RPC/clock/RNG): - Global: I(S; P) in bits with canonical-order summation; H(S) and H(P); normalisedMI = I / min(H(S), H(P)). - Per-subject: specialisation = 1 - H(P|S=s)/log2(D_s) in [0,1] (1 when D_s <= 1 — single-predicate subject is maximally specialised by convention), plus dominantPredicate (tie-broken by predicate ASC) and dominantPredicateShare. - All p·log2(p) and joint·log2 ratio sums walk pairs in canonical (sortedSubjects, sortedPredicates) order so the result is byte-identical regardless of relation insertion order (lesson from tinyhumansai#2978 Cohesion's float-order bug). Selected from the prior loop-19 design workflow's runner-up (8.35/10 — a genuinely new lens not covered by any of the 19 shipped features). Adds ZERO new core surface: composes the already-shipped memoryGraphQuery / memoryListNamespaces wrappers. Container/presentational split with a monotonic request-token race guard for load-on-mount; i18n across all 13 locales. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rust Core Coverage failed in its post-step with "No space left on device" during cache save — a runner-disk infra flake unrelated to this TS-only PR (Rust is byte-identical to main; the same flake hit tinyhumansai#2978 previously). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rmation) A new read-only "Specialisation" tab. The vocabulary lens (tinyhumansai#17 Predicate Diversity) tells you how varied the global predicate set is. The thickness lens (tinyhumansai#18 Predicate Bundles) tells you which entity pairs share many predicates. The frequency lens (tinyhumansai#7 Relationship Types) tells you which predicates dominate overall. None of them answers the question this lens does: how much does knowing the SUBJECT predict which predicate it will use? And which entities speak in a specialised vocabulary versus a generalist one? Engine (pure, deterministic — no React/RPC/clock/RNG): - Global: I(S; P) in bits with canonical-order summation; H(S) and H(P); normalisedMI = I / min(H(S), H(P)). - Per-subject: specialisation = 1 - H(P|S=s)/log2(D_s) in [0,1] (1 when D_s <= 1 — single-predicate subject is maximally specialised by convention), plus dominantPredicate (tie-broken by predicate ASC) and dominantPredicateShare. - All p·log2(p) and joint·log2 ratio sums walk pairs in canonical (sortedSubjects, sortedPredicates) order so the result is byte-identical regardless of relation insertion order (lesson from tinyhumansai#2978 Cohesion's float-order bug). Selected from the prior loop-19 design workflow's runner-up (8.35/10 — a genuinely new lens not covered by any of the 19 shipped features). Adds ZERO new core surface: composes the already-shipped memoryGraphQuery / memoryListNamespaces wrappers. Container/presentational split with a monotonic request-token race guard for load-on-mount; i18n across all 13 locales. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
Adds a new read-only "Cohesion" tab to the Intelligence view: clustering-coefficient analysis of the memory knowledge graph. Where the Centrality lens answers "which entities are important", this lens answers "how tightly knit is the neighbourhood around each entity" — and surfaces a structural signal neither centrality nor a frequency sort can reveal: brokers.
A broker (structural hole) is an entity whose neighbours are not connected to each other — it's the sole link holding otherwise-separate clusters together (local clustering ≈ 0). These are the single points of failure / brokerage opportunities in the user's accumulated memory.
Design
lib/memory/graphCohesion.ts): treats the(subject)-[predicate]->(object)triples as an undirected simple graph (direction dropped, parallel edges collapsed, self-loops dropped) and computes:C(v) = 2·triangles / (deg·(deg-1)),Cover degree≥2 nodes), and transitivity (the global clustering coefficient,3·triangles / connected-triples),findBrokers()— ranks the loosest-neighbourhood entities.averageClusteringis summed in canonical (sorted) order so it is byte-identical across input permutations despite IEEE-754 non-associativity.memoryGraphQuery/memoryListNamespacesJSON-RPC wrappers. Read-only — recomputed live from the graph, never persisted.Test plan
vitest— 33 tests (engine: empty/triangle/path/4-cycle/star/diamond fixtures with hand-computed clustering, triangle, avg & transitivity values; self-loop drop; parallel-edge & direction collapse; malformed-row drop; no case-folding; byte-identical determinism across permutations; broker ranking & limits — plus api facade, panel states + broker badge + no-brokers note, container load/namespace-requery/error)tsc --noEmit— cleaneslint— 0 errorsprettier --check— clean🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests