From 17d11e5e15d2b3545e163a7ebcc794a740189edd Mon Sep 17 00:00:00 2001
From: ai_xiaopei <xiaopei@aisim.cn>
Date: Fri, 21 Aug 2026 18:41:45 +0800
Subject: [PATCH] spec: tag/entity 虚拟节点落库设计(索引边扩展)

---
 docs/superpowers/specs/2026-08-21-kb-cli-tag-entity-edges-design.md |  132 ++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 132 insertions(+), 0 deletions(-)

diff --git a/docs/superpowers/specs/2026-08-21-kb-cli-tag-entity-edges-design.md b/docs/superpowers/specs/2026-08-21-kb-cli-tag-entity-edges-design.md
new file mode 100644
index 0000000..8496a7c
--- /dev/null
+++ b/docs/superpowers/specs/2026-08-21-kb-cli-tag-entity-edges-design.md
@@ -0,0 +1,132 @@
+# kb-cli 索引边扩展设计:tag/entity 虚拟节点落库
+
+日期:2026-08-21
+状态:草案(待用户审阅)
+
+## 背景与问题
+
+当前 `kb-cli index build` 后:814 个文件节点,仅 138 条边(全部是 wikilink)。
+
+根因(已在源码中确认):
+
+1. **tag/entity 边被丢弃**:`internal/graph/builder.go` 为每个文件的 tags/entities 生成了指向虚拟节点(ID 从 1000000 起)的边,但虚拟节点从不插入 SQLite。`cmd/rebuild.go` 写边时遇到 `idMap` 中不存在的节点直接跳过,导致全库 1100+ 个文件 frontmatter 中的 tags 一条边都没落库。
+2. **wikilink 只认精确标题匹配**:`matchesWikilink` 要求链接文本等于文件标题或去编号后的文件名。库内 3334 个 wikilink 中大量是实体名(`[[电子秤]]` 401 次、`[[运营管理平台]]` 315 次等),无同名文件,匹配失败。
+
+实际影响:
+- `graph query` / `graph related` 的关联遍历基本失效(每节点平均 0.17 条边)
+- `search` 排序不依赖边(FTS + 板块匹配),日常搜索未受影响
+- "相关文档推荐"(共同标签/共同实体)能力缺失
+
+## 目标
+
+- 边数从 138 提升到 2500~3500(tag + entity + wikilink)
+- `graph related` 能通过共同标签/实体找到真正相关的文档
+- **不破坏现有 `search` 行为**:tag/entity 节点不能污染 FTS 搜索结果和关键词匹配
+
+## 非目标(YAGNI)
+
+- 不做 wikilink 别名模糊匹配(方案B)——精确匹配语义正确,模糊匹配易引入误连
+- 不改 search 的排序算法(共同标签加分可作为后续独立优化)
+- 不做增量索引(现有全量重建已够用,814 文件重建秒级完成)
+
+## 设计
+
+### 1. Schema 变更:nodes 表增加 node_type
+
+```sql
+CREATE TABLE nodes (
+    id         INTEGER PRIMARY KEY AUTOINCREMENT,
+    path       TEXT NOT NULL UNIQUE,
+    node_type  TEXT NOT NULL DEFAULT 'file',   -- 'file' | 'tag' | 'entity'
+    title      TEXT,
+    section    TEXT,
+    tags       TEXT,
+    entities   TEXT,
+    wikilinks  TEXT,
+    content_fts TEXT,
+    created_at TEXT DEFAULT (datetime('now')),
+    updated_at TEXT DEFAULT (datetime('now'))
+);
+```
+
+迁移策略:`initTables` 建表时包含新列;对已存在的旧库执行
+`ALTER TABLE nodes ADD COLUMN node_type TEXT NOT NULL DEFAULT 'file'`
+(SQLite 支持 ADD COLUMN,幂等处理:先 PRAGMA table_info 检查列是否存在)。
+由于 index build 本来就是全量 ClearData + 重建,旧数据会被清掉,
+迁移只影响"旧库直接跑非 build 命令"的场景,DEFAULT 'file' 已兜底。
+
+### 2. 虚拟节点落库
+
+**builder.go**:虚拟节点不再用内存 ID,改为生成带命名空间的合成 path:
+
+- tag 节点:`path = "tag:<标签名>"`,`node_type = "tag"`,`title = 标签名`
+- entity 节点:`path = "entity:<实体名>"`,`node_type = "entity"`,`title = 实体名`
+- 文件节点:`node_type = "file"`
+
+Graph 结构增加 `NodeType` 字段。虚拟节点也进入 `g.Nodes` 切片(保证 idMap 完整),
+边生成逻辑不变(file → 虚拟节点的 tag/entity 边 + wikilink 边)。
+
+**rebuild.go**:写入逻辑不变(先节点后边,idMap 映射),虚拟节点现在有真实 ID,
+tag/entity 边不再被跳过。
+
+**path 命名空间安全性**:vault 内实际文件路径均不含 `tag:`/`entity:` 前缀
+(已验证),UNIQUE 约束不会冲突。
+
+### 3. 查询过滤:防止虚拟节点污染检索
+
+以下查询增加 `WHERE node_type = 'file'`(或等效过滤):
+
+| 函数 | 位置 | 过滤方式 |
+|------|------|---------|
+| `CreateFTS` / `PopulateFTS` | fts.go | PopulateFTS 的 INSERT...SELECT 增加 `WHERE node_type='file'`,tag 节点不进 FTS |
+| `FTSSearch` | fts.go | FTS 表只含 file 节点,天然隔离,无需改动 |
+| `FindNodesByKeyword` | sqlite.go | SQL 增加 `AND node_type='file'` |
+| `FindRelatedNodes` | sqlite.go | 种子节点查询增加 `AND node_type='file'`;共同标签/实体遍历逻辑不变(tag 边落库后此逻辑才真正生效) |
+| `GetAllNodes`(gc 用) | sqlite.go | gc 的 `os.Stat` 检查只对 `node_type='file'` 的节点执行,tag/entity 节点跳过(它们本就不是磁盘文件) |
+
+`GetNodeEdges` 图遍历不过滤 node_type——tag 节点正是遍历要经过的中间节点。
+
+### 4. stats 展示
+
+`graph stats` 的关系分布自然包含 tag/entity/wikilink 三类,无需改动;
+`index status` 的边数会如实反映新数量。
+
+## 数据流(rebuild 后)
+
+```
+vault 扫描(814 files)
+  → 构建图: 814 文件节点 + N 个 tag 节点 + M 个 entity 节点
+  → 边: file→tag, file→entity, file→file(wikilink)
+  → SQLite: 全部节点落库(node_type 区分), 全部边落库
+  → FTS: 仅 file 节点
+  → search: FTS+板块(行为不变)
+  → graph related: 共同 tag/entity 关联真正可用
+```
+
+## 错误处理
+
+- `ALTER TABLE` 迁移失败 → 报错退出(build 前保证 schema 就绪)
+- 虚拟节点 path 与真实文件冲突(理论上不会)→ UNIQUE 冲突报错,build 失败并提示
+- 边 UNIQUE 约束(from, to, relation, label):同一文件重复 tag 由 builder 去重(现有逻辑)
+
+## 测试
+
+1. `go test ./internal/graph/`:
+   - BuildGraph 生成的节点包含 tag/entity 节点,node_type 正确
+   - 合成 path 命名空间正确
+2. `go test ./internal/index/`:
+   - PopulateFTS 后 FTS 表不含 tag/entity 节点
+   - FindNodesByKeyword 不返回 tag 节点
+   - FindRelatedNodes 能通过共同 tag 找到相关文档
+   - gc 不误删 tag/entity 节点
+3. 端到端:`kb-cli index build` 后 `index status` 边数 ≥ 2000;
+   `graph related "电磁阀"` 返回结果包含共享 tag 的文档;
+   `search` 结果与改动前一致(不出现 tag 节点)。
+
+## 工作量估计
+
+- builder.go:~30 行(虚拟节点入 Nodes + NodeType 字段)
+- sqlite.go schema + 迁移:~40 行
+- fts.go / 各查询过滤:~20 行
+- 测试:~150 行
+- 总计约 1 小时实现 + 验证

--
Gitblit v1.10.0