@fanchao8609/agent_brain_sync 1.9.7 → 1.9.8
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/bin/abs.js +22 -3
- package/package.json +1 -1
- package/src/codegraph.js +87 -0
- package/src/install.js +5 -5
- package/src/query.js +10 -3
- package/src/store.js +81 -2
package/bin/abs.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// bin/abs.js — abs CLI 入口。
|
|
3
3
|
// abs <cmd> [args]
|
|
4
4
|
// 命令: init / board / status / load / task / install / uninstall / help
|
|
5
|
-
import { cmdInit, cmdStatus, cmdLoad, cmdTask, cmdLog, cmdQuery, cmdLint, cmdNote, cmdConcept, cmdShow, cmdRepair, cmdWrapup, cmdRule, cmdTeardownCheck, cmdTodoArchive, cmdResolve, cmdSupersede } from '../src/store.js';
|
|
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
6
|
import { setUser, getUser, userConfigPath } from '../src/userconfig.js';
|
|
7
7
|
import { runInstall, runUninstall } from '../src/install.js';
|
|
8
8
|
import { readFileSync } from 'node:fs';
|
|
@@ -76,6 +76,13 @@ const FLAG_SPEC = {
|
|
|
76
76
|
'port': { type: 'string' },
|
|
77
77
|
// note 的触发条件(何时该读这条经验)—— 供 load 的相关页推荐匹配。
|
|
78
78
|
'when': { type: 'string' },
|
|
79
|
+
// note 可选:借本机 CodeGraph 带出「改某符号会影响哪些代码」。
|
|
80
|
+
'impact': { type: 'string' },
|
|
81
|
+
// note 可选:经验类型(fact/pref/constraint/event,对齐 TencentDB L1 四分类)。
|
|
82
|
+
'type': { type: 'string' },
|
|
83
|
+
// review 用:--accept / --reject 各跟一个页名列表(逗号分隔或位置参数)。
|
|
84
|
+
'accept': { type: 'string' },
|
|
85
|
+
'reject': { type: 'string' },
|
|
79
86
|
// concept 骨架用: 不在 FLAG_SPEC 里的 `--title` 会被静默当布尔 true(见 parseArgv 注释),
|
|
80
87
|
// 于是 `--title "一句话"` 的正文会进位置参数 → 必须在这声明。
|
|
81
88
|
'title': { type: 'string' },
|
|
@@ -194,6 +201,7 @@ const usage = `abs — 跨会话记忆工具(.brain/ 图谱)
|
|
|
194
201
|
query <词…> [--all] 检索知识页 (多词 OR)
|
|
195
202
|
resolve <页名…> 反查页面路径
|
|
196
203
|
supersede <页名> [--by 页] 标经验已失效
|
|
204
|
+
review [--accept 页…|--reject 页…] 待确认页队列 (draft 升 active/否决)
|
|
197
205
|
lint 图谱体检 (死链/超限)
|
|
198
206
|
rule [add "一句话"] 硬规则读写
|
|
199
207
|
install|uninstall [--agent <宿主>] [--no-mcp] [--no-skill]
|
|
@@ -281,11 +289,13 @@ const subUsage = {
|
|
|
281
289
|
'abs note — 经验实时暂存 → sources/(幂等去重;先记后提炼)',
|
|
282
290
|
'',
|
|
283
291
|
'用法:',
|
|
284
|
-
' abs note "一句话经验" [--tags 坑,docker] [--when "什么时候该读这条"]',
|
|
292
|
+
' abs note "一句话经验" [--tags 坑,docker] [--when "什么时候该读这条"] [--impact 符号名] [--type fact|pref|constraint|event]',
|
|
285
293
|
'',
|
|
286
294
|
'说明:',
|
|
287
295
|
' • --tags 首个标签建议带类别(坑/技巧/决策…),检索按词 OR 命中',
|
|
288
296
|
' • --when 供 load 的相关页推荐匹配「何时该读」',
|
|
297
|
+
' • --impact 借本机 CodeGraph 带出「改该符号会影响哪些代码」(未装则静默跳过)',
|
|
298
|
+
' • --type 经验分类:fact 事实 / pref 偏好 / constraint 约束 / event 事件(默认不分类)',
|
|
289
299
|
' • sources 是暂存区: 提炼成 concept 后应清理',
|
|
290
300
|
].join('\n'),
|
|
291
301
|
query: [
|
|
@@ -447,7 +457,7 @@ async function main() {
|
|
|
447
457
|
break;
|
|
448
458
|
}
|
|
449
459
|
case 'note': {
|
|
450
|
-
console.log(await cmdNote({ dir: opts.dir, text: opts._.join(' '), tags: opts.tags, when: opts.when }));
|
|
460
|
+
console.log(await cmdNote({ dir: opts.dir, text: opts._.join(' '), tags: opts.tags, when: opts.when, impact: opts.impact, type: opts.type }));
|
|
451
461
|
break;
|
|
452
462
|
}
|
|
453
463
|
case 'concept': {
|
|
@@ -496,6 +506,15 @@ async function main() {
|
|
|
496
506
|
console.log(await cmdResolve({ dir: opts.dir, refs: opts._ }));
|
|
497
507
|
break;
|
|
498
508
|
}
|
|
509
|
+
case 'review': {
|
|
510
|
+
// 无 --accept/--reject → 列出全部 draft 页;有则对页名列表执行确认/否决。
|
|
511
|
+
const action = opts.accept ? 'accept' : opts.reject ? 'reject' : '';
|
|
512
|
+
const refs = action === 'accept' ? (opts.accept || '').split(',').map(s => s.trim()).filter(Boolean)
|
|
513
|
+
: action === 'reject' ? (opts.reject || '').split(',').map(s => s.trim()).filter(Boolean)
|
|
514
|
+
: opts._;
|
|
515
|
+
console.log(await cmdReview({ dir: opts.dir, refs, action }));
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
499
518
|
case 'lint': {
|
|
500
519
|
rejectExtra(opts._, 'abs lint');
|
|
501
520
|
console.log(await cmdLint({ dir: opts.dir }));
|
package/package.json
CHANGED
package/src/codegraph.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/codegraph.js — 借用本机 CodeGraph 的「代码影响面」,写回 abs 图谱。
|
|
2
|
+
//
|
|
3
|
+
// 为何要这个模块(而非直接塞进 cmdNote):
|
|
4
|
+
// abs 的定位是纯 markdown 图谱、零依赖、可进 git;CodeGraph 是二进制 SQLite、
|
|
5
|
+
// 不可进 git、且不是每个机器都装。两者职责不同,唯一值得打通的点是一个维度:
|
|
6
|
+
// abs 的经验页里「这个改动会影响哪些代码」是空白的,而 codegraph impact 正是干这个的。
|
|
7
|
+
//
|
|
8
|
+
// 融合姿势 = 借结果、不引依赖:
|
|
9
|
+
// 只在「用户显式传了 --impact <符号>」时才调 codegraph CLI,
|
|
10
|
+
// 把它的纯文本输出写进 .brain/ 的 markdown 页(可进 git)。
|
|
11
|
+
// codegraph 不存在 / 调用失败 / 超时 → 静默降级为「无影响面」,绝不阻断 note 落盘。
|
|
12
|
+
//
|
|
13
|
+
// 为什么走 child_process 调 CLI、不引 npm 包、不走 MCP:
|
|
14
|
+
// - CLI 是 codegraph 自己装的稳定接口(impact/callers/callees),零依赖;
|
|
15
|
+
// - MCP 走宿主钩子,之前实测在 pi 下报 _elicitationHandler 错(宿主 bug),CLI 稳定。
|
|
16
|
+
|
|
17
|
+
import { execFile } from 'node:child_process';
|
|
18
|
+
|
|
19
|
+
const CG_BIN = 'codegraph';
|
|
20
|
+
const CG_TIMEOUT_MS = 3000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 探测本机 codegraph 是否可用(不抛错,任何失败都返回 false)。
|
|
24
|
+
* 结果只影响「要不要在 note 里带影响面」,绝不影响 note 本身落盘。
|
|
25
|
+
*/
|
|
26
|
+
async function codegraphAvailable() {
|
|
27
|
+
return new Promise((resolve) => {
|
|
28
|
+
execFile(CG_BIN, ['--version'], { timeout: 1500 }, (err) => resolve(!err));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 索引是否新鲜(无 pending 改动)。
|
|
34
|
+
* codegraph 有文件 watcher,但 daemon 可能没在跑 / 停了很久,索引会过期。
|
|
35
|
+
* 过期时 impact 会返回旧数据 —— 把它写进经验页等于污染 abs 图谱。
|
|
36
|
+
* 用 `codegraph status --json` 的 pendingChanges 判定:全 0 才新鲜;
|
|
37
|
+
* 任何失败(命令不在/超时/解析失败)一律当「不新鲜」,宁缺毋滥。
|
|
38
|
+
*/
|
|
39
|
+
async function codegraphFresh(root) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
execFile(
|
|
42
|
+
CG_BIN,
|
|
43
|
+
['status', '--json', root],
|
|
44
|
+
{ timeout: 2000 },
|
|
45
|
+
(err, stdout) => {
|
|
46
|
+
if (err) return resolve(false);
|
|
47
|
+
try {
|
|
48
|
+
const j = JSON.parse(stdout);
|
|
49
|
+
const pc = j.pendingChanges || {};
|
|
50
|
+
const fresh = (pc.added || 0) + (pc.modified || 0) + (pc.removed || 0) === 0;
|
|
51
|
+
resolve(fresh);
|
|
52
|
+
} catch {
|
|
53
|
+
resolve(false);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 拿某符号的「影响面」:改它会波及哪些函数/文件。
|
|
62
|
+
* @param {string} symbol 符号名(如 cmdTask、requireBrain)
|
|
63
|
+
* @param {string} root 项目根(codegraph -p 参数)
|
|
64
|
+
* @returns {Promise<string|null>} 纯文本影响面,失败/不可用/索引过期返回 null
|
|
65
|
+
*/
|
|
66
|
+
export async function impactOf(symbol, root) {
|
|
67
|
+
const s = String(symbol || '').trim();
|
|
68
|
+
if (!s) return null;
|
|
69
|
+
if (!(await codegraphAvailable())) return null;
|
|
70
|
+
// 索引不新鲜 → 不写影响面(避免把过期结构写进经验页)。
|
|
71
|
+
// 代价:用户刚改完代码立刻 note 时拿不到影响面;收益:绝不写错数据。
|
|
72
|
+
// 新鲜度恢复只需 codegraph 的 watcher 约 1s 同步,或手动 codegraph sync。
|
|
73
|
+
if (!(await codegraphFresh(root))) return null;
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
execFile(
|
|
76
|
+
CG_BIN,
|
|
77
|
+
['impact', s, '--depth', '2', '-p', root],
|
|
78
|
+
{ timeout: CG_TIMEOUT_MS },
|
|
79
|
+
(err, stdout) => {
|
|
80
|
+
if (err) return resolve(null);
|
|
81
|
+
const text = String(stdout || '').trim();
|
|
82
|
+
// codegraph 无命中时输出形如 "No ..." 或空,统一当「无影响面」。
|
|
83
|
+
resolve(text ? text : null);
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
}
|
package/src/install.js
CHANGED
|
@@ -822,11 +822,11 @@ async function installPi({ withMcp, withSkill, log }) {
|
|
|
822
822
|
const p = join(dir, 'abs.ts');
|
|
823
823
|
await atomicWrite(p, piPluginSource()); // Pi 用专用模板, 语法与 OpenCode 不同构
|
|
824
824
|
steps.push(`✓ hook(ts extension) → ${p}`);
|
|
825
|
-
// MCP → ~/.pi/agent/mcp.json 的 mcpServers (stdio)
|
|
826
|
-
//
|
|
827
|
-
//
|
|
825
|
+
// MCP → ~/.pi/agent/mcp-adapter.json 的 mcpServers (stdio)
|
|
826
|
+
// 坑(2026-09-30 用户实报):曾写 mcp.json,但 pi-mcp-adapter 已不再读它
|
|
827
|
+
// (pi 自有 mcp.json 被 adapter 忽略,adapter 改读 mcp-adapter.json)→ 写了等于没写。
|
|
828
828
|
if (withMcp) {
|
|
829
|
-
const mcpP = join(hostConfigRoot('pi'), 'agent', 'mcp.json');
|
|
829
|
+
const mcpP = join(hostConfigRoot('pi'), 'agent', 'mcp-adapter.json');
|
|
830
830
|
await backup(mcpP);
|
|
831
831
|
const cfg = await readJson(mcpP);
|
|
832
832
|
cfg.mcpServers = cfg.mcpServers || {};
|
|
@@ -845,7 +845,7 @@ async function uninstallPi() {
|
|
|
845
845
|
await fs.rm(join(hostConfigRoot('pi'), 'agent', 'extensions', 'abs.ts'), { force: true });
|
|
846
846
|
steps.push(`✓ extension 已删除`);
|
|
847
847
|
steps.push(...await uninstallSkills('pi'));
|
|
848
|
-
const mcpP = join(hostConfigRoot('pi'), 'agent', 'mcp.json');
|
|
848
|
+
const mcpP = join(hostConfigRoot('pi'), 'agent', 'mcp-adapter.json');
|
|
849
849
|
const cfg = await readJson(mcpP, { strict: false });
|
|
850
850
|
if (cfg === null) {
|
|
851
851
|
steps.push(`⚠ ${mcpP} 无法解析为 JSON,已跳过(未改动你的配置)`);
|
package/src/query.js
CHANGED
|
@@ -81,7 +81,13 @@ export async function cmdQuery({ dir, terms, includeSuperseded }) {
|
|
|
81
81
|
const extra = hidden ? `(另外 ${hidden} 页已标记 superseded,用 abs query ${words.join(' ')} --all 查看)` : '';
|
|
82
82
|
return `query [${words.join(', ')}]: 无命中。${extra}用 abs lint 看图谱健康;首次使用先 abs init。`;
|
|
83
83
|
}
|
|
84
|
-
|
|
84
|
+
// 输出条数上限(2026-09-30 补):落实 read-side-output-must-not-scale 规则。
|
|
85
|
+
// query 会把命中页全部渲染;图谱长大到几百页时一次命中 50 页会爆输出。
|
|
86
|
+
// 硬上限 10 页,超出只给计数 + 收窄提示(不给全文 —— 那不是「不静默」,是「可再查」)。
|
|
87
|
+
const MAX_SHOWN = 10;
|
|
88
|
+
const overflow = shown.length - MAX_SHOWN;
|
|
89
|
+
const displayed = shown.slice(0, MAX_SHOWN);
|
|
90
|
+
const lines = displayed.map((h) => {
|
|
85
91
|
const by = h.author ? ` @${h.author}` : '';
|
|
86
92
|
// id 只在≠slug 时显示 —— 相同时再印一遗就是纯噪音(绝大多数页)。
|
|
87
93
|
// 目的:让 AI 拿到一个改名也不漂的引用句柄(abs resolve <id> 能反查回来)。
|
|
@@ -97,12 +103,13 @@ export async function cmdQuery({ dir, terms, includeSuperseded }) {
|
|
|
97
103
|
return `📄 ${h.slug}${by}${id}${st}${fuzzy} (命中: ${hitTxt}${full})${viaTag}\n ${h.snippet}`;
|
|
98
104
|
});
|
|
99
105
|
// 多词且无全命中时告知降级了 —— 不静默给一堆弱相关结果。
|
|
100
|
-
const anyFull =
|
|
106
|
+
const anyFull = displayed.some((h) => h.kind === 'exact' && h.matched.length === words.length);
|
|
101
107
|
const tail = [];
|
|
102
108
|
if (words.length > 1 && !anyFull) tail.push('', `(无页同时命中全部 ${words.length} 个词,以下按命中数排序)`);
|
|
103
109
|
if (fuzzySuppressed) tail.push('', `(另有 ${fuzzyHits.length} 页字形相近但字面未命中,已隐藏 —— 它们通常不相关)`);
|
|
104
110
|
if (hidden) tail.push(``, `(${hidden} 页 superseded 已隐藏;--all 可看)`);
|
|
105
|
-
|
|
111
|
+
if (overflow > 0) tail.push(``, `(另有 ${overflow} 页未展示 —— 用更具体的词收窄:abs query <词1> <词2>)`);
|
|
112
|
+
return [`query [${words.join(', ')}] → ${shown.length} 页(展示前 ${displayed.length} 页):`, '', ...lines, ...tail].join('\n');
|
|
106
113
|
}
|
|
107
114
|
|
|
108
115
|
/** 从页面正文取一段「像答案」的片段。
|
package/src/store.js
CHANGED
|
@@ -8,6 +8,7 @@ import { stripStateMark, ensureStateMark, normalizeTodo, addTask, upsertTask, bo
|
|
|
8
8
|
import { editFile, SKIP } from './lock.js';
|
|
9
9
|
import { appendWrapup, strandedFor } from './wrapup.js';
|
|
10
10
|
import { keywords, pickRelevant, renderRelevant, recentFiles, rankPage, topicStrength } from './relevant.js';
|
|
11
|
+
import { impactOf } from './codegraph.js';
|
|
11
12
|
|
|
12
13
|
// Re-export lint.js symbols so external importers (e.g. bin/mcp.js, bin/abs.js) still work.
|
|
13
14
|
export { cmdLint, listPages, hasTail, PAGE_DIRS } from './lint.js';
|
|
@@ -708,6 +709,71 @@ export async function resolvePage(root, idOrSlug) {
|
|
|
708
709
|
return null;
|
|
709
710
|
}
|
|
710
711
|
|
|
712
|
+
// ---------- review: 待确认页队列(draft → active/superseded) ----------
|
|
713
|
+
// 为何需要:abs note 落的是 status: draft(未经核实),但之前没有「确认」这一步 ——
|
|
714
|
+
// draft 只是标签,没人管,经验就永远停在「待核实」状态,从不正式化。
|
|
715
|
+
// 借鉴 TencentDB 的 review/route 治理环节:提取后必经审查,防止脏知识进入正式图谱。
|
|
716
|
+
// 本命令只做「把 draft 显式升为 active 或否决为 superseded」,不替人判断内容好坏。
|
|
717
|
+
// 动作收口在一处(editFile 锁内),并发安全同 supersede。
|
|
718
|
+
export async function cmdReview({ dir, refs, action }) {
|
|
719
|
+
let root;
|
|
720
|
+
try {
|
|
721
|
+
root = await requireBrain(dir || process.cwd());
|
|
722
|
+
} catch {
|
|
723
|
+
return `未找到 .brain/ 图谱。先在项目根运行: abs init`;
|
|
724
|
+
}
|
|
725
|
+
const act = String(action || '').toLowerCase();
|
|
726
|
+
if (act && !['accept', 'reject'].includes(act)) {
|
|
727
|
+
return '用法: abs review [--accept <页名…>] [--reject <页名…>] — 无参数列出全部 draft 页';
|
|
728
|
+
}
|
|
729
|
+
// 无动作 → 列出所有 draft 页(待确认队列)
|
|
730
|
+
if (!act) {
|
|
731
|
+
const pages = await listPages(brainPath(root));
|
|
732
|
+
// 只扫经验/知识目录(concepts/sources)。entities 是人页、sessions 是日志,
|
|
733
|
+
// 它们不是「待核实的经验」,不该进 review 队列(拉进来会把人页/日志当经验误确认)。
|
|
734
|
+
const REVIEW_DIRS = ['concepts', 'sources'];
|
|
735
|
+
const drafts = pages.filter((p) => REVIEW_DIRS.includes(p.dir) && statusOfPage(p.body) === 'draft');
|
|
736
|
+
if (!drafts.length) return '✓ 没有待确认的 draft 页。';
|
|
737
|
+
const lines = drafts.map((p) => {
|
|
738
|
+
const t = p.body.match(/^#\s*(.+)$/m);
|
|
739
|
+
const title = t ? t[1].trim() : p.slug;
|
|
740
|
+
return ` [draft] ${p.slug} — ${title}`;
|
|
741
|
+
});
|
|
742
|
+
return [
|
|
743
|
+
`待确认 draft 页 ${drafts.length} 条:`,
|
|
744
|
+
...lines,
|
|
745
|
+
'',
|
|
746
|
+
'确认: abs review --accept <页名> [更多…] 否决: abs review --reject <页名> [更多…]',
|
|
747
|
+
].join('\n');
|
|
748
|
+
}
|
|
749
|
+
// 有动作 → 对每个 ref 改 status
|
|
750
|
+
const list = (refs || []).map((r) => String(r).trim()).filter(Boolean);
|
|
751
|
+
if (!list.length) return `✗ --${act} 需要至少一个页名。用法: abs review --${act} <页名…>`;
|
|
752
|
+
const target = act === 'accept' ? 'active' : 'superseded';
|
|
753
|
+
const out = [];
|
|
754
|
+
for (const r of list) {
|
|
755
|
+
const hit = await resolvePage(root, r);
|
|
756
|
+
if (!hit) { out.push(`✗ ${r}: 未找到(试 abs review 看清单)`); continue; }
|
|
757
|
+
const res = await editFile(hit.full, (cur) => {
|
|
758
|
+
if (!cur || !cur.startsWith('---\n')) return SKIP;
|
|
759
|
+
const end = cur.indexOf('\n---', 3);
|
|
760
|
+
if (end === -1) return SKIP;
|
|
761
|
+
let fm = cur.slice(0, end);
|
|
762
|
+
const curSt = statusOfPage('', fm);
|
|
763
|
+
// 幂等:已是目标状态 → 不写盘
|
|
764
|
+
if (curSt === target) return SKIP;
|
|
765
|
+
fm = STATUS_RE.test(fm)
|
|
766
|
+
? fm.replace(STATUS_RE, `status: ${target}`)
|
|
767
|
+
: `${fm}\nstatus: ${target}`;
|
|
768
|
+
return { text: fm + cur.slice(end) };
|
|
769
|
+
});
|
|
770
|
+
out.push(res === SKIP
|
|
771
|
+
? `= ${hit.slug}: 已是 ${target}(无变化)`
|
|
772
|
+
: `✓ ${hit.slug} → ${target}`);
|
|
773
|
+
}
|
|
774
|
+
return out.join('\n');
|
|
775
|
+
}
|
|
776
|
+
|
|
711
777
|
// ---------- resolve: id/slug → 页面路径(引用的反查端) ----------
|
|
712
778
|
// 配合 frontmatter 的 id: 使用。页改名后 id 不变,靠本命令仍能找回来。
|
|
713
779
|
export async function cmdResolve({ dir, refs }) {
|
|
@@ -1119,7 +1185,7 @@ export { cmdQuery } from './query.js';
|
|
|
1119
1185
|
// ---------- note: 经验实时暂存(source 页,一念一落,防流失) ----------
|
|
1120
1186
|
const NOTE_DEDUP_MS = 60 * 1000;
|
|
1121
1187
|
|
|
1122
|
-
export async function cmdNote({ dir, text, tags, when }) {
|
|
1188
|
+
export async function cmdNote({ dir, text, tags, when, impact, type }) {
|
|
1123
1189
|
const clean = String(text || '').trim();
|
|
1124
1190
|
if (!clean) return '用法: abs note "经验/坑/技巧一句话" [--when "何时该读它"](落 sources/ 暂存页,实时不流失)';
|
|
1125
1191
|
let root;
|
|
@@ -1140,6 +1206,15 @@ export async function cmdNote({ dir, text, tags, when }) {
|
|
|
1140
1206
|
return `• 60s 内已落同文本 → ${f} (跳过重复)`;
|
|
1141
1207
|
}
|
|
1142
1208
|
}
|
|
1209
|
+
// 影响面(可选):显式传 --impact <符号> 时,借本机 CodeGraph 拿「改它波及谁」。
|
|
1210
|
+
// 失败/未装 codegraph 静默降级为无,绝不阻断 note 落盘。
|
|
1211
|
+
const impactText = impact ? await impactOf(impact, root) : null;
|
|
1212
|
+
// 类型(可选):借鉴 TencentDB 的 L1 四分类,把自由文本经验分成可分类的资产。
|
|
1213
|
+
// 默认不强制(自由文本仍是主体);显式 --type 时才写进 frontmatter,供检索/load 区分。
|
|
1214
|
+
// 合法值对齐 L1 四类:fact 事实 / pref 偏好 / constraint 约束 / event 事件。
|
|
1215
|
+
const NOTE_TYPES = ['fact', 'pref', 'constraint', 'event'];
|
|
1216
|
+
const noteType = NOTE_TYPES.includes(String(type || '').trim().toLowerCase())
|
|
1217
|
+
? String(type).trim().toLowerCase() : '';
|
|
1143
1218
|
const tagList = String(tags || '').split(',').map((t) => t.trim()).filter(Boolean);
|
|
1144
1219
|
const fmTags = ['source', ...tagList].join(', ');
|
|
1145
1220
|
const slugSrc = slugOf(clean);
|
|
@@ -1155,12 +1230,14 @@ export async function cmdNote({ dir, text, tags, when }) {
|
|
|
1155
1230
|
`author: ${who}`,
|
|
1156
1231
|
`updated: ${today()}`,
|
|
1157
1232
|
'status: draft',
|
|
1233
|
+
...(noteType ? [`type: ${noteType}`] : []),
|
|
1158
1234
|
'---',
|
|
1159
1235
|
'',
|
|
1160
1236
|
`# 来源:${heading}`,
|
|
1161
1237
|
'',
|
|
1162
1238
|
`TITLE: ${clean}`,
|
|
1163
1239
|
...(whenText ? ['', `WHEN: ${whenText}`] : []),
|
|
1240
|
+
...(impactText ? ['', '## 影响面(本机 CodeGraph 自动带出)', '```', impactText, '```'] : []),
|
|
1164
1241
|
'',
|
|
1165
1242
|
`## 记录(实时暂存,Teardown 时提炼进 concepts/ 后本页可删)`,
|
|
1166
1243
|
`- ${clean}`,
|
|
@@ -1325,6 +1402,8 @@ export async function registerInIndex(root, section, slug, desc) {
|
|
|
1325
1402
|
const next = after === -1
|
|
1326
1403
|
? `${index.replace(/\s*$/, '')}\n${line}\n`
|
|
1327
1404
|
: index.slice(0, after) + `\n${line}` + index.slice(after);
|
|
1328
|
-
|
|
1405
|
+
// 归一空行:历史手工编辑会留 3+ 空行(load 时 collapseIndex 会压掉,但文件本身没清)。
|
|
1406
|
+
// 追加新条目的同时顺手压一次,既清旧债又不改内容(与 collapseIndex 同一判据)。
|
|
1407
|
+
return { text: next.replace(/\n{3,}/g, '\n\n') };
|
|
1329
1408
|
});
|
|
1330
1409
|
}
|