From f88bca4b19bac6cbd5b0e061e45bf128b0b8909b Mon Sep 17 00:00:00 2001 From: Aashir Athar Date: Sat, 30 May 2026 01:32:34 +0500 Subject: [PATCH 1/5] feat(intelligence): add Graph Core (k-core decomposition) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A new read-only "Core" tab for the Intelligence view: k-core decomposition of the memory knowledge graph. It separates the load-bearing CORE of the user's knowledge — entities mutually reinforced by many interlinked facts — from the PERIPHERY of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core — something neither a frequency count nor PageRank can express. Engine (pure, deterministic — no React/RPC/clock/RNG): - coreness per node via the Batagelj-Zaversnik O(V+E) bucket algorithm, - degeneracy (max coreness) and the shell decomposition, - kCoreSize(k) for the nested k-core sizes. Coreness is a graph invariant and the peel is seeded from an id-sorted vertex list, so output is byte-identical regardless of relation input order. Adds ZERO new core surface: composes the already-shipped memoryGraphQuery / memoryListNamespaces JSON-RPC 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 --- .../intelligence/GraphCorePanel.test.tsx | 64 +++++ .../intelligence/GraphCorePanel.tsx | 236 ++++++++++++++++++ .../intelligence/GraphCoreTab.test.tsx | 61 +++++ .../components/intelligence/GraphCoreTab.tsx | 83 ++++++ app/src/lib/i18n/en.ts | 27 ++ app/src/lib/memory/graphCore.test.ts | 166 ++++++++++++ app/src/lib/memory/graphCore.ts | 204 +++++++++++++++ app/src/pages/Intelligence.tsx | 6 +- app/src/services/api/graphCoreApi.test.ts | 74 ++++++ app/src/services/api/graphCoreApi.ts | 29 +++ 10 files changed, 949 insertions(+), 1 deletion(-) create mode 100644 app/src/components/intelligence/GraphCorePanel.test.tsx create mode 100644 app/src/components/intelligence/GraphCorePanel.tsx create mode 100644 app/src/components/intelligence/GraphCoreTab.test.tsx create mode 100644 app/src/components/intelligence/GraphCoreTab.tsx create mode 100644 app/src/lib/memory/graphCore.test.ts create mode 100644 app/src/lib/memory/graphCore.ts create mode 100644 app/src/services/api/graphCoreApi.test.ts create mode 100644 app/src/services/api/graphCoreApi.ts diff --git a/app/src/components/intelligence/GraphCorePanel.test.tsx b/app/src/components/intelligence/GraphCorePanel.test.tsx new file mode 100644 index 0000000000..25a46fce54 --- /dev/null +++ b/app/src/components/intelligence/GraphCorePanel.test.tsx @@ -0,0 +1,64 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; + +import { computeGraphCore } from '../../lib/memory/graphCore'; +import type { GraphRelation } from '../../utils/tauriCommands/memory'; +import GraphCorePanel from './GraphCorePanel'; + +function rel(subject: string, object: string): GraphRelation { + return { + namespace: 'n', + subject, + predicate: 'p', + object, + attrs: {}, + updatedAt: 0, + evidenceCount: 1, + orderIndex: null, + documentIds: [], + chunkIds: [], + }; +} + +// Triangle A-B-C (2-core) plus pendant D off A -> degeneracy 2, shells {2:3,1:1}. +const cored = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A'), rel('A', 'D')]); + +describe('', () => { + it('renders the loading skeleton', () => { + render(); + expect(screen.getByTestId('graph-core-loading')).toBeInTheDocument(); + }); + + it('renders the empty state when there are no nodes', () => { + render(); + expect(screen.getByText('No knowledge graph yet.')).toBeInTheDocument(); + }); + + it('renders an error with a working retry button', () => { + const onRetry = vi.fn(); + render(); + expect(screen.getByRole('alert').textContent).toMatch(/graph unavailable/); + fireEvent.click(screen.getByRole('button', { name: 'Retry' })); + expect(onRetry).toHaveBeenCalledTimes(1); + }); + + it('renders metric tiles, the shell decomposition, and the ranked table', () => { + render(); + expect(screen.getByText('Entities')).toBeInTheDocument(); + expect(screen.getByText('Connections')).toBeInTheDocument(); + expect(screen.getByText('Degeneracy')).toBeInTheDocument(); + expect(screen.getByText('Shell decomposition')).toBeInTheDocument(); + expect(screen.getByText('Deepest-core entities')).toBeInTheDocument(); + // shell labels for the two coreness levels present. + expect(screen.getByText('2-core')).toBeInTheDocument(); + expect(screen.getByText('1-core')).toBeInTheDocument(); + // densest shell holds the triangle (3 entities at the 2-core). + expect(screen.getByText(/2-core · 3 entities/)).toBeInTheDocument(); + }); + + it('badges the deepest-core members and not the periphery', () => { + render(); + // three triangle members carry the core badge; the pendant D does not. + expect(screen.getAllByText('core')).toHaveLength(3); + }); +}); diff --git a/app/src/components/intelligence/GraphCorePanel.tsx b/app/src/components/intelligence/GraphCorePanel.tsx new file mode 100644 index 0000000000..b634c9170b --- /dev/null +++ b/app/src/components/intelligence/GraphCorePanel.tsx @@ -0,0 +1,236 @@ +/** + * Graph Core — presentational view. Pure: renders the core summary tiles + * (entities / connections / degeneracy), the shell decomposition, and a ranked + * table of the deepest-core entities. No data fetching, no clock, no randomness. + */ +import { useMemo } from 'react'; + +import { useT } from '../../lib/i18n/I18nContext'; +import { type CoreResult, kCoreSize } from '../../lib/memory/graphCore'; + +const MAX_ROWS = 25; + +interface GraphCorePanelProps { + result: CoreResult | null; + loading?: boolean; + error?: string | null; + onRetry?: () => void; +} + +const GraphCorePanel = ({ result, loading, error, onRetry }: GraphCorePanelProps) => { + const { t } = useT(); + + const degeneracyCoreSize = useMemo( + () => (result ? kCoreSize(result, result.degeneracy) : 0), + [result] + ); + const maxShellCount = useMemo( + () => (result ? result.shells.reduce((max, s) => (s.count > max ? s.count : max), 0) : 0), + [result] + ); + + const intro = ( +
+

{t('graphCore.title')}

+

{t('graphCore.intro')}

+
+ ); + + if (loading) { + return ( +
+ {intro} +
+
+ {[0, 1, 2].map(i => ( +
+ ))} +
+ {[0, 1, 2, 3].map(i => ( +
+ ))} +
+
+ ); + } + + if (error) { + return ( +
+ {intro} +
+

+ {t('graphCore.errorPrefix')} {error} +

+ {onRetry && ( + + )} +
+
+ ); + } + + if (!result || result.nodes.length === 0) { + return ( +
+ {intro} +
+

+ {t('graphCore.empty')} +

+

+ {t('graphCore.emptyHint')} +

+
+
+ ); + } + + const rows = result.nodes.slice(0, MAX_ROWS); + + return ( +
+ {intro} + + {/* Metric tiles */} +
+ {[ + { label: t('graphCore.metricEntities'), value: result.nodeCount }, + { label: t('graphCore.metricConnections'), value: result.edgeCount }, + { label: t('graphCore.metricDegeneracy'), value: result.degeneracy }, + ].map(tile => ( +
+
+ {tile.label} +
+
+ {tile.value} +
+
+ ))} +
+

+ {t('graphCore.degeneracyCaption') + .replace('{degeneracy}', String(result.degeneracy)) + .replace('{coreSize}', String(degeneracyCoreSize))} +

+ + {/* Shell decomposition */} +
+

+ {t('graphCore.shellsHeading')} +

+
    + {result.shells.map(shell => ( +
  • + + {t('graphCore.shellLabel').replace('{k}', String(shell.k))} + +
    +
    +
    + + {shell.count} + +
  • + ))} +
+
+ + {/* Ranked deepest-core entities */} +
+

+ {t('graphCore.rankedHeading')} +

+ + + + + + + + + + + {rows.map((node, i) => ( + + + + + + + ))} + +
+ {t('graphCore.colRank')} + + {t('graphCore.colEntity')} + + {t('graphCore.colCore')} + + {t('graphCore.colLinks')} +
{i + 1} + {node.id} + {result.degeneracy > 0 && node.coreness === result.degeneracy && ( + + {t('graphCore.coreBadge')} + + )} + +
+
+
+
+ + {node.coreness} + +
+
+ {node.degree} +
+
+
+ ); +}; + +export default GraphCorePanel; diff --git a/app/src/components/intelligence/GraphCoreTab.test.tsx b/app/src/components/intelligence/GraphCoreTab.test.tsx new file mode 100644 index 0000000000..b4b781380c --- /dev/null +++ b/app/src/components/intelligence/GraphCoreTab.test.tsx @@ -0,0 +1,61 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { computeGraphCore } from '../../lib/memory/graphCore'; +import type { GraphRelation } from '../../utils/tauriCommands/memory'; +import GraphCoreTab from './GraphCoreTab'; + +const mockLoadCore = vi.fn(); +const mockLoadNamespaces = vi.fn(); + +vi.mock('../../services/api/graphCoreApi', () => ({ + loadCore: (...args: unknown[]) => mockLoadCore(...args), + loadNamespaces: (...args: unknown[]) => mockLoadNamespaces(...args), +})); + +function rel(subject: string, object: string): GraphRelation { + return { + namespace: 'n', + subject, + predicate: 'p', + object, + attrs: {}, + updatedAt: 0, + evidenceCount: 1, + orderIndex: null, + documentIds: [], + chunkIds: [], + }; +} + +const result = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A')]); + +describe('', () => { + beforeEach(() => { + mockLoadCore.mockReset(); + mockLoadNamespaces.mockReset(); + mockLoadCore.mockResolvedValue(result); + mockLoadNamespaces.mockResolvedValue([]); + }); + + it('loads core (all namespaces) on mount and renders the result', async () => { + render(); + expect(mockLoadCore).toHaveBeenCalledWith(undefined); + await waitFor(() => expect(screen.getByText('Deepest-core entities')).toBeInTheDocument()); + }); + + it('shows the namespace selector and re-queries on change', async () => { + mockLoadNamespaces.mockResolvedValueOnce(['work', 'personal']); + render(); + await waitFor(() => screen.getByRole('combobox')); + fireEvent.change(screen.getByRole('combobox'), { target: { value: 'work' } }); + await waitFor(() => expect(mockLoadCore).toHaveBeenCalledWith('work')); + }); + + it('surfaces an error when the load fails', async () => { + mockLoadCore.mockReset(); + mockLoadCore.mockRejectedValueOnce(new Error('graph unavailable')); + render(); + await waitFor(() => expect(screen.getByRole('alert').textContent).toMatch(/graph unavailable/)); + }); +}); diff --git a/app/src/components/intelligence/GraphCoreTab.tsx b/app/src/components/intelligence/GraphCoreTab.tsx new file mode 100644 index 0000000000..a83bd87eea --- /dev/null +++ b/app/src/components/intelligence/GraphCoreTab.tsx @@ -0,0 +1,83 @@ +/** + * Graph Core tab (container). Owns load-on-mount and the namespace selector; + * delegates all rendering to the pure . Read-only — the result + * is recomputed from the live graph, never persisted. + */ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { useT } from '../../lib/i18n/I18nContext'; +import type { CoreResult } from '../../lib/memory/graphCore'; +import { loadCore, loadNamespaces } from '../../services/api/graphCoreApi'; +import GraphCorePanel from './GraphCorePanel'; + +const GraphCoreTab = () => { + const { t } = useT(); + const [result, setResult] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [namespaces, setNamespaces] = useState([]); + const [namespace, setNamespace] = useState(''); + // Monotonic token: ignore a response if a newer load has since started, so + // an out-of-order resolution can never overwrite the latest result. + const latestRequestId = useRef(0); + + const load = useCallback(async (ns: string) => { + const requestId = (latestRequestId.current += 1); + setLoading(true); + setError(null); + try { + const next = await loadCore(ns || undefined); + if (requestId !== latestRequestId.current) return; + setResult(next); + } catch (err) { + if (requestId !== latestRequestId.current) return; + setError(err instanceof Error ? err.message : String(err)); + } finally { + if (requestId === latestRequestId.current) setLoading(false); + } + }, []); + + useEffect(() => { + // Namespaces are optional UI sugar; a failure to list them must not block + // the core view, so swallow that error specifically. + loadNamespaces() + .then(setNamespaces) + .catch(() => setNamespaces([])); + void load(''); + }, [load]); + + const handleNamespace = (next: string): void => { + setNamespace(next); + void load(next); + }; + + return ( +
+ {namespaces.length > 0 && ( + + )} + + void load(namespace)} + /> +
+ ); +}; + +export default GraphCoreTab; diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index d6dd86c03d..e6db79c3b0 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -303,6 +303,7 @@ const en: TranslationMap = { 'memory.tab.calls': 'Calls', 'memory.tab.diagram': 'Diagram', 'memory.tab.centrality': 'Centrality', + 'memory.tab.core': 'Core', 'memory.tab.settings': 'Settings', 'memory.analyzeNow': 'Analyze Now', 'graphCentrality.title': 'Knowledge Graph Centrality', @@ -331,6 +332,32 @@ const en: TranslationMap = { 'graphCentrality.bridgeTitle': 'Connector — more influential than its link count suggests', 'graphCentrality.degreeTitle': '{in} in · {out} out', + 'graphCore.title': 'Graph Core', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.retry': 'Retry', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.metricEntities': 'Entities', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.colRank': '#', + 'graphCore.colEntity': 'Entity', + 'graphCore.colCore': 'Core', + 'graphCore.colLinks': 'Links', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + // Memory Tree status panel (#1856 Part 1) 'memoryTree.status.title': 'Memory Tree', 'memoryTree.status.autoSyncLabel': 'Auto-sync', diff --git a/app/src/lib/memory/graphCore.test.ts b/app/src/lib/memory/graphCore.test.ts new file mode 100644 index 0000000000..e3b7bf695d --- /dev/null +++ b/app/src/lib/memory/graphCore.test.ts @@ -0,0 +1,166 @@ +import { describe, expect, it } from 'vitest'; + +import type { GraphRelation } from '../../utils/tauriCommands/memory'; +import { computeGraphCore, kCoreSize } from './graphCore'; + +function rel(subject: string, object: string, predicate = 'knows'): GraphRelation { + return { + namespace: 'work', + subject, + predicate, + object, + attrs: {}, + updatedAt: 0, + evidenceCount: 1, + orderIndex: null, + documentIds: [], + chunkIds: [], + }; +} + +function coreness(result: ReturnType, id: string): number { + const node = result.nodes.find(n => n.id === id); + if (!node) throw new Error(`node ${id} not found`); + return node.coreness; +} + +describe('computeGraphCore — basic shapes', () => { + it('returns an empty result for no relations', () => { + const r = computeGraphCore([]); + expect(r.nodeCount).toBe(0); + expect(r.edgeCount).toBe(0); + expect(r.degeneracy).toBe(0); + expect(r.shells).toEqual([]); + expect(r.nodes).toEqual([]); + }); + + it('a single triangle is a uniform 2-core', () => { + const r = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A')]); + expect(r.nodeCount).toBe(3); + expect(r.edgeCount).toBe(3); + expect(r.degeneracy).toBe(2); + for (const id of ['A', 'B', 'C']) expect(coreness(r, id)).toBe(2); + expect(r.shells).toEqual([{ k: 2, count: 3 }]); + }); + + it('a path is a 1-core (no node survives to the 2-core)', () => { + const r = computeGraphCore([rel('A', 'B'), rel('B', 'C')]); + expect(r.degeneracy).toBe(1); + for (const id of ['A', 'B', 'C']) expect(coreness(r, id)).toBe(1); + expect(r.shells).toEqual([{ k: 1, count: 3 }]); + }); + + it('a star (tree) is a 1-core: the hub has high degree but coreness 1', () => { + const r = computeGraphCore([rel('X', 'A'), rel('X', 'B'), rel('X', 'C')]); + expect(r.degeneracy).toBe(1); + expect(r.nodes.find(n => n.id === 'X')?.degree).toBe(3); + expect(coreness(r, 'X')).toBe(1); // high degree, shallow core + expect(r.shells).toEqual([{ k: 1, count: 4 }]); + }); + + it('K4 (complete graph on four) is a uniform 3-core', () => { + const r = computeGraphCore([ + rel('A', 'B'), + rel('A', 'C'), + rel('A', 'D'), + rel('B', 'C'), + rel('B', 'D'), + rel('C', 'D'), + ]); + expect(r.nodeCount).toBe(4); + expect(r.edgeCount).toBe(6); + expect(r.degeneracy).toBe(3); + for (const id of ['A', 'B', 'C', 'D']) expect(coreness(r, id)).toBe(3); + }); +}); + +describe('computeGraphCore — core vs periphery separation', () => { + // Triangle A-B-C (a 2-core) with a pendant D hanging off A. + const r = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A'), rel('A', 'D')]); + + it('peels the pendant to coreness 1 while the triangle stays at 2', () => { + expect(coreness(r, 'A')).toBe(2); + expect(coreness(r, 'B')).toBe(2); + expect(coreness(r, 'C')).toBe(2); + expect(coreness(r, 'D')).toBe(1); + expect(r.degeneracy).toBe(2); + }); + + it('reports the shell decomposition (k DESC)', () => { + expect(r.shells).toEqual([ + { k: 2, count: 3 }, + { k: 1, count: 1 }, + ]); + }); + + it('A keeps the high degree but shares the core with the triangle', () => { + // A is adjacent to B, C and D -> degree 3, yet coreness 2 (pendant excluded). + expect(r.nodes.find(n => n.id === 'A')?.degree).toBe(3); + }); + + it('sorts nodes coreness DESC, then degree DESC, then id ASC', () => { + // Core nodes first; among the coreness-2 trio, A (degree 3) leads B, C. + expect(r.nodes[0]).toMatchObject({ id: 'A', coreness: 2, degree: 3 }); + expect(r.nodes[r.nodes.length - 1].id).toBe('D'); // the periphery sinks last + }); +}); + +describe('kCoreSize', () => { + const r = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A'), rel('A', 'D')]); + + it('counts nodes with coreness >= k (nested cores)', () => { + expect(kCoreSize(r, 1)).toBe(4); // everyone is in the 1-core + expect(kCoreSize(r, 2)).toBe(3); // only the triangle is in the 2-core + expect(kCoreSize(r, 3)).toBe(0); // no 3-core exists here + }); +}); + +describe('computeGraphCore — normalization & determinism', () => { + it('drops self-loops entirely', () => { + const r = computeGraphCore([rel('A', 'A'), rel('A', 'B'), rel('B', 'C'), rel('C', 'A')]); + expect(r.nodeCount).toBe(3); + expect(r.edgeCount).toBe(3); + expect(r.degeneracy).toBe(2); // self-loop ignored -> plain triangle + }); + + it('collapses parallel edges and ignores direction', () => { + const r = computeGraphCore([ + rel('A', 'B', 'knows'), + rel('B', 'A', 'likes'), + rel('A', 'B', 'trusts'), + rel('B', 'C'), + rel('C', 'A'), + ]); + expect(r.edgeCount).toBe(3); + expect(r.degeneracy).toBe(2); + }); + + it('drops malformed relations (non-string subject/object)', () => { + const malformed = { ...rel('A', 'B'), subject: 42 as unknown as string }; + const r = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A'), malformed]); + expect(r.nodeCount).toBe(3); + expect(r.degeneracy).toBe(2); + }); + + it('treats "Alice" and "alice" as distinct nodes (no case-folding)', () => { + const r = computeGraphCore([rel('Alice', 'Bob'), rel('alice', 'Bob')]); + expect(r.nodeCount).toBe(3); + }); + + it('is order-independent: shuffled input yields identical output', () => { + const edges = [ + rel('A', 'B'), + rel('B', 'C'), + rel('C', 'A'), + rel('A', 'D'), + rel('D', 'B'), + rel('D', 'C'), + rel('E', 'A'), + ]; + const forward = computeGraphCore(edges); + const reversed = computeGraphCore([...edges].reverse()); + const rotated = computeGraphCore([...edges.slice(3), ...edges.slice(0, 3)]); + expect(reversed).toEqual(forward); + expect(rotated).toEqual(forward); + }); +}); diff --git a/app/src/lib/memory/graphCore.ts b/app/src/lib/memory/graphCore.ts new file mode 100644 index 0000000000..31d08291ae --- /dev/null +++ b/app/src/lib/memory/graphCore.ts @@ -0,0 +1,204 @@ +/** + * Graph Core — pure k-core decomposition engine. + * + * The memory knowledge graph is a set of (subject)-[predicate]->(object) + * triples. Where the Centrality lens asks "which entities are important" + * (PageRank), this lens asks a complementary question: "how DEEP in the + * densely-connected core does each entity sit". + * + * The k-core of a graph is the maximal subgraph in which every vertex has at + * least k neighbours INSIDE that subgraph. A vertex's CORENESS is the largest k + * for which it survives in the k-core — found by repeatedly peeling away the + * lowest-degree vertex. The DEGENERACY of the whole graph is the maximum + * coreness: the depth of its densest shell. + * + * Why it matters: coreness separates the load-bearing CORE of the user's + * knowledge (entities mutually reinforced by many interlinked facts, high + * coreness) from the PERIPHERY (leaves and bridges, coreness 1). Neither a + * degree count nor PageRank captures this — a hub with many one-off pendants has + * high degree but coreness 1, while a modest entity embedded in a dense cluster + * has low degree but high coreness. + * + * Everything here is PURE and DETERMINISTIC: no React, no RPC, no clock, no + * randomness. Coreness is a graph INVARIANT (independent of peel order), and the + * peeling is seeded from a canonically id-sorted vertex list, so the same graph + * always yields byte-identical numbers — never dependent on insertion order — + * and every branch is unit-testable. + * + * Load-bearing design choices (do not "fix" without reading the tests): + * - Entity identity is the raw string AS-IS: NO trimming, NO case-folding — + * matching the sibling lenses, so "Alice" / "alice" stay distinct nodes. + * - The graph is UNDIRECTED and SIMPLE: direction is dropped, parallel edges + * (same unordered pair) collapse to one, and self-loops (subject === object) + * are dropped — a self-loop is not a neighbour and cannot deepen a core. + */ +import type { GraphRelation } from '../../utils/tauriCommands/memory'; + +export interface CoreNode { + id: string; + degree: number; // undirected degree in the full graph + coreness: number; // largest k such that this node is in the k-core +} + +export interface CoreShell { + k: number; // coreness level + count: number; // number of nodes whose coreness is EXACTLY k +} + +export interface CoreResult { + nodes: CoreNode[]; // sorted coreness DESC, then degree DESC, then id ASC + nodeCount: number; + edgeCount: number; // distinct undirected edges (self-loops excluded) + degeneracy: number; // max coreness over all nodes (0 if empty) + shells: CoreShell[]; // shell decomposition, sorted k DESC +} + +function isRelation(relation: GraphRelation): boolean { + return typeof relation.subject === 'string' && typeof relation.object === 'string'; +} + +/** Undirected simple-graph adjacency (self-loops and parallel edges removed). */ +function buildAdjacency(relations: GraphRelation[]): Map> { + const adjacency = new Map>(); + const neighbours = (id: string): Set => { + let set = adjacency.get(id); + if (set === undefined) { + set = new Set(); + adjacency.set(id, set); + } + return set; + }; + for (const relation of relations) { + if (!isRelation(relation)) continue; + const { subject, object } = relation; + if (subject === object) continue; + neighbours(subject).add(object); + neighbours(object).add(subject); + } + return adjacency; +} + +/** + * Core numbers via the Batagelj–Zaversnik O(V+E) bucket algorithm. `neighbours` + * is an index-based adjacency list; the returned array holds each vertex's + * coreness at the matching index. The vertex set must already be in a canonical + * order (we feed it id-sorted) so intermediate bucketing is reproducible. + */ +function corenessByIndex(neighbours: number[][]): number[] { + const n = neighbours.length; + const degree = new Array(n); + let maxDegree = 0; + for (let i = 0; i < n; i += 1) { + degree[i] = neighbours[i].length; + if (degree[i] > maxDegree) maxDegree = degree[i]; + } + + // Bin-sort vertices by current degree into vert[], tracking each vertex's + // position in pos[]. bin[d] marks where the degree-d block starts. + const bin = new Array(maxDegree + 1).fill(0); + for (let i = 0; i < n; i += 1) bin[degree[i]] += 1; + let start = 0; + for (let d = 0; d <= maxDegree; d += 1) { + const count = bin[d]; + bin[d] = start; + start += count; + } + const pos = new Array(n); + const vert = new Array(n); + for (let i = 0; i < n; i += 1) { + pos[i] = bin[degree[i]]; + vert[pos[i]] = i; + bin[degree[i]] += 1; + } + // Shift bin starts back: bin[d] now again points at the degree-d block start. + for (let d = maxDegree; d >= 1; d -= 1) bin[d] = bin[d - 1]; + bin[0] = 0; + + // Peel: process vertices in increasing current degree. When a vertex is + // removed, each still-present higher-degree neighbour drops one degree and + // slides one slot left into the next-lower bucket. + for (let i = 0; i < n; i += 1) { + const v = vert[i]; + for (const u of neighbours[v]) { + if (degree[u] > degree[v]) { + const du = degree[u]; + const pu = pos[u]; + const pw = bin[du]; + const w = vert[pw]; + if (u !== w) { + pos[u] = pw; + vert[pu] = w; + pos[w] = pu; + vert[pw] = u; + } + bin[du] += 1; + degree[u] -= 1; + } + } + } + + return degree; // degree[i] is now the core number of vertex i +} + +/** Compute the k-core decomposition of the memory graph. PURE. */ +export function computeGraphCore(relations: GraphRelation[]): CoreResult { + const adjacency = buildAdjacency(relations); + + // Canonical, id-sorted vertex ordering -> reproducible bucketing. + const ids = [...adjacency.keys()].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + const indexOf = new Map(); + ids.forEach((id, i) => indexOf.set(id, i)); + + let edgeDegreeSum = 0; + const neighbours: number[][] = ids.map(id => { + const set = adjacency.get(id); + const list: number[] = []; + if (set) { + for (const other of set) { + const idx = indexOf.get(other); + if (idx !== undefined) list.push(idx); + } + } + edgeDegreeSum += list.length; + return list; + }); + + const coreness = corenessByIndex(neighbours.map(list => [...list])); + + const nodes: CoreNode[] = ids.map((id, i) => ({ + id, + degree: neighbours[i].length, + coreness: coreness[i], + })); + + nodes.sort((a, b) => { + if (b.coreness !== a.coreness) return b.coreness - a.coreness; + if (b.degree !== a.degree) return b.degree - a.degree; + return a.id < b.id ? -1 : a.id > b.id ? 1 : 0; + }); + + let degeneracy = 0; + const shellCounts = new Map(); + for (const node of nodes) { + if (node.coreness > degeneracy) degeneracy = node.coreness; + shellCounts.set(node.coreness, (shellCounts.get(node.coreness) ?? 0) + 1); + } + const shells: CoreShell[] = [...shellCounts.entries()] + .map(([k, count]) => ({ k, count })) + .sort((a, b) => b.k - a.k); + + return { nodes, nodeCount: ids.length, edgeCount: edgeDegreeSum / 2, degeneracy, shells }; +} + +/** + * Size of the k-core: the number of nodes whose coreness is >= k (the nested + * subgraph in which every member has at least k neighbours among the members). + * Pure; derived entirely from the result. + */ +export function kCoreSize(result: CoreResult, k: number): number { + let size = 0; + for (const node of result.nodes) { + if (node.coreness >= k) size += 1; + } + return size; +} diff --git a/app/src/pages/Intelligence.tsx b/app/src/pages/Intelligence.tsx index 130be31234..caa11e7e8b 100644 --- a/app/src/pages/Intelligence.tsx +++ b/app/src/pages/Intelligence.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react'; import { ConfirmationModal } from '../components/intelligence/ConfirmationModal'; import DiagramViewerTab from '../components/intelligence/DiagramViewerTab'; import GraphCentralityTab from '../components/intelligence/GraphCentralityTab'; +import GraphCoreTab from '../components/intelligence/GraphCoreTab'; import IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab'; import IntelligenceTasksTab from '../components/intelligence/IntelligenceTasksTab'; import { MemoryWorkspace } from '../components/intelligence/MemoryWorkspace'; @@ -20,7 +21,7 @@ import type { } from '../types/intelligence'; import AgentWorkflows from './AgentWorkflows'; -type IntelligenceTab = 'memory' | 'subconscious' | 'tasks' | 'workflows' | 'diagram' | 'centrality'; +type IntelligenceTab = 'memory' | 'subconscious' | 'tasks' | 'workflows' | 'diagram' | 'centrality' | 'core'; export default function Intelligence() { const { t } = useT(); @@ -100,6 +101,7 @@ export default function Intelligence() { }, { id: 'diagram', label: t('memory.tab.diagram') }, { id: 'centrality', label: t('memory.tab.centrality') }, + { id: 'core', label: t('memory.tab.core') }, ]; const activeTabDef = tabs.find(tab => tab.id === activeTab); @@ -190,6 +192,8 @@ export default function Intelligence() { {activeTab === 'diagram' && } {activeTab === 'centrality' && } + + {activeTab === 'core' && }
diff --git a/app/src/services/api/graphCoreApi.test.ts b/app/src/services/api/graphCoreApi.test.ts new file mode 100644 index 0000000000..9ff9261e30 --- /dev/null +++ b/app/src/services/api/graphCoreApi.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { computeGraphCore } from '../../lib/memory/graphCore'; +import type { GraphRelation } from '../../utils/tauriCommands/memory'; +import { graphCoreApi, loadCore, loadNamespaces } from './graphCoreApi'; + +const mockGraphQuery = vi.fn(); +const mockListNamespaces = vi.fn(); + +vi.mock('../../utils/tauriCommands/memory', () => ({ + memoryGraphQuery: (...args: unknown[]) => mockGraphQuery(...args), + memoryListNamespaces: (...args: unknown[]) => mockListNamespaces(...args), +})); + +function rel(subject: string, object: string): GraphRelation { + return { + namespace: 'work', + subject, + predicate: 'p', + object, + attrs: {}, + updatedAt: 0, + evidenceCount: 1, + orderIndex: null, + documentIds: [], + chunkIds: [], + }; +} + +describe('graphCoreApi.loadCore', () => { + beforeEach(() => { + mockGraphQuery.mockReset(); + }); + + it('passes the namespace through and returns the engine result for those triples', async () => { + const triples = [rel('A', 'B'), rel('B', 'C'), rel('C', 'A')]; + mockGraphQuery.mockResolvedValueOnce(triples); + const out = await loadCore('work'); + expect(mockGraphQuery).toHaveBeenCalledWith('work'); + expect(out).toEqual(computeGraphCore(triples)); + expect(out.degeneracy).toBe(2); + }); + + it('queries all namespaces when none is given', async () => { + mockGraphQuery.mockResolvedValueOnce([]); + const out = await loadCore(); + expect(mockGraphQuery).toHaveBeenCalledWith(undefined); + expect(out.nodes).toEqual([]); + expect(out.nodeCount).toBe(0); + }); + + it('propagates query errors', async () => { + mockGraphQuery.mockRejectedValueOnce(new Error('graph unavailable')); + await expect(loadCore()).rejects.toThrow('graph unavailable'); + }); +}); + +describe('graphCoreApi.loadNamespaces', () => { + beforeEach(() => { + mockListNamespaces.mockReset(); + }); + + it('returns the namespace list from the RPC', async () => { + mockListNamespaces.mockResolvedValueOnce(['work', 'personal']); + expect(await loadNamespaces()).toEqual(['work', 'personal']); + }); +}); + +describe('graphCoreApi object', () => { + it('exposes the public surface', () => { + expect(typeof graphCoreApi.loadCore).toBe('function'); + expect(typeof graphCoreApi.loadNamespaces).toBe('function'); + }); +}); diff --git a/app/src/services/api/graphCoreApi.ts b/app/src/services/api/graphCoreApi.ts new file mode 100644 index 0000000000..bc33b7dfe4 --- /dev/null +++ b/app/src/services/api/graphCoreApi.ts @@ -0,0 +1,29 @@ +/** + * RPC facade for Graph Core (k-core decomposition). + * + * Adds ZERO new core surface. It composes two already-shipped JSON-RPC wrappers: + * - memoryGraphQuery (openhuman.memory_graph_query) — the triples + * - memoryListNamespaces (openhuman.memory_list_namespaces) — the selector + * and delegates all math to the pure, deterministic engine. Read-only: there is + * no persistence — the result is always reproducible from the current graph. + */ +import debug from 'debug'; + +import { computeGraphCore, type CoreResult } from '../../lib/memory/graphCore'; +import { memoryGraphQuery, memoryListNamespaces } from '../../utils/tauriCommands/memory'; + +const log = debug('graph-core:api'); + +/** Fetch the graph relations for a namespace (or all) and decompose into cores. */ +export async function loadCore(namespace?: string): Promise { + const relations = await memoryGraphQuery(namespace); + log('loadCore namespace=%s relations=%d', namespace ?? '(all)', relations.length); + return computeGraphCore(relations); +} + +/** List the namespaces available for the namespace selector. */ +export async function loadNamespaces(): Promise { + return memoryListNamespaces(); +} + +export const graphCoreApi = { loadCore, loadNamespaces }; From bdf117c571b70daa65183282d64a8d9e97596bed Mon Sep 17 00:00:00 2001 From: Aashir Athar Date: Sat, 30 May 2026 02:14:55 +0500 Subject: [PATCH 2/5] fix(intelligence): preserve self-loop-only entities in Graph Core MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address CodeRabbit review on #2980: previously `computeGraphCore([A→A])` returned an empty graph (the entire relation was dropped, taking the endpoint with it), so a user whose only recorded fact was "Alice→Alice" would vanish into the empty state. Now the self-loop EDGE is still dropped (a self-loop is not a neighbour and cannot deepen a core), but the endpoint is REGISTERED as a node with degree 0 and coreness 0 — so Alice appears in the graph. Adds a regression test for the self-loop-only case, updates the buildAdjacency comment, and refines the module header to spell out the new node-vs-edge distinction. The "missing I18nContext provider" criticals were verified as false positives: I18nContext at lib/i18n/I18nContext.tsx:66 ships a fully functional English default that useT() picks up without a provider, so the existing test pattern (matching the merged graphCentrality tests) works correctly — every test passes. Co-Authored-By: Claude Opus 4.7 --- app/src/lib/i18n/ar.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/bn.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/de.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/es.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/fr.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/hi.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/id.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/it.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/ko.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/pl.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/pt.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/ru.ts | 23 +++++++++++++++++++++++ app/src/lib/i18n/zh-CN.ts | 23 +++++++++++++++++++++++ app/src/lib/memory/graphCore.test.ts | 15 +++++++++++++-- app/src/lib/memory/graphCore.ts | 18 ++++++++++++++---- app/src/pages/Intelligence.tsx | 9 ++++++++- 16 files changed, 334 insertions(+), 7 deletions(-) diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 365517f4a0..9e9c4550b4 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -4063,6 +4063,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': 'لا يمكن تعديل العوامل المدمجة. يمكنك تفعيلها أو تعطيلها أو إعادة ضبطها من قائمة العوامل.', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index bd45e4b692..9cf02cfa58 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -4135,6 +4135,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': 'বিল্ট-ইন এজেন্ট সম্পাদনা করা যাবে না। আপনি এজেন্ট তালিকা থেকে সেগুলো সক্রিয়, নিষ্ক্রিয় বা পুনরায় সেট করতে পারেন।', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 2055080985..b2078d89e2 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -4246,6 +4246,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': 'Integrierte Agenten können nicht bearbeitet werden. Sie können sie in der Agentenliste aktivieren, deaktivieren oder zurücksetzen.', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index d6cd23f668..80118c5571 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -4212,6 +4212,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': 'Los agentes integrados no se pueden editar. Puedes activarlos, desactivarlos o restablecerlos desde la lista de agentes.', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index e8c53c0509..aaaff403a8 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -4229,6 +4229,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': 'Les agents intégrés ne peuvent pas être modifiés. Vous pouvez les activer, les désactiver ou les réinitialiser depuis la liste des agents.', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 8118b4c7e7..26f75658b7 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -4142,6 +4142,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': 'बिल्ट-इन एजेंट संपादित नहीं किए जा सकते। आप एजेंट सूची से उन्हें सक्षम, अक्षम या रीसेट कर सकते हैं।', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index f8d7f68e18..32980c7913 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -4152,6 +4152,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': 'Agen bawaan tidak dapat diedit. Anda dapat mengaktifkan, menonaktifkan, atau meresetnya dari daftar agen.', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index d81b9a8c07..8d769e5902 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -4207,6 +4207,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': "Gli agenti integrati non possono essere modificati. Puoi abilitarli, disabilitarli o reimpostarli dall'elenco degli agenti.", + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 6a518c1e44..f96b15da48 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -4105,6 +4105,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': '기본 제공 에이전트는 편집할 수 없습니다. 에이전트 목록에서 활성화, 비활성화 또는 초기화할 수 있습니다.', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 49391b1298..23d2b946ec 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -4208,6 +4208,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': 'Wbudowanych agentów nie można edytować. Możesz je włączyć, wyłączyć lub zresetować z poziomu listy agentów.', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 2aad0a3cd9..b8d6660a2c 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -4207,6 +4207,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': 'Agentes integrados não podem ser editados. Você pode ativá-los, desativá-los ou redefini-los na lista de agentes.', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index f127453074..f99b4692fe 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -4176,6 +4176,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': 'Встроенные агенты нельзя редактировать. Вы можете включить, отключить или сбросить их в списке агентов.', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 5586cabd61..e9549db840 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -3938,6 +3938,29 @@ const messages: TranslationMap = { 'settings.agents.editor.toolsDone': 'Done', 'settings.agents.editor.builtInReadonly': '内置智能体不可编辑。您可以在智能体列表中启用、禁用或重置它们。', + 'graphCore.colCore': 'Core', + 'graphCore.colEntity': 'Entity', + 'graphCore.colLinks': 'Links', + 'graphCore.colRank': '#', + 'graphCore.coreBadge': 'core', + 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', + 'graphCore.empty': 'No knowledge graph yet.', + 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.errorPrefix': 'Could not load the graph:', + 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.loading': 'Computing core structure…', + 'graphCore.metricConnections': 'Connections', + 'graphCore.metricDegeneracy': 'Degeneracy', + 'graphCore.metricEntities': 'Entities', + 'graphCore.namespaceAll': 'All namespaces', + 'graphCore.namespaceLabel': 'Namespace', + 'graphCore.rankedHeading': 'Deepest-core entities', + 'graphCore.retry': 'Retry', + 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsHeading': 'Shell decomposition', + 'graphCore.title': 'Graph Core', + 'memory.tab.core': 'Core', }; export default messages; diff --git a/app/src/lib/memory/graphCore.test.ts b/app/src/lib/memory/graphCore.test.ts index e3b7bf695d..3e22f6cc1b 100644 --- a/app/src/lib/memory/graphCore.test.ts +++ b/app/src/lib/memory/graphCore.test.ts @@ -116,11 +116,22 @@ describe('kCoreSize', () => { }); describe('computeGraphCore — normalization & determinism', () => { - it('drops self-loops entirely', () => { + it('drops the self-loop EDGE but keeps the endpoint as a node', () => { const r = computeGraphCore([rel('A', 'A'), rel('A', 'B'), rel('B', 'C'), rel('C', 'A')]); expect(r.nodeCount).toBe(3); expect(r.edgeCount).toBe(3); - expect(r.degeneracy).toBe(2); // self-loop ignored -> plain triangle + expect(r.degeneracy).toBe(2); // self-loop edge ignored -> plain triangle + }); + + it('preserves an entity whose only relation is a self-loop (no empty result)', () => { + // A user with the single fact "Alice→Alice" must still see Alice in the + // graph (degree 0, coreness 0), not vanish into the empty state. + const r = computeGraphCore([rel('Alice', 'Alice')]); + expect(r.nodeCount).toBe(1); + expect(r.edgeCount).toBe(0); + expect(r.degeneracy).toBe(0); + expect(r.nodes).toEqual([{ id: 'Alice', degree: 0, coreness: 0 }]); + expect(r.shells).toEqual([{ k: 0, count: 1 }]); }); it('collapses parallel edges and ignores direction', () => { diff --git a/app/src/lib/memory/graphCore.ts b/app/src/lib/memory/graphCore.ts index 31d08291ae..583279ab9e 100644 --- a/app/src/lib/memory/graphCore.ts +++ b/app/src/lib/memory/graphCore.ts @@ -29,8 +29,11 @@ * - Entity identity is the raw string AS-IS: NO trimming, NO case-folding — * matching the sibling lenses, so "Alice" / "alice" stay distinct nodes. * - The graph is UNDIRECTED and SIMPLE: direction is dropped, parallel edges - * (same unordered pair) collapse to one, and self-loops (subject === object) - * are dropped — a self-loop is not a neighbour and cannot deepen a core. + * (same unordered pair) collapse to one, and self-loop EDGES (subject === + * object) are dropped — a self-loop is not a neighbour and cannot deepen a + * core. The endpoint of a self-loop is still REGISTERED as a node, so a + * user whose only recorded fact is "A→A" still appears with coreness 0 + * rather than vanishing into an empty result. */ import type { GraphRelation } from '../../utils/tauriCommands/memory'; @@ -57,7 +60,10 @@ function isRelation(relation: GraphRelation): boolean { return typeof relation.subject === 'string' && typeof relation.object === 'string'; } -/** Undirected simple-graph adjacency (self-loops and parallel edges removed). */ +/** Undirected simple-graph adjacency: self-loop EDGES and parallel edges are + * removed, but a self-loop's endpoint is still registered as a (neighbour-less) + * NODE so a user whose only recorded fact is "A→A" still appears in the graph + * with degree 0 and coreness 0, rather than vanishing into an empty result. */ function buildAdjacency(relations: GraphRelation[]): Map> { const adjacency = new Map>(); const neighbours = (id: string): Set => { @@ -71,7 +77,11 @@ function buildAdjacency(relations: GraphRelation[]): Map> { for (const relation of relations) { if (!isRelation(relation)) continue; const { subject, object } = relation; - if (subject === object) continue; + if (subject === object) { + // Self-loop: register the endpoint as a node but add no self-edge. + neighbours(subject); + continue; + } neighbours(subject).add(object); neighbours(object).add(subject); } diff --git a/app/src/pages/Intelligence.tsx b/app/src/pages/Intelligence.tsx index caa11e7e8b..691c485aca 100644 --- a/app/src/pages/Intelligence.tsx +++ b/app/src/pages/Intelligence.tsx @@ -21,7 +21,14 @@ import type { } from '../types/intelligence'; import AgentWorkflows from './AgentWorkflows'; -type IntelligenceTab = 'memory' | 'subconscious' | 'tasks' | 'workflows' | 'diagram' | 'centrality' | 'core'; +type IntelligenceTab = + | 'memory' + | 'subconscious' + | 'tasks' + | 'workflows' + | 'diagram' + | 'centrality' + | 'core'; export default function Intelligence() { const { t } = useT(); From 02eeb1b7068271871cb3b8f2613b36f5f1514494 Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Sun, 31 May 2026 20:08:44 +0200 Subject: [PATCH 3/5] refactor(intelligence): async/await + diagnostics for Graph Core tab load Convert the namespace-list fetch from a .then()/.catch() chain to async/await with try/catch, and add grep-friendly [graph-core:tab] debug logs across the load lifecycle (start / success / stale / error) so the read-only Core tab's data flow is traceable end-to-end. Addresses @coderabbitai on app/src/components/intelligence/GraphCoreTab.tsx:47. Co-Authored-By: Claude --- .../components/intelligence/GraphCoreTab.tsx | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/app/src/components/intelligence/GraphCoreTab.tsx b/app/src/components/intelligence/GraphCoreTab.tsx index a83bd87eea..95bdb7728a 100644 --- a/app/src/components/intelligence/GraphCoreTab.tsx +++ b/app/src/components/intelligence/GraphCoreTab.tsx @@ -3,6 +3,7 @@ * delegates all rendering to the pure . Read-only — the result * is recomputed from the live graph, never persisted. */ +import debug from 'debug'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useT } from '../../lib/i18n/I18nContext'; @@ -10,6 +11,8 @@ import type { CoreResult } from '../../lib/memory/graphCore'; import { loadCore, loadNamespaces } from '../../services/api/graphCoreApi'; import GraphCorePanel from './GraphCorePanel'; +const log = debug('graph-core:tab'); + const GraphCoreTab = () => { const { t } = useT(); const [result, setResult] = useState(null); @@ -23,14 +26,23 @@ const GraphCoreTab = () => { const load = useCallback(async (ns: string) => { const requestId = (latestRequestId.current += 1); + log('load:start request=%d namespace=%s', requestId, ns || '(all)'); setLoading(true); setError(null); try { const next = await loadCore(ns || undefined); - if (requestId !== latestRequestId.current) return; + if (requestId !== latestRequestId.current) { + log('load:stale request=%d (newer load in flight)', requestId); + return; + } + log('load:success request=%d nodes=%d degeneracy=%d', requestId, next.nodeCount, next.degeneracy); setResult(next); } catch (err) { - if (requestId !== latestRequestId.current) return; + if (requestId !== latestRequestId.current) { + log('load:stale-error request=%d', requestId); + return; + } + log('load:error request=%d %o', requestId, err); setError(err instanceof Error ? err.message : String(err)); } finally { if (requestId === latestRequestId.current) setLoading(false); @@ -40,9 +52,17 @@ const GraphCoreTab = () => { useEffect(() => { // Namespaces are optional UI sugar; a failure to list them must not block // the core view, so swallow that error specifically. - loadNamespaces() - .then(setNamespaces) - .catch(() => setNamespaces([])); + const loadNamespaceOptions = async (): Promise => { + try { + const next = await loadNamespaces(); + log('namespaces:loaded count=%d', next.length); + setNamespaces(next); + } catch (err) { + log('namespaces:error (non-blocking) %o', err); + setNamespaces([]); + } + }; + void loadNamespaceOptions(); void load(''); }, [load]); From 3788f2d97e3509f6ad3d43499aa7cca93155e708 Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Sun, 31 May 2026 20:11:20 +0200 Subject: [PATCH 4/5] fix(intelligence): cap Graph Core shell histogram for high-degeneracy graphs A dense knowledge graph can have many coreness shells (degeneracy 20-50+), which rendered one unbounded bar per shell and silently overflowed the panel. Cap the histogram at the 12 densest shells (shells are sorted k DESC) and fold the remainder into a '+N more shells' footer. Bars now scale against the largest displayed shell so the capped view stays legible. New i18n key graphCore.shellsMore added to en + all locales with real translations. Addresses @staimoorulhassan MAJOR finding on GraphCorePanel.tsx. Co-Authored-By: Claude --- .../intelligence/GraphCorePanel.test.tsx | 25 ++++++++++++++++++ .../intelligence/GraphCorePanel.tsx | 26 ++++++++++++++++--- app/src/lib/i18n/ar.ts | 1 + app/src/lib/i18n/bn.ts | 1 + app/src/lib/i18n/de.ts | 1 + app/src/lib/i18n/en.ts | 1 + app/src/lib/i18n/es.ts | 1 + app/src/lib/i18n/fr.ts | 1 + app/src/lib/i18n/hi.ts | 1 + app/src/lib/i18n/id.ts | 1 + app/src/lib/i18n/it.ts | 1 + app/src/lib/i18n/ko.ts | 1 + app/src/lib/i18n/pl.ts | 1 + app/src/lib/i18n/pt.ts | 1 + app/src/lib/i18n/ru.ts | 1 + app/src/lib/i18n/zh-CN.ts | 1 + 16 files changed, 62 insertions(+), 3 deletions(-) diff --git a/app/src/components/intelligence/GraphCorePanel.test.tsx b/app/src/components/intelligence/GraphCorePanel.test.tsx index 25a46fce54..0d338d7d59 100644 --- a/app/src/components/intelligence/GraphCorePanel.test.tsx +++ b/app/src/components/intelligence/GraphCorePanel.test.tsx @@ -23,6 +23,20 @@ function rel(subject: string, object: string): GraphRelation { // Triangle A-B-C (2-core) plus pendant D off A -> degeneracy 2, shells {2:3,1:1}. const cored = computeGraphCore([rel('A', 'B'), rel('B', 'C'), rel('C', 'A'), rel('A', 'D')]); +// All pairs of a `size`-node clique -> every member has coreness size-1. +function clique(prefix: string, size: number): GraphRelation[] { + const out: GraphRelation[] = []; + for (let i = 0; i < size; i += 1) { + for (let j = i + 1; j < size; j += 1) out.push(rel(`${prefix}${i}`, `${prefix}${j}`)); + } + return out; +} +// 14 disjoint cliques of sizes 2..15 -> 14 distinct coreness levels (1..14), +// i.e. 14 shells, exceeding the MAX_SHELLS=12 histogram cap by 2. +const manyShells = computeGraphCore( + Array.from({ length: 14 }, (_, idx) => clique(`c${idx}_`, idx + 2)).flat() +); + describe('', () => { it('renders the loading skeleton', () => { render(); @@ -61,4 +75,15 @@ describe('', () => { // three triangle members carry the core badge; the pendant D does not. expect(screen.getAllByText('core')).toHaveLength(3); }); + + it('caps the shell histogram and folds the rest into a "+N more" footer', () => { + expect(manyShells.shells.length).toBe(14); + render(); + // densest shells survive the cap; the two shallowest (1-core, 2-core) fold away. + expect(screen.getByText('14-core')).toBeInTheDocument(); + expect(screen.getByText('3-core')).toBeInTheDocument(); + expect(screen.queryByText('2-core')).not.toBeInTheDocument(); + expect(screen.queryByText('1-core')).not.toBeInTheDocument(); + expect(screen.getByText('+2 more shells')).toBeInTheDocument(); + }); }); diff --git a/app/src/components/intelligence/GraphCorePanel.tsx b/app/src/components/intelligence/GraphCorePanel.tsx index b634c9170b..885f7245eb 100644 --- a/app/src/components/intelligence/GraphCorePanel.tsx +++ b/app/src/components/intelligence/GraphCorePanel.tsx @@ -9,6 +9,11 @@ import { useT } from '../../lib/i18n/I18nContext'; import { type CoreResult, kCoreSize } from '../../lib/memory/graphCore'; const MAX_ROWS = 25; +// Cap the shell histogram so a high-degeneracy graph (many coreness levels) +// can't render an unbounded run of bars and overflow the panel. Shells are +// sorted densest-first (k DESC), so the cap keeps the most meaningful deep +// shells and folds the rest into a "+N more" footer. +const MAX_SHELLS = 12; interface GraphCorePanelProps { result: CoreResult | null; @@ -24,10 +29,20 @@ const GraphCorePanel = ({ result, loading, error, onRetry }: GraphCorePanelProps () => (result ? kCoreSize(result, result.degeneracy) : 0), [result] ); - const maxShellCount = useMemo( - () => (result ? result.shells.reduce((max, s) => (s.count > max ? s.count : max), 0) : 0), + const displayedShells = useMemo( + () => (result ? result.shells.slice(0, MAX_SHELLS) : []), [result] ); + const hiddenShellCount = useMemo( + () => (result ? result.shells.length - displayedShells.length : 0), + [result, displayedShells] + ); + // Scale bars against the largest displayed shell so the capped view stays + // legible even when a hidden periphery shell holds the global maximum. + const maxShellCount = useMemo( + () => displayedShells.reduce((max, s) => (s.count > max ? s.count : max), 0), + [displayedShells] + ); const intro = (
    - {result.shells.map(shell => ( + {displayedShells.map(shell => (
  • {t('graphCore.shellLabel').replace('{k}', String(shell.k))} @@ -161,6 +176,11 @@ const GraphCorePanel = ({ result, loading, error, onRetry }: GraphCorePanelProps
  • ))}
+ {hiddenShellCount > 0 && ( +

+ {t('graphCore.shellsMore').replace('{count}', String(hiddenShellCount))} +

+ )} {/* Ranked deepest-core entities */} diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 90922b5128..5a4e207623 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -4284,6 +4284,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} طبقة إضافية', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index b6af87dbd4..339e599b43 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -4359,6 +4359,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+আরও {count}টি স্তর', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 865aefa236..fe1b37d87e 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -4474,6 +4474,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} weitere Schalen', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index d69ed20c8e..561953ea8b 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -507,6 +507,7 @@ const en: TranslationMap = { 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} more shells', 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.colRank': '#', 'graphCore.colEntity': 'Entity', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 4e1ef3c44b..e2f942f1c0 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -4442,6 +4442,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} capas más', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 32a1397cc5..e9981ac3c9 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -4457,6 +4457,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} couches supplémentaires', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index f46104bd33..2ef2437f0a 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -4367,6 +4367,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} और परतें', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 8c7e5b264d..5ced484e13 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -4376,6 +4376,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} lapisan lainnya', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 6b4ed29aa7..1c70e39ebe 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -4434,6 +4434,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} livelli in più', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index e94522f790..b398c01932 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -4326,6 +4326,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '레이어 {count}개 더', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 2c1c901967..5196017160 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -4433,6 +4433,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} więcej warstw', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index ca122a2b1c..0b76492ddc 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -4431,6 +4431,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} camadas a mais', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 725cdf9c9f..4099712fe4 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -4401,6 +4401,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} слоёв', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 3282bb9474..97c7ab424a 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -4150,6 +4150,7 @@ const messages: TranslationMap = { 'graphCore.rankedHeading': 'Deepest-core entities', 'graphCore.retry': 'Retry', 'graphCore.shellLabel': '{k}-core', + 'graphCore.shellsMore': '+{count} 个层', 'graphCore.shellsHeading': 'Shell decomposition', 'graphCore.title': 'Graph Core', 'memory.tab.core': 'Core', From 20186243cdeee22943a459dbe18f9c29d74a864c Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Sun, 31 May 2026 20:12:57 +0200 Subject: [PATCH 5/5] chore(pr-fix): apply formatting Prettier: wrap the [graph-core:tab] success log and the over-length graphCore.* locale strings to the configured print width. Co-Authored-By: Claude --- app/src/components/intelligence/GraphCoreTab.tsx | 7 ++++++- app/src/lib/i18n/ar.ts | 9 ++++++--- app/src/lib/i18n/bn.ts | 9 ++++++--- app/src/lib/i18n/de.ts | 9 ++++++--- app/src/lib/i18n/es.ts | 9 ++++++--- app/src/lib/i18n/fr.ts | 9 ++++++--- app/src/lib/i18n/hi.ts | 9 ++++++--- app/src/lib/i18n/id.ts | 9 ++++++--- app/src/lib/i18n/it.ts | 9 ++++++--- app/src/lib/i18n/ko.ts | 9 ++++++--- app/src/lib/i18n/pl.ts | 9 ++++++--- app/src/lib/i18n/pt.ts | 9 ++++++--- app/src/lib/i18n/ru.ts | 9 ++++++--- app/src/lib/i18n/zh-CN.ts | 9 ++++++--- 14 files changed, 84 insertions(+), 40 deletions(-) diff --git a/app/src/components/intelligence/GraphCoreTab.tsx b/app/src/components/intelligence/GraphCoreTab.tsx index 95bdb7728a..93635a36b1 100644 --- a/app/src/components/intelligence/GraphCoreTab.tsx +++ b/app/src/components/intelligence/GraphCoreTab.tsx @@ -35,7 +35,12 @@ const GraphCoreTab = () => { log('load:stale request=%d (newer load in flight)', requestId); return; } - log('load:success request=%d nodes=%d degeneracy=%d', requestId, next.nodeCount, next.degeneracy); + log( + 'load:success request=%d nodes=%d degeneracy=%d', + requestId, + next.nodeCount, + next.degeneracy + ); setResult(next); } catch (err) { if (requestId !== latestRequestId.current) { diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 5a4e207623..25667f0e72 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -4269,12 +4269,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index 339e599b43..80ac61424a 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -4344,12 +4344,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index fe1b37d87e..fef0f5e14c 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -4459,12 +4459,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index e2f942f1c0..277640ba17 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -4427,12 +4427,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index e9981ac3c9..1bb8c77f2d 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -4442,12 +4442,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 2ef2437f0a..37abf6244a 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -4352,12 +4352,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 5ced484e13..ddd7b2f67b 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -4361,12 +4361,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 1c70e39ebe..9127c42109 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -4419,12 +4419,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index b398c01932..5ddb19204f 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -4311,12 +4311,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 5196017160..669cca66de 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -4418,12 +4418,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 0b76492ddc..ef24cdb8d6 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -4416,12 +4416,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 4099712fe4..992a691580 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -4386,12 +4386,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 97c7ab424a..fd4814fd3a 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -4135,12 +4135,15 @@ const messages: TranslationMap = { 'graphCore.colLinks': 'Links', 'graphCore.colRank': '#', 'graphCore.coreBadge': 'core', - 'graphCore.coreTitle': 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', + 'graphCore.coreTitle': + 'In the densest {degeneracy}-core — every member here keeps at least {degeneracy} links to other core members.', 'graphCore.degeneracyCaption': 'Densest shell: {degeneracy}-core · {coreSize} entities', 'graphCore.empty': 'No knowledge graph yet.', - 'graphCore.emptyHint': 'As the assistant records connected facts about you, the densely-linked core will surface here.', + 'graphCore.emptyHint': + 'As the assistant records connected facts about you, the densely-linked core will surface here.', 'graphCore.errorPrefix': 'Could not load the graph:', - 'graphCore.intro': 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', + 'graphCore.intro': + 'k-core decomposition separates the load-bearing core of your knowledge — entities mutually reinforced by many interlinked facts — from the periphery of leaves and bridges. A high-degree hub of one-off facts still has coreness 1; depth, not degree, marks the core.', 'graphCore.loading': 'Computing core structure…', 'graphCore.metricConnections': 'Connections', 'graphCore.metricDegeneracy': 'Degeneracy',