project-node-memory 1.1.3 → 1.2.1
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 +9 -10
- package/README_EN.md +9 -11
- package/lib/engine/registry.js +10 -5
- package/lib/host/prompt.js +1 -1
- package/lib/host/routes.js +0 -9
- package/lib/host/tools.js +17 -68
- package/package.json +1 -1
- package/skills/project-node-memory-guide/SKILL.md +1 -1
- package/lib/engine/migrate.js +0 -171
package/README.md
CHANGED
|
@@ -104,13 +104,14 @@ config:
|
|
|
104
104
|
| `kb_init` | 建 `.nodekb` 骨架(幂等),铺出 `doc/ lessons/ todo/ index/` 与落位约定 |
|
|
105
105
|
| `kb_write` | 写库:`append` / `replace` / `create` / `node`(就地改写一个 `<node>` 块);写前校验、写后自检 |
|
|
106
106
|
| `kb_rename` | 文件改名 + 全库引用一起改写;写前重读,别人刚改的不覆盖 |
|
|
107
|
-
| `kb_migrate` | 旧语法裸引用 `[[x]]` → `[[node="x"]]` / `[[tscl="x"]]`;**默认只演练**,`dry_run=false` 才落盘 |
|
|
108
107
|
| `kb_vcs_status` | 库内嵌 git 的快照历史与未提交改动(git 缺失时只说明,不报错) |
|
|
109
108
|
|
|
110
109
|
## 库怎么定位
|
|
111
110
|
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
所有 `kb_*` 工具只有一个定位参数 `kb`,它同时接受**名字**与**路径**:
|
|
112
|
+
|
|
113
|
+
1. `kb` 给路径(技能目录,或其下的 `.nodekb`,绝对或相对工作区)——必须落在工作区或已注册库内
|
|
114
|
+
2. `kb` 给名字(注册表里的名字,或工作区里发现的名字,如某个技能名)
|
|
114
115
|
3. 都不给:工作区自带的 `.nodekb/` → 工作区里发现且唯一的库 → 注册表里唯一的库
|
|
115
116
|
|
|
116
117
|
**自动发现**:给了工作区之后,库里不只在工作区根找,还会按约定位置下钻(有界 BFS,默认 2 层):
|
|
@@ -193,20 +194,18 @@ config:
|
|
|
193
194
|
2. **`kb_init` 不再建 `facts/`**。已有的 `facts/` 不会被动,`kb_read`/`kb_search` 照常;只是新库默认只有
|
|
194
195
|
`doc/ lessons/ todo/ index/`,事实改写成 `<tscl>` 定义放进它所属的 `doc/` 里。
|
|
195
196
|
|
|
196
|
-
老库要不要迁?**不迁也能用**,只是 `kb_diag` 会给每条裸引用一条 warning
|
|
197
|
+
老库要不要迁?**不迁也能用**,只是 `kb_diag` 会给每条裸引用一条 warning。要迁就让 Agent 按
|
|
198
|
+
`kb_diag` 的清单逐条改成 `[[node="…"]]` / `[[tscl="…"]]`(只改写法,不动 TID 与文件):
|
|
197
199
|
|
|
198
200
|
```text
|
|
199
|
-
|
|
200
|
-
|
|
201
|
+
kb_diag # 列出每条 `旧语法引用`
|
|
202
|
+
kb_write mode=node target=<TID> content=… # 逐条改,或直接改 md 再 kb_diag 复验
|
|
201
203
|
```
|
|
202
204
|
|
|
203
|
-
迁移只改写法(`[[doc:x]]` → `[[node="doc:x"]]`,命中定义则 `[[tscl="…"]]`),不动任何 TID、不搬文件,
|
|
204
|
-
所以引用关系与渲染结果都不变;围栏代码块内一律跳过。
|
|
205
|
-
|
|
206
205
|
## 自检(不装 DSH 也能跑)
|
|
207
206
|
|
|
208
207
|
```bash
|
|
209
|
-
node scripts/check.mjs #
|
|
208
|
+
node scripts/check.mjs # 280 项:清单契约 / 工具 / 路由 / 客户端槽位 / 改名改写 / 建库写库 / tscl / 提示词段落 / 随包技能
|
|
210
209
|
node test/parity.mjs # 与 Python 原实现逐条对拍(需要 python 与 nodekb 源码)
|
|
211
210
|
```
|
|
212
211
|
|
package/README_EN.md
CHANGED
|
@@ -101,13 +101,14 @@ The right column renders at most 10,000 characters per view (with a truncation n
|
|
|
101
101
|
| `kb_init` | Scaffold `.nodekb` (idempotent): `doc/ lessons/ todo/ index/` plus placement conventions |
|
|
102
102
|
| `kb_write` | Write into the library: `append` / `replace` / `create` / `node` (rewrite one `<node>` block in place); validates before writing, self-checks after |
|
|
103
103
|
| `kb_rename` | Rename a file and rewrite every reference pointing at it; re-reads before writing, so concurrent edits are never clobbered |
|
|
104
|
-
| `kb_migrate` | Rewrites legacy bare refs `[[x]]` into `[[node="x"]]` / `[[tscl="x"]]`; **dry-run by default**, pass `dry_run=false` to write |
|
|
105
104
|
| `kb_vcs_status` | Snapshot history and uncommitted changes of the library's embedded git repo (explains itself when git is missing) |
|
|
106
105
|
|
|
107
106
|
## How a library is located
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
Every `kb_*` tool takes exactly one location parameter, `kb`, which accepts **either a name or a path**:
|
|
109
|
+
|
|
110
|
+
1. `kb` = a path (a skill directory or its `.nodekb`, absolute or workspace-relative) — must fall inside the workspace or a registered library.
|
|
111
|
+
2. `kb` = a name (from a registry, or discovered in the workspace, e.g. a skill name).
|
|
111
112
|
3. Neither — the workspace's own `.nodekb/` → the single library discovered in the workspace → the single registered library.
|
|
112
113
|
|
|
113
114
|
**Discovery**: given a workspace, it looks beyond the root at conventional locations (bounded BFS, depth 2 by default):
|
|
@@ -176,21 +177,18 @@ readable after the plugin is removed. Only two things changed:
|
|
|
176
177
|
`<tscl>` definitions inside the `doc/` file they belong to.
|
|
177
178
|
|
|
178
179
|
Do old libraries need migrating? **No** — they keep working, `kb_diag` just emits one warning per
|
|
179
|
-
bare reference. To migrate
|
|
180
|
+
bare reference. To migrate, have the agent walk the `kb_diag` list and fix each one (syntax only, no
|
|
181
|
+
TID or file changes):
|
|
180
182
|
|
|
181
183
|
```text
|
|
182
|
-
|
|
183
|
-
|
|
184
|
+
kb_diag # lists every `旧语法引用` (legacy bare ref)
|
|
185
|
+
kb_write mode=node target=<TID> content=… # fix one at a time, or edit the md and re-run kb_diag
|
|
184
186
|
```
|
|
185
187
|
|
|
186
|
-
Migration only changes syntax (`[[doc:x]]` → `[[node="doc:x"]]`, or `[[tscl="…"]]` when the id is a
|
|
187
|
-
definition). No TID changes, no file moves, so references and rendered output stay identical; fenced
|
|
188
|
-
code blocks are skipped.
|
|
189
|
-
|
|
190
188
|
## Self-check (no DSH required)
|
|
191
189
|
|
|
192
190
|
```bash
|
|
193
|
-
node scripts/check.mjs #
|
|
191
|
+
node scripts/check.mjs # 280 checks: manifest contract / tools / routes / client slots / rename rewriting / scaffold + write / tscl / prompt section / bundled skill
|
|
194
192
|
node test/parity.mjs # case-by-case parity against the Python implementation (needs python and nodekb sources)
|
|
195
193
|
```
|
|
196
194
|
|
package/lib/engine/registry.js
CHANGED
|
@@ -194,6 +194,11 @@ export function discoverLibraries(root, { depth = 2, budget = 300, limit = 60, e
|
|
|
194
194
|
*/
|
|
195
195
|
export function resolveLibrary({ kb, lib, workspace, config } = {}) {
|
|
196
196
|
const ws = path.resolve(workspace || process.cwd())
|
|
197
|
+
// kb 同时接受「库名」与「路径」:带分隔符 / 绝对路径 / 指向 .nodekb 的一律当路径。
|
|
198
|
+
const raw = kb === undefined || kb === null ? '' : String(kb).trim()
|
|
199
|
+
const looksLikePath = !!raw && (path.isAbsolute(raw) || /[\\/]/.test(raw) || /\.nodekb$/i.test(raw))
|
|
200
|
+
const explicit = lib || (looksLikePath ? raw : undefined)
|
|
201
|
+
const kbName = looksLikePath ? undefined : raw
|
|
197
202
|
const exclude = config && config.exclude
|
|
198
203
|
const discover = () => discoverLibraries(ws, { exclude })
|
|
199
204
|
const registries = loadRegistries(
|
|
@@ -202,10 +207,10 @@ export function resolveLibrary({ kb, lib, workspace, config } = {}) {
|
|
|
202
207
|
)
|
|
203
208
|
|
|
204
209
|
// 1) 显式路径
|
|
205
|
-
if (
|
|
206
|
-
const p = path.resolve(ws,
|
|
210
|
+
if (explicit) {
|
|
211
|
+
const p = path.resolve(ws, explicit)
|
|
207
212
|
const nodekb = path.basename(p) === '.nodekb' ? p : path.join(p, '.nodekb')
|
|
208
|
-
if (!isDir(nodekb)) throw new Error(`库目录不存在: ${nodekb}(你传入: ${
|
|
213
|
+
if (!isDir(nodekb)) throw new Error(`库目录不存在: ${nodekb}(你传入: ${explicit})`)
|
|
209
214
|
const allowed = [ws, ...[...registries.values()].map((e) => e.path)]
|
|
210
215
|
if (!allowed.some((root) => isInside(nodekb, root))) {
|
|
211
216
|
throw new Error(
|
|
@@ -217,7 +222,7 @@ export function resolveLibrary({ kb, lib, workspace, config } = {}) {
|
|
|
217
222
|
}
|
|
218
223
|
|
|
219
224
|
// 2) 名字
|
|
220
|
-
const name =
|
|
225
|
+
const name = kbName
|
|
221
226
|
if (name) {
|
|
222
227
|
const entry = registries.get(name)
|
|
223
228
|
if (entry) {
|
|
@@ -234,7 +239,7 @@ export function resolveLibrary({ kb, lib, workspace, config } = {}) {
|
|
|
234
239
|
if (wsLib) return wsLib
|
|
235
240
|
}
|
|
236
241
|
const names = [...registries.keys(), ...discover().map((r) => r.name)]
|
|
237
|
-
throw new Error(`未注册的库: ${name}(可用: ${names.join(', ') || '无'}
|
|
242
|
+
throw new Error(`未注册的库: ${name}(可用: ${names.join(', ') || '无'};kb 也可以直接传库目录路径)`)
|
|
238
243
|
}
|
|
239
244
|
|
|
240
245
|
// 3) 无参:工作区自带 → 工作区里唯一发现 → 唯一的注册表库
|
package/lib/host/prompt.js
CHANGED
|
@@ -21,7 +21,7 @@ export const DEFAULT_PROMPT = [
|
|
|
21
21
|
'- 读库:先 `kb_read mode=toc` 看结构(几百 token),再按 TID 读一支;找不到就 `kb_search`。别一次性把整库读进上下文。',
|
|
22
22
|
'- 改名前先 `kb_backlinks`,改名用 `kb_rename`(会重写全库引用并跳过被并发改过的文件)。',
|
|
23
23
|
'- 引用分两种,别混:**结构**用 `[[node="doc:03-build:3.1"]]`(只出一个链接,不复制正文);**值只写一处**——定义 `<tscl id="fact:abi">…</tscl>`,各处写 `[[tscl="fact:abi"]]`,同一 id 全库只能定义一次。',
|
|
24
|
-
'- 旧写法 `[[TID]]` 仍能用但 `kb_diag` 会 warning,2.0
|
|
24
|
+
'- 旧写法 `[[TID]]` 仍能用但 `kb_diag` 会 warning,2.0 移除(老库按 diag 清单逐处改成 `[[node=…]]` / `[[tscl=…]]`)。',
|
|
25
25
|
].join('\n')
|
|
26
26
|
|
|
27
27
|
function textOf(config) {
|
package/lib/host/routes.js
CHANGED
|
@@ -12,7 +12,6 @@ import { search, formatSearch } from '../engine/search.js'
|
|
|
12
12
|
import { diag } from '../engine/diag.js'
|
|
13
13
|
import { extractRefs } from '../engine/parse.js'
|
|
14
14
|
import { renameFileNode, formatRename } from '../engine/rename.js'
|
|
15
|
-
import { migrateLibrary, formatMigrate } from '../engine/migrate.js'
|
|
16
15
|
import { openLibrary, knownLibraries, whereInfo, workspaceOf } from './runtime.js'
|
|
17
16
|
import { promptState } from './prompt.js'
|
|
18
17
|
import { skillState } from './skill.js'
|
|
@@ -280,14 +279,6 @@ export function registerRoutes(ctx, config) {
|
|
|
280
279
|
return
|
|
281
280
|
}
|
|
282
281
|
|
|
283
|
-
if (url.pathname === `${PREFIX}/migrate`) {
|
|
284
|
-
const lib = open()
|
|
285
|
-
const dryRun = !(q.get('dry_run') === '0' || q.get('dry_run') === 'false')
|
|
286
|
-
const report = migrateLibrary(lib, { dryRun })
|
|
287
|
-
send(res, 200, { kb: lib.name, ...report, text: formatMigrate(report) })
|
|
288
|
-
return
|
|
289
|
-
}
|
|
290
|
-
|
|
291
282
|
send(res, 404, { error: `未知端点: ${url.pathname}` })
|
|
292
283
|
} catch (e) {
|
|
293
284
|
send(res, 400, { error: e && e.message ? e.message : String(e) })
|
package/lib/host/tools.js
CHANGED
|
@@ -14,7 +14,6 @@ import { search, formatSearch } from '../engine/search.js'
|
|
|
14
14
|
import { diag } from '../engine/diag.js'
|
|
15
15
|
import { extractRefs } from '../engine/parse.js'
|
|
16
16
|
import { renameFileNode, formatRename } from '../engine/rename.js'
|
|
17
|
-
import { migrateLibrary, formatMigrate } from '../engine/migrate.js'
|
|
18
17
|
import { scaffold } from '../engine/scaffold.js'
|
|
19
18
|
import { writeInto } from '../engine/write.js'
|
|
20
19
|
import { isInside } from '../engine/registry.js'
|
|
@@ -31,11 +30,7 @@ export function toolState() {
|
|
|
31
30
|
|
|
32
31
|
const KB_PARAM = {
|
|
33
32
|
type: 'string',
|
|
34
|
-
description: '
|
|
35
|
-
}
|
|
36
|
-
const LIB_PARAM = {
|
|
37
|
-
type: 'string',
|
|
38
|
-
description: '库根路径的备用写法:技能目录或其下的 .nodekb 绝对路径(也可相对工作区)。仅当库没有注册名时使用。',
|
|
33
|
+
description: '库:名字(注册表/工作区里的名字)或目录路径(技能目录或其 .nodekb)。省略则用工作区默认库。',
|
|
39
34
|
}
|
|
40
35
|
|
|
41
36
|
function text(t) {
|
|
@@ -61,7 +56,7 @@ function allowedRoots(config, exec) {
|
|
|
61
56
|
/** 库定位失败时补一句「有哪些库可用」,别让模型瞎猜 */
|
|
62
57
|
function locate(config, exec, args) {
|
|
63
58
|
try {
|
|
64
|
-
return openLibrary(config, exec, { kb: args.kb
|
|
59
|
+
return openLibrary(config, exec, { kb: args.kb })
|
|
65
60
|
} catch (e) {
|
|
66
61
|
const hint = libraryHint(config, exec)
|
|
67
62
|
const err = new Error(`${e.message}${hint ? `\n${hint}` : ''}`)
|
|
@@ -101,17 +96,13 @@ export function buildTools(ctx, config) {
|
|
|
101
96
|
tools.push({
|
|
102
97
|
name: 'kb_read',
|
|
103
98
|
description:
|
|
104
|
-
'
|
|
105
|
-
'
|
|
106
|
-
'toc = 只看结构不看正文(每个节点一行:tid · 标题 · 字符数,适合先定位再读);' +
|
|
107
|
-
'node = 只读本节点自身(不含子节点,也不展开子节);raw = 元信息 + 原始内容(含未展开的 [[TID]])。' +
|
|
108
|
-
'比 read 工具好在:一次拿到装配好的上下文,且 TID 不受文件行号漂移影响。超过 maxlen 会报错并提示改用 toc/node。',
|
|
99
|
+
'读知识库的一个节点。target = TID 或其唯一前缀(空/"." = 库根)。mode: subtree(默认,整支投影)| toc(只看结构,' +
|
|
100
|
+
'先定位再读)| node(只本节点)| raw(元信息+原文)。比 read 好在 TID 不随行号漂移;超 maxlen 会报错并提示换 toc/node。',
|
|
109
101
|
parameters: {
|
|
110
102
|
type: 'object',
|
|
111
103
|
properties: {
|
|
112
104
|
target: { type: 'string', description: '节点 TID 或其唯一前缀。空字符串或 "." 表示库根。' },
|
|
113
105
|
kb: KB_PARAM,
|
|
114
|
-
lib: LIB_PARAM,
|
|
115
106
|
mode: {
|
|
116
107
|
type: 'string',
|
|
117
108
|
enum: ['subtree', 'node', 'toc', 'raw'],
|
|
@@ -174,15 +165,13 @@ export function buildTools(ctx, config) {
|
|
|
174
165
|
tools.push({
|
|
175
166
|
name: 'kb_search',
|
|
176
167
|
description:
|
|
177
|
-
'
|
|
178
|
-
'
|
|
179
|
-
'用 kb_search 找到 tid,再用 kb_read 读,比 grep 后逐个 read 省往返。',
|
|
168
|
+
'检索知识库(中文 2-gram + 多词加权 + 子串兜底)。返回稳定 TID、类型、标题、源行、片段。' +
|
|
169
|
+
'先 kb_search 拿 tid 再 kb_read,比 grep 后逐个 read 省往返。',
|
|
180
170
|
parameters: {
|
|
181
171
|
type: 'object',
|
|
182
172
|
properties: {
|
|
183
173
|
query: { type: 'string', description: '检索词或自然语言问句。' },
|
|
184
174
|
kb: KB_PARAM,
|
|
185
|
-
lib: LIB_PARAM,
|
|
186
175
|
type: { type: 'string', description: '只保留该类型的节点(如 doc / todo / handover)。' },
|
|
187
176
|
limit: { type: 'integer', description: '返回条数上限,默认 10。' },
|
|
188
177
|
offset: { type: 'integer', description: '跳过前 N 条,用于翻页。' },
|
|
@@ -215,14 +204,12 @@ export function buildTools(ctx, config) {
|
|
|
215
204
|
tools.push({
|
|
216
205
|
name: 'kb_diag',
|
|
217
206
|
description:
|
|
218
|
-
'
|
|
219
|
-
'warnings =
|
|
220
|
-
'改完 md 之后跑一次,比人工核对可靠。库根可放 diag_ignore.json 豁免样板块。',
|
|
207
|
+
'结构自检。errors = 真损坏(悬空/位置型引用、循环、孤儿、树环、重复 TID、tscl 定义问题);' +
|
|
208
|
+
'warnings = 疑似漂移(同一段文字出现在多处),默认不算失败。写完 md 就跑一次。',
|
|
221
209
|
parameters: {
|
|
222
210
|
type: 'object',
|
|
223
211
|
properties: {
|
|
224
212
|
kb: KB_PARAM,
|
|
225
|
-
lib: LIB_PARAM,
|
|
226
213
|
strict: { type: 'boolean', description: '严格模式:把 warnings 也算失败。' },
|
|
227
214
|
},
|
|
228
215
|
},
|
|
@@ -247,14 +234,13 @@ export function buildTools(ctx, config) {
|
|
|
247
234
|
tools.push({
|
|
248
235
|
name: 'kb_backlinks',
|
|
249
236
|
description:
|
|
250
|
-
'
|
|
251
|
-
'比全库 grep
|
|
237
|
+
'反查:谁引用了这个 id(`[[node=…]]`/`[[tscl=…]]`/旧式 `[[TID]]` 都算)。改 TID 或删节点前先跑,' +
|
|
238
|
+
'比全库 grep 准——只认真实引用语法。',
|
|
252
239
|
parameters: {
|
|
253
240
|
type: 'object',
|
|
254
241
|
properties: {
|
|
255
242
|
target: { type: 'string', description: '节点 TID 或其唯一前缀。' },
|
|
256
243
|
kb: KB_PARAM,
|
|
257
|
-
lib: LIB_PARAM,
|
|
258
244
|
},
|
|
259
245
|
required: ['target'],
|
|
260
246
|
},
|
|
@@ -283,9 +269,8 @@ export function buildTools(ctx, config) {
|
|
|
283
269
|
tools.push({
|
|
284
270
|
name: 'kb_rename',
|
|
285
271
|
description:
|
|
286
|
-
'
|
|
287
|
-
'
|
|
288
|
-
'要么改名后自己跑 kb_diag 逐个修。写前会重读校验,别人刚改过的文件不覆盖、只报出来。',
|
|
272
|
+
'重命名 md 文件并改写全库指向它的引用(含子节点)。TID 由文件名派生,所以用 mv 改名必然留悬空引用;' +
|
|
273
|
+
'写前重读校验,别人刚改过的文件跳过不覆盖。',
|
|
289
274
|
parameters: {
|
|
290
275
|
type: 'object',
|
|
291
276
|
properties: {
|
|
@@ -293,7 +278,6 @@ export function buildTools(ctx, config) {
|
|
|
293
278
|
new_name: { type: 'string', description: '新文件名(.md 可省略,不能带目录)。' },
|
|
294
279
|
dry_run: { type: 'boolean', description: 'true = 只报「会改哪些引用」,不动文件。' },
|
|
295
280
|
kb: KB_PARAM,
|
|
296
|
-
lib: LIB_PARAM,
|
|
297
281
|
},
|
|
298
282
|
required: ['target', 'new_name'],
|
|
299
283
|
},
|
|
@@ -310,44 +294,15 @@ export function buildTools(ctx, config) {
|
|
|
310
294
|
},
|
|
311
295
|
})
|
|
312
296
|
|
|
313
|
-
// ── kb_migrate ───────────────────────────────────────────────────────────
|
|
314
|
-
tools.push({
|
|
315
|
-
name: 'kb_migrate',
|
|
316
|
-
description:
|
|
317
|
-
'把库里的旧写法裸引用 [[x]] 迁成 1.1 的双语法:[[tscl="x"]](x 是定义)或 [[node="x"]](x 是节点 TID)。' +
|
|
318
|
-
'默认只演练(dry_run 缺省为 true),返回每个文件会改几处、哪些引用指向不存在的 id 会原样保留。' +
|
|
319
|
-
'围栏代码块内不动;写前重读校验,别人刚改过的文件不覆盖。',
|
|
320
|
-
parameters: {
|
|
321
|
-
type: 'object',
|
|
322
|
-
properties: {
|
|
323
|
-
dry_run: { type: 'boolean', description: '默认 true(只报告)。确认无误后传 false 才真正写盘。' },
|
|
324
|
-
kb: KB_PARAM,
|
|
325
|
-
lib: LIB_PARAM,
|
|
326
|
-
},
|
|
327
|
-
},
|
|
328
|
-
output: {
|
|
329
|
-
schema: {},
|
|
330
|
-
render: (_args, value) => text(value.text),
|
|
331
|
-
},
|
|
332
|
-
async execute(args, exec) {
|
|
333
|
-
const lib = locate(config, exec, args)
|
|
334
|
-
const dryRun = args.dry_run === undefined ? true : Boolean(args.dry_run)
|
|
335
|
-
const report = migrateLibrary(lib, { dryRun })
|
|
336
|
-
return { ...report, kb: lib.name, text: warnPrefix(lib) + formatMigrate(report) }
|
|
337
|
-
},
|
|
338
|
-
})
|
|
339
|
-
|
|
340
297
|
// ── kb_vcs_status ────────────────────────────────────────────────────────
|
|
341
298
|
tools.push({
|
|
342
299
|
name: 'kb_vcs_status',
|
|
343
300
|
description:
|
|
344
|
-
'
|
|
345
|
-
'所以这里能看到「库的改动历史」;文件系统上有改动但没进快照时也会列出来。git 不可用时返回说明而不报错。',
|
|
301
|
+
'库内嵌 git 的快照历史与未提交改动(每次解析后自动快照);git 不可用时只说明不报错。',
|
|
346
302
|
parameters: {
|
|
347
303
|
type: 'object',
|
|
348
304
|
properties: {
|
|
349
305
|
kb: KB_PARAM,
|
|
350
|
-
lib: LIB_PARAM,
|
|
351
306
|
},
|
|
352
307
|
},
|
|
353
308
|
output: {
|
|
@@ -393,11 +348,8 @@ export function buildTools(ctx, config) {
|
|
|
393
348
|
tools.push({
|
|
394
349
|
name: 'kb_init',
|
|
395
350
|
description:
|
|
396
|
-
'
|
|
397
|
-
'
|
|
398
|
-
'它一次铺出 doc/ facts/ lessons/ todo/ index/entry.md 与 .nodekb/README.md 里的落位约定,' +
|
|
399
|
-
'比你自己拿 write 从零设计目录结构更省事,也让后续 Agent 知道该往哪写。' +
|
|
400
|
-
'已经有 .nodekb/ 就不要调(先用 kb_list 看)。',
|
|
351
|
+
'建 .nodekb 骨架(幂等,已有的不覆盖):doc/ lessons/ todo/ index/entry.md + 落位约定。' +
|
|
352
|
+
'准备手写 docs/*.md 之前先调这个;已经有 .nodekb/ 就别调(先 kb_list 看)。',
|
|
401
353
|
parameters: {
|
|
402
354
|
type: 'object',
|
|
403
355
|
properties: {
|
|
@@ -431,10 +383,8 @@ export function buildTools(ctx, config) {
|
|
|
431
383
|
tools.push({
|
|
432
384
|
name: 'kb_write',
|
|
433
385
|
description:
|
|
434
|
-
'
|
|
435
|
-
'
|
|
436
|
-
'直接用 write/edit 没有这一层。' +
|
|
437
|
-
'mode=append 追加到文件(不存在则创建)、replace 整篇覆盖、create 只新建、node 就地改写一个 <node> 块。',
|
|
386
|
+
'写库并立刻自检(悬空引用/重复定义/结构错误当场报回来)。写前校验 TID 与 <node> 独占一行;' +
|
|
387
|
+
'用 write/edit 直接改没有这层。mode=append(追加,不存在则建)| replace(整篇)| create(只新建)| node(就地改一个 <node> 块)。',
|
|
438
388
|
parameters: {
|
|
439
389
|
type: 'object',
|
|
440
390
|
properties: {
|
|
@@ -449,7 +399,6 @@ export function buildTools(ctx, config) {
|
|
|
449
399
|
description: '默认:路径→append,TID→node。create 遇到已存在会报错,避免覆盖。',
|
|
450
400
|
},
|
|
451
401
|
kb: KB_PARAM,
|
|
452
|
-
lib: LIB_PARAM,
|
|
453
402
|
},
|
|
454
403
|
required: ['target', 'content'],
|
|
455
404
|
},
|
package/package.json
CHANGED
|
@@ -55,7 +55,7 @@ kb_search query=... # toc 里找不到再搜
|
|
|
55
55
|
- 定义块内部的 `##` **不会**变成节点——它只是文本。
|
|
56
56
|
|
|
57
57
|
旧写法 `[[TID]]` 仍能解析(有定义时当宏、否则当链接),但 `kb_diag` 会 warning,2.0 移除。
|
|
58
|
-
|
|
58
|
+
接手老库时按 `kb_diag` 的「旧语法引用」清单逐处改成 `[[node=…]]` 或 `[[tscl=…]]`,改完再跑一次 `kb_diag`。
|
|
59
59
|
|
|
60
60
|
## TID 规则
|
|
61
61
|
|
package/lib/engine/migrate.js
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* migrate —— 把旧写法裸引用 `[[x]]` 迁成 1.1 的双语法。
|
|
3
|
-
*
|
|
4
|
-
* 规则:
|
|
5
|
-
* - x 是 tscl 定义 id → `[[tscl="x"]]`(值嵌入)
|
|
6
|
-
* - x 是节点 TID → `[[node="x"]]`(链接)
|
|
7
|
-
* - 两者都不是 → 原样保留,记进 unresolved(本来就悬空,迁移不该猜)
|
|
8
|
-
*
|
|
9
|
-
* 三条纪律与 rename 一致:围栏内不动、写前重读复核 mtime/size、默认只演练(dry_run)。
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import fs from 'node:fs'
|
|
13
|
-
import path from 'node:path'
|
|
14
|
-
import { ROOT_TID } from './mdlib.js'
|
|
15
|
-
|
|
16
|
-
const FENCE = '```'
|
|
17
|
-
const BARE_REF = /\[\[([^[\]"=\s][^[\]]*)\]\]/g
|
|
18
|
-
|
|
19
|
-
function absOf(lib, node) {
|
|
20
|
-
const raw = String(node.source || '').split('#')[0]
|
|
21
|
-
if (!raw) return ''
|
|
22
|
-
return path.isAbsolute(raw) ? path.normalize(raw) : path.join(lib.nodekb, raw)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function relOf(lib, abs) {
|
|
26
|
-
return path.relative(lib.nodekb, abs).split(path.sep).join('/')
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function allMarkdown(nodekb) {
|
|
30
|
-
const out = []
|
|
31
|
-
const walk = (d) => {
|
|
32
|
-
let entries
|
|
33
|
-
try {
|
|
34
|
-
entries = fs.readdirSync(d, { withFileTypes: true })
|
|
35
|
-
} catch {
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
for (const e of entries) {
|
|
39
|
-
if (e.name.startsWith('.')) continue
|
|
40
|
-
const p = path.join(d, e.name)
|
|
41
|
-
if (e.isDirectory()) walk(p)
|
|
42
|
-
else if (e.name.endsWith('.md')) out.push(p)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
walk(nodekb)
|
|
46
|
-
return out
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** 纯函数:把一段 md 文本里的裸引用改写(可单测) */
|
|
50
|
-
export function migrateText(text, { isTid, isDef }) {
|
|
51
|
-
const out = []
|
|
52
|
-
const hits = { node: 0, tscl: 0, unresolved: [] }
|
|
53
|
-
let fenced = false
|
|
54
|
-
for (const line of String(text).split('\n')) {
|
|
55
|
-
if (line.trim().startsWith(FENCE)) {
|
|
56
|
-
fenced = !fenced
|
|
57
|
-
out.push(line)
|
|
58
|
-
continue
|
|
59
|
-
}
|
|
60
|
-
if (fenced) {
|
|
61
|
-
out.push(line)
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
out.push(
|
|
65
|
-
line.replace(BARE_REF, (whole, inner) => {
|
|
66
|
-
const id = inner.trim()
|
|
67
|
-
if (isDef(id)) {
|
|
68
|
-
hits.tscl += 1
|
|
69
|
-
return `[[tscl="${id}"]]`
|
|
70
|
-
}
|
|
71
|
-
if (isTid(id)) {
|
|
72
|
-
hits.node += 1
|
|
73
|
-
return `[[node="${id}"]]`
|
|
74
|
-
}
|
|
75
|
-
hits.unresolved.push(id)
|
|
76
|
-
return whole
|
|
77
|
-
}),
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
return { text: out.join('\n'), hits }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* 迁移整个库。
|
|
85
|
-
* @param {{nodekb:string, nodes:Map, defs?:Map}} lib openLibrary 的结果
|
|
86
|
-
* @param {{dryRun?:boolean}} [opts] 默认演练
|
|
87
|
-
*/
|
|
88
|
-
export function migrateLibrary(lib, opts = {}) {
|
|
89
|
-
const dryRun = opts.dryRun !== false
|
|
90
|
-
const defs = lib.defs || new Map()
|
|
91
|
-
const nodes = lib.nodes
|
|
92
|
-
const isTid = (id) => nodes.has(id)
|
|
93
|
-
const isDef = (id) => defs.has(id)
|
|
94
|
-
|
|
95
|
-
const report = { dry_run: dryRun, files: [], node: 0, tscl: 0, unresolved: [], skipped: [] }
|
|
96
|
-
const edits = []
|
|
97
|
-
const visited = new Set()
|
|
98
|
-
|
|
99
|
-
for (const node of nodes.values()) {
|
|
100
|
-
if (node.kind !== 'file') continue
|
|
101
|
-
const abs = absOf(lib, node)
|
|
102
|
-
if (!abs) continue
|
|
103
|
-
visited.add(path.resolve(abs))
|
|
104
|
-
let text
|
|
105
|
-
let st1
|
|
106
|
-
try {
|
|
107
|
-
st1 = fs.statSync(abs)
|
|
108
|
-
text = fs.readFileSync(abs, 'utf8')
|
|
109
|
-
} catch (e) {
|
|
110
|
-
report.skipped.push({ path: relOf(lib, abs), why: `读失败: ${e.message}` })
|
|
111
|
-
continue
|
|
112
|
-
}
|
|
113
|
-
if (!text.includes('[[')) continue
|
|
114
|
-
const { text: next, hits } = migrateText(text, { isTid, isDef })
|
|
115
|
-
const changed = hits.node + hits.tscl
|
|
116
|
-
for (const u of hits.unresolved) report.unresolved.push({ path: relOf(lib, abs), ref: u })
|
|
117
|
-
if (!changed) continue
|
|
118
|
-
report.node += hits.node
|
|
119
|
-
report.tscl += hits.tscl
|
|
120
|
-
report.files.push({ path: relOf(lib, abs), count: changed, node: hits.node, tscl: hits.tscl })
|
|
121
|
-
edits.push({ abs, st1, next, path: relOf(lib, abs) })
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (!dryRun) {
|
|
125
|
-
for (const e of edits) {
|
|
126
|
-
try {
|
|
127
|
-
const st2 = fs.statSync(e.abs)
|
|
128
|
-
if (st2.mtimeMs !== e.st1.mtimeMs || st2.size !== e.st1.size) {
|
|
129
|
-
report.skipped.push({ path: e.path, why: '读后被改动,未覆盖' })
|
|
130
|
-
continue
|
|
131
|
-
}
|
|
132
|
-
fs.writeFileSync(e.abs, e.next, 'utf8')
|
|
133
|
-
} catch (err) {
|
|
134
|
-
report.skipped.push({ path: e.path, why: err.message })
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// 兜底:只报「存在但没进索引」的 md(解析失败的孤儿文件),不重复报已处理的
|
|
140
|
-
for (const abs of allMarkdown(lib.nodekb)) {
|
|
141
|
-
if (visited.has(path.resolve(abs))) continue
|
|
142
|
-
report.skipped.push({ path: relOf(lib, abs), why: '未被解析为目标节点,未处理' })
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return report
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function formatMigrate(report) {
|
|
149
|
-
const lines = []
|
|
150
|
-
lines.push(report.dry_run ? '(演练,未落盘;确认后传 dry_run=false 才写)' : '已写入')
|
|
151
|
-
const total = report.files.reduce((s, f) => s + f.count, 0)
|
|
152
|
-
if (!total) {
|
|
153
|
-
lines.push('没有需要迁移的裸引用')
|
|
154
|
-
} else {
|
|
155
|
-
lines.push(`共 ${total} 处裸引用 → ${report.tscl} 处 [[tscl="…"]]、${report.node} 处 [[node="…"]]`)
|
|
156
|
-
for (const f of report.files.slice(0, 20)) lines.push(` - ${f.path}: ${f.count}(tscl ${f.tscl} / node ${f.node})`)
|
|
157
|
-
if (report.files.length > 20) lines.push(` …另有 ${report.files.length - 20} 个文件`)
|
|
158
|
-
}
|
|
159
|
-
if (report.unresolved.length) {
|
|
160
|
-
lines.push(`保留未改 ${report.unresolved.length} 处(指向的 id 既不是节点也不是定义,本来悬空):`)
|
|
161
|
-
for (const u of report.unresolved.slice(0, 10)) lines.push(` - ${u.path}: [[${u.ref}]]`)
|
|
162
|
-
if (report.unresolved.length > 10) lines.push(` …另有 ${report.unresolved.length - 10} 处`)
|
|
163
|
-
}
|
|
164
|
-
if (report.skipped.length) {
|
|
165
|
-
lines.push(`跳过 ${report.skipped.length} 个文件:`)
|
|
166
|
-
for (const s of report.skipped.slice(0, 10)) lines.push(` - ${s.path}: ${s.why}`)
|
|
167
|
-
}
|
|
168
|
-
return lines.join('\n')
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export { ROOT_TID }
|