@fanchao8609/agent_brain_sync 1.29.0 → 1.31.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.
@@ -1,135 +0,0 @@
1
- // src/brain/evolve.js — 知识自我进化:从图谱里看出「知识在往哪走」。
2
- //
3
- // 为何需要它(2026-11-02 用户定目标「可自我成长的知识库」):
4
- // 图谱现在能存能查,但不会告诉人「哪里在变好、哪里在堆积、哪里该合并」。
5
- // 光靠人定期回头看,就是靠记忆维护记忆 —— 必然漏。
6
- //
7
- // 四条判据,都只**报告**不判断(判断是内容决策,程序不替人做):
8
- // 1. 被推翻的:哪些页已 superseded、被谁取代、有没有留下空链
9
- // 2. 主题堆积:同一主题词下页数过多,说明该合并成规律页
10
- // 3. 只进不出:有新页被建立但没人链它,等于没接进网络
11
- // 4. stale:长期没改动、且没有任何入边的页 —— 考察是否该删
12
- //
13
- // 输出纪律:按类型折叠计数,同类超阈值只报条数 + 样例(见 read-side-output-must-not-scale)。
14
-
15
- /** 收集进化信号。只读,不改任何东西。 */
16
- export function evolveSignals(handle, { staleDays = 90, now = Date.now() } = {}) {
17
- const { db } = handle;
18
- const all = db.prepare("SELECT id, slug, project, layer, status, superseded_by, mtime FROM nodes WHERE is_ledger = 0").all();
19
- const byId = new Map(all.map((r) => [r.id, r]));
20
- const inbound = new Map();
21
- for (const r of db.prepare('SELECT dst, count(*) c FROM edges WHERE dst > 0 GROUP BY dst').all()) inbound.set(r.dst, r.c);
22
- const outbound = new Map();
23
- for (const r of db.prepare('SELECT src, count(*) c FROM edges WHERE src > 0 GROUP BY src').all()) outbound.set(r.src, r.c);
24
-
25
- // 1. 被推翻的 vs 单纯弃用
26
- //
27
- // 两者都是 status=superseded,但含义不同(2026-11-02 修正):
28
- // · 被取代 —— 有 superseded-by,指向新页(知识更新了)
29
- // · 单纯弃用 —— 没有 superseded-by(如「需求已否决,本页存档设计」)
30
- // 原来把「没写 superseded-by」一律报成断链,于是用户每跑一次 evolve 都被提醒
31
- // 一件并不存在问题的事(实测:corp_agent 的 run-command-controlled-exec
32
- // 正文写着「需求已否决」,那是存档不是断链)。
33
- // 真断链只有一种:**写了 by、但指向的页不存在**。
34
- const superseded = all.filter((r) => r.status === 'superseded');
35
- const retired = superseded.filter((r) => !r.superseded_by); // 单纯弃用,不报警
36
- const replaced = superseded.filter((r) => r.superseded_by); // 真被取代
37
- const danglingSupersede = replaced.filter((r) =>
38
- !all.some((x) => x.slug === r.superseded_by && x.project === r.project));
39
-
40
- // 2. 主题堆积:按 tags 里的**业务标签**归类。
41
- //
42
- // 坑(2026-11-02 实测):tags 的第一项是**页面类型**(concept/source/session-log/entity),
43
- // 拿它归类必然把 58 页 concept 报成「一组堆积」—— 那是类型不是主题,全是噪音。
44
- // 类型词是固定的几个,直接按白名单跳过;主题看第二项起。
45
- // 另:会话流水页不参与主题堆积判断。
46
- // 它们的 tags 里带 `todo-archive`/`历史`,按主题归类会把 13 个归档快照
47
- // 报成「该合并成规律页」—— 但归档快照本来就该各自独立(一天一文件是目录契约)。
48
- // 判据看 dir:sessions/ 下的都是流水,不是知识页。
49
- const TYPE_TAGS = new Set(['concept', 'source', 'synthesis', 'entity', 'session-log']);
50
- const tagBuckets = new Map();
51
- for (const row of db.prepare("SELECT tags, slug FROM nodes WHERE is_ledger = 0 AND dir != 'sessions' AND tags != ''").all()) {
52
- const parts = String(row.tags).replace(/^\[|\]$/g, '').split(',').map((s) => s.trim()).filter(Boolean);
53
- const topic = parts.find((x) => !TYPE_TAGS.has(x.toLowerCase()));
54
- if (!topic) continue;
55
- if (!tagBuckets.has(topic)) tagBuckets.set(topic, []);
56
- tagBuckets.get(topic).push(row.slug);
57
- }
58
- const piled = [...tagBuckets.entries()]
59
- .filter(([, v]) => v.length >= 8) // 一个主题下 8 页以上算堆积
60
- .sort((a, b) => b[1].length - a[1].length);
61
-
62
- // 3. 只进不出:有入边但没有出边 —— 说明它是终点,不连回别处
63
- const sinkOnly = all.filter((r) => (inbound.get(r.id) || 0) > 0 && !(outbound.get(r.id) || 0));
64
-
65
- // 4. stale:长期没改 + 没人链
66
- const staleMs = staleDays * 86400_000;
67
- const stale = all.filter((r) => (now - (r.mtime || 0) > staleMs) && !(inbound.get(r.id) || 0));
68
-
69
- return {
70
- total: all.length,
71
- superseded, replaced, retired, danglingSupersede,
72
- piled, sinkOnly, stale,
73
- byId, inbound, outbound,
74
- };
75
- }
76
-
77
- /** 渲染成给人看的报告。按类型分组,同类超阈值只报样例。 */
78
- export function renderEvolve(sig, { sample = 5, staleDays = 90 } = {}) {
79
- const out = [];
80
- const trunc = (arr, fmt) => {
81
- const shown = arr.slice(0, sample).map(fmt);
82
- if (arr.length > sample) shown.push(` …另有 ${arr.length - sample} 条`);
83
- return shown;
84
- };
85
-
86
- out.push(`📈 图谱进化体检 — ${sig.total} 页`);
87
- out.push('');
88
-
89
- // 好消息放前面:推翻说明知识在更新
90
- if (sig.replaced.length) {
91
- out.push(`✓ 已推翻 ${sig.replaced.length} 页(知识在更新,不是坏事)`);
92
- out.push(...trunc(sig.replaced, (r) => ` ${r.slug} → ${r.superseded_by}`));
93
- } else {
94
- out.push('· 还没有被推翻的页 —— 要么知识很稳,要么没人回头修正');
95
- }
96
- if (sig.retired.length) {
97
- out.push(`· 另有 ${sig.retired.length} 页是单纯弃用(需求否决/方案作废),不是断链`);
98
- out.push(...trunc(sig.retired, (r) => ` ${r.slug}`));
99
- }
100
- out.push('');
101
-
102
- if (sig.danglingSupersede.length) {
103
- out.push(`⚠ ${sig.danglingSupersede.length} 页写了取代者但目标页不存在(历史断了)`);
104
- out.push(...trunc(sig.danglingSupersede, (r) => ` ${r.slug}`));
105
- out.push(' 修: abs supersede <旧页> --by <新页>');
106
- out.push('');
107
- }
108
-
109
- if (sig.piled.length) {
110
- out.push(`⚠ 主题堆积 ${sig.piled.length} 组(同一主题下页数多,考虑合并成规律页)`);
111
- out.push(...trunc(sig.piled, ([tag, list]) => ` ${tag}: ${list.length} 页`));
112
- out.push('');
113
- }
114
-
115
- if (sig.sinkOnly.length) {
116
- out.push(`· 只被链、不链别人 ${sig.sinkOnly.length} 页(是终点,不回连——若它其实是枢纽,该补出链)`);
117
- out.push(...trunc(sig.sinkOnly, (r) => ` ${r.slug} (入 ${sig.inbound.get(r.id)})`));
118
- out.push('');
119
- }
120
-
121
- if (sig.stale.length) {
122
- out.push(`· ${staleDays} 天没动且无人链 ${sig.stale.length} 页(可能该删或该接进网络)`);
123
- out.push(...trunc(sig.stale, (r) => {
124
- const days = Math.round((Date.now() - r.mtime) / 86400_000);
125
- return ` ${r.slug} (${days} 天前)`;
126
- }));
127
- out.push('');
128
- }
129
-
130
- const issues = sig.danglingSupersede.length + sig.piled.length + sig.stale.length;
131
- out.push(issues === 0
132
- ? '✓ 没有需要立刻处理的进化问题'
133
- : `共 ${issues} 处待处理。这只是体检,改不改由你判断 —— 程序不替人做内容决策。`);
134
- return out.join('\n');
135
- }