project-node-memory 1.1.2 → 1.1.4

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
@@ -104,7 +104,6 @@ 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
  ## 库怎么定位
@@ -184,10 +183,27 @@ config:
184
183
  同一 `tscl` id 全库只能定义一次,且不能与节点 TID 同名。
185
184
  旧写法 `[[TID]]` 仍兼容(有定义时当宏、否则当链接),`kb_diag` 会 warning,2.0 移除。
186
185
 
186
+ ## 从 1.0 升到 1.1
187
+
188
+ 数据格式没变——md 还是 md,TID 还是那些 TID,插件卸了照样能读。变化只有两条:
189
+
190
+ 1. **`[[x]]` 现在只出链接**,不再把被引用节点的正文复制进来(要复制原文请显式写 `<tscl>`)。
191
+ 好处是引用不再撑大渲染结果,位置型小节 `## 3.1` 也能点了。
192
+ 2. **`kb_init` 不再建 `facts/`**。已有的 `facts/` 不会被动,`kb_read`/`kb_search` 照常;只是新库默认只有
193
+ `doc/ lessons/ todo/ index/`,事实改写成 `<tscl>` 定义放进它所属的 `doc/` 里。
194
+
195
+ 老库要不要迁?**不迁也能用**,只是 `kb_diag` 会给每条裸引用一条 warning。要迁就让 Agent 按
196
+ `kb_diag` 的清单逐条改成 `[[node="…"]]` / `[[tscl="…"]]`(只改写法,不动 TID 与文件):
197
+
198
+ ```text
199
+ kb_diag # 列出每条 `旧语法引用`
200
+ kb_write mode=node target=<TID> content=… # 逐条改,或直接改 md 再 kb_diag 复验
201
+ ```
202
+
187
203
  ## 自检(不装 DSH 也能跑)
188
204
 
189
205
  ```bash
190
- node scripts/check.mjs # 275 项:清单契约 / 工具 / 路由 / 客户端槽位 / 改名改写 / 建库写库 / 提示词段落 / 随包技能
206
+ node scripts/check.mjs # 275 项:清单契约 / 工具 / 路由 / 客户端槽位 / 改名改写 / 建库写库 / tscl / 提示词段落 / 随包技能
191
207
  node test/parity.mjs # 与 Python 原实现逐条对拍(需要 python 与 nodekb 源码)
192
208
  ```
193
209
 
package/README_EN.md CHANGED
@@ -101,7 +101,6 @@ 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
@@ -163,10 +162,31 @@ as structure (a `##` there is not a heading); a `tscl` id may be defined exactly
163
162
  collide with a node TID. The old `[[TID]]` form still works (macro when a definition exists, link
164
163
  otherwise) with a `kb_diag` warning, and goes away in 2.0.
165
164
 
165
+ ## Upgrading from 1.0
166
+
167
+ The data format is unchanged — Markdown stays Markdown, TIDs stay the same, and the library remains
168
+ readable after the plugin is removed. Only two things changed:
169
+
170
+ 1. **`[[x]]` now renders a link only**; it no longer copies the referenced node's body into the
171
+ output (use an explicit `<tscl>` for that). References stop inflating rendered size, and
172
+ positional sections (`## 3.1`) become clickable.
173
+ 2. **`kb_init` no longer creates `facts/`.** Existing `facts/` folders are untouched and still
174
+ readable/searchable; new libraries get `doc/ lessons/ todo/ index/` only, and facts become
175
+ `<tscl>` definitions inside the `doc/` file they belong to.
176
+
177
+ Do old libraries need migrating? **No** — they keep working, `kb_diag` just emits one warning per
178
+ bare reference. To migrate, have the agent walk the `kb_diag` list and fix each one (syntax only, no
179
+ TID or file changes):
180
+
181
+ ```text
182
+ kb_diag # lists every `旧语法引用` (legacy bare ref)
183
+ kb_write mode=node target=<TID> content=… # fix one at a time, or edit the md and re-run kb_diag
184
+ ```
185
+
166
186
  ## Self-check (no DSH required)
167
187
 
168
188
  ```bash
169
- node scripts/check.mjs # 275 checks: manifest contract / tools / routes / client slots / rename rewriting / scaffold + write / prompt section / bundled skill
189
+ node scripts/check.mjs # 275 checks: manifest contract / tools / routes / client slots / rename rewriting / scaffold + write / tscl / prompt section / bundled skill
170
190
  node test/parity.mjs # case-by-case parity against the Python implementation (needs python and nodekb sources)
171
191
  ```
172
192
 
@@ -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 移除(老库可先跑 `kb_migrate` 演练,确认后 `dry_run=false` 批量迁)。',
24
+ '- 旧写法 `[[TID]]` 仍能用但 `kb_diag` 会 warning,2.0 移除(老库按 diag 清单逐处改成 `[[node=…]]` / `[[tscl=…]]`)。',
25
25
  ].join('\n')
26
26
 
27
27
  function textOf(config) {
@@ -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'
@@ -310,33 +309,6 @@ export function buildTools(ctx, config) {
310
309
  },
311
310
  })
312
311
 
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
312
  // ── kb_vcs_status ────────────────────────────────────────────────────────
341
313
  tools.push({
342
314
  name: 'kb_vcs_status',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-node-memory",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Project node knowledge base for DSH agents: markdown-as-database, stable TIDs, transclusion projection, structure self-check.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -55,7 +55,7 @@ kb_search query=... # toc 里找不到再搜
55
55
  - 定义块内部的 `##` **不会**变成节点——它只是文本。
56
56
 
57
57
  旧写法 `[[TID]]` 仍能解析(有定义时当宏、否则当链接),但 `kb_diag` 会 warning,2.0 移除。
58
- 接手老库时先跑 `kb_migrate`(默认只演练),看清每处会改成 `[[node=…]]` 还是 `[[tscl=…]]` 再 `dry_run=false`。
58
+ 接手老库时按 `kb_diag` 的「旧语法引用」清单逐处改成 `[[node=…]]` 或 `[[tscl=…]]`,改完再跑一次 `kb_diag`。
59
59
 
60
60
  ## TID 规则
61
61
 
@@ -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 }