@fanchao8609/agent_brain_sync 1.21.0 → 1.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,14 +17,17 @@ npm install -g @fanchao8609/agent_brain_sync
17
17
  abs install
18
18
  ```
19
19
 
20
- `abs install` 会交互式让你勾选智能体;也可以直接指定:
20
+ **默认只装 pi**(其余宿主 opt-in)——装到不用的宿主上会往那边配置文件塞 hook/MCP 条目。
21
21
 
22
22
  ```bash
23
- abs install --agent pi # 只装 pi
24
- abs install # 全部: claude-code / codex / opencode / pi / omp
23
+ abs install # 默认: 只装 pi
24
+ abs install --all # 全装: claude-code / codex / opencode / pi / omp
25
+ abs install --agent claude-code # 只装指定宿主
25
26
  abs install --agent pi --no-mcp # 只装 hook + skill,不要 MCP
26
27
  ```
27
28
 
29
+ 交互模式下回车 = 只装 pi;输入 `all` 才全装。
30
+
28
31
  装了什么:
29
32
 
30
33
  | 宿主 | hook | MCP | skill |
@@ -117,11 +120,12 @@ abs todo done TASK-1
117
120
  abs note "一句话经验" --tags 坑,docker # 经验暂存 → sources/
118
121
  abs log "完成 X" # 记一行流水;abs log 无参 = 查看
119
122
  abs query <词> # 检索图谱(多词 OR);已被推翻的经验默认隐藏(--all 可看)
120
- abs capture [--layer 1-7] "一句话" # 按七层判层后直接落到位(不传 = L4 认知)
123
+ abs capture [--layer 1-7] [--kind …] "一句话" # 按七层判层后落到位(--kind: gotcha|decision|tradeoff|howto)
121
124
  abs db rebuild # 扫所有登记项目 → 建图数据库(~/.abs/graph.db)
122
- abs db search <词> # 走库检索:FTS 拿种子 + 亮度扩散,L1 铁律恒在最前
125
+ abs db search <词> [--full] # 走库检索:默认只给摘要+token 成本,--full 才附正文
123
126
  abs db status # 库体检:七层分布 / 悬挂链接 / 已入库项目
124
127
  abs db add # 把当前项目登记进 ~/.abs/projects.json
128
+ abs db evolve # 知识进化体检:被推翻/断链/主题堆积/僵化页
125
129
  abs summon [--mark|--status] # 会话总结托底:列出未总结 / 有更新的会话(不写盘)
126
130
  abs resolve <页名或id> # 反查页面路径(页改名后 id 不变)
127
131
  abs supersede <页名> [--by <新页>] # 标记经验已失效(不删文件,保留历史;query 默认不再返回)
package/bin/abs.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // abs <cmd> [args]
4
4
  // 命令: init / board / status / load / task / install / uninstall / help
5
5
  import { cmdInit, cmdStatus, cmdLoad, cmdTask, cmdLog, cmdQuery, cmdLint, cmdNote, cmdConcept, cmdShow, cmdRepair, cmdWrapup, cmdRule, cmdTeardownCheck, cmdTodoArchive, cmdResolve, cmdSupersede, cmdReview } from '../src/store.js';
6
- import { cmdDbRebuild, cmdDbSearch, cmdDbStatus, addProject } from '../src/brain/db-cmd.js';
6
+ import { cmdDbRebuild, cmdDbSearch, cmdDbStatus, cmdDbEvolve, addProject } from '../src/brain/db-cmd.js';
7
7
  import { cmdCapture } from '../src/brain/capture.js';
8
8
  import { cmdSummon } from '../src/brain/summon-cmd.js';
9
9
  import { setUser, getUser, userConfigPath } from '../src/userconfig.js';
@@ -210,6 +210,7 @@ function parseArgv(args) {
210
210
  tags: values.tags,
211
211
  limit: values.limit === undefined ? undefined : Number(values.limit),
212
212
  layer: values.layer,
213
+ kind: values.kind,
213
214
  mark: !!values.mark,
214
215
  status: !!values.status,
215
216
  title: values.title,
@@ -249,7 +250,7 @@ const usage = `abs — 跨会话记忆工具(.brain/ 图谱)
249
250
  summon [--mark|--status] 会话总结托底: 列出未总结/有更新的会话 (不写盘)
250
251
  concept <slug> --title … 建概念页骨架
251
252
  query <词…> [--all] 检索知识页 (多词 OR)
252
- db rebuild|search|status 图数据库: 建库 / 亮度搜索 / 体检
253
+ db rebuild|search|status|evolve 图数据库: 建库 / 亮度搜索 / 体检 / 进化体检
253
254
  resolve <页名…> 反查页面路径
254
255
  supersede <页名> [--by 页] 标经验已失效
255
256
  review [--accept 页…|--reject 页…] 待确认页队列 (draft 升 active/否决)
@@ -534,7 +535,7 @@ async function main() {
534
535
  break;
535
536
  }
536
537
  case 'capture': {
537
- console.log(await cmdCapture({ dir: opts.dir, text: opts._.join(' '), layer: opts.layer, tags: opts.tags, title: opts.title }));
538
+ console.log(await cmdCapture({ dir: opts.dir, text: opts._.join(' '), layer: opts.layer, tags: opts.tags, title: opts.title, kind: opts.kind }));
538
539
  break;
539
540
  }
540
541
  case 'note': {
@@ -556,7 +557,7 @@ async function main() {
556
557
  case 'uninstall': {
557
558
  // --help 已在 switch 前统一拦截(subUsage)。
558
559
  if (cmd === 'install') {
559
- await runInstall({ agent: opts.agent, mcp: opts.mcp !== false, skill: opts.skill !== false, yes: opts.yes });
560
+ await runInstall({ agent: opts.agent, all: !!opts.all, mcp: opts.mcp !== false, skill: opts.skill !== false, yes: opts.yes });
560
561
  } else {
561
562
  await runUninstall({ agent: opts.agent, yes: opts.yes });
562
563
  }
@@ -579,14 +580,15 @@ async function main() {
579
580
  const sub = opts._[0];
580
581
  const rest = opts._.slice(1);
581
582
  if (sub === 'rebuild') { console.log(await cmdDbRebuild({ dir: opts.dir })); break; }
582
- if (sub === 'search') { console.log(await cmdDbSearch({ query: rest.join(' '), limit: Number(opts.limit) || 8 })); break; }
583
+ if (sub === 'search') { console.log(await cmdDbSearch({ query: rest.join(' '), limit: Number(opts.limit) || 8, full: !!opts.full })); break; }
583
584
  if (sub === 'status') { console.log(await cmdDbStatus()); break; }
585
+ if (sub === 'evolve') { console.log(await cmdDbEvolve({ staleDays: Number(opts['stale-days']) || 90 })); break; }
584
586
  if (sub === 'add') {
585
587
  const r = await addProject(opts.dir || process.cwd());
586
588
  console.log(`✓ 已登记项目: ${r}\n 建库: abs db rebuild`);
587
589
  break;
588
590
  }
589
- console.log('用法: abs db rebuild | search <词> | status | add [--dir <项目根>]');
591
+ console.log('用法: abs db rebuild | search <词> | status | evolve | add [--dir <项目根>]');
590
592
  break;
591
593
  }
592
594
  case 'query': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanchao8609/agent_brain_sync",
3
- "version": "1.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "agent-brain-sync: 跨会话 AI 编码记忆 — hook 纯触发 + CLI/MCP 读写 .brain markdown 图谱, 防并发写保护。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,7 +19,7 @@ description: "USE FOR abs / agent-brain-sync 跨会话记忆与任务续接。
19
19
  abs load # 开机读状态
20
20
  abs query <词> # 检索;abs resolve <页名> 反查路径
21
21
  abs todo add <id> --note "做什么" # 登记;note/memo/state/done 见「进行中」
22
- abs capture [--layer 1-7] "…" # 按七层判定落点后直接写入(不传=L4)
22
+ abs capture [--layer 1-7] [--kind …] "…" # 按七层判定落点后写入(--kind 分作用)
23
23
  abs note "经验" [--tags a,b] # 暂存经验(一律 L7 混沌层)
24
24
  abs concept <slug> --title "…" # 建概念页骨架
25
25
  abs rule add "一句话" # 加 Rules;abs lint 体检;abs init 建图谱
@@ -110,6 +110,18 @@ abs rule add "一句话" # 加 Rules;abs lint 体检;abs init
110
110
 
111
111
  判据不明时用 L4 —— 但**知道是工具经验就写 L3**:实测算过,本仓 52 页 concepts 里 23 页其实是 L3。
112
112
 
113
+ **`--kind` 是另一条正交轴**(借 cl​aude-mem 的 observation 类型):`layer` 说「什么性质」,`kind` 说「起什么作用」。
114
+
115
+ | kind | 含义 | 何时用 |
116
+ |---|---|---|
117
+ | `gotcha` | 关键坑 | 踩了会白干、想不到的那种 |
118
+ | `decision` | 决策 | 记「当时为什么这么选」 |
119
+ | `tradeoff` | 权衡 | A 好但 B 也有理,最后选了哪个 |
120
+ | `howto` | 做法 | 这类事该怎么做 |
121
+
122
+ 不传就不写(旧页留空,不强制补)。实测只有 `gotcha` 能靠 tags/标题机械识别,
123
+ 另三类判据在内容里 —— 别拿关键词瞎猜。
124
+
113
125
  `abs capture` 之后跑 `abs db rebuild` 让图数据库收录,否则检索拿不到它。
114
126
 
115
127
  ## 进行中:什么时候动它
@@ -19,6 +19,7 @@ import { cmdLog } from '../note.js';
19
19
  import { clip, slugOf } from '../text.js';
20
20
  import { LAYERS, layerOf, DEFAULT_LAYER } from './layers.js';
21
21
  import { autoIndex } from './db-cmd.js';
22
+ import { summonTail } from './summon.js';
22
23
 
23
24
  /** 层 → 落点目录。L1/L6 不立页,由下面的分支单独处理。 */
24
25
  const LAYER_DIR = {
@@ -51,13 +52,13 @@ export function layerGuide() {
51
52
  ].join('\n');
52
53
  }
53
54
 
54
- export async function cmdCapture({ dir, text, layer, tags, title }) {
55
+ export async function cmdCapture({ dir, text, layer, tags, title, kind }) {
55
56
  const clean = String(text || '').trim();
56
57
  const layerArg = String(layer || '').trim();
57
58
 
58
59
  if (!clean) {
59
60
  return [
60
- '用法: abs capture [--layer 1-7] "一句话" [--tags a,b] [--title "页标题"]',
61
+ '用法: abs capture [--layer 1-7] [--kind gotcha|decision|tradeoff|howto] "一句话" [--tags a,b] [--title "页标题"]',
61
62
  '',
62
63
  layerGuide(),
63
64
  ].join('\n');
@@ -85,6 +86,16 @@ export async function cmdCapture({ dir, text, layer, tags, title }) {
85
86
  return r;
86
87
  }
87
88
 
89
+ // 作用分档(2026-11-02 借 clo​ude-mem):gotcha 坑 / decision 决策 / tradeoff 权衡 / howto 做法。
90
+ // 与 layer 正交:layer 说「这是什么性质的知识」,kind 说「它起什么作用」。
91
+ const KIND = {
92
+ gotcha: '坑', decision: '决策', tradeoff: '权衡', howto: '做法',
93
+ };
94
+ const kindArg = String(kind || '').trim().toLowerCase();
95
+ if (kindArg && !KIND[kindArg]) {
96
+ return `✗ --kind 只认 ${Object.keys(KIND).join(' / ')}(收到 "${kind}")`;
97
+ }
98
+
88
99
  let root;
89
100
  try {
90
101
  root = await requireBrain(dir || process.cwd());
@@ -120,6 +131,7 @@ export async function cmdCapture({ dir, text, layer, tags, title }) {
120
131
  '---',
121
132
  `tags: [${['concept', ...tagList].join(', ')}]`,
122
133
  `layer: ${L}`,
134
+ ...(kindArg ? [`kind: ${kindArg}`] : []),
123
135
  `author: ${who}`,
124
136
  `updated: ${today()}`,
125
137
  'status: draft',
@@ -153,18 +165,22 @@ export async function cmdCapture({ dir, text, layer, tags, title }) {
153
165
  const slug = file.replace(/\.md$/, '');
154
166
  const section = dirName === 'concepts' ? 'Concepts' : dirName === 'sessions' ? 'Sessions' : 'Sources';
155
167
  await registerInIndex(root, section, slug, head);
156
- await cmdLog({ dir: root, title: clean, kind: 'note' });
168
+ await cmdLog({ dir: root, title: clean, kind: 'note', summon: false });
157
169
 
158
170
  // 落盘即入库 —— 否则用户以为记下了,检索却空手而归(静默失效)。
159
171
  // autoIndex 永不抛错也不阻塞:库坏了只是检索旧一点,不能让记笔记失败。
160
172
  const idx = await autoIndex();
161
173
 
174
+ // 每次总结都顺手查一遍会话历史,避免漏下没总结的会话(2026-11-02 用户要求)。
175
+ // 只给线索不写盘 —— 落哪一条仍是人的判断。
176
+ const tail = await summonTail(root);
177
+
162
178
  return [
163
179
  `✓ 已落入 L${L} ${meta.name}层 → ${dirName}/${file}`,
164
180
  ` ${clip(clean, 60)} ${atTag(who)}`,
165
181
  ` 该层衰减 ${meta.decay}(${meta.note})`,
166
182
  idx.ok ? ' 已同步入库,abs db search 可查到' : ` (未入库:${idx.reason})`,
167
- ].join('\n');
183
+ ].join('\n') + tail;
168
184
  }
169
185
 
170
186
  export { LAYERS };
@@ -6,12 +6,72 @@
6
6
  import { promises as fs } from 'node:fs';
7
7
  import { join, resolve, dirname } from 'node:path';
8
8
  import { findBrainRoot, brainPath } from '../index.js';
9
- import { openDb, rebuild, search, illuminate, rulesOf, stats, ensureDbDir, dbPath } from './db.js';
9
+ import { openDb, rebuild, search, illuminate, rulesOf, stats, ensureDbDir, dbPath, lastOpenRebuilt } from './db.js';
10
10
  import { LAYERS } from './layers.js';
11
11
 
12
12
  const PAGE_DIRS = ['concepts', 'entities', 'sources', 'syntheses', 'sessions'];
13
13
  const FLAT_FILES = ['index.md', 'todo.md', 'log.md'];
14
14
 
15
+ /** 项目根路径 → 项目名(入库时的 project 字段)。
16
+ *
17
+ * 取路径末段。为何不引入 UUID(2026-11-02 评估):项目名是给人看的标识,
18
+ * 出现在每条检索结果里;UUID 会让输出不可读。末段同名的风险用 `db status` 暴露,
19
+ * 不做静默处理。
20
+ */
21
+ function projectNameOf(root) {
22
+ return String(root).split('/').filter(Boolean).pop() || 'unknown';
23
+ }
24
+
25
+ /** 给一批项目根算出**互不重名**的项目名。
26
+ *
27
+ * 为何需要(2026-11-02 实测):项目名默认取路径末段,而机器上同时存在
28
+ * ~/.agents 与 ~/Documents/软件/.agents,两者末段都是 `.agents` →
29
+ * 入库时撞 UNIQUE(project, dir, slug) 约束,整个 rebuild 失败。
30
+ *
31
+ * 消歧规则:重名的那些改用「父目录-末段」,仍重名就继续往上加,直到唯一。
32
+ * 只给重名的项目加前缀 —— 没重名的保持短名(检索结果里读起来更清楚)。
33
+ */
34
+ export function uniqueProjectNames(roots) {
35
+ const seg = (p) => String(p).split('/').filter(Boolean);
36
+ const base = new Map(roots.map((r) => [r, seg(r).pop() || 'unknown']));
37
+ const groups = new Map();
38
+ for (const [r, n] of base) {
39
+ if (!groups.has(n)) groups.set(n, []);
40
+ groups.get(n).push(r);
41
+ }
42
+ const out = new Map();
43
+ for (const [name, list] of groups) {
44
+ if (list.length === 1) { out.set(list[0], name); continue; }
45
+ // 重名 → 逐层加父目录前缀,直到组内唯一
46
+ for (let depth = 2; depth <= 4; depth++) {
47
+ const cand = new Map();
48
+ for (const r of list) {
49
+ const s = seg(r);
50
+ cand.set(r, s.slice(Math.max(0, s.length - depth)).join('-'));
51
+ }
52
+ const vals = [...cand.values()];
53
+ if (new Set(vals).size === vals.length) {
54
+ for (const [r, v] of cand) out.set(r, v);
55
+ break;
56
+ }
57
+ if (depth === 4) for (const [r, v] of cand) out.set(r, v); // 兜底:认了
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+
63
+ /** 当前所在项目名。查询时用它决定「读哪几条铁律」。
64
+ *
65
+ * 为何要单独一个函数(2026-11-02):原来只在 cmdDbSearch 里内联算,
66
+ * 与 rebuild 用的算法是两份代码 —— 任一边改了项目名规则,铁律就会读错项目的。
67
+ * 判据:项目名的算法只能有一处。 */
68
+ async function currentProjectName(dir) {
69
+ if (process.env.ABS_PROJECT) return process.env.ABS_PROJECT;
70
+ const from = dir || process.cwd();
71
+ const root = await findBrainRoot(from);
72
+ return root ? projectNameOf(root) : '';
73
+ }
74
+
15
75
  /** 扫一个项目的 .brain/ → 页面数组(含 index/todo/log 三份平铺文件)。 */
16
76
  async function scanProject(root, project) {
17
77
  const out = [];
@@ -80,14 +140,19 @@ export async function cmdDbRebuild({ dir } = {}) {
80
140
  return '✗ 没有可入的项目。先在项目根跑: abs db add 或 abs db rebuild';
81
141
  }
82
142
 
143
+ // 先收集所有有效根,再统一算项目名 —— 名字要去重,必须看到全局才知道谁重名
144
+ const found = [];
145
+ for (const root of roots) {
146
+ const r = await findBrainRoot(root);
147
+ if (r) found.push(r); // 登记了但目录没了 → 跳过,不报错
148
+ }
149
+ const names = uniqueProjectNames([...new Set(found)]);
83
150
  const pages = [];
84
151
  const used = [];
85
- for (const root of roots) {
86
- const found = await findBrainRoot(root);
87
- if (!found) continue; // 登记了但目录没了 → 跳过,不报错
88
- const project = found.split('/').filter(Boolean).pop() || 'unknown';
89
- pages.push(...await scanProject(found, project));
90
- used.push(`${project} (${found})`);
152
+ for (const f of [...new Set(found)]) {
153
+ const project = names.get(f);
154
+ pages.push(...await scanProject(f, project));
155
+ used.push(`${project} (${f})`);
91
156
  }
92
157
  if (!pages.length) return '✗ 登记的项目都没有 .brain/ 图谱,无可入库内容。';
93
158
 
@@ -111,7 +176,7 @@ export async function cmdDbRebuild({ dir } = {}) {
111
176
  }
112
177
 
113
178
  /** `abs db search <词>` —— 走库搜索:FTS 种子 → 亮度扩散。 */
114
- export async function cmdDbSearch({ query, limit = 8 } = {}) {
179
+ export async function cmdDbSearch({ query, limit = 8, full = false } = {}) {
115
180
  const q = String(query || '').trim();
116
181
  if (!q) return '用法: abs db search <词>';
117
182
 
@@ -122,9 +187,7 @@ export async function cmdDbSearch({ query, limit = 8 } = {}) {
122
187
  const h = openDb(path);
123
188
  try {
124
189
  // L1 铁律永远先出 —— 「不可逾越」的代码保证,不靠读到文本才遵守
125
- const project = process.env.ABS_PROJECT
126
- || (await findBrainRoot(process.cwd()) || '').split('/').filter(Boolean).pop()
127
- || '';
190
+ const project = await currentProjectName();
128
191
  const rules = rulesOf(h, project);
129
192
 
130
193
  const seeds = search(h, q, { limit: 12 });
@@ -136,17 +199,54 @@ export async function cmdDbSearch({ query, limit = 8 } = {}) {
136
199
  for (const r of rules) out.push(`- ${r}`);
137
200
  }
138
201
  if (!lit.length) {
139
- out.push('', '(无命中。库可能是旧的:abs db rebuild 重扫)');
202
+ out.push('', lastOpenRebuilt()
203
+ ? '(库文件曾损坏被清空,现在没有内容:跑 abs db rebuild 重建)'
204
+ : '(无命中。库可能是旧的:abs db rebuild 重扫)');
140
205
  return out.join('\n');
141
206
  }
207
+ // --full:显式要求时才读正文。默认不给 —— 这是索引优先的核心。
208
+ // 实测(2026-11-02):ABS 单页平均 824 token,8 页全带就是 6600 token,
209
+ // 而真正被用上的往往只有 1-2 页。让「看全文」成为一个显式动作。
210
+ const bodies = new Map();
211
+ if (full) {
212
+ const root = await findBrainRoot(process.cwd());
213
+ if (root) {
214
+ for (const p of lit) {
215
+ const body = await fs.readFile(brainPath(root, p.dir, `${p.slug}.md`), 'utf8').catch(() => '');
216
+ if (body) bodies.set(p.id, body);
217
+ }
218
+ }
219
+ }
220
+
142
221
  out.push('', '--- 按亮度排序 ---');
143
222
  for (const p of lit) {
144
- const bar = '█'.repeat(Math.max(1, Math.round(Math.min(p.bright, 3) / 3 * 12)));
223
+ // 亮度现在是 0.3~1.0(不再是早期可能到 165 万的异常值),
224
+ // 条长按 1.0 满格算:0.3 → 4 格,1.0 → 12 格。
225
+ const bar = '█'.repeat(Math.max(1, Math.round(Math.min(p.bright, 1) * 12)));
145
226
  const where = [p.project, p.dir, p.slug].filter(Boolean).join('/');
146
227
  const tag = p.from === 'spread' ? ' ↝' : '';
147
- out.push(`${bar} ${p.bright.toFixed(2)} L${p.layer} ${where}${tag}`);
228
+ // 跨项目的结果要显式标出来 —— 用户得知道「这条是别的项目的经验」,
229
+ // 否则会以为是自己项目的结论(判据、上下文都可能不同)。
230
+ const other = project && p.project && p.project !== project ? ' [跨项目]' : '';
231
+ // 索引优先:给摘要 + 取全文成本,让「要不要看全文」成为一个可算的决定。
232
+ const about = p.digest ? `\n ${p.digest}` : '';
233
+ const cost = p.tokens ? ` ~${p.tokens}tok` : '';
234
+ // 作用分档(gotcha/decision/tradeoff/howto)—— 同层里「哪种更该先看」的线索
235
+ const KIND_ICON = { gotcha: '🔴坑', decision: '🟤决策', tradeoff: '⚖️权衡', howto: '🔵做法' };
236
+ const kind = p.kind && KIND_ICON[p.kind] ? ` ${KIND_ICON[p.kind]}` : '';
237
+ out.push(`${bar} ${p.bright.toFixed(2)} L${p.layer} ${where}${tag}${other}${kind}${cost}${about}`);
238
+ if (full && bodies.has(p.id)) {
239
+ // 正文缩进显示,便于与索引行区分
240
+ const body = bodies.get(p.id).replace(/^---[\s\S]*?---\n/, ''); // 去掉 frontmatter
241
+ out.push(...body.split('\n').slice(0, 60).map((l) => ` │ ${l}`));
242
+ if (body.split('\n').length > 60) out.push(' │ …(已截断,用 abs resolve 看全文)');
243
+ }
148
244
  }
149
245
  out.push('', '(L1 铁律恒在最前;↝ 表示由邻居扩散点亮,非直接命中)');
246
+ out.push('(只给摘要与成本;要看全文: abs db search <词> --full 或 abs resolve <页名>)');
247
+ if (project) {
248
+ out.push(`(铁律只显示本项目 "${project}" 的 —— 别的项目的铁律不外借)`);
249
+ }
150
250
  return out.join('\n');
151
251
  } finally {
152
252
  h.close();
@@ -161,9 +261,11 @@ export async function cmdDbStatus() {
161
261
 
162
262
  const h = openDb(path);
163
263
  try {
264
+ const rebuilt = lastOpenRebuilt();
164
265
  const s = stats(h);
165
266
  const out = [
166
267
  `📀 库: ${path}`,
268
+ ...(rebuilt ? ['⚠ 库文件曾损坏,已自动清空重建 —— 跑 abs db rebuild 恢复内容'] : []),
167
269
  `节点 ${s.nodes} | 边 ${s.edges} | 悬挂链接 ${s.dangling} | 断点词 ${s.anchors} | 铁律 ${s.rules}`,
168
270
  '',
169
271
  '--- 七层分布 ---',
@@ -231,14 +333,31 @@ async function cmdDbRebuildQuiet() {
231
333
  const here = await findBrainRoot(process.cwd());
232
334
  const roots = [...new Set([...registered, ...(here ? [here] : [])])];
233
335
  if (!roots.length) return;
234
- const pages = [];
336
+ const found = [];
235
337
  for (const root of roots) {
236
- const found = await findBrainRoot(root);
237
- if (!found) continue;
238
- const project = found.split('/').filter(Boolean).pop() || 'unknown';
239
- pages.push(...await scanProject(found, project));
338
+ const r = await findBrainRoot(root);
339
+ if (r) found.push(r);
240
340
  }
341
+ const uniq = [...new Set(found)];
342
+ const names = uniqueProjectNames(uniq);
343
+ const pages = [];
344
+ for (const f of uniq) pages.push(...await scanProject(f, names.get(f)));
241
345
  if (!pages.length) return;
242
346
  const h = openDb(dbPath());
243
347
  try { rebuild(h, pages); } finally { h.close(); }
244
348
  }
349
+
350
+ /** `abs db evolve` — 知识自我进化的体检:哪些被推翻、哪些堆积、哪些该合并。 */
351
+ export async function cmdDbEvolve({ staleDays = 90 } = {}) {
352
+ const path = dbPath();
353
+ const exists = await fs.stat(path).then(() => true).catch(() => false);
354
+ if (!exists) return `库不存在: ${path}\n 先跑: abs db rebuild`;
355
+
356
+ const h = openDb(path);
357
+ try {
358
+ const { evolveSignals, renderEvolve } = await import('./evolve.js');
359
+ return renderEvolve(evolveSignals(h, { staleDays }), { staleDays });
360
+ } finally {
361
+ h.close();
362
+ }
363
+ }