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..04419faa78
--- /dev/null
+++ b/app/src/components/intelligence/GraphCorePanel.tsx
@@ -0,0 +1,245 @@
+/**
+ * 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;
+const MAX_SHELLS = 10;
+
+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.slice(0, MAX_SHELLS).map(shell => (
+ -
+
+ {t('graphCore.shellLabel').replace('{k}', String(shell.k))}
+
+
+
+ {shell.count}
+
+
+ ))}
+ {result.shells.length > MAX_SHELLS && (
+ -
+ {t('graphCore.shellsMore').replace(
+ '{count}',
+ String(result.shells.length - MAX_SHELLS)
+ )}
+
+ )}
+
+
+
+ {/* Ranked deepest-core entities */}
+
+
+ {t('graphCore.rankedHeading')}
+
+
+
+
+ |
+ {t('graphCore.colRank')}
+ |
+
+ {t('graphCore.colEntity')}
+ |
+
+ {t('graphCore.colCore')}
+ |
+
+ {t('graphCore.colLinks')}
+ |
+
+
+
+ {rows.map((node, i) => (
+
+ | {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/ar.ts b/app/src/lib/i18n/ar.ts
index be57d0c4c7..77d8713e11 100644
--- a/app/src/lib/i18n/ar.ts
+++ b/app/src/lib/i18n/ar.ts
@@ -4359,6 +4359,32 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': 'متوسط التجميع {avg} · التعدّي {transitivity}',
'graphCohesion.title': 'تماسك الرسم البياني',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': 'النواة',
+ 'graphCore.title': 'نواة الرسم البياني',
+ 'graphCore.intro':
+ 'يفصل تحليل k-core النواة الحاملة للمعرفة — الكيانات التي يعززها عدد كبير من الحقائق المترابطة — عن هوامش الأوراق والجسور. حتى المحور عالي الدرجة المليء بحقائق منفردة يبقى coreness 1؛ العمق لا الدرجة هو ما يميز النواة.',
+ 'graphCore.loading': 'جارٍ حساب بنية النواة…',
+ 'graphCore.errorPrefix': 'تعذّر تحميل الرسم البياني:',
+ 'graphCore.retry': 'إعادة المحاولة',
+ 'graphCore.empty': 'لا يوجد رسم بياني للمعرفة بعد.',
+ 'graphCore.emptyHint': 'عندما يسجّل المساعد حقائق مترابطة عنك، ستظهر النواة كثيفة الروابط هنا.',
+ 'graphCore.namespaceLabel': 'مساحة الاسم',
+ 'graphCore.namespaceAll': 'جميع مساحات الاسم',
+ 'graphCore.metricEntities': 'الكيانات',
+ 'graphCore.metricConnections': 'الاتصالات',
+ 'graphCore.metricDegeneracy': 'الانحلالية',
+ 'graphCore.degeneracyCaption': 'أكثف قشرة: {degeneracy}-core · {coreSize} كيانات',
+ 'graphCore.shellsHeading': 'تحليل القشرة',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… و{count} أصداف أخرى',
+ 'graphCore.rankedHeading': 'كيانات النواة الأعمق',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'الكيان',
+ 'graphCore.colCore': 'النواة',
+ 'graphCore.colLinks': 'الروابط',
+ 'graphCore.coreBadge': 'نواة',
+ 'graphCore.coreTitle':
+ 'في أكثف {degeneracy}-core — كل عضو هنا يحافظ على {degeneracy} روابط على الأقل مع أعضاء النواة الآخرين.',
};
export default messages;
diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts
index 9ec2d1481b..7266676cd3 100644
--- a/app/src/lib/i18n/bn.ts
+++ b/app/src/lib/i18n/bn.ts
@@ -4434,6 +4434,33 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': 'গড় ক্লাস্টারিং {avg} · সংক্রমণতা {transitivity}',
'graphCohesion.title': 'গ্রাফ সংসক্তি',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': 'মূল',
+ 'graphCore.title': 'গ্রাফ কোর',
+ 'graphCore.intro':
+ 'k-core বিশ্লেষণ আপনার জ্ঞানের ভারবাহী মূল অংশকে আলাদা করে — এমন সত্তাগুলি যেগুলি পরস্পর সংযুক্ত অনেক তথ্য দ্বারা পারস্পরিকভাবে শক্তিশালী হয় — পাতা ও সেতুর প্রান্ত থেকে। একক তথ্যসমৃদ্ধ উচ্চ-ডিগ্রি হাবেরও coreness ১ থাকে; গভীরতা, ডিগ্রি নয়, মূলকে চিহ্নিত করে।',
+ 'graphCore.loading': 'কোর কাঠামো গণনা করা হচ্ছে…',
+ 'graphCore.errorPrefix': 'গ্রাফ লোড করা যায়নি:',
+ 'graphCore.retry': 'পুনরায় চেষ্টা করুন',
+ 'graphCore.empty': 'এখনো কোনো জ্ঞান গ্রাফ নেই।',
+ 'graphCore.emptyHint':
+ 'সহকারী আপনার সম্পর্কে সংযুক্ত তথ্য রেকর্ড করার সাথে সাথে ঘন-সংযুক্ত কোরটি এখানে প্রকাশ পাবে।',
+ 'graphCore.namespaceLabel': 'নেমস্পেস',
+ 'graphCore.namespaceAll': 'সমস্ত নেমস্পেস',
+ 'graphCore.metricEntities': 'সত্তা',
+ 'graphCore.metricConnections': 'সংযোগ',
+ 'graphCore.metricDegeneracy': 'ডিজেনারেসি',
+ 'graphCore.degeneracyCaption': 'সর্বাধিক ঘন শেল: {degeneracy}-core · {coreSize} সত্তা',
+ 'graphCore.shellsHeading': 'শেল বিশ্লেষণ',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… এবং আরো {count}টি শেল',
+ 'graphCore.rankedHeading': 'গভীরতম কোর সত্তা',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'সত্তা',
+ 'graphCore.colCore': 'কোর',
+ 'graphCore.colLinks': 'লিঙ্ক',
+ 'graphCore.coreBadge': 'কোর',
+ 'graphCore.coreTitle':
+ 'সবচেয়ে ঘন {degeneracy}-core-এ — এখানকার প্রতিটি সদস্য অন্য কোর সদস্যদের সাথে কমপক্ষে {degeneracy}টি লিঙ্ক বজায় রাখে।',
};
export default messages;
diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts
index b3ae7764b3..f8c1e6f3f7 100644
--- a/app/src/lib/i18n/de.ts
+++ b/app/src/lib/i18n/de.ts
@@ -4551,6 +4551,33 @@ const messages: TranslationMap = {
'Durchschnittliches Clustering {avg} · Transitivität {transitivity}',
'graphCohesion.title': 'Graph-Kohäsion',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': 'Kern',
+ 'graphCore.title': 'Graph-Kern',
+ 'graphCore.intro':
+ 'Die k-core-Zerlegung trennt den tragenden Kern Ihres Wissens — Entitäten, die durch viele vernetzte Fakten gegenseitig gestärkt werden — von der Peripherie aus Blättern und Brücken. Selbst ein hochgradiger Hub mit einmaligen Fakten hat coreness 1; Tiefe, nicht Grad, kennzeichnet den Kern.',
+ 'graphCore.loading': 'Kernstruktur wird berechnet…',
+ 'graphCore.errorPrefix': 'Graph konnte nicht geladen werden:',
+ 'graphCore.retry': 'Wiederholen',
+ 'graphCore.empty': 'Noch kein Wissensgraph.',
+ 'graphCore.emptyHint':
+ 'Sobald der Assistent verknüpfte Fakten über Sie erfasst, erscheint der dicht vernetzte Kern hier.',
+ 'graphCore.namespaceLabel': 'Namensraum',
+ 'graphCore.namespaceAll': 'Alle Namensräume',
+ 'graphCore.metricEntities': 'Entitäten',
+ 'graphCore.metricConnections': 'Verbindungen',
+ 'graphCore.metricDegeneracy': 'Degeneriertheit',
+ 'graphCore.degeneracyCaption': 'Dichteste Schale: {degeneracy}-core · {coreSize} Entitäten',
+ 'graphCore.shellsHeading': 'Schalenzerlegung',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… und {count} weitere Schalen',
+ 'graphCore.rankedHeading': 'Entitäten mit tiefstem Kern',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'Entität',
+ 'graphCore.colCore': 'Kern',
+ 'graphCore.colLinks': 'Verknüpfungen',
+ 'graphCore.coreBadge': 'Kern',
+ 'graphCore.coreTitle':
+ 'Im dichtesten {degeneracy}-core — jedes Mitglied hier hält mindestens {degeneracy} Verbindungen zu anderen Kernmitgliedern.',
};
export default messages;
diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts
index 2a61c636fb..aa0769f5a1 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.namespaces': 'Namespaces',
'memory.tab.timeline': 'Timeline',
'memory.tab.cohesion': 'Cohesion',
@@ -488,6 +489,33 @@ const en: TranslationMap = {
'graphCohesion.brokerTitle':
"Structural hole: this entity's neighbours aren't connected to each other — it's the sole link between them.",
+ '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.shellsMore': '… and {count} more shells',
+ '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/i18n/es.ts b/app/src/lib/i18n/es.ts
index 42d9faf057..c1f6f46f14 100644
--- a/app/src/lib/i18n/es.ts
+++ b/app/src/lib/i18n/es.ts
@@ -4520,6 +4520,33 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': 'Agrupamiento promedio {avg} · transitividad {transitivity}',
'graphCohesion.title': 'Cohesión del grafo',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': 'Núcleo',
+ 'graphCore.title': 'Núcleo del grafo',
+ 'graphCore.intro':
+ 'La descomposición k-core separa el núcleo estructural de tu conocimiento — entidades reforzadas mutuamente por muchos hechos interconectados — de la periferia de hojas y puentes. Incluso un hub de alto grado con hechos únicos tiene coreness 1; la profundidad, no el grado, marca el núcleo.',
+ 'graphCore.loading': 'Calculando la estructura del núcleo…',
+ 'graphCore.errorPrefix': 'No se pudo cargar el grafo:',
+ 'graphCore.retry': 'Reintentar',
+ 'graphCore.empty': 'Aún no hay grafo de conocimiento.',
+ 'graphCore.emptyHint':
+ 'A medida que el asistente registre hechos conectados sobre ti, el núcleo densamente enlazado aparecerá aquí.',
+ 'graphCore.namespaceLabel': 'Espacio de nombres',
+ 'graphCore.namespaceAll': 'Todos los espacios de nombres',
+ 'graphCore.metricEntities': 'Entidades',
+ 'graphCore.metricConnections': 'Conexiones',
+ 'graphCore.metricDegeneracy': 'Degeneración',
+ 'graphCore.degeneracyCaption': 'Capa más densa: {degeneracy}-core · {coreSize} entidades',
+ 'graphCore.shellsHeading': 'Descomposición en capas',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… y {count} capas más',
+ 'graphCore.rankedHeading': 'Entidades del núcleo más profundo',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'Entidad',
+ 'graphCore.colCore': 'Núcleo',
+ 'graphCore.colLinks': 'Vínculos',
+ 'graphCore.coreBadge': 'núcleo',
+ 'graphCore.coreTitle':
+ 'En el {degeneracy}-core más denso — cada miembro aquí mantiene al menos {degeneracy} vínculos con otros miembros del núcleo.',
};
export default messages;
diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts
index d76e393c00..4050ad4d16 100644
--- a/app/src/lib/i18n/fr.ts
+++ b/app/src/lib/i18n/fr.ts
@@ -4536,6 +4536,33 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': 'Regroupement moyen {avg} · transitivité {transitivity}',
'graphCohesion.title': 'Cohésion du graphe',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': 'Noyau',
+ 'graphCore.title': 'Noyau du graphe',
+ 'graphCore.intro':
+ 'La décomposition k-core sépare le noyau porteur de votre connaissance — des entités mutuellement renforcées par de nombreux faits interconnectés — de la périphérie des feuilles et des ponts. Même un hub de haut degré avec des faits ponctuels a une coreness de 1 ; la profondeur, et non le degré, marque le noyau.',
+ 'graphCore.loading': 'Calcul de la structure du noyau…',
+ 'graphCore.errorPrefix': 'Impossible de charger le graphe :',
+ 'graphCore.retry': 'Réessayer',
+ 'graphCore.empty': "Aucun graphe de connaissance pour l'instant.",
+ 'graphCore.emptyHint':
+ "Au fur et à mesure que l'assistant enregistre des faits connectés vous concernant, le noyau densément lié apparaîtra ici.",
+ 'graphCore.namespaceLabel': 'Espace de noms',
+ 'graphCore.namespaceAll': 'Tous les espaces de noms',
+ 'graphCore.metricEntities': 'Entités',
+ 'graphCore.metricConnections': 'Connexions',
+ 'graphCore.metricDegeneracy': 'Dégénérescence',
+ 'graphCore.degeneracyCaption': 'Couche la plus dense : {degeneracy}-core · {coreSize} entités',
+ 'graphCore.shellsHeading': 'Décomposition en couches',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… et {count} coques de plus',
+ 'graphCore.rankedHeading': 'Entités du noyau le plus profond',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'Entité',
+ 'graphCore.colCore': 'Noyau',
+ 'graphCore.colLinks': 'Liens',
+ 'graphCore.coreBadge': 'noyau',
+ 'graphCore.coreTitle':
+ "Dans le {degeneracy}-core le plus dense — chaque membre ici maintient au moins {degeneracy} liens vers d'autres membres du noyau.",
};
export default messages;
diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts
index 5c149a403b..955f1c778f 100644
--- a/app/src/lib/i18n/hi.ts
+++ b/app/src/lib/i18n/hi.ts
@@ -4442,6 +4442,33 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': 'औसत क्लस्टरिंग {avg} · सकर्मकता {transitivity}',
'graphCohesion.title': 'ग्राफ संसक्ति',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': 'केंद्र',
+ 'graphCore.title': 'ग्राफ केंद्र',
+ 'graphCore.intro':
+ 'k-core विघटन आपके ज्ञान के भार-वाहक केंद्र को अलग करता है — ऐसी संस्थाएँ जो कई परस्पर जुड़े तथ्यों द्वारा परस्पर सुदृढ़ होती हैं — पत्तियों और पुलों की परिधि से। एकल तथ्यों का उच्च-डिग्री हब भी coreness 1 रखता है; गहराई, डिग्री नहीं, केंद्र को परिभाषित करती है।',
+ 'graphCore.loading': 'केंद्र संरचना की गणना हो रही है…',
+ 'graphCore.errorPrefix': 'ग्राफ लोड नहीं हो सका:',
+ 'graphCore.retry': 'पुनः प्रयास',
+ 'graphCore.empty': 'अभी तक कोई ज्ञान ग्राफ नहीं।',
+ 'graphCore.emptyHint':
+ 'जैसे-जैसे सहायक आपके बारे में जुड़े हुए तथ्य दर्ज करेगा, घनिष्ठ-जुड़ा केंद्र यहाँ उभर कर आएगा।',
+ 'graphCore.namespaceLabel': 'नामस्थान',
+ 'graphCore.namespaceAll': 'सभी नामस्थान',
+ 'graphCore.metricEntities': 'संस्थाएँ',
+ 'graphCore.metricConnections': 'कनेक्शन',
+ 'graphCore.metricDegeneracy': 'अपभ्रंशता',
+ 'graphCore.degeneracyCaption': 'सबसे घना आवरण: {degeneracy}-core · {coreSize} संस्थाएँ',
+ 'graphCore.shellsHeading': 'आवरण विघटन',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… और {count} और शेल',
+ 'graphCore.rankedHeading': 'सबसे गहरे केंद्र की संस्थाएँ',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'संस्था',
+ 'graphCore.colCore': 'केंद्र',
+ 'graphCore.colLinks': 'लिंक',
+ 'graphCore.coreBadge': 'केंद्र',
+ 'graphCore.coreTitle':
+ 'सबसे घने {degeneracy}-core में — यहाँ का प्रत्येक सदस्य अन्य केंद्र सदस्यों के साथ कम से कम {degeneracy} लिंक बनाए रखता है।',
};
export default messages;
diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts
index c8542b0c47..f1cfe0cede 100644
--- a/app/src/lib/i18n/id.ts
+++ b/app/src/lib/i18n/id.ts
@@ -4451,6 +4451,33 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': 'Pengelompokan rata-rata {avg} · transitivitas {transitivity}',
'graphCohesion.title': 'Kohesi Graf',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': 'Inti',
+ 'graphCore.title': 'Inti Graf',
+ 'graphCore.intro':
+ 'Dekomposisi k-core memisahkan inti penopang pengetahuan Anda — entitas yang saling diperkuat oleh banyak fakta yang saling terhubung — dari tepi berupa daun dan jembatan. Hub berderajat tinggi dari fakta-fakta sekali pakai pun tetap memiliki coreness 1; kedalaman, bukan derajat, yang menandai inti.',
+ 'graphCore.loading': 'Menghitung struktur inti…',
+ 'graphCore.errorPrefix': 'Gagal memuat graf:',
+ 'graphCore.retry': 'Coba lagi',
+ 'graphCore.empty': 'Belum ada graf pengetahuan.',
+ 'graphCore.emptyHint':
+ 'Saat asisten mencatat fakta-fakta terhubung tentang Anda, inti yang padat-terhubung akan muncul di sini.',
+ 'graphCore.namespaceLabel': 'Namespace',
+ 'graphCore.namespaceAll': 'Semua namespace',
+ 'graphCore.metricEntities': 'Entitas',
+ 'graphCore.metricConnections': 'Koneksi',
+ 'graphCore.metricDegeneracy': 'Degenerasi',
+ 'graphCore.degeneracyCaption': 'Cangkang paling padat: {degeneracy}-core · {coreSize} entitas',
+ 'graphCore.shellsHeading': 'Dekomposisi cangkang',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… dan {count} shell lainnya',
+ 'graphCore.rankedHeading': 'Entitas inti terdalam',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'Entitas',
+ 'graphCore.colCore': 'Inti',
+ 'graphCore.colLinks': 'Tautan',
+ 'graphCore.coreBadge': 'inti',
+ 'graphCore.coreTitle':
+ 'Dalam {degeneracy}-core paling padat — setiap anggota di sini mempertahankan setidaknya {degeneracy} tautan ke anggota inti lainnya.',
};
export default messages;
diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts
index abe89fd4c3..35c462e019 100644
--- a/app/src/lib/i18n/it.ts
+++ b/app/src/lib/i18n/it.ts
@@ -4512,6 +4512,33 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': 'Raggruppamento medio {avg} · transitività {transitivity}',
'graphCohesion.title': 'Coesione del grafo',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': 'Nucleo',
+ 'graphCore.title': 'Nucleo del grafo',
+ 'graphCore.intro':
+ 'La decomposizione k-core separa il nucleo portante della tua conoscenza — entità mutuamente rinforzate da molti fatti interconnessi — dalla periferia di foglie e ponti. Anche un hub di alto grado con fatti isolati ha coreness 1; la profondità, non il grado, contraddistingue il nucleo.',
+ 'graphCore.loading': 'Calcolo della struttura del nucleo…',
+ 'graphCore.errorPrefix': 'Impossibile caricare il grafo:',
+ 'graphCore.retry': 'Riprova',
+ 'graphCore.empty': 'Nessun grafo della conoscenza ancora.',
+ 'graphCore.emptyHint':
+ "Man mano che l'assistente registra fatti connessi su di te, il nucleo densamente collegato apparirà qui.",
+ 'graphCore.namespaceLabel': 'Spazio dei nomi',
+ 'graphCore.namespaceAll': 'Tutti gli spazi dei nomi',
+ 'graphCore.metricEntities': 'Entità',
+ 'graphCore.metricConnections': 'Connessioni',
+ 'graphCore.metricDegeneracy': 'Degenerazione',
+ 'graphCore.degeneracyCaption': 'Guscio più denso: {degeneracy}-core · {coreSize} entità',
+ 'graphCore.shellsHeading': 'Decomposizione in gusci',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… e altri {count} gusci',
+ 'graphCore.rankedHeading': 'Entità del nucleo più profondo',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'Entità',
+ 'graphCore.colCore': 'Nucleo',
+ 'graphCore.colLinks': 'Collegamento',
+ 'graphCore.coreBadge': 'nucleo',
+ 'graphCore.coreTitle':
+ 'Nel {degeneracy}-core più denso — ogni membro qui mantiene almeno {degeneracy} collegamenti verso altri membri del nucleo.',
};
export default messages;
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index 855b0f4c45..fa38d7ca64 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -4401,6 +4401,32 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': '평균 군집계수 {avg} · 전이성 {transitivity}',
'graphCohesion.title': '그래프 응집도',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': '핵심',
+ 'graphCore.title': '그래프 핵심',
+ 'graphCore.intro':
+ 'k-core 분해는 지식의 핵심 구조 — 수많은 연결된 사실에 의해 상호 강화되는 개체들 — 를 잎과 다리의 주변부로부터 분리합니다. 일회성 사실로 이루어진 고차수 허브도 coreness가 1에 불과합니다; 핵심을 나타내는 것은 차수가 아닌 깊이입니다.',
+ 'graphCore.loading': '핵심 구조 계산 중…',
+ 'graphCore.errorPrefix': '그래프를 불러올 수 없습니다:',
+ 'graphCore.retry': '다시 시도',
+ 'graphCore.empty': '아직 지식 그래프가 없습니다.',
+ 'graphCore.emptyHint': '어시스턴트가 연결된 사실을 기록하면 밀집된 핵심이 여기에 나타납니다.',
+ 'graphCore.namespaceLabel': '네임스페이스',
+ 'graphCore.namespaceAll': '모든 네임스페이스',
+ 'graphCore.metricEntities': '개체',
+ 'graphCore.metricConnections': '연결',
+ 'graphCore.metricDegeneracy': '퇴화도',
+ 'graphCore.degeneracyCaption': '가장 밀집된 셸: {degeneracy}-core · {coreSize}개 개체',
+ 'graphCore.shellsHeading': '셸 분해',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… 그리고 {count}개의 셸 더',
+ 'graphCore.rankedHeading': '가장 깊은 핵심 개체',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': '개체',
+ 'graphCore.colCore': '핵심',
+ 'graphCore.colLinks': '링크',
+ 'graphCore.coreBadge': '핵심',
+ 'graphCore.coreTitle':
+ '가장 밀집된 {degeneracy}-core — 여기의 모든 구성원은 다른 핵심 구성원과 최소 {degeneracy}개의 링크를 유지합니다.',
};
export default messages;
diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts
index 27c9d94351..d00991e6e8 100644
--- a/app/src/lib/i18n/pl.ts
+++ b/app/src/lib/i18n/pl.ts
@@ -4509,6 +4509,33 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': 'Średnia klasteryzacja {avg} · tranzytywność {transitivity}',
'graphCohesion.title': 'Spójność grafu',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': 'Rdzeń',
+ 'graphCore.title': 'Rdzeń grafu',
+ 'graphCore.intro':
+ 'Dekompozycja k-core oddziela nośny rdzeń wiedzy — encje wzajemnie wzmacniane przez wiele powiązanych faktów — od peryferii złożonej z liści i mostów. Nawet wysoce połączony węzeł z jednostkowymi faktami ma coreness równy 1; to głębokość, a nie stopień, wyznacza rdzeń.',
+ 'graphCore.loading': 'Obliczanie struktury rdzenia…',
+ 'graphCore.errorPrefix': 'Nie można załadować grafu:',
+ 'graphCore.retry': 'Spróbuj ponownie',
+ 'graphCore.empty': 'Brak grafu wiedzy.',
+ 'graphCore.emptyHint':
+ 'Gdy asystent zarejestruje połączone fakty na Twój temat, gęsto powiązany rdzeń pojawi się tutaj.',
+ 'graphCore.namespaceLabel': 'Przestrzeń nazw',
+ 'graphCore.namespaceAll': 'Wszystkie przestrzenie nazw',
+ 'graphCore.metricEntities': 'Encje',
+ 'graphCore.metricConnections': 'Połączenia',
+ 'graphCore.metricDegeneracy': 'Degeneracja',
+ 'graphCore.degeneracyCaption': 'Najgęstsza powłoka: {degeneracy}-core · {coreSize} encji',
+ 'graphCore.shellsHeading': 'Dekompozycja powłokowa',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… i jeszcze {count} powłok',
+ 'graphCore.rankedHeading': 'Encje najgłębszego rdzenia',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'Encja',
+ 'graphCore.colCore': 'Rdzeń',
+ 'graphCore.colLinks': 'Łącza',
+ 'graphCore.coreBadge': 'rdzeń',
+ 'graphCore.coreTitle':
+ 'W najgęstszym {degeneracy}-core — każdy członek utrzymuje co najmniej {degeneracy} połączeń z innymi członkami rdzenia.',
};
export default messages;
diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts
index e0d058d990..acba1d38ba 100644
--- a/app/src/lib/i18n/pt.ts
+++ b/app/src/lib/i18n/pt.ts
@@ -4508,6 +4508,33 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': 'Agrupamento médio {avg} · transitividade {transitivity}',
'graphCohesion.title': 'Coesão do grafo',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': 'Núcleo',
+ 'graphCore.title': 'Núcleo do grafo',
+ 'graphCore.intro':
+ 'A decomposição k-core separa o núcleo estrutural do seu conhecimento — entidades mutuamente reforçadas por muitos fatos interligados — da periferia de folhas e pontes. Mesmo um hub de alto grau com fatos avulsos tem coreness 1; a profundidade, não o grau, marca o núcleo.',
+ 'graphCore.loading': 'Calculando a estrutura do núcleo…',
+ 'graphCore.errorPrefix': 'Não foi possível carregar o grafo:',
+ 'graphCore.retry': 'Tentar novamente',
+ 'graphCore.empty': 'Nenhum grafo de conhecimento ainda.',
+ 'graphCore.emptyHint':
+ 'À medida que o assistente registar factos conectados sobre si, o núcleo densamente ligado surgirá aqui.',
+ 'graphCore.namespaceLabel': 'Espaço de nomes',
+ 'graphCore.namespaceAll': 'Todos os espaços de nomes',
+ 'graphCore.metricEntities': 'Entidades',
+ 'graphCore.metricConnections': 'Conexões',
+ 'graphCore.metricDegeneracy': 'Degeneração',
+ 'graphCore.degeneracyCaption': 'Camada mais densa: {degeneracy}-core · {coreSize} entidades',
+ 'graphCore.shellsHeading': 'Decomposição em camadas',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… e mais {count} camadas',
+ 'graphCore.rankedHeading': 'Entidades do núcleo mais profundo',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'Entidade',
+ 'graphCore.colCore': 'Núcleo',
+ 'graphCore.colLinks': 'Ligações',
+ 'graphCore.coreBadge': 'núcleo',
+ 'graphCore.coreTitle':
+ 'No {degeneracy}-core mais denso — cada membro aqui mantém pelo menos {degeneracy} ligações para outros membros do núcleo.',
};
export default messages;
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index 4082908b76..f3cea40e07 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -4478,6 +4478,33 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': 'Средняя кластеризация {avg} · транзитивность {transitivity}',
'graphCohesion.title': 'Связность графа',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': 'Ядро',
+ 'graphCore.title': 'Ядро графа',
+ 'graphCore.intro':
+ 'Разложение k-core выделяет несущее ядро ваших знаний — сущности, взаимно подкреплённые многими взаимосвязанными фактами — из периферии листьев и мостов. Даже высококонтактный хаб из разрозненных фактов имеет coreness 1; ядро определяется глубиной, а не степенью.',
+ 'graphCore.loading': 'Вычисление структуры ядра…',
+ 'graphCore.errorPrefix': 'Не удалось загрузить граф:',
+ 'graphCore.retry': 'Повторить',
+ 'graphCore.empty': 'Граф знаний пока отсутствует.',
+ 'graphCore.emptyHint':
+ 'По мере того как ассистент фиксирует связанные факты о вас, плотно связанное ядро появится здесь.',
+ 'graphCore.namespaceLabel': 'Пространство имён',
+ 'graphCore.namespaceAll': 'Все пространства имён',
+ 'graphCore.metricEntities': 'Сущности',
+ 'graphCore.metricConnections': 'Связи',
+ 'graphCore.metricDegeneracy': 'Вырожденность',
+ 'graphCore.degeneracyCaption': 'Плотнейший слой: {degeneracy}-core · {coreSize} сущностей',
+ 'graphCore.shellsHeading': 'Послойное разложение',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… и ещё {count} оболочек',
+ 'graphCore.rankedHeading': 'Сущности глубочайшего ядра',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': 'Сущность',
+ 'graphCore.colCore': 'Ядро',
+ 'graphCore.colLinks': 'Связи',
+ 'graphCore.coreBadge': 'ядро',
+ 'graphCore.coreTitle':
+ 'В плотнейшем {degeneracy}-core — каждый участник поддерживает не менее {degeneracy} связей с другими участниками ядра.',
};
export default messages;
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index f1f2a1cfa6..2925226ac0 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -4224,6 +4224,32 @@ const messages: TranslationMap = {
'graphCohesion.summaryCaption': '平均聚类系数 {avg} · 传递性 {transitivity}',
'graphCohesion.title': '图的凝聚度',
'memory.tab.cohesion': 'Cohesion',
+ 'memory.tab.core': '核心',
+ 'graphCore.title': '图核心',
+ 'graphCore.intro':
+ 'k-core 分解将知识的承重核心——被许多相互关联的事实共同强化的实体——从叶子和桥梁的外围中分离出来。即使是充满一次性事实的高度数枢纽,其 coreness 也仅为 1;标志核心的是深度,而非度数。',
+ 'graphCore.loading': '正在计算核心结构…',
+ 'graphCore.errorPrefix': '无法加载图:',
+ 'graphCore.retry': '重试',
+ 'graphCore.empty': '暂无知识图谱。',
+ 'graphCore.emptyHint': '随着助手记录关于您的关联事实,密集连接的核心将在此显示。',
+ 'graphCore.namespaceLabel': '命名空间',
+ 'graphCore.namespaceAll': '所有命名空间',
+ 'graphCore.metricEntities': '实体',
+ 'graphCore.metricConnections': '连接',
+ 'graphCore.metricDegeneracy': '简并度',
+ 'graphCore.degeneracyCaption': '最密集的壳层:{degeneracy}-core · {coreSize} 个实体',
+ 'graphCore.shellsHeading': '壳层分解',
+ 'graphCore.shellLabel': '{k}-core',
+ 'graphCore.shellsMore': '… 以及另外 {count} 个层',
+ 'graphCore.rankedHeading': '最深核心的实体',
+ 'graphCore.colRank': '#',
+ 'graphCore.colEntity': '实体',
+ 'graphCore.colCore': '核心',
+ 'graphCore.colLinks': '链接',
+ 'graphCore.coreBadge': '核心',
+ 'graphCore.coreTitle':
+ '在最密集的 {degeneracy}-core 中——这里的每个成员与其他核心成员保持至少 {degeneracy} 条链接。',
};
export default messages;
diff --git a/app/src/lib/memory/graphCore.test.ts b/app/src/lib/memory/graphCore.test.ts
new file mode 100644
index 0000000000..3e22f6cc1b
--- /dev/null
+++ b/app/src/lib/memory/graphCore.test.ts
@@ -0,0 +1,177 @@
+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 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 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', () => {
+ 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..583279ab9e
--- /dev/null
+++ b/app/src/lib/memory/graphCore.ts
@@ -0,0 +1,214 @@
+/**
+ * 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-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';
+
+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-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 => {
+ 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) {
+ // Self-loop: register the endpoint as a node but add no self-edge.
+ neighbours(subject);
+ 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 9235646219..88c1a0f972 100644
--- a/app/src/pages/Intelligence.tsx
+++ b/app/src/pages/Intelligence.tsx
@@ -6,6 +6,7 @@ import DiagramViewerTab from '../components/intelligence/DiagramViewerTab';
import EntityAssociationsTab from '../components/intelligence/EntityAssociationsTab';
import GraphCentralityTab from '../components/intelligence/GraphCentralityTab';
import GraphCohesionTab from '../components/intelligence/GraphCohesionTab';
+import GraphCoreTab from '../components/intelligence/GraphCoreTab';
import IntelligenceSubconsciousTab from '../components/intelligence/IntelligenceSubconsciousTab';
import IntelligenceTasksTab from '../components/intelligence/IntelligenceTasksTab';
import MemoryFreshnessTab from '../components/intelligence/MemoryFreshnessTab';
@@ -35,6 +36,7 @@ type IntelligenceTab =
| 'diagram'
| 'centrality'
| 'cohesion'
+ | 'core'
| 'associations'
| 'freshness'
| 'timeline'
@@ -121,6 +123,7 @@ export default function Intelligence() {
{ id: 'diagram', label: t('memory.tab.diagram') },
{ id: 'centrality', label: t('memory.tab.centrality') },
{ id: 'cohesion', label: t('memory.tab.cohesion') },
+ { id: 'core', label: t('memory.tab.core') },
{ id: 'associations', label: t('memory.tab.associations') },
{ id: 'freshness', label: t('memory.tab.freshness') },
{ id: 'timeline', label: t('memory.tab.timeline') },
@@ -220,6 +223,8 @@ export default function Intelligence() {
{activeTab === 'cohesion' && }
+ {activeTab === 'core' && }
+
{activeTab === 'associations' && }
{activeTab === 'freshness' && }
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 };