fix(kbdb): 藏書地圖 M3 收尾——讀端自動核對重算,不再依賴 ingest 接鏈

真因(總管實測,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>
This commit is contained in:
uncle6me-web
2026-08-08 00:44:55 +08:00
parent 7dbd4f59e7
commit 962d863ef7
8 changed files with 340 additions and 13 deletions
+22 -7
View File
@@ -23,10 +23,19 @@ import { kbdbFetch } from "../lib/kbdb-client.js";
import { errorResponse, successResponse } from "../lib/cypher-client.js";
import { entityNames, parseSlotArray, type LibraryMapRow } from "../lib/library-map.js";
/** 空庫/404 時的 backfill 指引(誠實回報+給下一步,鐵律:不假綠)。 */
/**
* 空庫/404 時的指引(誠實回報+給下一步,鐵律:不假綠)。
*
* 2026-08-08 修正:舊版寫「地圖由 ingest 尾端自動重算(M3)」——那件事從沒接上過(總管實測
* grep 全 repo 查無任何呼叫點),是假話。真正的機制:kbdb `GET /map``GET /map/:library`
* 每次查詢都會自動核對即時三元組數,落差就地重算(`kbdb/src/actions/library-map.ts`
* `ensureFreshLibraryMaps`)——資料進來後下一次查詢就會反映,不需要任何人記得呼叫任何端點。
* `POST /map/recompute` 仍然存在,但只在極少數情況才需要手動打:舊三元組沒有 `library` slot
* 值、只能靠 `source_uri` 前綴回填時(`source_prefix` 參數)。
*/
const RECOMPUTE_HINTS = [
"地圖由 ingest 尾端自動重算(M3);尚未接鏈的庫要手動 backfill:對 kbdb 呼 POST /map/recompute?library=<庫名>(可帶 body {narrative, source_prefix}",
"backfill 過渡期(triplet 還沒有 library slot 值)用 source_prefix 以 source_uri 前綴歸庫,如 {\"source_prefix\":\"gitea:Leo/kb@\"}",
"地圖每次查詢都會自動核對即時三元組數並重算過期的庫,不必手動處理",
"少見情況(舊三元組沒有 library 標記)才需要手動:POST /map/recompute?library=<庫名>(可帶 body {narrative, source_prefix}",
];
/** 註冊全部藏書地圖工具(library-map M4)。 */
@@ -89,9 +98,11 @@ export function registerGetMap(server: McpServer, env: Env) {
triplet_count: Number(l.triplet_count ?? 0) || 0,
}));
if (libraries.length === 0) {
// 空庫誠實回報:不是錯誤(端點正常、就是還沒有地圖),給 backfill 指引。
// 空庫誠實回報:不是錯誤(端點正常)。地圖是讀時即時核對重算的(見 RECOMPUTE_HINTS
// 註解),所以「地圖是空的」現在真的等於「這個租戶目前沒有任何三元組資料」,
// 不再是「沒人跑過 recompute」那種曖昧狀態。
return successResponse({ libraries: [], count: 0 }, [
"全館地圖是空的:還沒有任何庫跑過 recompute",
"全館地圖是空的:這個租戶目前沒有任何三元組資料(不是地圖沒算,是真的還沒有資料)",
...RECOMPUTE_HINTS,
]);
}
@@ -104,9 +115,13 @@ export function registerGetMap(server: McpServer, env: Env) {
// 單庫詳圖:完整 slotsslot 陣列 parse 成物件再回)。
const res = await kbdbFetch(env, `/map/${encodeURIComponent(library)}${qs}`);
if (res.status === 404) {
// 地圖是讀時即時核對重算的:只要這個庫「已知」(有三元組、entries 蓋過章、或登記過),
// 上一步就會自動把它補成一筆 triplet_count:0 的地圖,走不到這個分支。真的落到 404,
// 代表這個名字在這個租戶的資料裡從沒出現過——不是「這庫是空的」,是根本沒有這個庫
// (可能打錯字,或這個庫在別的租戶/別的 owner_id 底下)。
return errorResponse(
"map_not_found",
`庫「${library}還沒有地圖(從未 recompute,或庫名打錯`,
`查無庫「${library}——這個名字在這個租戶的資料裡從沒出現過(不是「這庫是空的」,是根本沒有這個庫;地圖是即時核對重算的,不是忘了 recompute`,
["kbdb_get_map 不帶參數看全館有哪些庫(確認庫名)", ...RECOMPUTE_HINTS],
);
}
@@ -129,7 +144,7 @@ export function registerGetMap(server: McpServer, env: Env) {
triplet_count: Number(raw.triplet_count ?? 0) || 0,
};
return successResponse({ map }, [
"bridges=此庫 entity 同時出現在哪些其他庫(M3 backfill 前會偏稀疏,是誠實現況不是 bug)",
"bridges=此庫 entity 同時出現在哪些其他庫(只有兩側三元組都標了 library 值才抓得到,舊資料若沒標會偏稀疏,是誠實現況不是 bug)",
"沿核心 entity 挖關係:kbdb_graph_neighbors(subject=entity 名)",
]);
} catch (e) {
+23
View File
@@ -127,6 +127,25 @@ describe("kbdb_get_map: 全館地圖(無參數)", () => {
expect(JSON.stringify(body.hints)).toContain("POST /map/recompute");
});
// 2026-08-08:舊版說明文字宣稱「地圖由 ingest 尾端自動重算(M3)」——那件事從沒接上過
// (總管實測 grep 全 repo 查無任何呼叫點),是假話。修法:GET /map 讀端本身自動核對
// 即時三元組數並補算,不靠任何外部呼叫者。這裡釘住那句謊言不會再出現在任何 hint 裡。
it("不再宣稱「地圖由 ingest 尾端自動重算(M3)」——那件事從沒接上過,是假話(已修正措辭)", async () => {
const { server, tools } = makeServer();
const { env } = makeEnv(
() => new Response(JSON.stringify({ success: true, libraries: [], count: 0 })),
);
registerGetMap(server, env);
const res = await tools.get("kbdb_get_map")!.handler({});
const body = parseResult(res);
const hintsText = JSON.stringify(body.hints);
expect(hintsText).not.toContain("地圖由 ingest 尾端自動重算");
expect(hintsText).not.toContain("(M3)");
expect(hintsText).not.toContain("M3");
// 誠實的新措辭:空=這個租戶真的沒資料,不是「沒人跑過 recompute」
expect(hintsText).toContain("這個租戶目前沒有任何三元組資料");
});
it("HTTP error → map_fetch_failed with recompute hint, not a crash", async () => {
const { server, tools } = makeServer();
const { env } = makeEnv(() => new Response("boom", { status: 500 }));
@@ -199,6 +218,10 @@ describe("kbdb_get_map: 單庫詳圖(library 參數)", () => {
const body = parseResult(res);
expect(body.error_code).toBe("map_not_found");
expect(JSON.stringify(body.next_actions)).toContain("POST /map/recompute");
// 2026-08-08:404 現在的語意是「查無此庫」(地圖是即時核對重算的,已知庫即使空也回 200),
// 不再是舊版那句「從未 recompute」的曖昧說法。
expect(String(body.human_message)).toContain("查無庫");
expect(String(body.human_message)).toContain("從沒出現過");
});
it("binding throws → internal_error, not an unhandled crash", async () => {