- ○載入中…
diff --git a/README.md b/README.md index 3b0f13b..2d00649 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Served via jsDelivr; fetched automatically during install — you never need to - `arcrun-wait/` — **arcrun-wait**(首裝) - `daemon/` — 桌面 App(Mac/Windows)安裝檔 -Built from `Arcrun@cacaa33f7d4e` by `installer/scripts/ship.mjs`(arcrun-rag repo,release 1.4.46,built 2026-08-15)。 +Built from `Arcrun@466a59c5616f` by `installer/scripts/ship.mjs`(arcrun-rag repo,release 1.4.48,built 2026-08-18)。 ⚠️ 這份檔案由出貨管線每次自動重寫(`installer/scripts/render-bundles-readme.mjs`)—— 不要手動改這裡列的零件清單——它是算出來的:公庫=Arcrun 這一版編了什麼, diff --git a/arcrun-cypher-executor/worker.mjs b/arcrun-cypher-executor/worker.mjs index 549b6a2..6235f5d 100644 --- a/arcrun-cypher-executor/worker.mjs +++ b/arcrun-cypher-executor/worker.mjs @@ -3324,7 +3324,7 @@ var init_kbdb_proxy = __esm({ const { base, headers } = kbdbBase(c.env); const params = new URLSearchParams(); params.set("owner_id", owner); - for (const k of ["entry_type", "parent_id", "page_name", "source", "library", "limit", "offset"]) { + for (const k of ["entry_type", "parent_id", "page_name", "source", "library", "exclude_kind", "limit", "offset"]) { const v = c.req.query(k); if (v) params.set(k, v); } @@ -3374,6 +3374,42 @@ var init_kbdb_proxy = __esm({ return c.json({ success: false, error: `KBDB \u4E0D\u53EF\u9054\uFF08${base}\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502); } }); + kbdbProxyRouter.put("/kbdb/map/:library/narrative", async (c) => { + if (!tenant(c)) return c.json(NEED_KEY, 401); + const body = await c.req.json().catch(() => null); + const narrative = typeof body?.narrative === "string" ? body.narrative : ""; + if (!narrative.trim()) return c.json({ error: "narrative \u5FC5\u586B\uFF08\u4E0D\u5F97\u7A7A\u767D\uFF09" }, 400); + const owner = (typeof body?.owner_id === "string" ? body.owner_id : c.req.query("owner_id")) || void 0; + const { base, headers } = kbdbBase(c.env); + try { + const res = await fetch(`${base}/map/${encodeURIComponent(c.req.param("library"))}/narrative`, { + method: "PUT", + headers, + body: JSON.stringify({ narrative, ...owner ? { owner_id: owner } : {} }) + }); + return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } }); + } catch (e) { + return c.json({ success: false, error: `KBDB \u4E0D\u53EF\u9054\uFF08${base}\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502); + } + }); + kbdbProxyRouter.post("/kbdb/map/recompute", async (c) => { + if (!tenant(c)) return c.json(NEED_KEY, 401); + const body = await c.req.json().catch(() => ({})); + const library = (typeof body.library === "string" ? body.library : c.req.query("library")) || ""; + if (!library.trim()) return c.json({ error: "library \u5FC5\u586B" }, 400); + const owner = (typeof body.owner_id === "string" ? body.owner_id : c.req.query("owner_id")) || void 0; + const { base, headers } = kbdbBase(c.env); + try { + const res = await fetch(`${base}/map/recompute`, { + method: "POST", + headers, + body: JSON.stringify({ ...body, library, ...owner ? { owner_id: owner } : {} }) + }); + return new Response(res.body, { status: res.status, headers: { "Content-Type": "application/json" } }); + } catch (e) { + return c.json({ success: false, error: `KBDB \u4E0D\u53EF\u9054\uFF08${base}\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}` }, 502); + } + }); kbdbProxyRouter.patch("/kbdb/entries/:id", async (c) => { if (!tenant(c)) return c.json(NEED_KEY, 401); const body = await c.req.json().catch(() => ({})); @@ -15323,10 +15359,12 @@ portalDataRouter.get( } const inner = unwrapWorkflowData(result.data, "answer"); const rawSources = Array.isArray(inner.sources) ? inner.sources : []; + const retrieval = inner.retrieval; return c.json({ answer: typeof inner.answer === "string" ? inner.answer : "", sources: dedupeSourcesByPage(rawSources), - graph_facts: inner.graph_facts ?? null + graph_facts: inner.graph_facts ?? null, + retrieval: retrieval && typeof retrieval === "object" ? retrieval : null }); }) ); diff --git a/arcrun-kbdb/worker.mjs b/arcrun-kbdb/worker.mjs index 240eccb..adde495 100644 --- a/arcrun-kbdb/worker.mjs +++ b/arcrun-kbdb/worker.mjs @@ -2119,6 +2119,15 @@ var Hono2 = class extends Hono { } }; +// kbdb/src/actions/library-predicate.ts +var UNLABELLED_LIBRARY = "general"; +function libraryOf(expr) { + return `COALESCE(NULLIF(${expr}, ''), '${UNLABELLED_LIBRARY}')`; +} +var ENTRY_LIBRARY_EXPR = "json_extract(metadata_json, '$.library')"; +var ENTRY_LIBRARY = libraryOf(ENTRY_LIBRARY_EXPR); +var ENTRY_UNLABELLED = `(${ENTRY_LIBRARY_EXPR} IS NULL OR ${ENTRY_LIBRARY_EXPR} = '')`; + // kbdb/src/actions/entry-crud.ts function uid(prefix) { return `${prefix}_${crypto.randomUUID()}`; @@ -2179,6 +2188,11 @@ async function listEntries(db, f = {}) { conds.push(libraryPredicate(f.library)); params.push(...f.library); } + if (f.exclude_kind && f.exclude_kind.length > 0) { + const ph = f.exclude_kind.map(() => "?").join(","); + conds.push(`COALESCE(json_extract(metadata_json, '$.kind'), '') NOT IN (${ph})`); + params.push(...f.exclude_kind); + } if (f.q) { const m = buildContentLike(f.q); conds.push(...m.conds); @@ -2218,7 +2232,7 @@ async function embeddedIdsByLibrary(db, ownerId, library) { const rows = await db.prepare( `SELECT id FROM entries WHERE owner_id = ? - AND COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') = ? + AND ${ENTRY_LIBRARY} = ? AND is_embedded = 1` ).bind(ownerId, library).all(); return (rows.results ?? []).map((r) => r.id); @@ -2230,11 +2244,12 @@ async function markUnembedded(db, ids) { } async function deprecateEntriesByLibrary(db, ownerId, library) { const result = await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在 worktree ⇒ hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到,同 887463c/962d863/5919c6b 已記載的假警報,非繞牆 `UPDATE entries SET metadata_json = json_set(COALESCE(metadata_json, '{}'), '$.status', 'deprecated'), updated_at = unixepoch() WHERE owner_id = ? - AND COALESCE(json_extract(metadata_json, '$.library'), 'general') = ? + AND ${ENTRY_LIBRARY} = ? AND (json_extract(metadata_json, '$.status') IS NULL OR json_extract(metadata_json, '$.status') != 'deprecated')` ).bind(ownerId, library).run(); @@ -2423,7 +2438,7 @@ function applyRelativeCut(rows) { } function libraryPredicate(libraries) { const placeholders = libraries.map(() => "?").join(","); - return `COALESCE(json_extract(metadata_json, '$.library'), 'general') IN (${placeholders})`; + return `${ENTRY_LIBRARY} IN (${placeholders})`; } var NOT_DEPRECATED_PREDICATE = "(json_extract(metadata_json, '$.status') IS NULL OR json_extract(metadata_json, '$.status') != 'deprecated')"; function isDeprecatedEntry(entry) { @@ -2641,7 +2656,7 @@ function selectionCriteriaPredicate(opts) { params.push(opts.source); } if (opts.library) { - conds.push("COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') = ?"); + conds.push(`${ENTRY_LIBRARY} = ?`); params.push(opts.library); } if (typeof opts.since === "number") { @@ -2924,9 +2939,7 @@ async function migrateLegacyCredentialsForOwner(db, ownerId) { var MAX_PAGE_NAMES = 300; var HARD_LIMIT_CAP = 500; function criteriaPredicate(c) { - const conds = [ - "(json_extract(metadata_json, '$.library') IS NULL OR json_extract(metadata_json, '$.library') = '')" - ]; + const conds = [ENTRY_UNLABELLED]; const params = []; if (c.owner_id) { conds.push("owner_id = ?"); @@ -3034,7 +3047,7 @@ entryRoutes.post("/", async (c) => { entryRoutes.get("/libraries", async (c) => { const owner = c.req.query("owner_id") || ""; const rows = await c.env.DB.prepare( - `SELECT DISTINCT COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') AS library + `SELECT DISTINCT ${ENTRY_LIBRARY} AS library FROM entries WHERE (?1 = '' OR owner_id = ?1) AND COALESCE(json_extract(metadata_json, '$.status'), '') != 'deprecated' @@ -3047,7 +3060,7 @@ entryRoutes.get("/library-stats", async (c) => { const owner = c.req.query("owner_id") || ""; const rows = await c.env.DB.prepare( `SELECT - COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') AS library, + ${ENTRY_LIBRARY} AS library, COUNT(DISTINCT page_name) AS card_count FROM entries WHERE (?1 = '' OR owner_id = ?1) @@ -3075,6 +3088,12 @@ entryRoutes.get("/", async (c) => { source: c.req.query("source") || void 0, library: parseLibraryParam(c.req.query("library")), q: c.req.query("q") || c.req.query("search") || void 0, + // e.g. 只要使用者的原文、不要地圖自己產的摘要: ?exclude_kind=library_map(Arcrun#44) + // 與軟刪(metadata.status=deprecated)互補、不重疊:軟刪只標「非現役」的舊地圖, + // 而且 listEntries 這條路**根本沒有 deprecated 謂詞**(只有 searchEntries 有) + // ⇒ 對本端點軟刪救不到。這個 filter 連現役那顆一起排除——對「找原文」的呼叫端來說, + // 地圖摘要不管新舊都不是使用者寫的內容。 + exclude_kind: parseLibraryParam(c.req.query("exclude_kind")), limit: c.req.query("limit") ? Number(c.req.query("limit")) : void 0, offset: c.req.query("offset") ? Number(c.req.query("offset")) : void 0 }); @@ -3472,19 +3491,22 @@ async function getRecord(db, recordId) { const identity = await db.prepare("SELECT owner_id FROM entries WHERE id = ?").bind(recordId).first(); return { record_id: recordId, template_id: belongs.dst_id, values, owner_id: identity?.owner_id ?? null }; } -async function searchByTemplate(db, template, owner_id, limit = 100) { +async function searchByTemplate(db, template, owner_id, limit = 100, offset = 0) { const tpl = await getTemplate(db, template); if (!tpl) return []; - const cap = Math.min(limit, 500); + const cap = Math.min(Math.max(limit, 1), 500); + const skip = Math.max(offset, 0); const res = owner_id ? await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/);本次 checkout 開在 worktree /private/tmp/wt-graph-first-44/,hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到,與 962d863/5919c6b 記載的是同一個假警報 `SELECT src_id AS record_id FROM entries WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ? AND owner_id = ? - ORDER BY created_at DESC, rowid DESC LIMIT ?` - ).bind(tpl.id, owner_id, cap).all() : await db.prepare( + ORDER BY created_at DESC, rowid DESC LIMIT ? OFFSET ?` + ).bind(tpl.id, owner_id, cap, skip).all() : await db.prepare( + // kbdb-sql-ok:同上(worktree 路徑假警報) `SELECT src_id AS record_id FROM entries WHERE rel_id = '${SYS_BELONGS}' AND dst_id = ? - ORDER BY created_at DESC, rowid DESC LIMIT ?` - ).bind(tpl.id, cap).all(); + ORDER BY created_at DESC, rowid DESC LIMIT ? OFFSET ?` + ).bind(tpl.id, cap, skip).all(); const ids = (res.results ?? []).map((r) => r.record_id); if (ids.length === 0) return []; const byId = /* @__PURE__ */ new Map(); @@ -3561,6 +3583,500 @@ templateRoutes.patch("/:id", async (c) => { return c.json({ success: true, template: tpl }); }); +// kbdb/src/actions/library-map.ts +var LIBRARY_MAP_TEMPLATE_ID = "tpl-library-map"; +var LIBRARY_MAP_TEMPLATE_NAME = "library_map"; +var LIBRARY_MAP_SLOTS = [ + "library", + "narrative", + "top_entities", + "relation_profile", + "bridges", + "triplet_count", + "commit_hash", + "status" +]; +var DEFAULT_TRIPLET_TEMPLATE = "triplet"; +function mapContent(library, narrative, coreNames) { + const core = coreNames.length ? coreNames.join("\u3001") : "\uFF08\u5C1A\u7121 entities\uFF09"; + const text = narrative?.trim(); + if (!text) return `${library}\u3002\u6838\u5FC3\uFF1A${core}`; + const sep = /[。..!!??;;]$/.test(text) ? "" : "\u3002"; + return `${library}\uFF1A${text}${sep}\u6838\u5FC3\uFF1A${core}`; +} +var LIBRARY_MAP_TEMPLATE_DESCRIPTION = "per-library map block\uFF08\u85CF\u66F8\u5730\u5716\uFF1Atop_entities\uFF0Frelation_profile\uFF0Fbridges\uFF0Ftriplet_count \u7531 graph \u6A5F\u68B0\u5C0E\u51FA\uFF1Bnarrative \u7531 LLM \u8457\u4F5C\uFF0C\u7B97\u4E0D\u51FA\u4F86\u3001\u4E1F\u4E86\u56DE\u4E0D\u4F86\uFF1BArcrun#39\uFF0F#44\uFF09"; +async function ensureLibraryMapTemplate(db) { + const existing = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); + if (existing) { + if (existing.description !== LIBRARY_MAP_TEMPLATE_DESCRIPTION) { + await updateTemplate(db, existing.id, { description: LIBRARY_MAP_TEMPLATE_DESCRIPTION }); + } + return; + } + try { + await createTemplate(db, { + id: LIBRARY_MAP_TEMPLATE_ID, + name: LIBRARY_MAP_TEMPLATE_NAME, + description: LIBRARY_MAP_TEMPLATE_DESCRIPTION, + slots: LIBRARY_MAP_SLOTS, + created_by: "system" + }); + } catch { + if (!await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME)) throw new Error("ensureLibraryMapTemplate failed"); + } +} +async function ensureTripletLibrarySlot(db, tripletTemplate) { + const tpl = await getTemplate(db, tripletTemplate); + if (!tpl) throw new Error(`triplet template not found: ${tripletTemplate}`); + const slots = JSON.parse(tpl.slots_json); + if (slots.includes("library")) return false; + await updateTemplate(db, tpl.id, { slots: [...slots, "library"] }); + return true; +} +function tripletPivotSql(ownerFiltered) { + return `SELECT b.src_id AS rid, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_subject' THEN v.content END) AS subject, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_object' THEN v.content END) AS object, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_predicate' THEN v.content END) AS predicate, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_source_uri' THEN v.content END) AS source_uri + FROM entries b + LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' + LEFT JOIN entries v ON v.id = r.dst_id + WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerFiltered ? " AND b.owner_id = ?" : ""} + GROUP BY b.src_id`; +} +function mapPivotSql(ownerFiltered) { + return `SELECT b.src_id AS rid, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_narrative' THEN v.content END) AS narrative, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_top_entities' THEN v.content END) AS top_entities, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_relation_profile' THEN v.content END) AS relation_profile, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_bridges' THEN v.content END) AS bridges, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_triplet_count' THEN v.content END) AS triplet_count, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_commit_hash' THEN v.content END) AS commit_hash, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, + MAX(r.created_at) AS ts + FROM entries b + LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' + LEFT JOIN entries v ON v.id = r.dst_id + WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerFiltered ? " AND b.owner_id = ?" : ""} + GROUP BY b.src_id`; +} +function parseJsonArray(raw2) { + if (!raw2) return []; + try { + const v = JSON.parse(raw2); + return Array.isArray(v) ? v : []; + } catch { + return []; + } +} +async function deprecateStaleMapBlocks(db, library, keepId, owner_id) { + const params = [library, keepId]; + if (owner_id) params.push(owner_id); + const res = await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/),worktree 路徑造成的假警報同本檔既有註解 + `UPDATE entries + SET metadata_json = json_set(COALESCE(metadata_json, '{}'), '$.status', 'deprecated'), + updated_at = unixepoch() + WHERE entry_type = 'block' + AND json_extract(metadata_json, '$.kind') = 'library_map' + AND json_extract(metadata_json, '$.library') = ? + AND id != ? + ${owner_id ? "AND owner_id = ?" : ""} + AND (json_extract(metadata_json, '$.status') IS NULL + OR json_extract(metadata_json, '$.status') != 'deprecated')` + ).bind(...params).run(); + return res.meta?.changes ?? 0; +} +async function recomputeLibraryMap(db, input) { + const library = input.library.trim(); + if (!library) throw new Error("library required"); + const tripletTemplateName = input.triplet_template ?? DEFAULT_TRIPLET_TEMPLATE; + const topN = Math.min(Math.max(Math.floor(input.top_n ?? 10), 1), 50); + await ensureLibraryMapTemplate(db); + const librarySlotAdded = await ensureTripletLibrarySlot(db, tripletTemplateName); + const tripletTpl = await getTemplate(db, tripletTemplateName); + if (!tripletTpl) throw new Error(`triplet template not found: ${tripletTemplateName}`); + const owner = input.owner_id || void 0; + const pivot = tripletPivotSql(!!owner); + const pivotParams = owner ? [tripletTpl.id, owner] : [tripletTpl.id]; + const libCond = input.source_prefix ? `(${libraryOf("t.library")} = ? OR (t.library IS NULL AND t.source_uri LIKE ? || '%'))` : `${libraryOf("t.library")} = ?`; + const libParams = input.source_prefix ? [library, input.source_prefix] : [library]; + const withLib = `WITH t AS (${pivot}), lib AS ( + SELECT * FROM t WHERE COALESCE(t.status, 'active') = 'active' AND ${libCond})`; + const baseParams = [...pivotParams, ...libParams]; + const [countRow, topRes, relRes, bridgeRes] = await Promise.all([ + db.prepare(`${withLib} SELECT COUNT(*) AS n FROM lib`).bind(...baseParams).first(), + // degree=entity 在該庫 active triplet 的出現次數(subject+object 兩側都算;同名並列取名字序穩定輸出) + db.prepare( + `${withLib} SELECT name, COUNT(*) AS degree FROM ( + SELECT subject AS name FROM lib UNION ALL SELECT object AS name FROM lib) + WHERE name IS NOT NULL GROUP BY name ORDER BY degree DESC, name ASC LIMIT ?` + ).bind(...baseParams, topN).all(), + // predicate 分布=庫的「性格」(spec §3 relation_profile) + db.prepare( + `${withLib} SELECT predicate, COUNT(*) AS n FROM lib + WHERE predicate IS NOT NULL GROUP BY predicate ORDER BY n DESC, predicate ASC LIMIT 100` + ).bind(...baseParams).all(), + // bridges=本庫 entity 同時出現在其他庫(跨庫 join)。對面那側只能靠 library slot 標記值 + //(source_prefix 只描述本庫的前綴,無法反推他庫)→ M3 backfill 前 bridges 會偏稀疏,誠實現況。 + db.prepare( + `${withLib}, labeled AS ( + SELECT DISTINCT name, library FROM ( + SELECT subject AS name, ${libraryOf("library")} AS library FROM t WHERE COALESCE(status,'active') = 'active' + UNION SELECT object AS name, ${libraryOf("library")} AS library FROM t WHERE COALESCE(status,'active') = 'active') + WHERE name IS NOT NULL AND library != ?), + mine AS ( + SELECT DISTINCT subject AS name FROM lib WHERE subject IS NOT NULL + UNION SELECT DISTINCT object AS name FROM lib WHERE object IS NOT NULL) + SELECT l.name AS entity, l.library AS library FROM labeled l + JOIN mine m ON m.name = l.name ORDER BY l.name ASC, l.library ASC` + ).bind(...baseParams, library).all() + ]); + const tripletCount = countRow?.n ?? 0; + const topEntities = (topRes.results ?? []).map((r) => ({ name: r.name, degree: r.degree })); + const relationProfile = (relRes.results ?? []).map((r) => ({ predicate: r.predicate, count: r.n })); + const bridgeMap = /* @__PURE__ */ new Map(); + for (const r of bridgeRes.results ?? []) { + if (!bridgeMap.has(r.entity) && bridgeMap.size >= 50) continue; + const libs = bridgeMap.get(r.entity) ?? []; + if (!libs.includes(r.library)) libs.push(r.library); + bridgeMap.set(r.entity, libs); + } + const bridges = [...bridgeMap.entries()].map(([entity, libraries]) => ({ entity, libraries })); + let narrative = input.narrative?.trim(); + if (!narrative) { + const prev = await getLibraryMapDetail(db, library, owner); + narrative = prev?.narrative?.trim() || ""; + } + const coreNames = topEntities.slice(0, 3).map((t) => t.name); + const content = mapContent(library, narrative, coreNames); + const blockEntry = await createEntry(db, { + content, + entry_type: "block", + owner_id: owner ?? null, + page_name: `library-map:${library}`, + metadata_json: JSON.stringify({ kind: "library_map", library }) + }); + const values = { + library, + narrative, + top_entities: JSON.stringify(topEntities), + relation_profile: JSON.stringify(relationProfile), + bridges: JSON.stringify(bridges), + triplet_count: String(tripletCount), + status: "active" + }; + if (input.commit_hash) values.commit_hash = input.commit_hash; + await createRecord(db, { + template: LIBRARY_MAP_TEMPLATE_NAME, + record_id: blockEntry.id, + values, + owner_id: owner ?? null + }); + const mapTpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); + const oldParams = owner ? [mapTpl.id, owner, library, blockEntry.id] : [mapTpl.id, library, blockEntry.id]; + const oldRes = await db.prepare( + `WITH m AS (${mapPivotSql(!!owner)}) + SELECT rid FROM m WHERE m.library = ? AND COALESCE(m.status, 'active') = 'active' AND m.rid != ?` + ).bind(...oldParams).all(); + const superseded = []; + for (const row of oldRes.results ?? []) { + await updateRecord(db, row.rid, { status: "superseded" }); + superseded.push(row.rid); + } + const deprecatedStaleMaps = await deprecateStaleMapBlocks(db, library, blockEntry.id, owner); + const entryCount = (await liveEntryCountsByLibrary(db, owner)).get(library) ?? 0; + return { + map: { + record_id: blockEntry.id, + library, + narrative: narrative || null, + content, + top_entities: topEntities, + relation_profile: relationProfile, + bridges, + triplet_count: tripletCount, + entry_count: entryCount, + commit_hash: input.commit_hash ?? null, + status: "active", + updated_at: blockEntry.created_at + }, + superseded, + deprecated_stale_maps: deprecatedStaleMaps, + triplet_template: tripletTemplateName, + triplet_library_slot_added: librarySlotAdded + }; +} +async function liveTripletCountsByLibrary(db, tripletTemplateId, owner_id) { + const params = owner_id ? [tripletTemplateId, owner_id] : [tripletTemplateId]; + const res = await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree matrix/arcrun/.worktree-fix-87/(避免打斷另一 session 佔用中的 matrix/arcrun 主 checkout),hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆 + `SELECT ${libraryOf("tr.library")} AS library, COUNT(*) AS n + FROM ( + SELECT b.src_id AS rid, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, + MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library + FROM entries b + LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' + LEFT JOIN entries v ON v.id = r.dst_id + WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${owner_id ? " AND b.owner_id = ?" : ""} + GROUP BY b.src_id + ) AS tr + WHERE COALESCE(tr.status, 'active') = 'active' + GROUP BY ${libraryOf("tr.library")}` + ).bind(...params).all(); + const m = /* @__PURE__ */ new Map(); + for (const r of res.results ?? []) m.set(r.library, r.n); + return m; +} +async function tripletCountsByLibrary(db, owner_id, tripletTemplateName = DEFAULT_TRIPLET_TEMPLATE) { + const tpl = await getTemplate(db, tripletTemplateName); + if (!tpl) return /* @__PURE__ */ new Map(); + return liveTripletCountsByLibrary(db, tpl.id, owner_id); +} +async function liveEntryCountsByLibrary(db, owner_id) { + const params = owner_id ? [owner_id] : []; + const res = await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree /private/tmp/wt-arcrun-library-map-honesty-87/(同 962d863/5919c6b 已記載的假警報成因:hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆) + `SELECT ${ENTRY_LIBRARY} AS library, + COUNT(*) AS n + FROM entries + WHERE ${owner_id ? "owner_id = ? AND " : ""}entry_type != 'value' + AND src_id IS NULL + AND entry_type NOT IN ('record', 'sheet', 'field', 'system') + AND NOT (entry_type = 'block' AND COALESCE(json_extract(metadata_json, '$.kind'), '') = 'library_map') + GROUP BY ${ENTRY_LIBRARY}` + ).bind(...params).all(); + const m = /* @__PURE__ */ new Map(); + for (const r of res.results ?? []) m.set(r.library, r.n); + return m; +} +async function knownLibraryNames(db, owner_id) { + const names = /* @__PURE__ */ new Set(); + const entryParams = owner_id ? [owner_id] : []; + const entryRows = await db.prepare( + // 🔴 Arcrun#44(2026-08-17):這裡原本直接回**原始值**,沒有套歸庫規則 + // ⇒ library 是空字串的 entry:SQL 的 IS NOT NULL 放它過、下面那行 `if (r.library)` + // 又把 '' 當 falsy 丟掉 ⇒ 它**兩邊都不算**,一個庫都沒進。 + // 而同一顆 DB 上 entry_count 把它算進 general、`?library=general` 也撈得到它 + // ⇒ 「general 這個庫存不存在」會出現第三個答案(GET /map/general 回 404)。 + // 套上 ENTRY_LIBRARY 之後,'' 跟其他地方一樣正規化成 general。 + // ⚠️ WHERE 的 `IS NOT NULL` 是**刻意保留**的,不是漏掉:本函式問的是「有沒有被蓋過章」 + // (見上面註解的 t52 慣例),從來沒有 library 這個 key 的舊資料不該憑空生出一個庫名。 + // 「標了但標成空的」則是蓋過章,歸 general——這才是本次要對齊的那一格。 + `SELECT DISTINCT ${ENTRY_LIBRARY} AS library FROM entries + WHERE ${owner_id ? "owner_id = ?" : "1=1"} AND ${ENTRY_LIBRARY_EXPR} IS NOT NULL` + ).bind(...entryParams).all(); + for (const r of entryRows.results ?? []) if (r.library) names.add(r.library); + const libTpl = await getTemplate(db, "portal_library"); + if (libTpl) { + const libParams = owner_id ? [libTpl.id, owner_id] : [libTpl.id]; + const libRows = await db.prepare( + `SELECT MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_name' THEN v.content END) AS name + FROM entries b + LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' + LEFT JOIN entries v ON v.id = r.dst_id + WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${owner_id ? " AND b.owner_id = ?" : ""} + GROUP BY b.src_id` + ).bind(...libParams).all(); + for (const r of libRows.results ?? []) if (r.name) names.add(r.name); + } + return names; +} +async function ensureFreshLibraryMaps(db, owner_id, tripletTemplateName = DEFAULT_TRIPLET_TEMPLATE) { + const tripletTpl = await getTemplate(db, tripletTemplateName); + if (!tripletTpl) return; + const [liveCounts, cached, known] = await Promise.all([ + liveTripletCountsByLibrary(db, tripletTpl.id, owner_id), + listLibraryMaps(db, owner_id), + knownLibraryNames(db, owner_id) + ]); + const cachedByLib = new Map(cached.map((m) => [m.library, m])); + const stale = /* @__PURE__ */ new Set(); + for (const [library, count] of liveCounts) { + const c = cachedByLib.get(library); + if (!c || c.triplet_count !== count) stale.add(library); + } + for (const name of known) { + if (!liveCounts.has(name) && !cachedByLib.has(name)) stale.add(name); + } + for (const c of cached) { + if (!liveCounts.has(c.library) && c.triplet_count !== 0) stale.add(c.library); + } + await Promise.all( + [...stale].map( + (library) => recomputeLibraryMap(db, { library, owner_id, triplet_template: tripletTemplateName }).catch(() => { + }) + ) + ); +} +async function listLibraryMaps(db, owner_id) { + const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); + if (!tpl) return []; + const params = owner_id ? [tpl.id, owner_id] : [tpl.id]; + const [res, entryCounts] = await Promise.all([ + db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/),既有查詢(listLibraryMaps 原本就有)此次改包進 Promise.all 才重新觸發掃描,非新增違規;worktree 路徑假警報同上方 liveEntryCountsByLibrary 註解 + `WITH m AS (${mapPivotSql(!!owner_id)}) + SELECT * FROM m WHERE COALESCE(m.status, 'active') = 'active' AND m.library IS NOT NULL + ORDER BY m.ts DESC` + ).bind(...params).all(), + liveEntryCountsByLibrary(db, owner_id) + ]); + const byLib = /* @__PURE__ */ new Map(); + for (const r of res.results ?? []) { + if (!r.library || byLib.has(r.library)) continue; + byLib.set(r.library, { + library: r.library, + narrative: r.narrative || null, + top_entities: parseJsonArray(r.top_entities).slice(0, 3).map((t) => t.name), + triplet_count: Number(r.triplet_count ?? 0) || 0, + entry_count: entryCounts.get(r.library) ?? 0, + updated_at: r.ts + }); + } + return [...byLib.values()].sort((a, b) => a.library.localeCompare(b.library)); +} +async function getLibraryMapDetail(db, library, owner_id) { + const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); + if (!tpl) return null; + const params = owner_id ? [tpl.id, owner_id, library] : [tpl.id, library]; + const row = await db.prepare( + `WITH m AS (${mapPivotSql(!!owner_id)}) + SELECT * FROM m WHERE m.library = ? AND COALESCE(m.status, 'active') = 'active' + ORDER BY m.ts DESC LIMIT 1` + ).bind(...params).first(); + if (!row) return null; + const [blockEntry, entryCounts] = await Promise.all([ + getEntry(db, row.rid), + liveEntryCountsByLibrary(db, owner_id) + ]); + return { + record_id: row.rid, + library, + narrative: row.narrative || null, + content: blockEntry?.content ?? null, + top_entities: parseJsonArray(row.top_entities), + relation_profile: parseJsonArray(row.relation_profile), + bridges: parseJsonArray(row.bridges), + triplet_count: Number(row.triplet_count ?? 0) || 0, + entry_count: entryCounts.get(library) ?? 0, + commit_hash: row.commit_hash || null, + status: row.status ?? "active", + updated_at: row.ts + }; +} +async function setLibraryNarrative(db, library, narrative, owner_id) { + const lib = library.trim(); + if (!lib) throw new Error("library required"); + const text = narrative.trim(); + if (!text) throw new Error("narrative required"); + const current = await getLibraryMapDetail(db, lib, owner_id); + if (!current) { + const created = await recomputeLibraryMap(db, { library: lib, narrative: text, owner_id }); + return created.map; + } + await updateRecord(db, current.record_id, { narrative: text }); + await updateEntry(db, current.record_id, { + content: mapContent(lib, text, current.top_entities.slice(0, 3).map((t) => t.name)) + }); + const after = await getLibraryMapDetail(db, lib, owner_id); + if (!after) throw new Error("narrative written but map not readable back"); + return after; +} +function questionTokens(question) { + const t = question.normalize("NFKC").toLowerCase(); + const out = /* @__PURE__ */ new Set(); + for (const w of t.match(/[a-z0-9]{2,}/g) ?? []) out.add(w); + for (const seg of t.replace(/[^一-鿿]/g, " ").split(/\s+/)) { + for (let i = 0; i + 1 < seg.length; i++) out.add(seg.slice(i, i + 2)); + } + return out; +} +async function selectLibrariesForQuestion(db, question, opts = {}) { + const q = question.trim(); + if (!q) throw new Error("question required"); + const owner = opts.owner_id || void 0; + const maxLibs = Math.min(Math.max(Math.floor(opts.limit ?? 3), 1), 10); + const qNorm = q.normalize("NFKC").toLowerCase(); + const allMaps = await listLibraryMaps(db, owner); + const considered = allMaps.length; + const tpl = await getTemplate(db, opts.triplet_template ?? DEFAULT_TRIPLET_TEMPLATE); + const hits = /* @__PURE__ */ new Map(); + if (tpl) { + const pivot = tripletPivotSql(!!owner); + const params = owner ? [tpl.id, owner] : [tpl.id]; + const res = await db.prepare( + // kbdb-sql-ok:牆內本體(kbdb/src/actions/)。worktree 開在 /private/tmp/…/wt-gfr44/,hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到多出的目錄層——與 962d863/5919c6b/887463c 記載的是同一個假警報,非繞牆。 + `WITH t AS (${pivot}), + act AS (SELECT * FROM t WHERE COALESCE(t.status, 'active') = 'active'), + ent AS ( + SELECT subject AS name, ${libraryOf("library")} AS library FROM act WHERE subject IS NOT NULL + UNION ALL + SELECT object AS name, ${libraryOf("library")} AS library FROM act WHERE object IS NOT NULL) + SELECT name, library, COUNT(*) AS degree + FROM ent + WHERE LENGTH(name) >= 2 AND instr(?, lower(name)) > 0 + GROUP BY name, library + ORDER BY degree DESC, name ASC + LIMIT 200` + ).bind(...params, qNorm).all(); + for (const r of res.results ?? []) { + const cur = hits.get(r.library) ?? { entities: /* @__PURE__ */ new Set(), score: 0 }; + cur.entities.add(r.name); + cur.score += 1 + Math.log(1 + r.degree); + hits.set(r.library, cur); + } + } + let libraries = [...hits.entries()].map(([library, v]) => ({ + library, + score: Math.round(v.score * 100) / 100, + matched_entities: [...v.entities].slice(0, 8), + via: "graph" + })).sort((a, b) => b.score - a.score || a.library.localeCompare(b.library)).slice(0, maxLibs); + let route = libraries.length ? "graph" : "all"; + if (!libraries.length && considered) { + const qToks = questionTokens(q); + const scored = allMaps.map((m) => { + const matched = []; + let score = 0; + for (const name of [m.library, ...m.top_entities]) { + const n = name.normalize("NFKC").toLowerCase(); + if (n.length >= 2 && qNorm.includes(n)) { + matched.push(name); + score += 2; + } + } + if (m.narrative) { + const nToks = questionTokens(m.narrative); + let overlap = 0; + for (const t of qToks) if (nToks.has(t)) overlap += 1; + score += Math.min(overlap, 6) * 0.25; + } + return { library: m.library, score: Math.round(score * 100) / 100, matched_entities: matched, via: "index" }; + }).filter((r) => r.score > 0).sort((a, b) => b.score - a.score || a.library.localeCompare(b.library)).slice(0, maxLibs); + if (scored.length) { + libraries = scored; + route = "index"; + } + } + const selected = new Set(libraries.map((l) => l.library)); + return { + route, + vector_used: false, + // 檢索路徑上沒有向量——這個欄位是給前端徽章的證據,不是宣傳詞 + libraries_considered: considered, + libraries, + // 索引整份回傳(不只選中的那幾個):它就是「館藏目錄」, + // 「知識庫裡有哪些子庫、各自談什麼」這類問題的答案在這裡,不在任何一頁原文裡。 + indexes: allMaps.map((m) => ({ ...m, selected: selected.has(m.library) })) + }; +} + // kbdb/src/routes/records.ts var recordRoutes = new Hono2(); var isStringMap = (v) => !!v && typeof v === "object" && !Array.isArray(v) && Object.values(v).every((x) => typeof x === "string"); @@ -3583,25 +4099,28 @@ recordRoutes.post("/", async (c) => { } }); recordRoutes.get("/triplet-stats", async (c) => { - const owner = c.req.query("owner_id") || ""; - const rows = await c.env.DB.prepare( - `SELECT - COALESCE(NULLIF(lib_v.content, ''), 'general') AS library, - COUNT(*) AS triplet_count - FROM entries b - JOIN templates t ON b.dst_id = t.id AND t.name = 'triplet' - LEFT JOIN entries lr ON lr.src_id = b.src_id AND lr.rel_id = ('fld_' || b.dst_id || '_library') - LEFT JOIN entries lib_v ON lib_v.id = lr.dst_id - WHERE b.rel_id = 'sys_belongs' AND (?1 = '' OR b.owner_id = ?1) - GROUP BY COALESCE(NULLIF(lib_v.content, ''), 'general') - ORDER BY library` - ).bind(owner).all(); - const stats = (rows.results ?? []).map((r) => ({ library: r.library, triplet_count: r.triplet_count })); + const owner = c.req.query("owner_id") || void 0; + const counts = await tripletCountsByLibrary(c.env.DB, owner); + const stats = [...counts.entries()].map(([library, triplet_count]) => ({ library, triplet_count })).sort((a, b) => a.library.localeCompare(b.library)); return c.json({ success: true, stats }); }); +var intParam = (raw2, fallback, min, max) => { + const n = Number(raw2); + if (raw2 === void 0 || raw2 === "" || !Number.isFinite(n)) return fallback; + const v = Math.floor(n); + return v < min ? fallback : Math.min(v, max); +}; recordRoutes.get("/by-template/:template", async (c) => { - const records = await searchByTemplate(c.env.DB, c.req.param("template"), c.req.query("owner_id") || void 0); - return c.json({ success: true, records, count: records.length }); + const limit = intParam(c.req.query("limit"), 100, 1, 500); + const offset = intParam(c.req.query("offset"), 0, 0, Number.MAX_SAFE_INTEGER); + const records = await searchByTemplate( + c.env.DB, + c.req.param("template"), + c.req.query("owner_id") || void 0, + limit, + offset + ); + return c.json({ success: true, records, count: records.length, limit, offset }); }); recordRoutes.get("/:recordId", async (c) => { const rec = await getRecord(c.env.DB, c.req.param("recordId")); @@ -3735,346 +4254,27 @@ embedRoutes.get("/selftest", async (c) => { return c.json({ success: true, ...result }); }); -// kbdb/src/actions/library-map.ts -var LIBRARY_MAP_TEMPLATE_ID = "tpl-library-map"; -var LIBRARY_MAP_TEMPLATE_NAME = "library_map"; -var LIBRARY_MAP_SLOTS = [ - "library", - "narrative", - "top_entities", - "relation_profile", - "bridges", - "triplet_count", - "commit_hash", - "status" -]; -var DEFAULT_TRIPLET_TEMPLATE = "triplet"; -async function ensureLibraryMapTemplate(db) { - const existing = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); - if (existing) return; - try { - await createTemplate(db, { - id: LIBRARY_MAP_TEMPLATE_ID, - name: LIBRARY_MAP_TEMPLATE_NAME, - description: "per-library map block\uFF08\u85CF\u66F8\u5730\u5716\uFF1Agraph \u6A5F\u68B0\u5C0E\u51FA\uFF0C\u96F6 LLM \u751F\u6210\uFF1BArcrun#39\uFF09", - slots: LIBRARY_MAP_SLOTS, - created_by: "system" - }); - } catch { - if (!await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME)) throw new Error("ensureLibraryMapTemplate failed"); - } -} -async function ensureTripletLibrarySlot(db, tripletTemplate) { - const tpl = await getTemplate(db, tripletTemplate); - if (!tpl) throw new Error(`triplet template not found: ${tripletTemplate}`); - const slots = JSON.parse(tpl.slots_json); - if (slots.includes("library")) return false; - await updateTemplate(db, tpl.id, { slots: [...slots, "library"] }); - return true; -} -function tripletPivotSql(ownerFiltered) { - return `SELECT b.src_id AS rid, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_subject' THEN v.content END) AS subject, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_object' THEN v.content END) AS object, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_predicate' THEN v.content END) AS predicate, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_source_uri' THEN v.content END) AS source_uri - FROM entries b - LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' - LEFT JOIN entries v ON v.id = r.dst_id - WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerFiltered ? " AND b.owner_id = ?" : ""} - GROUP BY b.src_id`; -} -function mapPivotSql(ownerFiltered) { - return `SELECT b.src_id AS rid, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_narrative' THEN v.content END) AS narrative, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_top_entities' THEN v.content END) AS top_entities, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_relation_profile' THEN v.content END) AS relation_profile, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_bridges' THEN v.content END) AS bridges, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_triplet_count' THEN v.content END) AS triplet_count, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_commit_hash' THEN v.content END) AS commit_hash, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, - MAX(r.created_at) AS ts - FROM entries b - LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' - LEFT JOIN entries v ON v.id = r.dst_id - WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${ownerFiltered ? " AND b.owner_id = ?" : ""} - GROUP BY b.src_id`; -} -function parseJsonArray(raw2) { - if (!raw2) return []; - try { - const v = JSON.parse(raw2); - return Array.isArray(v) ? v : []; - } catch { - return []; - } -} -async function recomputeLibraryMap(db, input) { - const library = input.library.trim(); - if (!library) throw new Error("library required"); - const tripletTemplateName = input.triplet_template ?? DEFAULT_TRIPLET_TEMPLATE; - const topN = Math.min(Math.max(Math.floor(input.top_n ?? 10), 1), 50); - await ensureLibraryMapTemplate(db); - const librarySlotAdded = await ensureTripletLibrarySlot(db, tripletTemplateName); - const tripletTpl = await getTemplate(db, tripletTemplateName); - if (!tripletTpl) throw new Error(`triplet template not found: ${tripletTemplateName}`); - const owner = input.owner_id || void 0; - const pivot = tripletPivotSql(!!owner); - const pivotParams = owner ? [tripletTpl.id, owner] : [tripletTpl.id]; - const libCond = input.source_prefix ? `(t.library = ? OR (t.library IS NULL AND t.source_uri LIKE ? || '%'))` : `t.library = ?`; - const libParams = input.source_prefix ? [library, input.source_prefix] : [library]; - const withLib = `WITH t AS (${pivot}), lib AS ( - SELECT * FROM t WHERE COALESCE(t.status, 'active') = 'active' AND ${libCond})`; - const baseParams = [...pivotParams, ...libParams]; - const [countRow, topRes, relRes, bridgeRes] = await Promise.all([ - db.prepare(`${withLib} SELECT COUNT(*) AS n FROM lib`).bind(...baseParams).first(), - // degree=entity 在該庫 active triplet 的出現次數(subject+object 兩側都算;同名並列取名字序穩定輸出) - db.prepare( - `${withLib} SELECT name, COUNT(*) AS degree FROM ( - SELECT subject AS name FROM lib UNION ALL SELECT object AS name FROM lib) - WHERE name IS NOT NULL GROUP BY name ORDER BY degree DESC, name ASC LIMIT ?` - ).bind(...baseParams, topN).all(), - // predicate 分布=庫的「性格」(spec §3 relation_profile) - db.prepare( - `${withLib} SELECT predicate, COUNT(*) AS n FROM lib - WHERE predicate IS NOT NULL GROUP BY predicate ORDER BY n DESC, predicate ASC LIMIT 100` - ).bind(...baseParams).all(), - // bridges=本庫 entity 同時出現在其他庫(跨庫 join)。對面那側只能靠 library slot 標記值 - //(source_prefix 只描述本庫的前綴,無法反推他庫)→ M3 backfill 前 bridges 會偏稀疏,誠實現況。 - db.prepare( - `${withLib}, labeled AS ( - SELECT DISTINCT name, library FROM ( - SELECT subject AS name, library FROM t WHERE COALESCE(status,'active') = 'active' - UNION SELECT object AS name, library FROM t WHERE COALESCE(status,'active') = 'active') - WHERE name IS NOT NULL AND library IS NOT NULL AND library != ?), - mine AS ( - SELECT DISTINCT subject AS name FROM lib WHERE subject IS NOT NULL - UNION SELECT DISTINCT object AS name FROM lib WHERE object IS NOT NULL) - SELECT l.name AS entity, l.library AS library FROM labeled l - JOIN mine m ON m.name = l.name ORDER BY l.name ASC, l.library ASC` - ).bind(...baseParams, library).all() - ]); - const tripletCount = countRow?.n ?? 0; - const topEntities = (topRes.results ?? []).map((r) => ({ name: r.name, degree: r.degree })); - const relationProfile = (relRes.results ?? []).map((r) => ({ predicate: r.predicate, count: r.n })); - const bridgeMap = /* @__PURE__ */ new Map(); - for (const r of bridgeRes.results ?? []) { - if (!bridgeMap.has(r.entity) && bridgeMap.size >= 50) continue; - const libs = bridgeMap.get(r.entity) ?? []; - if (!libs.includes(r.library)) libs.push(r.library); - bridgeMap.set(r.entity, libs); - } - const bridges = [...bridgeMap.entries()].map(([entity, libraries]) => ({ entity, libraries })); - let narrative = input.narrative?.trim(); - if (!narrative) { - const prev = await getLibraryMapDetail(db, library, owner); - narrative = prev?.narrative?.trim() || ""; - } - const coreNames = topEntities.slice(0, 3).map((t) => t.name); - const content = `${library}\uFF1A${narrative || "\uFF08narrative \u5F85 ingest \u88DC\u5BEB\uFF09"}\u3002\u6838\u5FC3\uFF1A${coreNames.length ? coreNames.join("\u3001") : "\uFF08\u5C1A\u7121 entities\uFF09"}`; - const blockEntry = await createEntry(db, { - content, - entry_type: "block", - owner_id: owner ?? null, - page_name: `library-map:${library}`, - metadata_json: JSON.stringify({ kind: "library_map", library }) - }); - const values = { - library, - narrative, - top_entities: JSON.stringify(topEntities), - relation_profile: JSON.stringify(relationProfile), - bridges: JSON.stringify(bridges), - triplet_count: String(tripletCount), - status: "active" - }; - if (input.commit_hash) values.commit_hash = input.commit_hash; - await createRecord(db, { - template: LIBRARY_MAP_TEMPLATE_NAME, - record_id: blockEntry.id, - values, - owner_id: owner ?? null - }); - const mapTpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); - const oldParams = owner ? [mapTpl.id, owner, library, blockEntry.id] : [mapTpl.id, library, blockEntry.id]; - const oldRes = await db.prepare( - `WITH m AS (${mapPivotSql(!!owner)}) - SELECT rid FROM m WHERE m.library = ? AND COALESCE(m.status, 'active') = 'active' AND m.rid != ?` - ).bind(...oldParams).all(); - const superseded = []; - for (const row of oldRes.results ?? []) { - await updateRecord(db, row.rid, { status: "superseded" }); - superseded.push(row.rid); - } - const entryCount = (await liveEntryCountsByLibrary(db, owner)).get(library) ?? 0; - return { - map: { - record_id: blockEntry.id, - library, - narrative: narrative || null, - content, - top_entities: topEntities, - relation_profile: relationProfile, - bridges, - triplet_count: tripletCount, - entry_count: entryCount, - commit_hash: input.commit_hash ?? null, - status: "active", - updated_at: blockEntry.created_at - }, - superseded, - triplet_template: tripletTemplateName, - triplet_library_slot_added: librarySlotAdded - }; -} -async function liveTripletCountsByLibrary(db, tripletTemplateId, owner_id) { - const params = owner_id ? [tripletTemplateId, owner_id] : [tripletTemplateId]; - const res = await db.prepare( - // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree matrix/arcrun/.worktree-fix-87/(避免打斷另一 session 佔用中的 matrix/arcrun 主 checkout),hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆 - `SELECT COALESCE(NULLIF(tr.library, ''), 'general') AS library, COUNT(*) AS n - FROM ( - SELECT b.src_id AS rid, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_status' THEN v.content END) AS status, - MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_library' THEN v.content END) AS library - FROM entries b - LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' - LEFT JOIN entries v ON v.id = r.dst_id - WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${owner_id ? " AND b.owner_id = ?" : ""} - GROUP BY b.src_id - ) AS tr - WHERE COALESCE(tr.status, 'active') = 'active' - GROUP BY COALESCE(NULLIF(tr.library, ''), 'general')` - ).bind(...params).all(); - const m = /* @__PURE__ */ new Map(); - for (const r of res.results ?? []) m.set(r.library, r.n); - return m; -} -async function liveEntryCountsByLibrary(db, owner_id) { - const params = owner_id ? [owner_id] : []; - const res = await db.prepare( - // kbdb-sql-ok:牆內本體(kbdb/src/actions/),checkout 開在巢狀 worktree /private/tmp/wt-arcrun-library-map-honesty-87/(同 962d863/5919c6b 已記載的假警報成因:hook 逐字比對 matrix/arcrun/kbdb/src/ 吃不到中間多出的 worktree 目錄層,非繞牆) - `SELECT COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general') AS library, - COUNT(*) AS n - FROM entries - WHERE ${owner_id ? "owner_id = ? AND " : ""}entry_type != 'value' - AND src_id IS NULL - AND entry_type NOT IN ('record', 'sheet', 'field', 'system') - AND NOT (entry_type = 'block' AND COALESCE(json_extract(metadata_json, '$.kind'), '') = 'library_map') - GROUP BY COALESCE(NULLIF(json_extract(metadata_json, '$.library'), ''), 'general')` - ).bind(...params).all(); - const m = /* @__PURE__ */ new Map(); - for (const r of res.results ?? []) m.set(r.library, r.n); - return m; -} -async function knownLibraryNames(db, owner_id) { - const names = /* @__PURE__ */ new Set(); - const entryParams = owner_id ? [owner_id] : []; - const entryRows = await db.prepare( - `SELECT DISTINCT json_extract(metadata_json, '$.library') AS library FROM entries - WHERE ${owner_id ? "owner_id = ?" : "1=1"} AND json_extract(metadata_json, '$.library') IS NOT NULL` - ).bind(...entryParams).all(); - for (const r of entryRows.results ?? []) if (r.library) names.add(r.library); - const libTpl = await getTemplate(db, "portal_library"); - if (libTpl) { - const libParams = owner_id ? [libTpl.id, owner_id] : [libTpl.id]; - const libRows = await db.prepare( - `SELECT MAX(CASE WHEN r.rel_id = 'fld_' || b.dst_id || '_name' THEN v.content END) AS name - FROM entries b - LEFT JOIN entries r ON r.src_id = b.src_id AND r.rel_id != 'sys_belongs' - LEFT JOIN entries v ON v.id = r.dst_id - WHERE b.rel_id = 'sys_belongs' AND b.dst_id = ?${owner_id ? " AND b.owner_id = ?" : ""} - GROUP BY b.src_id` - ).bind(...libParams).all(); - for (const r of libRows.results ?? []) if (r.name) names.add(r.name); - } - return names; -} -async function ensureFreshLibraryMaps(db, owner_id, tripletTemplateName = DEFAULT_TRIPLET_TEMPLATE) { - const tripletTpl = await getTemplate(db, tripletTemplateName); - if (!tripletTpl) return; - const [liveCounts, cached, known] = await Promise.all([ - liveTripletCountsByLibrary(db, tripletTpl.id, owner_id), - listLibraryMaps(db, owner_id), - knownLibraryNames(db, owner_id) - ]); - const cachedByLib = new Map(cached.map((m) => [m.library, m])); - const stale = /* @__PURE__ */ new Set(); - for (const [library, count] of liveCounts) { - const c = cachedByLib.get(library); - if (!c || c.triplet_count !== count) stale.add(library); - } - for (const name of known) { - if (!liveCounts.has(name) && !cachedByLib.has(name)) stale.add(name); - } - await Promise.all( - [...stale].map( - (library) => recomputeLibraryMap(db, { library, owner_id, triplet_template: tripletTemplateName }).catch(() => { - }) - ) - ); -} -async function listLibraryMaps(db, owner_id) { - const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); - if (!tpl) return []; - const params = owner_id ? [tpl.id, owner_id] : [tpl.id]; - const [res, entryCounts] = await Promise.all([ - db.prepare( - // kbdb-sql-ok:牆內本體(kbdb/src/actions/),既有查詢(listLibraryMaps 原本就有)此次改包進 Promise.all 才重新觸發掃描,非新增違規;worktree 路徑假警報同上方 liveEntryCountsByLibrary 註解 - `WITH m AS (${mapPivotSql(!!owner_id)}) - SELECT * FROM m WHERE COALESCE(m.status, 'active') = 'active' AND m.library IS NOT NULL - ORDER BY m.ts DESC` - ).bind(...params).all(), - liveEntryCountsByLibrary(db, owner_id) - ]); - const byLib = /* @__PURE__ */ new Map(); - for (const r of res.results ?? []) { - if (!r.library || byLib.has(r.library)) continue; - byLib.set(r.library, { - library: r.library, - narrative: r.narrative || null, - top_entities: parseJsonArray(r.top_entities).slice(0, 3).map((t) => t.name), - triplet_count: Number(r.triplet_count ?? 0) || 0, - entry_count: entryCounts.get(r.library) ?? 0, - updated_at: r.ts - }); - } - return [...byLib.values()].sort((a, b) => a.library.localeCompare(b.library)); -} -async function getLibraryMapDetail(db, library, owner_id) { - const tpl = await getTemplate(db, LIBRARY_MAP_TEMPLATE_NAME); - if (!tpl) return null; - const params = owner_id ? [tpl.id, owner_id, library] : [tpl.id, library]; - const row = await db.prepare( - `WITH m AS (${mapPivotSql(!!owner_id)}) - SELECT * FROM m WHERE m.library = ? AND COALESCE(m.status, 'active') = 'active' - ORDER BY m.ts DESC LIMIT 1` - ).bind(...params).first(); - if (!row) return null; - const [blockEntry, entryCounts] = await Promise.all([ - getEntry(db, row.rid), - liveEntryCountsByLibrary(db, owner_id) - ]); - return { - record_id: row.rid, - library, - narrative: row.narrative || null, - content: blockEntry?.content ?? null, - top_entities: parseJsonArray(row.top_entities), - relation_profile: parseJsonArray(row.relation_profile), - bridges: parseJsonArray(row.bridges), - triplet_count: Number(row.triplet_count ?? 0) || 0, - entry_count: entryCounts.get(library) ?? 0, - commit_hash: row.commit_hash || null, - status: row.status ?? "active", - updated_at: row.ts - }; -} - // kbdb/src/routes/map.ts var mapRoutes = new Hono2(); +mapRoutes.get("/select", async (c) => { + const q = c.req.query("q") || c.req.query("question") || ""; + if (!q.trim()) return c.json({ success: false, error: "q required" }, 400); + const owner = c.req.query("owner_id") || void 0; + const limitNum = Number(c.req.query("limit")); + const limit = Number.isFinite(limitNum) && limitNum > 0 ? Math.floor(limitNum) : void 0; + await ensureFreshLibraryMaps(c.env.DB, owner).catch(() => { + }); + try { + const selection = await selectLibrariesForQuestion(c.env.DB, q, { + owner_id: owner, + limit, + triplet_template: c.req.query("triplet_template") || void 0 + }); + return c.json({ success: true, ...selection }); + } catch (e) { + return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400); + } +}); mapRoutes.post("/recompute", async (c) => { const body = await c.req.json().catch(() => ({})); const library = c.req.query("library") || (typeof body.library === "string" ? body.library : ""); @@ -4094,6 +4294,19 @@ mapRoutes.post("/recompute", async (c) => { return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400); } }); +mapRoutes.put("/:library/narrative", async (c) => { + const body = await c.req.json().catch(() => ({})); + const library = c.req.param("library"); + const narrative = typeof body.narrative === "string" ? body.narrative : ""; + if (!narrative.trim()) return c.json({ success: false, error: "narrative required" }, 400); + const owner = (typeof body.owner_id === "string" ? body.owner_id : c.req.query("owner_id")) || void 0; + try { + const map = await setLibraryNarrative(c.env.DB, library, narrative, owner); + return c.json({ success: true, map }); + } catch (e) { + return c.json({ success: false, error: e instanceof Error ? e.message : String(e) }, 400); + } +}); mapRoutes.get("/", async (c) => { const owner = c.req.query("owner_id") || void 0; await ensureFreshLibraryMaps(c.env.DB, owner).catch(() => { diff --git a/manifest.json b/manifest.json index 7fe5b8a..7bdd925 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "schema": 2, - "built": "2026-08-15", - "source": "Arcrun@cacaa33f7d4e", + "built": "2026-08-18", + "source": "Arcrun@466a59c5616f", "core": [ { "name": "arcrun-array-ops", @@ -198,7 +198,7 @@ "name": "arcrun-cypher-executor", "main_module": "worker.mjs", "main_file": "arcrun-cypher-executor/worker.mjs", - "js_bytes": 590499, + "js_bytes": 592768, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -239,10 +239,10 @@ "stripped": { "services": 13 }, - "source_commit": "9fa88065abdc5a795d3847711f04101480621af0", - "source_content_sha256": "94a21ad012ee0c48d78e9a74752048917ddb4616f1ea089976834acd887a9d14", - "sha256": "94a21ad012ee0c48d78e9a74752048917ddb4616f1ea089976834acd887a9d14", - "bytes": 590499 + "source_commit": "5d5dc9e3ceadaf40448ff286009e01299623ced1", + "source_content_sha256": "426a77c4566c177ed0f26e2178d7c7f8b989ccd16fdfb1fc446405b08d4bf4ba", + "sha256": "426a77c4566c177ed0f26e2178d7c7f8b989ccd16fdfb1fc446405b08d4bf4ba", + "bytes": 592768 }, { "name": "arcrun-date-ops", @@ -396,7 +396,7 @@ "name": "arcrun-kbdb", "main_module": "worker.mjs", "main_file": "arcrun-kbdb/worker.mjs", - "js_bytes": 160306, + "js_bytes": 170840, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -416,10 +416,10 @@ "ENVIRONMENT": "production" } }, - "source_commit": "ceb7638d746f0e8847552aef721f6f53ef87ff0b", - "source_content_sha256": "47b09d60f8d122f7ec234d146a99bc68b2661c56ad6a98eca146d99ec5de03ea", - "sha256": "47b09d60f8d122f7ec234d146a99bc68b2661c56ad6a98eca146d99ec5de03ea", - "bytes": 160306 + "source_commit": "466a59c5616fcd61206bc056a7a10052e2e6f3fe", + "source_content_sha256": "30d1ac22d67228cd63e04ebaa94d0d9f40597307075ac5504563590de9876776", + "sha256": "30d1ac22d67228cd63e04ebaa94d0d9f40597307075ac5504563590de9876776", + "bytes": 170840 }, { "name": "arcrun-mcp", @@ -507,7 +507,7 @@ "name": "arcrun-rag-ui", "main_module": "index.js", "main_file": "tier2/ui/index.js", - "js_bytes": 524513, + "js_bytes": 531477, "modules": [], "compat_date": "2026-07-01", "compat_flags": [], @@ -518,10 +518,10 @@ "ai": false, "vars": {} }, - "source_commit": "614fe448124dbd25d8228e37723473f1704f3a7a", - "source_content_sha256": "5825f427e4211dd4b0eee969c94c8bfa856f65b8a48fc718e137f3b5d074e1ff", - "sha256": "5825f427e4211dd4b0eee969c94c8bfa856f65b8a48fc718e137f3b5d074e1ff", - "bytes": 524513 + "source_commit": "b48f02b242a094cd542e5d9349326b9127a006eb", + "source_content_sha256": "374dec4544348df364dc4f09c40944529f8b18cd4c42687fc318642f638b0860", + "sha256": "374dec4544348df364dc4f09c40944529f8b18cd4c42687fc318642f638b0860", + "bytes": 531477 }, { "name": "arcrun-set", @@ -716,12 +716,12 @@ "size": 9125072 } }, - "release": "1.4.46", + "release": "1.4.48", "built_for": "oauth-installer-lazy-load", "notes": [ "rag_takedown_direct 用了 __CARDS_PREFIX__,但安裝器的代換表沒有它 ⇒ 這個佔位符會原封不動被推進使用者的工作流。" ], - "fingerprint": "ba0cc9bbd58980b4a4505c365382455b307b826fad0afbc1b738f4139ee96d2c", + "fingerprint": "b5f21ff6b7a7f236be14381646075dbe491ba52e09c584cc73d6efeb5ddda642", "library": [ { "name": "arcrun-array-ops", @@ -906,7 +906,7 @@ "name": "arcrun-cypher-executor", "main_module": "worker.mjs", "main_file": "arcrun-cypher-executor/worker.mjs", - "js_bytes": 590499, + "js_bytes": 592768, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -947,8 +947,8 @@ "stripped": { "services": 13 }, - "source_commit": "9fa88065abdc5a795d3847711f04101480621af0", - "source_content_sha256": "94a21ad012ee0c48d78e9a74752048917ddb4616f1ea089976834acd887a9d14", + "source_commit": "5d5dc9e3ceadaf40448ff286009e01299623ced1", + "source_content_sha256": "426a77c4566c177ed0f26e2178d7c7f8b989ccd16fdfb1fc446405b08d4bf4ba", "first_install": true }, { @@ -1093,7 +1093,7 @@ "name": "arcrun-kbdb", "main_module": "worker.mjs", "main_file": "arcrun-kbdb/worker.mjs", - "js_bytes": 160306, + "js_bytes": 170840, "modules": [], "compat_date": "2025-02-19", "compat_flags": [ @@ -1113,8 +1113,8 @@ "ENVIRONMENT": "production" } }, - "source_commit": "ceb7638d746f0e8847552aef721f6f53ef87ff0b", - "source_content_sha256": "47b09d60f8d122f7ec234d146a99bc68b2661c56ad6a98eca146d99ec5de03ea", + "source_commit": "466a59c5616fcd61206bc056a7a10052e2e6f3fe", + "source_content_sha256": "30d1ac22d67228cd63e04ebaa94d0d9f40597307075ac5504563590de9876776", "first_install": true }, { @@ -1198,7 +1198,7 @@ "name": "arcrun-rag-ui", "main_module": "index.js", "main_file": "tier2/ui/index.js", - "js_bytes": 524513, + "js_bytes": 531477, "modules": [], "compat_date": "2026-07-01", "compat_flags": [], @@ -1209,8 +1209,8 @@ "ai": false, "vars": {} }, - "source_commit": "614fe448124dbd25d8228e37723473f1704f3a7a", - "source_content_sha256": "5825f427e4211dd4b0eee969c94c8bfa856f65b8a48fc718e137f3b5d074e1ff", + "source_commit": "b48f02b242a094cd542e5d9349326b9127a006eb", + "source_content_sha256": "374dec4544348df364dc4f09c40944529f8b18cd4c42687fc318642f638b0860", "first_install": true }, { @@ -1411,27 +1411,15 @@ }, { "worker": "arcrun-http-request", - "why": "rag_chat/kw_search 用 __HTTP_REQ_URL__" + "why": "rag_chat/select_libraries 用 __HTTP_REQ_URL__" + }, + { + "worker": "arcrun-code", + "why": "rag_chat/pick_libs 用 __CODE_URL__" }, { "worker": "arcrun-http-request", - "why": "rag_chat/sem_search 用 __HTTP_REQ_URL__" - }, - { - "worker": "arcrun-http-request", - "why": "rag_chat/fetch_triplets 用 __HTTP_REQ_URL__" - }, - { - "worker": "arcrun-http-request", - "why": "rag_chat/fetch_blocks_a 用 __HTTP_REQ_URL__" - }, - { - "worker": "arcrun-http-request", - "why": "rag_chat/fetch_blocks_b 用 __HTTP_REQ_URL__" - }, - { - "worker": "arcrun-http-request", - "why": "rag_chat/fetch_blocks_c 用 __HTTP_REQ_URL__" + "why": "rag_chat/fetch_pages 用 __HTTP_REQ_URL__" }, { "worker": "arcrun-code", diff --git a/tier2/ui/index.js b/tier2/ui/index.js index 0b6daaf..945aba1 100644 --- a/tier2/ui/index.js +++ b/tier2/ui/index.js @@ -1,6 +1,6 @@ // arcrun-rag-ui — 用戶實例的 GUI(console-ui/public 內嵌;由 Arcrun scripts/build-ui-worker.mjs 產生,勿手改) -const FILES = {"/apple-touch-icon.png":{"type":"image/png","b64":true,"data":"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"},"/console/dashboard/index.html":{"type":"text/html; charset=utf-8","b64":false,"data":"\n\n
\n\n\nMCP_TOKEN_TTL(mcp worker)設定。\n 正在前往搜尋頁… 沒有自動跳轉請點這裡
\n\n\n"},"/portal/index.html":{"type":"text/html; charset=utf-8","b64":false,"data":"\n\n\n\n\n\n \n —\n \n \n \n MCP_TOKEN_TTL(mcp worker)設定。\n 正在前往搜尋頁… 沒有自動跳轉請點這裡
\n\n\n"},"/portal/index.html":{"type":"text/html; charset=utf-8","b64":false,"data":"\n\n\n\n\n\n \n —\n \n \n \n