diff --git a/app/src/components/intelligence/NamespaceOverviewPanel.test.tsx b/app/src/components/intelligence/NamespaceOverviewPanel.test.tsx
new file mode 100644
index 000000000..9d91dcb8f
--- /dev/null
+++ b/app/src/components/intelligence/NamespaceOverviewPanel.test.tsx
@@ -0,0 +1,57 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import { describe, expect, it, vi } from 'vitest';
+
+import { computeNamespaceOverview } from '../../lib/memory/namespaceOverview';
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+import NamespaceOverviewPanel from './NamespaceOverviewPanel';
+
+function rel(namespace: string | null, subject: string, object: string): GraphRelation {
+ return {
+ namespace,
+ subject,
+ predicate: 'p',
+ object,
+ attrs: {},
+ updatedAt: 0,
+ evidenceCount: 1,
+ orderIndex: null,
+ documentIds: [],
+ chunkIds: [],
+ };
+}
+
+const report = computeNamespaceOverview([
+ rel('work', 'A', 'B'),
+ rel('work', 'B', 'C'),
+ rel(null, 'P', 'Q'),
+]);
+
+describe('', () => {
+ it('renders the loading skeleton', () => {
+ render();
+ expect(screen.getByTestId('namespace-overview-loading')).toBeInTheDocument();
+ });
+
+ it('renders the empty state when there are no namespaces', () => {
+ 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 summary tiles and the per-namespace list (un-namespaced labeled)', () => {
+ render();
+ expect(screen.getByText('Namespaces')).toBeInTheDocument();
+ expect(screen.getByText('Facts')).toBeInTheDocument();
+ expect(screen.getByText('By namespace')).toBeInTheDocument();
+ expect(screen.getByText('work')).toBeInTheDocument();
+ // the null namespace renders with the "un-namespaced" label
+ expect(screen.getByText('(un-namespaced)')).toBeInTheDocument();
+ });
+});
diff --git a/app/src/components/intelligence/NamespaceOverviewPanel.tsx b/app/src/components/intelligence/NamespaceOverviewPanel.tsx
new file mode 100644
index 000000000..a9a98a6a6
--- /dev/null
+++ b/app/src/components/intelligence/NamespaceOverviewPanel.tsx
@@ -0,0 +1,186 @@
+/**
+ * Namespace Overview — presentational view. Pure: renders per-namespace fact /
+ * entity counts as a ranked bar list + summary tiles. No data fetching, no
+ * clock, no RNG.
+ */
+import { useT } from '../../lib/i18n/I18nContext';
+import type { NamespaceOverviewReport } from '../../lib/memory/namespaceOverview';
+
+const MAX_ROWS = 50;
+
+interface NamespaceOverviewPanelProps {
+ report: NamespaceOverviewReport | null;
+ loading?: boolean;
+ error?: string | null;
+ onRetry?: () => void;
+}
+
+const NamespaceOverviewPanel = ({
+ report,
+ loading,
+ error,
+ onRetry,
+}: NamespaceOverviewPanelProps) => {
+ const { t } = useT();
+
+ const intro = (
+
+
{t('namespaceOverview.title')}
+
{t('namespaceOverview.intro')}
+
+ );
+
+ if (loading) {
+ return (
+
+ {intro}
+
+
+ {[0, 1, 2].map(i => (
+
+ ))}
+
+ {[0, 1, 2].map(i => (
+
+ ))}
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ {intro}
+
+
+ {t('namespaceOverview.errorPrefix')} {error}
+
+ {onRetry && (
+
+ )}
+
+
+ );
+ }
+
+ if (!report || report.namespaceCount === 0) {
+ return (
+
+ {intro}
+
+
+ {t('namespaceOverview.empty')}
+
+
+ {t('namespaceOverview.emptyHint')}
+
+
+
+ );
+ }
+
+ const maxFacts = report.namespaces[0]?.factCount || 1;
+ const rows = report.namespaces.slice(0, MAX_ROWS);
+ const truncated = report.namespaces.length > MAX_ROWS;
+
+ return (
+
+ {intro}
+
+ {/* Summary tiles */}
+
+ {[
+ { label: t('namespaceOverview.metricNamespaces'), value: report.namespaceCount },
+ { label: t('namespaceOverview.metricFacts'), value: report.totalFacts },
+ { label: t('namespaceOverview.metricEntities'), value: report.totalEntities },
+ ].map(tile => (
+
+
+ {tile.label}
+
+
+ {tile.value}
+
+
+ ))}
+
+
+ {/* Ranked namespace list */}
+
+
+ {t('namespaceOverview.heading')}
+
+
+ {rows.map(stat => (
+ -
+
+ {stat.namespace ?? t('namespaceOverview.unnamespaced')}
+
+
+
+ {stat.factCount}
+
+
+ {t('namespaceOverview.entitiesShort').replace('{count}', String(stat.entityCount))}
+
+
+ ))}
+
+ {truncated && (
+
+ {t('namespaceOverview.truncated')
+ .replace('{shown}', String(rows.length))
+ .replace('{total}', String(report.namespaces.length))}
+
+ )}
+
+
+ );
+};
+
+export default NamespaceOverviewPanel;
diff --git a/app/src/components/intelligence/NamespaceOverviewTab.test.tsx b/app/src/components/intelligence/NamespaceOverviewTab.test.tsx
new file mode 100644
index 000000000..c82b7da49
--- /dev/null
+++ b/app/src/components/intelligence/NamespaceOverviewTab.test.tsx
@@ -0,0 +1,49 @@
+import { render, screen, waitFor } from '@testing-library/react';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { computeNamespaceOverview } from '../../lib/memory/namespaceOverview';
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+import NamespaceOverviewTab from './NamespaceOverviewTab';
+
+const mockLoad = vi.fn();
+
+vi.mock('../../services/api/namespaceOverviewApi', () => ({
+ loadNamespaceOverview: (...args: unknown[]) => mockLoad(...args),
+}));
+
+function rel(namespace: string | null, subject: string, object: string): GraphRelation {
+ return {
+ namespace,
+ subject,
+ predicate: 'p',
+ object,
+ attrs: {},
+ updatedAt: 0,
+ evidenceCount: 1,
+ orderIndex: null,
+ documentIds: [],
+ chunkIds: [],
+ };
+}
+
+const report = computeNamespaceOverview([rel('work', 'A', 'B')]);
+
+describe('', () => {
+ beforeEach(() => {
+ mockLoad.mockReset();
+ mockLoad.mockResolvedValue(report);
+ });
+
+ it('loads on mount and renders the per-namespace list', async () => {
+ render();
+ await waitFor(() => expect(screen.getByText('By namespace')).toBeInTheDocument());
+ expect(mockLoad).toHaveBeenCalledTimes(1);
+ });
+
+ it('surfaces an error when the load fails', async () => {
+ mockLoad.mockReset();
+ mockLoad.mockRejectedValueOnce(new Error('graph unavailable'));
+ render();
+ await waitFor(() => expect(screen.getByRole('alert').textContent).toMatch(/graph unavailable/));
+ });
+});
diff --git a/app/src/components/intelligence/NamespaceOverviewTab.tsx b/app/src/components/intelligence/NamespaceOverviewTab.tsx
new file mode 100644
index 000000000..1a7adb5e3
--- /dev/null
+++ b/app/src/components/intelligence/NamespaceOverviewTab.tsx
@@ -0,0 +1,49 @@
+/**
+ * Namespace Overview tab (container). Loads the whole graph on mount and
+ * delegates rendering to the pure . Read-only. No
+ * namespace selector — this view's axis IS the namespace, so it shows them all.
+ */
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+import type { NamespaceOverviewReport } from '../../lib/memory/namespaceOverview';
+import { loadNamespaceOverview } from '../../services/api/namespaceOverviewApi';
+import NamespaceOverviewPanel from './NamespaceOverviewPanel';
+
+const NamespaceOverviewTab = () => {
+ const [report, setReport] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ // Monotonic token: ignore a response if a newer load has since started.
+ const latestRequestId = useRef(0);
+
+ const load = useCallback(async () => {
+ const requestId = (latestRequestId.current += 1);
+ setLoading(true);
+ setError(null);
+ try {
+ const next = await loadNamespaceOverview();
+ if (requestId !== latestRequestId.current) return;
+ setReport(next);
+ } catch (err) {
+ if (requestId !== latestRequestId.current) return;
+ setError(err instanceof Error ? err.message : String(err));
+ } finally {
+ if (requestId === latestRequestId.current) setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void load();
+ }, [load]);
+
+ return (
+ void load()}
+ />
+ );
+};
+
+export default NamespaceOverviewTab;
diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts
index 437fe33bb..f44629bed 100644
--- a/app/src/lib/i18n/ar.ts
+++ b/app/src/lib/i18n/ar.ts
@@ -275,6 +275,7 @@ const messages: TranslationMap = {
'memory.tab.calls': 'المكالمات',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'مساحات الأسماء',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'الإعدادات',
'memory.analyzeNow': 'تحليل الآن',
@@ -296,6 +297,23 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': 'نظرة عامة على مساحات الأسماء',
+ 'namespaceOverview.intro':
+ 'كيفية توزيع معرفتك عبر السياقات — عدد الحقائق والكيانات المتميزة المسجَّلة في كل مساحة اسم.',
+ 'namespaceOverview.loading': 'تجميع مساحات الأسماء…',
+ 'namespaceOverview.errorPrefix': 'لا يمكن تحميل الرسم البياني:',
+ 'namespaceOverview.retry': 'إعادة المحاولة',
+ 'namespaceOverview.empty': 'لا يوجد رسم بياني للمعرفة بعد.',
+ 'namespaceOverview.emptyHint': 'عندما يسجّل المساعد حقائق عبر السياقات، ستظهر كل مساحة اسم هنا.',
+ 'namespaceOverview.metricNamespaces': 'مساحات الأسماء',
+ 'namespaceOverview.metricFacts': 'الحقائق',
+ 'namespaceOverview.metricEntities': 'الكيانات',
+ 'namespaceOverview.heading': 'حسب مساحة الاسم',
+ 'namespaceOverview.unnamespaced': '(بدون مساحة اسم)',
+ 'namespaceOverview.factsLabel': '{count} حقيقة',
+ 'namespaceOverview.entitiesLabel': '{count} كيان',
+ 'namespaceOverview.entitiesShort': '{count} كيان',
+ 'namespaceOverview.truncated': 'عرض أعلى {shown} من أصل {total} مساحة اسم.',
'graphCentrality.title': 'مركز المعرفة',
'graphCentrality.intro':
'ويظهر الإرسال على صور ذاكرتك مراكز الحمل - والكيانات الموصلة التي تربط المجموعات المنفصلة عن بعضها البعض، والتي لا يمكن لإحصاء الترددات الخام أن يكشف عنها.',
diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts
index ec25d99ac..2e85b97c5 100644
--- a/app/src/lib/i18n/bn.ts
+++ b/app/src/lib/i18n/bn.ts
@@ -278,6 +278,7 @@ const messages: TranslationMap = {
'memory.tab.calls': 'কল',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'নেমস্পেস',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'সেটিংস',
'memory.analyzeNow': 'এখনই বিশ্লেষণ করুন',
@@ -299,6 +300,24 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': 'নেমস্পেস ওভারভিউ',
+ 'namespaceOverview.intro':
+ 'প্রসঙ্গভেদে আপনার জ্ঞান কীভাবে বিতরণ করা হয়েছে — প্রতিটি নেমস্পেসে নথিভুক্ত তথ্য ও স্বতন্ত্র সত্তার সংখ্যা।',
+ 'namespaceOverview.loading': 'নেমস্পেস একত্রিত করা হচ্ছে…',
+ 'namespaceOverview.errorPrefix': 'রেখাচিত্র লোড করা যায়নি:',
+ 'namespaceOverview.retry': 'পুনরায় চেষ্টা করুন',
+ 'namespaceOverview.empty': 'এখনও কোনো জ্ঞান গ্রাফ নেই।',
+ 'namespaceOverview.emptyHint':
+ 'সহকারী প্রসঙ্গভেদে তথ্য রেকর্ড করার সাথে সাথে প্রতিটি নেমস্পেস এখানে দেখা যাবে।',
+ 'namespaceOverview.metricNamespaces': 'নেমস্পেস',
+ 'namespaceOverview.metricFacts': 'তথ্য',
+ 'namespaceOverview.metricEntities': 'সত্তা',
+ 'namespaceOverview.heading': 'নেমস্পেস অনুযায়ী',
+ 'namespaceOverview.unnamespaced': '(নেমস্পেসবিহীন)',
+ 'namespaceOverview.factsLabel': '{count}টি তথ্য',
+ 'namespaceOverview.entitiesLabel': '{count}টি সত্তা',
+ 'namespaceOverview.entitiesShort': '{count} সত্তা',
+ 'namespaceOverview.truncated': '{total}টির মধ্যে শীর্ষ {shown}টি নেমস্পেস দেখানো হচ্ছে।',
'graphCentrality.title': 'জ্ঞান গ্রাফ',
'graphCentrality.intro':
'আপনার মেমরি গ্রাফের উপর প্রদর্শিত পেজের ছবি- এবং সংযোগকারী সত্তার সাথে সংযুক্ত একটি সংযুক্ত চক্রের সংযোগ রয়েছে, যা একটি raw ফ্রিকোয়েন্সি গণনা প্রকাশ করতে পারে না।',
diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts
index 4d948b779..2989539a5 100644
--- a/app/src/lib/i18n/de.ts
+++ b/app/src/lib/i18n/de.ts
@@ -287,6 +287,7 @@ const messages: TranslationMap = {
'memory.tab.calls': 'Anrufe',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'Namensräume',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'Einstellungen',
'memory.analyzeNow': 'Jetzt analysieren',
@@ -308,6 +309,24 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': 'Namensraum-Übersicht',
+ 'namespaceOverview.intro':
+ 'Wie sich dein Wissen über Kontexte verteilt — die Anzahl der Fakten und eindeutigen Entitäten, die in jedem Namensraum erfasst sind.',
+ 'namespaceOverview.loading': 'Namensräume werden aggregiert…',
+ 'namespaceOverview.errorPrefix': 'Das Diagramm konnte nicht geladen werden:',
+ 'namespaceOverview.retry': 'Wiederholen',
+ 'namespaceOverview.empty': 'Noch kein Wissensgraph.',
+ 'namespaceOverview.emptyHint':
+ 'Sobald der Assistent Fakten über Kontexte hinweg erfasst, erscheint hier jeder Namensraum.',
+ 'namespaceOverview.metricNamespaces': 'Namensräume',
+ 'namespaceOverview.metricFacts': 'Fakten',
+ 'namespaceOverview.metricEntities': 'Entitäten',
+ 'namespaceOverview.heading': 'Nach Namensraum',
+ 'namespaceOverview.unnamespaced': '(ohne Namensraum)',
+ 'namespaceOverview.factsLabel': '{count} Fakten',
+ 'namespaceOverview.entitiesLabel': '{count} Entitäten',
+ 'namespaceOverview.entitiesShort': '{count} Ent.',
+ 'namespaceOverview.truncated': 'Die obersten {shown} von {total} Namensräumen werden angezeigt.',
'graphCentrality.title': 'Zentralität des Wissensgraphen',
'graphCentrality.intro':
'Der PageRank über Ihr Speicherdiagramm zeigt die tragenden Knotenpunkte auf – und die Konnektorentitäten, die ansonsten getrennte Cluster verbinden, die eine reine Häufigkeitszählung nicht aufdecken kann.',
diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts
index 2ec4ef0ec..d57ee9619 100644
--- a/app/src/lib/i18n/en.ts
+++ b/app/src/lib/i18n/en.ts
@@ -303,9 +303,28 @@ const en: TranslationMap = {
'memory.tab.calls': 'Calls',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'Namespaces',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'Settings',
'memory.analyzeNow': 'Analyze Now',
+ 'namespaceOverview.title': 'Namespace Overview',
+ 'namespaceOverview.intro':
+ 'How your knowledge is distributed across contexts — the number of facts and distinct entities recorded in each namespace.',
+ 'namespaceOverview.loading': 'Aggregating namespaces…',
+ 'namespaceOverview.errorPrefix': 'Could not load the graph:',
+ 'namespaceOverview.retry': 'Retry',
+ 'namespaceOverview.empty': 'No knowledge graph yet.',
+ 'namespaceOverview.emptyHint':
+ 'As the assistant records facts across contexts, each namespace will appear here.',
+ 'namespaceOverview.metricNamespaces': 'Namespaces',
+ 'namespaceOverview.metricFacts': 'Facts',
+ 'namespaceOverview.metricEntities': 'Entities',
+ 'namespaceOverview.heading': 'By namespace',
+ 'namespaceOverview.unnamespaced': '(un-namespaced)',
+ 'namespaceOverview.factsLabel': '{count} facts',
+ 'namespaceOverview.entitiesLabel': '{count} entities',
+ 'namespaceOverview.entitiesShort': '{count} ent.',
+ 'namespaceOverview.truncated': 'Showing the top {shown} of {total} namespaces.',
'memoryTimeline.title': 'Memory Timeline',
'memoryTimeline.intro':
'When the assistant learned about you — facts grouped by the month they were last reinforced. Surfaces growth, bursts of activity, and quiet stretches.',
diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts
index 611e7eb9a..823af2043 100644
--- a/app/src/lib/i18n/es.ts
+++ b/app/src/lib/i18n/es.ts
@@ -287,6 +287,7 @@ const messages: TranslationMap = {
'memory.tab.calls': 'Llamadas',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'Espacios de nombres',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'Configuración',
'memory.analyzeNow': 'Analizar ahora',
@@ -308,6 +309,25 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': 'Resumen de espacios de nombres',
+ 'namespaceOverview.intro':
+ 'Cómo se distribuye tu conocimiento entre contextos: el número de hechos y entidades distintas registradas en cada espacio de nombres.',
+ 'namespaceOverview.loading': 'Agregando espacios de nombres…',
+ 'namespaceOverview.errorPrefix': 'No se pudo cargar el gráfico:',
+ 'namespaceOverview.retry': 'Reintentar',
+ 'namespaceOverview.empty': 'Aún no hay gráfico de conocimiento.',
+ 'namespaceOverview.emptyHint':
+ 'A medida que el asistente registra hechos entre contextos, cada espacio de nombres aparecerá aquí.',
+ 'namespaceOverview.metricNamespaces': 'Espacios de nombres',
+ 'namespaceOverview.metricFacts': 'Hechos',
+ 'namespaceOverview.metricEntities': 'Entidades',
+ 'namespaceOverview.heading': 'Por espacio de nombres',
+ 'namespaceOverview.unnamespaced': '(sin espacio de nombres)',
+ 'namespaceOverview.factsLabel': '{count} hechos',
+ 'namespaceOverview.entitiesLabel': '{count} entidades',
+ 'namespaceOverview.entitiesShort': '{count} ent.',
+ 'namespaceOverview.truncated':
+ 'Mostrando los {shown} principales de {total} espacios de nombres.',
'graphCentrality.title': 'Centralidad del gráfico de conocimiento',
'graphCentrality.intro':
'El PageRank sobre su gráfico de memoria muestra los centros de carga y las entidades conectoras que vinculan grupos que de otro modo estarían separados, que un recuento de frecuencia sin procesar no puede revelar.',
diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts
index ba863a439..3fcdcb2e8 100644
--- a/app/src/lib/i18n/fr.ts
+++ b/app/src/lib/i18n/fr.ts
@@ -286,6 +286,7 @@ const messages: TranslationMap = {
'memory.tab.calls': 'Appels',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'Espaces de noms',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'Paramètres',
'memory.analyzeNow': 'Analyser maintenant',
@@ -307,6 +308,24 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': 'Aperçu des espaces de noms',
+ 'namespaceOverview.intro':
+ 'Comment vos connaissances sont réparties entre les contextes — le nombre de faits et d’entités distinctes enregistrés dans chaque espace de noms.',
+ 'namespaceOverview.loading': 'Agrégation des espaces de noms…',
+ 'namespaceOverview.errorPrefix': 'Impossible de charger le graphique:',
+ 'namespaceOverview.retry': 'Réessayer',
+ 'namespaceOverview.empty': 'Pas encore de graphe de connaissances.',
+ 'namespaceOverview.emptyHint':
+ 'À mesure que l’assistant enregistre des faits entre les contextes, chaque espace de noms apparaîtra ici.',
+ 'namespaceOverview.metricNamespaces': 'Espaces de noms',
+ 'namespaceOverview.metricFacts': 'Faits',
+ 'namespaceOverview.metricEntities': 'Entités',
+ 'namespaceOverview.heading': 'Par espace de noms',
+ 'namespaceOverview.unnamespaced': '(sans espace de noms)',
+ 'namespaceOverview.factsLabel': '{count} faits',
+ 'namespaceOverview.entitiesLabel': '{count} entités',
+ 'namespaceOverview.entitiesShort': '{count} ent.',
+ 'namespaceOverview.truncated': 'Affichage des {shown} premiers espaces de noms sur {total}.',
'graphCentrality.title': 'Centralité du graphe de connaissances',
'graphCentrality.intro':
"PageRank sur votre graphe de mémoire met en évidence les hubs porteurs de charge — et les entités connectrices qui relient des clusters autrement séparés, ce qu'un simple comptage de fréquence ne peut révéler.",
diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts
index ac8a88881..4a6e13965 100644
--- a/app/src/lib/i18n/hi.ts
+++ b/app/src/lib/i18n/hi.ts
@@ -277,6 +277,7 @@ const messages: TranslationMap = {
'memory.tab.calls': 'कॉल्स',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'नेमस्पेस',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'सेटिंग्स',
'memory.analyzeNow': 'अभी एनालाइज़ करें',
@@ -298,6 +299,24 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': 'नेमस्पेस अवलोकन',
+ 'namespaceOverview.intro':
+ 'आपका ज्ञान संदर्भों में कैसे वितरित है — प्रत्येक नेमस्पेस में दर्ज तथ्यों और विशिष्ट इकाइयों की संख्या।',
+ 'namespaceOverview.loading': 'नेमस्पेस एकत्र किए जा रहे हैं…',
+ 'namespaceOverview.errorPrefix': 'ग्राफ़ लोड नहीं किया जा सका:',
+ 'namespaceOverview.retry': 'पुनः प्रयास करें',
+ 'namespaceOverview.empty': 'अभी तक कोई ज्ञान ग्राफ़ नहीं है।',
+ 'namespaceOverview.emptyHint':
+ 'जैसे-जैसे सहायक संदर्भों में तथ्य दर्ज करता है, प्रत्येक नेमस्पेस यहाँ दिखाई देगा।',
+ 'namespaceOverview.metricNamespaces': 'नेमस्पेस',
+ 'namespaceOverview.metricFacts': 'तथ्य',
+ 'namespaceOverview.metricEntities': 'इकाइयाँ',
+ 'namespaceOverview.heading': 'नेमस्पेस के अनुसार',
+ 'namespaceOverview.unnamespaced': '(बिना नेमस्पेस)',
+ 'namespaceOverview.factsLabel': '{count} तथ्य',
+ 'namespaceOverview.entitiesLabel': '{count} इकाइयाँ',
+ 'namespaceOverview.entitiesShort': '{count} इका.',
+ 'namespaceOverview.truncated': '{total} में से शीर्ष {shown} नेमस्पेस दिखाए जा रहे हैं।',
'graphCentrality.title': 'ज्ञान ग्राफ केंद्रीयता',
'graphCentrality.intro':
'अपने मेमोरी ग्राफ़ पर पेजरैंक लोड-असर हब सतहों - और कनेक्टर इकाइयां जो अन्यथा अलग-अलग समूहों को जोड़ती हैं, जो एक कच्चे आवृत्ति गिनती प्रकट नहीं हो सकती हैं।',
diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts
index e3b99b1f7..25e9306db 100644
--- a/app/src/lib/i18n/id.ts
+++ b/app/src/lib/i18n/id.ts
@@ -279,6 +279,7 @@ const messages: TranslationMap = {
'memory.tab.calls': 'Panggilan',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'Namespace',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'Pengaturan',
'memory.analyzeNow': 'Analisis Sekarang',
@@ -300,6 +301,24 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': 'Ikhtisar Namespace',
+ 'namespaceOverview.intro':
+ 'Bagaimana pengetahuan Anda terdistribusi di seluruh konteks — jumlah fakta dan entitas berbeda yang tercatat di setiap namespace.',
+ 'namespaceOverview.loading': 'Menggabungkan namespace…',
+ 'namespaceOverview.errorPrefix': 'Tidak dapat memuat grafiknya:',
+ 'namespaceOverview.retry': 'Coba lagi',
+ 'namespaceOverview.empty': 'Belum ada grafik pengetahuan.',
+ 'namespaceOverview.emptyHint':
+ 'Saat asisten mencatat fakta di seluruh konteks, setiap namespace akan muncul di sini.',
+ 'namespaceOverview.metricNamespaces': 'Namespace',
+ 'namespaceOverview.metricFacts': 'Fakta',
+ 'namespaceOverview.metricEntities': 'Entitas',
+ 'namespaceOverview.heading': 'Berdasarkan namespace',
+ 'namespaceOverview.unnamespaced': '(tanpa namespace)',
+ 'namespaceOverview.factsLabel': '{count} fakta',
+ 'namespaceOverview.entitiesLabel': '{count} entitas',
+ 'namespaceOverview.entitiesShort': '{count} ent.',
+ 'namespaceOverview.truncated': 'Menampilkan {shown} teratas dari {total} namespace.',
'graphCentrality.title': 'Centralitas Grafik Pengetahuan',
'graphCentrality.intro':
'PageRank melalui grafik memori Anda permukaan hub load- bantalan - dan entiti konektor yang menghubungkan lain-terpisah cluster, yang jumlah frekuensi mentah tidak dapat mengungkapkan.',
diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts
index f1dfb0867..c6ca7f0b5 100644
--- a/app/src/lib/i18n/it.ts
+++ b/app/src/lib/i18n/it.ts
@@ -284,6 +284,7 @@ const messages: TranslationMap = {
'memory.tab.calls': 'Chiamate',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'Spazi dei nomi',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'Impostazioni',
'memory.analyzeNow': 'Analizza ora',
@@ -305,6 +306,24 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': 'Panoramica degli spazi dei nomi',
+ 'namespaceOverview.intro':
+ 'Come la tua conoscenza è distribuita tra i contesti — il numero di fatti ed entità distinte registrate in ogni spazio dei nomi.',
+ 'namespaceOverview.loading': 'Aggregazione degli spazi dei nomi…',
+ 'namespaceOverview.errorPrefix': 'Impossibile caricare il grafico:',
+ 'namespaceOverview.retry': 'Riprova',
+ 'namespaceOverview.empty': 'Ancora nessun knowledge graph.',
+ 'namespaceOverview.emptyHint':
+ 'Man mano che l’assistente registra fatti tra i contesti, ogni spazio dei nomi apparirà qui.',
+ 'namespaceOverview.metricNamespaces': 'Spazi dei nomi',
+ 'namespaceOverview.metricFacts': 'Fatti',
+ 'namespaceOverview.metricEntities': 'Entità',
+ 'namespaceOverview.heading': 'Per spazio dei nomi',
+ 'namespaceOverview.unnamespaced': '(senza spazio dei nomi)',
+ 'namespaceOverview.factsLabel': '{count} fatti',
+ 'namespaceOverview.entitiesLabel': '{count} entità',
+ 'namespaceOverview.entitiesShort': '{count} ent.',
+ 'namespaceOverview.truncated': 'Mostrando i primi {shown} di {total} spazi dei nomi.',
'graphCentrality.title': 'Centralità del Grafo della Conoscenza',
'graphCentrality.intro':
'PageRank sul tuo grafo della memoria mette in evidenza gli hub portanti — e le entità di collegamento che connettono cluster altrimenti separati, cosa che un semplice conteggio di frequenza non può rivelare.',
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index 21c9f06f7..5a8c392e5 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -277,6 +277,7 @@ const messages: TranslationMap = {
'memory.tab.calls': '통화',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': '네임스페이스',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': '설정',
'memory.analyzeNow': '지금 분석',
@@ -298,6 +299,24 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': '네임스페이스 개요',
+ 'namespaceOverview.intro':
+ '컨텍스트 전반에 걸쳐 지식이 어떻게 분포되어 있는지 — 각 네임스페이스에 기록된 사실과 고유 엔터티의 수입니다.',
+ 'namespaceOverview.loading': '네임스페이스 집계 중…',
+ 'namespaceOverview.errorPrefix': '그래프를 로드할 수 없습니다:',
+ 'namespaceOverview.retry': '다시 시도',
+ 'namespaceOverview.empty': '아직 지식 그래프가 없습니다.',
+ 'namespaceOverview.emptyHint':
+ '어시스턴트가 컨텍스트 전반에 걸쳐 사실을 기록하면 각 네임스페이스가 여기에 표시됩니다.',
+ 'namespaceOverview.metricNamespaces': '네임스페이스',
+ 'namespaceOverview.metricFacts': '사실',
+ 'namespaceOverview.metricEntities': '엔터티',
+ 'namespaceOverview.heading': '네임스페이스별',
+ 'namespaceOverview.unnamespaced': '(네임스페이스 없음)',
+ 'namespaceOverview.factsLabel': '사실 {count}개',
+ 'namespaceOverview.entitiesLabel': '엔터티 {count}개',
+ 'namespaceOverview.entitiesShort': '{count}개 엔터티',
+ 'namespaceOverview.truncated': '전체 {total}개 네임스페이스 중 상위 {shown}개를 표시합니다.',
'graphCentrality.title': '지식 그래프 중심성',
'graphCentrality.intro':
'메모리 그래프에 PageRank를 적용해 핵심 허브와, 단순 빈도 계산으로는 드러나지 않는 분리된 클러스터를 연결하는 엔터티를 드러냅니다.',
diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts
index cc1d88b69..97f2e423f 100644
--- a/app/src/lib/i18n/pl.ts
+++ b/app/src/lib/i18n/pl.ts
@@ -282,6 +282,7 @@ const messages: TranslationMap = {
'memory.tab.calls': 'Połączenia',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'Przestrzenie nazw',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'Ustawienia',
'memory.analyzeNow': 'Analizuj teraz',
@@ -303,6 +304,24 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': 'Przegląd przestrzeni nazw',
+ 'namespaceOverview.intro':
+ 'Jak Twoja wiedza jest rozłożona w kontekstach — liczba faktów i odrębnych encji zapisanych w każdej przestrzeni nazw.',
+ 'namespaceOverview.loading': 'Agregowanie przestrzeni nazw…',
+ 'namespaceOverview.errorPrefix': 'Nie udało się załadować grafu:',
+ 'namespaceOverview.retry': 'Ponów',
+ 'namespaceOverview.empty': 'Nie ma jeszcze grafu wiedzy.',
+ 'namespaceOverview.emptyHint':
+ 'Gdy asystent zapisuje fakty w różnych kontekstach, każda przestrzeń nazw pojawi się tutaj.',
+ 'namespaceOverview.metricNamespaces': 'Przestrzenie nazw',
+ 'namespaceOverview.metricFacts': 'Fakty',
+ 'namespaceOverview.metricEntities': 'Encje',
+ 'namespaceOverview.heading': 'Według przestrzeni nazw',
+ 'namespaceOverview.unnamespaced': '(bez przestrzeni nazw)',
+ 'namespaceOverview.factsLabel': '{count} faktów',
+ 'namespaceOverview.entitiesLabel': '{count} encji',
+ 'namespaceOverview.entitiesShort': '{count} enc.',
+ 'namespaceOverview.truncated': 'Wyświetlanie {shown} najważniejszych z {total} przestrzeni nazw.',
'graphCentrality.title': 'Centralność grafu wiedzy',
'graphCentrality.intro':
'PageRank nad grafem pamięci pokazuje kluczowe węzły oraz encje-łączniki, które spajają oddzielne klastry, czego nie ujawnia proste zliczanie częstotliwości.',
diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts
index 54ad3bf25..73ba3d657 100644
--- a/app/src/lib/i18n/pt.ts
+++ b/app/src/lib/i18n/pt.ts
@@ -286,6 +286,7 @@ const messages: TranslationMap = {
'memory.tab.calls': 'Chamadas',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'Namespaces',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'Configurações',
'memory.analyzeNow': 'Analisar Agora',
@@ -307,6 +308,24 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': 'Visão geral dos namespaces',
+ 'namespaceOverview.intro':
+ 'Como o seu conhecimento está distribuído entre os contextos — o número de fatos e entidades distintas registradas em cada namespace.',
+ 'namespaceOverview.loading': 'Agregando namespaces…',
+ 'namespaceOverview.errorPrefix': 'Não foi possível carregar o gráfico:',
+ 'namespaceOverview.retry': 'Tentar novamente',
+ 'namespaceOverview.empty': 'Ainda não há gráfico de conhecimento.',
+ 'namespaceOverview.emptyHint':
+ 'À medida que o assistente registra fatos entre contextos, cada namespace aparecerá aqui.',
+ 'namespaceOverview.metricNamespaces': 'Namespaces',
+ 'namespaceOverview.metricFacts': 'Fatos',
+ 'namespaceOverview.metricEntities': 'Entidades',
+ 'namespaceOverview.heading': 'Por namespace',
+ 'namespaceOverview.unnamespaced': '(sem namespace)',
+ 'namespaceOverview.factsLabel': '{count} fatos',
+ 'namespaceOverview.entitiesLabel': '{count} entidades',
+ 'namespaceOverview.entitiesShort': '{count} ent.',
+ 'namespaceOverview.truncated': 'Mostrando os {shown} principais de {total} namespaces.',
'graphCentrality.title': 'Centralidade do Grafo de Conhecimento',
'graphCentrality.intro':
'O PageRank sobre seu grafo de memória revela os hubs que suportam carga — e as entidades conectoras que ligam clusters que, de outra forma, seriam separados, algo que uma contagem de frequência bruta não consegue revelar.',
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index a3252110c..8b1a0cb47 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -279,6 +279,7 @@ const messages: TranslationMap = {
'memory.tab.calls': 'Звонки',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': 'Пространства имён',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': 'Настройки',
'memory.analyzeNow': 'Анализировать сейчас',
@@ -300,6 +301,24 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': 'Обзор пространств имён',
+ 'namespaceOverview.intro':
+ 'Как ваши знания распределены по контекстам — количество фактов и уникальных сущностей, записанных в каждом пространстве имён.',
+ 'namespaceOverview.loading': 'Агрегирование пространств имён…',
+ 'namespaceOverview.errorPrefix': 'Не удалось загрузить график:',
+ 'namespaceOverview.retry': 'Повторить попытку',
+ 'namespaceOverview.empty': 'Графа знаний пока нет.',
+ 'namespaceOverview.emptyHint':
+ 'По мере того как ассистент записывает факты в разных контекстах, каждое пространство имён будет появляться здесь.',
+ 'namespaceOverview.metricNamespaces': 'Пространства имён',
+ 'namespaceOverview.metricFacts': 'Факты',
+ 'namespaceOverview.metricEntities': 'Сущности',
+ 'namespaceOverview.heading': 'По пространству имён',
+ 'namespaceOverview.unnamespaced': '(без пространства имён)',
+ 'namespaceOverview.factsLabel': '{count} фактов',
+ 'namespaceOverview.entitiesLabel': '{count} сущностей',
+ 'namespaceOverview.entitiesShort': '{count} сущ.',
+ 'namespaceOverview.truncated': 'Показаны топ-{shown} из {total} пространств имён.',
'graphCentrality.title': 'Централизованность графа знаний',
'graphCentrality.intro':
'PageRank по вашему графику памяти отображает несущие нагрузку концентраторы — и объекты-соединители, которые связывают отдельные кластеры, которые не может выявить необработанный подсчет частоты.',
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index 2cf5f916d..40f5f354f 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -267,6 +267,7 @@ const messages: TranslationMap = {
'memory.tab.calls': '调用记录',
'memory.tab.diagram': 'Diagram',
'memory.tab.centrality': 'Centrality',
+ 'memory.tab.namespaces': '命名空间',
'memory.tab.timeline': 'Timeline',
'memory.tab.settings': '设置',
'memory.analyzeNow': '立即分析',
@@ -288,6 +289,23 @@ const messages: TranslationMap = {
'memoryTimeline.busiestCaption': 'Busiest: {period} ({count})',
'memoryTimeline.undatedNote': '{count} fact(s) have no recorded date.',
'memoryTimeline.truncated': 'Showing the {shown} most recent of {total} months.',
+ 'namespaceOverview.title': '命名空间概览',
+ 'namespaceOverview.intro':
+ '您的知识如何在各个上下文中分布——每个命名空间中记录的事实和不同实体的数量。',
+ 'namespaceOverview.loading': '正在汇总命名空间…',
+ 'namespaceOverview.errorPrefix': '无法加载图谱:',
+ 'namespaceOverview.retry': '重试',
+ 'namespaceOverview.empty': '暂无知识图谱。',
+ 'namespaceOverview.emptyHint': '随着助手在各个上下文中记录事实,每个命名空间都会显示在这里。',
+ 'namespaceOverview.metricNamespaces': '命名空间',
+ 'namespaceOverview.metricFacts': '事实',
+ 'namespaceOverview.metricEntities': '实体',
+ 'namespaceOverview.heading': '按命名空间',
+ 'namespaceOverview.unnamespaced': '(无命名空间)',
+ 'namespaceOverview.factsLabel': '{count} 条事实',
+ 'namespaceOverview.entitiesLabel': '{count} 个实体',
+ 'namespaceOverview.entitiesShort': '{count} 实体',
+ 'namespaceOverview.truncated': '显示 {total} 个命名空间中的前 {shown} 个。',
'graphCentrality.title': '知识图谱中心性',
'graphCentrality.intro':
'对你的记忆图谱运行 PageRank,可找出关键枢纽以及连接原本分离聚类的连接实体,这是单纯频次统计无法揭示的。',
diff --git a/app/src/lib/memory/namespaceOverview.test.ts b/app/src/lib/memory/namespaceOverview.test.ts
new file mode 100644
index 000000000..9472daf8d
--- /dev/null
+++ b/app/src/lib/memory/namespaceOverview.test.ts
@@ -0,0 +1,80 @@
+import { describe, expect, it } from 'vitest';
+
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+import { computeNamespaceOverview } from './namespaceOverview';
+
+function rel(namespace: string | null, subject: string, object: string): GraphRelation {
+ return {
+ namespace,
+ subject,
+ predicate: 'p',
+ object,
+ attrs: {},
+ updatedAt: 0,
+ evidenceCount: 1,
+ orderIndex: null,
+ documentIds: [],
+ chunkIds: [],
+ };
+}
+
+describe('computeNamespaceOverview', () => {
+ it('returns an empty report for no relations', () => {
+ expect(computeNamespaceOverview([])).toEqual({
+ namespaces: [],
+ namespaceCount: 0,
+ totalFacts: 0,
+ totalEntities: 0,
+ });
+ });
+
+ it('aggregates distinct facts and entities per namespace', () => {
+ const r = computeNamespaceOverview([
+ rel('work', 'A', 'B'),
+ rel('work', 'B', 'C'),
+ rel('personal', 'X', 'Y'),
+ rel(null, 'P', 'Q'),
+ ]);
+ expect(r.namespaceCount).toBe(3);
+ expect(r.totalFacts).toBe(4);
+ expect(r.totalEntities).toBe(7); // A,B,C,X,Y,P,Q
+ // Sorted by factCount desc; ties by namespace asc with null last.
+ expect(r.namespaces).toEqual([
+ { namespace: 'work', factCount: 2, entityCount: 3 },
+ { namespace: 'personal', factCount: 1, entityCount: 2 },
+ { namespace: null, factCount: 1, entityCount: 2 },
+ ]);
+ });
+
+ it('de-duplicates repeated triples within a namespace', () => {
+ const r = computeNamespaceOverview([rel('work', 'A', 'B'), rel('work', 'A', 'B')]);
+ expect(r.namespaces[0].factCount).toBe(1);
+ expect(r.totalFacts).toBe(1);
+ });
+
+ it('counts a shared entity per-namespace but once globally', () => {
+ const r = computeNamespaceOverview([rel('work', 'A', 'B'), rel('personal', 'A', 'C')]);
+ const byNs = Object.fromEntries(r.namespaces.map(s => [s.namespace, s]));
+ expect(byNs.work.entityCount).toBe(2); // A, B
+ expect(byNs.personal.entityCount).toBe(2); // A, C
+ expect(r.totalEntities).toBe(3); // A counted once globally
+ });
+
+ it('sorts the un-namespaced (null) bucket last on a tie', () => {
+ const r = computeNamespaceOverview([rel(null, 'A', 'B'), rel('aaa', 'C', 'D')]);
+ expect(r.namespaces.map(s => s.namespace)).toEqual(['aaa', null]);
+ });
+
+ it('drops malformed relations with a non-string field', () => {
+ const malformed = { ...rel('work', 'A', 'B'), object: null as unknown as string };
+ const r = computeNamespaceOverview([rel('work', 'A', 'B'), malformed, rel('work', 'C', 'D')]);
+ expect(r.totalFacts).toBe(2);
+ });
+
+ it('is invariant to relation order', () => {
+ const triples = [rel('work', 'A', 'B'), rel('personal', 'X', 'Y'), rel('work', 'B', 'C')];
+ const forward = computeNamespaceOverview(triples);
+ const reversed = computeNamespaceOverview([...triples].reverse());
+ expect(reversed).toEqual(forward);
+ });
+});
diff --git a/app/src/lib/memory/namespaceOverview.ts b/app/src/lib/memory/namespaceOverview.ts
new file mode 100644
index 000000000..ffd384a6f
--- /dev/null
+++ b/app/src/lib/memory/namespaceOverview.ts
@@ -0,0 +1,108 @@
+/**
+ * Namespace Overview — pure per-namespace aggregation engine.
+ *
+ * Every memory fact carries a `namespace` (e.g. "work", "personal", or null for
+ * un-namespaced). This is the only lens that uses the NAMESPACE as its primary
+ * axis: it shows how the user's knowledge is distributed across contexts — how
+ * many facts and distinct entities live in each — so lopsided or empty contexts
+ * are visible at a glance.
+ *
+ * Everything here is PURE and DETERMINISTIC: no React, no RPC, no clock, no
+ * randomness. Output depends only on the relations, never on insertion order.
+ * Facts are de-duplicated per namespace by their (subject, predicate, object)
+ * triple; an entity counts once per namespace it appears in.
+ */
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+
+export interface NamespaceStat {
+ namespace: string | null; // raw namespace; null means un-namespaced
+ factCount: number; // distinct (subject, predicate, object) triples in this namespace
+ entityCount: number; // distinct entities (subject or object) in this namespace
+}
+
+export interface NamespaceOverviewReport {
+ namespaces: NamespaceStat[]; // sorted by factCount desc, then namespace asc (null last)
+ namespaceCount: number;
+ totalFacts: number; // distinct triples summed across namespaces
+ totalEntities: number; // distinct entities across the WHOLE graph (deduped globally)
+}
+
+const EMPTY_REPORT: NamespaceOverviewReport = {
+ namespaces: [],
+ namespaceCount: 0,
+ totalFacts: 0,
+ totalEntities: 0,
+};
+
+/** Canonical, collision-free key for a directed triple. */
+function tripleKey(subject: string, predicate: string, object: string): string {
+ return JSON.stringify([subject, predicate, object]);
+}
+
+function compareIds(a: string, b: string): number {
+ return a < b ? -1 : a > b ? 1 : 0;
+}
+
+interface Bucket {
+ facts: Set; // distinct triple keys
+ entities: Set; // distinct entity ids
+}
+
+/**
+ * Compute the per-namespace overview. Pure function of `relations`.
+ */
+export function computeNamespaceOverview(relations: GraphRelation[]): NamespaceOverviewReport {
+ const buckets = new Map();
+ const allEntities = new Set();
+ const ensure = (ns: string | null): Bucket => {
+ let bucket = buckets.get(ns);
+ if (!bucket) {
+ bucket = { facts: new Set(), entities: new Set() };
+ buckets.set(ns, bucket);
+ }
+ return bucket;
+ };
+ for (const relation of relations) {
+ const { subject, predicate, object } = relation;
+ if (
+ typeof subject !== 'string' ||
+ typeof predicate !== 'string' ||
+ typeof object !== 'string'
+ ) {
+ continue;
+ }
+ const ns = typeof relation.namespace === 'string' ? relation.namespace : null;
+ const bucket = ensure(ns);
+ bucket.facts.add(tripleKey(subject, predicate, object));
+ bucket.entities.add(subject);
+ bucket.entities.add(object);
+ allEntities.add(subject);
+ allEntities.add(object);
+ }
+
+ if (buckets.size === 0) return EMPTY_REPORT;
+
+ const namespaces: NamespaceStat[] = [...buckets.entries()].map(([namespace, bucket]) => ({
+ namespace,
+ factCount: bucket.facts.size,
+ entityCount: bucket.entities.size,
+ }));
+
+ // Sort by factCount desc, then namespace asc with null (un-namespaced) last.
+ namespaces.sort((a, b) => {
+ if (b.factCount !== a.factCount) return b.factCount - a.factCount;
+ if (a.namespace === null) return b.namespace === null ? 0 : 1;
+ if (b.namespace === null) return -1;
+ return compareIds(a.namespace, b.namespace);
+ });
+
+ let totalFacts = 0;
+ for (const stat of namespaces) totalFacts += stat.factCount;
+
+ return {
+ namespaces,
+ namespaceCount: namespaces.length,
+ totalFacts,
+ totalEntities: allEntities.size,
+ };
+}
diff --git a/app/src/pages/Intelligence.tsx b/app/src/pages/Intelligence.tsx
index 2ea871ed3..4800a2ad2 100644
--- a/app/src/pages/Intelligence.tsx
+++ b/app/src/pages/Intelligence.tsx
@@ -10,6 +10,7 @@ import IntelligenceTasksTab from '../components/intelligence/IntelligenceTasksTa
import MemoryFreshnessTab from '../components/intelligence/MemoryFreshnessTab';
import MemoryTimelineTab from '../components/intelligence/MemoryTimelineTab';
import { MemoryWorkspace } from '../components/intelligence/MemoryWorkspace';
+import NamespaceOverviewTab from '../components/intelligence/NamespaceOverviewTab';
import { ToastContainer } from '../components/intelligence/Toast';
import PillTabBar from '../components/PillTabBar';
import {
@@ -34,7 +35,8 @@ type IntelligenceTab =
| 'associations'
| 'freshness'
| 'timeline'
- | 'path';
+ | 'path'
+ | 'namespaces';
export default function Intelligence() {
const { t } = useT();
@@ -118,6 +120,7 @@ export default function Intelligence() {
{ id: 'freshness', label: t('memory.tab.freshness') },
{ id: 'timeline', label: t('memory.tab.timeline') },
{ id: 'path', label: t('memory.tab.path') },
+ { id: 'namespaces', label: t('memory.tab.namespaces') },
];
const activeTabDef = tabs.find(tab => tab.id === activeTab);
@@ -216,6 +219,8 @@ export default function Intelligence() {
{activeTab === 'timeline' && }
{activeTab === 'path' && }
+
+ {activeTab === 'namespaces' && }
diff --git a/app/src/services/api/namespaceOverviewApi.test.ts b/app/src/services/api/namespaceOverviewApi.test.ts
new file mode 100644
index 000000000..a2310585d
--- /dev/null
+++ b/app/src/services/api/namespaceOverviewApi.test.ts
@@ -0,0 +1,57 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { computeNamespaceOverview } from '../../lib/memory/namespaceOverview';
+import type { GraphRelation } from '../../utils/tauriCommands/memory';
+import { loadNamespaceOverview, namespaceOverviewApi } from './namespaceOverviewApi';
+
+const mockGraphQuery = vi.fn();
+
+vi.mock('../../utils/tauriCommands/memory', () => ({
+ memoryGraphQuery: (...args: unknown[]) => mockGraphQuery(...args),
+}));
+
+function rel(namespace: string | null, subject: string, object: string): GraphRelation {
+ return {
+ namespace,
+ subject,
+ predicate: 'p',
+ object,
+ attrs: {},
+ updatedAt: 0,
+ evidenceCount: 1,
+ orderIndex: null,
+ documentIds: [],
+ chunkIds: [],
+ };
+}
+
+describe('namespaceOverviewApi.loadNamespaceOverview', () => {
+ beforeEach(() => {
+ mockGraphQuery.mockReset();
+ });
+
+ it('fetches the whole graph (no namespace arg) and returns the engine report', async () => {
+ const triples = [rel('work', 'A', 'B'), rel('personal', 'X', 'Y')];
+ mockGraphQuery.mockResolvedValueOnce(triples);
+ const out = await loadNamespaceOverview();
+ expect(mockGraphQuery).toHaveBeenCalledWith();
+ expect(out).toEqual(computeNamespaceOverview(triples));
+ });
+
+ it('returns an empty report when the graph is empty', async () => {
+ mockGraphQuery.mockResolvedValueOnce([]);
+ const out = await loadNamespaceOverview();
+ expect(out.namespaceCount).toBe(0);
+ });
+
+ it('propagates query errors', async () => {
+ mockGraphQuery.mockRejectedValueOnce(new Error('graph unavailable'));
+ await expect(loadNamespaceOverview()).rejects.toThrow('graph unavailable');
+ });
+});
+
+describe('namespaceOverviewApi object', () => {
+ it('exposes the public surface', () => {
+ expect(typeof namespaceOverviewApi.loadNamespaceOverview).toBe('function');
+ });
+});
diff --git a/app/src/services/api/namespaceOverviewApi.ts b/app/src/services/api/namespaceOverviewApi.ts
new file mode 100644
index 000000000..7755f4b9a
--- /dev/null
+++ b/app/src/services/api/namespaceOverviewApi.ts
@@ -0,0 +1,26 @@
+/**
+ * RPC facade for Namespace Overview.
+ *
+ * Adds ZERO new core surface. Reuses ONE already-shipped JSON-RPC wrapper —
+ * memoryGraphQuery (openhuman.memory_graph_query) — fetching ALL namespaces in
+ * one call (no namespace arg) and grouping by each relation's `namespace`
+ * field in the pure engine. Read-only — nothing is persisted.
+ */
+import debug from 'debug';
+
+import {
+ computeNamespaceOverview,
+ type NamespaceOverviewReport,
+} from '../../lib/memory/namespaceOverview';
+import { memoryGraphQuery } from '../../utils/tauriCommands/memory';
+
+const log = debug('namespace-overview:api');
+
+/** Fetch the whole graph and aggregate per-namespace stats. */
+export async function loadNamespaceOverview(): Promise {
+ const relations = await memoryGraphQuery();
+ log('loadNamespaceOverview relations=%d', relations.length);
+ return computeNamespaceOverview(relations);
+}
+
+export const namespaceOverviewApi = { loadNamespaceOverview };