962d863ef7
真因(總管實測,system-dev/wiki/mistakes.md 08-08 段):design 原訂「ingest 尾端呼 POST /map/recompute」,但 repo 內查無任何呼叫點,三週沒接上,沒手動 backfill 過的租戶 (絕大多數)GET /map 恆回空;MCP 說明文字還宣稱「地圖由 ingest 尾端自動重算(M3)」——假話。 leo 否決「降級成即時聚合、不維護快取」的提案(會丟失 narrative 這類摘要本體,只算得出 count)。改法:GET /map/GET /map/:library 讀端自己核對即時三元組數,落差就地呼叫既有的 recomputeLibraryMap 補算(kbdb/src/actions/library-map.ts ensureFreshLibraryMaps)。聚合 SQL 沒有第二套、narrative/relation_profile/bridges 摘要欄位原封不動,只是觸發時機從「等 外部呼叫」改成「讀的當下順手核對」。同時解掉:全租戶自動 backfill/跟得上新資料/不依賴 跨 repo 的 ingest 接鏈。 附帶修 recomputeLibraryMap 的 narrative 欄位:沒帶值時原本會清空,改成沿用上一版(避免 自動重算把 ingest 端/人工填過的 narrative 靜默洗掉)。 修正三處說謊的說明文字(mcp/src/tools/kbdb_map.ts、console-ui console/index.html): 「地圖由 ingest 尾端自動重算(M3)」不存在,改為誠實描述讀端即時核對機制;404 語意從 「從未 recompute」改為「查無此庫」(已知但空的庫現在會自動補成 triplet_count:0 的 200, 不會落到 404)。 測試:kbdb 新增 6 案(18/18 全綠,覆蓋自動 backfill/跟得上資料/narrative 保留/ 404 vs 空庫誠實分辨/owner 隔離/無 triplet template 不報錯);mcp 新增 1 案釘住舊謊言 不再出現。kbdb 125/125、mcp 69/77(同基線 8 個 oauth 既有失敗,非本次引入)全綠; tsc 兩包乾淨(kbdb 1 個既有 auth.test.ts 錯誤與 stash 前一致,非本次引入)。 SDD:system-dev/docs/3-specs/library-map/tasks.md M3 從「07-19 誤標 ✅」更正為實況; design.md §3 加 2026-08-08 更正說明。未動 frontmatter status(仍 draft,D35 生命週期 鐵律留給總管/leo 裁)。 殘項:本次修改只在本機驗證(真 SQLite + 假 binding 單元測試),未部署 prod;未在真實 KBDB(如 yuga3bse 租戶)重新實測 kbdb_get_map 非空——需部署後才能貼實測輸出。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
65 lines
3.8 KiB
TypeScript
65 lines
3.8 KiB
TypeScript
// Map route — 藏書地圖(library-map SDD M2;源頭 Arcrun#39)。
|
||
// 聚合 SQL 只准住基本盤(D6 推論,design §2 關鍵歸屬裁定)——本 route 是薄殼,SQL 全在
|
||
// actions/library-map.ts。auth 照 base route 既有慣例:raw worker 不驗 key,owner 隔離由
|
||
// cypher proxy(X-Arcrun-API-Key → owner_id 注入)/caller 帶 owner_id 參數完成。
|
||
import { Hono } from 'hono';
|
||
import type { Bindings } from '../types';
|
||
import {
|
||
ensureFreshLibraryMaps,
|
||
getLibraryMapDetail,
|
||
listLibraryMaps,
|
||
recomputeLibraryMap,
|
||
} from '../actions/library-map';
|
||
|
||
export const mapRoutes = new Hono<{ Bindings: Bindings }>();
|
||
|
||
// POST /map/recompute?library=X — 對該庫重算地圖(ingest 尾端/backfill 呼叫;R2 增量重算)。
|
||
// body(皆選填):{ narrative, commit_hash, owner_id, source_prefix, triplet_template, top_n }
|
||
// - narrative:本輪由 caller 傳入(wiki 首段抽取屬 ingest 端 M3,base 不抽取)。
|
||
// - source_prefix:舊 triplet(無 library slot 值)的 source_uri 前綴 fallback(M3 backfill 前過渡)。
|
||
// 寫入順序安全:先建新 active map block,再把舊 block 標 superseded(見 action 註解)。
|
||
mapRoutes.post('/recompute', async (c) => {
|
||
const body = (await c.req.json().catch(() => ({}))) as Record<string, unknown>;
|
||
const library = c.req.query('library') || (typeof body.library === 'string' ? body.library : '');
|
||
if (!library || !library.trim()) return c.json({ success: false, error: 'library required' }, 400);
|
||
try {
|
||
const result = await recomputeLibraryMap(c.env.DB, {
|
||
library,
|
||
narrative: typeof body.narrative === 'string' ? body.narrative : undefined,
|
||
commit_hash: typeof body.commit_hash === 'string' ? body.commit_hash : undefined,
|
||
owner_id: typeof body.owner_id === 'string' ? body.owner_id : c.req.query('owner_id') || undefined,
|
||
source_prefix: typeof body.source_prefix === 'string' ? body.source_prefix : undefined,
|
||
triplet_template: typeof body.triplet_template === 'string' ? body.triplet_template : undefined,
|
||
top_n: typeof body.top_n === 'number' ? body.top_n : undefined,
|
||
});
|
||
return c.json({ success: true, ...result });
|
||
} catch (e) {
|
||
return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400);
|
||
}
|
||
});
|
||
|
||
// GET /map — 全館地圖:每庫一行(library+narrative+top 3 entities+triplet_count)。
|
||
// 形狀給 MCP instructions/GUI 首頁共用(R3/R4),設計在數百 token 內。
|
||
//
|
||
// 2026-08-08:讀前先 ensureFreshLibraryMaps(即時新鮮度層,見 actions/library-map.ts 段落註解)——
|
||
// 不再只讀靜態快取,讀的當下順手核對即時三元組數、落差就地補算。失敗吞掉不擋讀取(地圖是加分)。
|
||
mapRoutes.get('/', async (c) => {
|
||
const owner = c.req.query('owner_id') || undefined;
|
||
await ensureFreshLibraryMaps(c.env.DB, owner).catch(() => {});
|
||
const libraries = await listLibraryMaps(c.env.DB, owner);
|
||
return c.json({ success: true, libraries, count: libraries.length });
|
||
});
|
||
|
||
// GET /map/:library — 該庫詳圖(完整 slots+可嵌人話 content)。
|
||
// 同樣先跑即時新鮮度層。之後仍查不到 → 誠實 404(這個名字這個租戶的資料裡從沒出現過,
|
||
// 不是「這庫是空的」——已知但目前 0 三元組的庫會被上一步補成一筆 triplet_count:0 的 map,
|
||
// 走得到 200,不會落到這條 404)。
|
||
mapRoutes.get('/:library', async (c) => {
|
||
const owner = c.req.query('owner_id') || undefined;
|
||
const library = c.req.param('library');
|
||
await ensureFreshLibraryMaps(c.env.DB, owner).catch(() => {});
|
||
const map = await getLibraryMapDetail(c.env.DB, library, owner);
|
||
if (!map) return c.json({ success: false, error: 'not found' }, 404);
|
||
return c.json({ success: true, map });
|
||
});
|