From 46db096c59e2548c0aa0d0bc7b5c7598e13e8e6d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Apr 2026 00:44:28 +0000 Subject: [PATCH] fix: register /api/concepts and fix /api/coherence 500 (issue #27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two REST bugs reported in issue #27 against @fozikio/cortex-engine@1.1.0: 1. GET /api/concepts → 404 Route was never registered. The dashboard SPA uses 'concepts' as its vocabulary for memory nodes. Added /api/concepts route that reads all memories from the store and maps them to { id, name, definition, category, confidence, salience, access_count, tags, fsrs, created_at, updated_at } alongside a total count. 2. GET /api/coherence → 500 Route called invokeTool(engine, 'validate', {}) but validate requires a prediction_id argument — it confirms individual FSRS predictions, not graph coherence. Fixed to call graph_report instead and derive a coherence score: (total_memories - orphaned_concepts) / total_memories. Returns { score, total_memories, orphaned_concepts, total_edges }. This also fixes the downstream SPA crash (toLocaleString on undefined) caused by the error response landing where a number was expected. https://claude.ai/code/session_012qB5PebzhifzEkoLfVKYfy --- src/rest/server.ts | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/rest/server.ts b/src/rest/server.ts index e49fd6a..988c0db 100644 --- a/src/rest/server.ts +++ b/src/rest/server.ts @@ -228,8 +228,13 @@ route('GET', '/api/vitals', async (_req, res, _params, engine) => { // Coherence route('GET', '/api/coherence', async (_req, res, _params, engine) => { - const result = await invokeTool(engine, 'validate', {}); - json(res, result); + const report = await invokeTool(engine, 'graph_report', {}) as Record; + const total = typeof report['total_memories'] === 'number' ? report['total_memories'] : 0; + const orphans = typeof report['orphaned_concepts'] === 'number' ? report['orphaned_concepts'] : 0; + const edges = typeof report['total_edges'] === 'number' ? report['total_edges'] : 0; + // Fraction of connected memories (1.0 = fully coherent, 0.0 = all orphans). + const score = total > 0 ? (total - orphans) / total : 1; + json(res, { score, total_memories: total, orphaned_concepts: orphans, total_edges: edges }); }); // Home (aggregate) @@ -247,6 +252,33 @@ route('GET', '/api/home', async (_req, res, _params, engine) => { }); }); +// Concepts (memories exposed under the dashboard's preferred name) +route('GET', '/api/concepts', async (req, res, _params, engine) => { + const url = new URL(req.url!, `http://localhost`); + const limit = parseInt(url.searchParams.get('limit') ?? '100', 10); + const offset = parseInt(url.searchParams.get('offset') ?? '0', 10); + const store = engine.ctx.namespaces.getStore(); + const allMemories = await store.getAllMemories(); + + const concepts = allMemories + .slice(offset, offset + limit) + .map(m => ({ + id: m.id, + name: m.name, + definition: m.definition, + category: m.category, + confidence: m.confidence, + salience: m.salience, + access_count: m.access_count, + tags: m.tags ?? [], + fsrs: m.fsrs, + created_at: m.created_at?.toISOString?.() ?? new Date().toISOString(), + updated_at: m.updated_at?.toISOString?.() ?? new Date().toISOString(), + })); + + json(res, { concepts, total: allMemories.length }); +}); + // ── Memories ────────────────────────────────────────────────────────────────── route('GET', '/api/memories', async (req, res, _params, engine) => {