feat(intelligence): add Namespace Overview (#2964)

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
Aashir Athar
2026-05-30 09:45:58 -07:00
committed by GitHub
co-authored by Claude Opus 4.7 Steven Enamakel
parent 29f8806fdb
commit 6c3ad6388d
23 changed files with 883 additions and 1 deletions
@@ -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('<NamespaceOverviewPanel />', () => {
it('renders the loading skeleton', () => {
render(<NamespaceOverviewPanel report={null} loading />);
expect(screen.getByTestId('namespace-overview-loading')).toBeInTheDocument();
});
it('renders the empty state when there are no namespaces', () => {
render(<NamespaceOverviewPanel report={computeNamespaceOverview([])} />);
expect(screen.getByText('No knowledge graph yet.')).toBeInTheDocument();
});
it('renders an error with a working retry button', () => {
const onRetry = vi.fn();
render(<NamespaceOverviewPanel report={null} error="graph unavailable" onRetry={onRetry} />);
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(<NamespaceOverviewPanel report={report} />);
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();
});
});
@@ -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 = (
<div
role="note"
className="rounded-lg border border-primary-200 dark:border-primary-500/30 bg-primary-50 dark:bg-primary-500/10 px-3 py-2 text-xs text-stone-700 dark:text-neutral-200">
<p className="font-medium mb-1">{t('namespaceOverview.title')}</p>
<p>{t('namespaceOverview.intro')}</p>
</div>
);
if (loading) {
return (
<div className="space-y-4">
{intro}
<div
className="space-y-3"
role="status"
aria-label={t('namespaceOverview.loading')}
data-testid="namespace-overview-loading">
<div className="grid gap-2 sm:grid-cols-3">
{[0, 1, 2].map(i => (
<div
key={i}
className="animate-pulse rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 h-16"
/>
))}
</div>
{[0, 1, 2].map(i => (
<div
key={i}
className="animate-pulse rounded-lg border border-stone-200 dark:border-neutral-800 bg-stone-50 dark:bg-neutral-800/60 h-6"
/>
))}
</div>
</div>
);
}
if (error) {
return (
<div className="space-y-4">
{intro}
<div className="rounded-lg border border-coral-200 dark:border-coral-500/30 p-4 text-center">
<p role="alert" className="text-xs text-coral-700 dark:text-coral-300">
{t('namespaceOverview.errorPrefix')} {error}
</p>
{onRetry && (
<button
type="button"
onClick={onRetry}
className="mt-2 rounded-lg bg-primary-500 px-3 py-1.5 text-xs font-semibold text-white hover:bg-primary-600">
{t('namespaceOverview.retry')}
</button>
)}
</div>
</div>
);
}
if (!report || report.namespaceCount === 0) {
return (
<div className="space-y-4">
{intro}
<div className="py-8 text-center">
<h3 className="text-sm font-semibold text-stone-700 dark:text-neutral-200">
{t('namespaceOverview.empty')}
</h3>
<p className="mt-1 text-xs text-stone-500 dark:text-neutral-400">
{t('namespaceOverview.emptyHint')}
</p>
</div>
</div>
);
}
const maxFacts = report.namespaces[0]?.factCount || 1;
const rows = report.namespaces.slice(0, MAX_ROWS);
const truncated = report.namespaces.length > MAX_ROWS;
return (
<div className="space-y-4">
{intro}
{/* Summary tiles */}
<div className="grid gap-2 sm:grid-cols-3">
{[
{ label: t('namespaceOverview.metricNamespaces'), value: report.namespaceCount },
{ label: t('namespaceOverview.metricFacts'), value: report.totalFacts },
{ label: t('namespaceOverview.metricEntities'), value: report.totalEntities },
].map(tile => (
<div
key={tile.label}
className="rounded-lg border border-stone-200 dark:border-neutral-800 p-3">
<div className="text-[10px] uppercase tracking-wider text-stone-400 dark:text-neutral-500">
{tile.label}
</div>
<div className="text-lg font-semibold tabular-nums text-stone-900 dark:text-neutral-100">
{tile.value}
</div>
</div>
))}
</div>
{/* Ranked namespace list */}
<section aria-labelledby="namespace-overview-heading" className="space-y-1">
<h3
id="namespace-overview-heading"
className="text-xs font-semibold uppercase tracking-wider text-stone-500 dark:text-neutral-400">
{t('namespaceOverview.heading')}
</h3>
<ul className="space-y-1">
{rows.map(stat => (
<li
key={JSON.stringify(stat.namespace)}
className="flex items-center gap-2 text-[11px] tabular-nums">
<span
className={`w-28 shrink-0 truncate ${
stat.namespace === null
? 'italic text-stone-400 dark:text-neutral-500'
: 'text-stone-700 dark:text-neutral-200'
}`}
title={stat.namespace ?? t('namespaceOverview.unnamespaced')}>
{stat.namespace ?? t('namespaceOverview.unnamespaced')}
</span>
<div className="flex-1 h-3 rounded bg-stone-100 dark:bg-neutral-800 overflow-hidden">
<div
className="h-full bg-primary-400/70"
style={{ width: `${(stat.factCount / maxFacts) * 100}%` }}
/>
</div>
<span
className="w-16 shrink-0 text-right text-stone-500 dark:text-neutral-400"
title={t('namespaceOverview.factsLabel').replace(
'{count}',
String(stat.factCount)
)}>
{stat.factCount}
</span>
<span
className="w-16 shrink-0 text-right text-stone-400 dark:text-neutral-500"
title={t('namespaceOverview.entitiesLabel').replace(
'{count}',
String(stat.entityCount)
)}>
{t('namespaceOverview.entitiesShort').replace('{count}', String(stat.entityCount))}
</span>
</li>
))}
</ul>
{truncated && (
<p className="text-center text-xs text-stone-400 dark:text-neutral-500">
{t('namespaceOverview.truncated')
.replace('{shown}', String(rows.length))
.replace('{total}', String(report.namespaces.length))}
</p>
)}
</section>
</div>
);
};
export default NamespaceOverviewPanel;
@@ -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('<NamespaceOverviewTab />', () => {
beforeEach(() => {
mockLoad.mockReset();
mockLoad.mockResolvedValue(report);
});
it('loads on mount and renders the per-namespace list', async () => {
render(<NamespaceOverviewTab />);
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(<NamespaceOverviewTab />);
await waitFor(() => expect(screen.getByRole('alert').textContent).toMatch(/graph unavailable/));
});
});
@@ -0,0 +1,49 @@
/**
* Namespace Overview tab (container). Loads the whole graph on mount and
* delegates rendering to the pure <NamespaceOverviewPanel>. 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<NamespaceOverviewReport | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<NamespaceOverviewPanel
report={report}
loading={loading}
error={error}
onRetry={() => void load()}
/>
);
};
export default NamespaceOverviewTab;
+18
View File
@@ -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':
'ويظهر الإرسال على صور ذاكرتك مراكز الحمل - والكيانات الموصلة التي تربط المجموعات المنفصلة عن بعضها البعض، والتي لا يمكن لإحصاء الترددات الخام أن يكشف عنها.',
+19
View File
@@ -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 ফ্রিকোয়েন্সি গণনা প্রকাশ করতে পারে না।',
+19
View File
@@ -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.',
+19
View File
@@ -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.',
+20
View File
@@ -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.',
+19
View File
@@ -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 dentité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 lassistant 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.",
+19
View File
@@ -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':
'अपने मेमोरी ग्राफ़ पर पेजरैंक लोड-असर हब सतहों - और कनेक्टर इकाइयां जो अन्यथा अलग-अलग समूहों को जोड़ती हैं, जो एक कच्चे आवृत्ति गिनती प्रकट नहीं हो सकती हैं।',
+19
View File
@@ -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.',
+19
View File
@@ -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 lassistente 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.',
+19
View File
@@ -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를 적용해 핵심 허브와, 단순 빈도 계산으로는 드러나지 않는 분리된 클러스터를 연결하는 엔터티를 드러냅니다.',
+19
View File
@@ -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.',
+19
View File
@@ -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.',
+19
View File
@@ -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 по вашему графику памяти отображает несущие нагрузку концентраторы — и объекты-соединители, которые связывают отдельные кластеры, которые не может выявить необработанный подсчет частоты.',
+18
View File
@@ -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,可找出关键枢纽以及连接原本分离聚类的连接实体,这是单纯频次统计无法揭示的。',
@@ -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);
});
});
+108
View File
@@ -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<string>; // distinct triple keys
entities: Set<string>; // distinct entity ids
}
/**
* Compute the per-namespace overview. Pure function of `relations`.
*/
export function computeNamespaceOverview(relations: GraphRelation[]): NamespaceOverviewReport {
const buckets = new Map<string | null, Bucket>();
const allEntities = new Set<string>();
const ensure = (ns: string | null): Bucket => {
let bucket = buckets.get(ns);
if (!bucket) {
bucket = { facts: new Set<string>(), entities: new Set<string>() };
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,
};
}
+6 -1
View File
@@ -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' && <MemoryTimelineTab />}
{activeTab === 'path' && <ConnectionPathTab />}
{activeTab === 'namespaces' && <NamespaceOverviewTab />}
</div>
</div>
</div>
@@ -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');
});
});
@@ -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<NamespaceOverviewReport> {
const relations = await memoryGraphQuery();
log('loadNamespaceOverview relations=%d', relations.length);
return computeNamespaceOverview(relations);
}
export const namespaceOverviewApi = { loadNamespaceOverview };