@kira905/dsh-memory-toolkit 0.0.0-stage → 0.1.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.
Files changed (132) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +661 -0
  3. package/README.en.md +110 -0
  4. package/README.md +102 -2
  5. package/dsh-memory-local/FORK-BASE.json +24 -0
  6. package/dsh-memory-local/README.md +113 -0
  7. package/dsh-memory-local/build.mjs +101 -0
  8. package/dsh-memory-local/cordis.patch.yml +20 -0
  9. package/dsh-memory-local/deploy.mjs +306 -0
  10. package/dsh-memory-local/entity-status.mjs +75 -0
  11. package/dsh-memory-local/lib/bridge.mjs +108 -0
  12. package/dsh-memory-local/lib/host-adapter.js +1171 -0
  13. package/dsh-memory-local/lib/index.d.ts +128 -0
  14. package/dsh-memory-local/lib/index.js +944 -0
  15. package/dsh-memory-local/lib/injector.mjs +400 -0
  16. package/dsh-memory-local/lib/memory-read.mjs +116 -0
  17. package/dsh-memory-local/lib/module-gate.mjs +68 -0
  18. package/dsh-memory-local/lib/pointer.mjs +152 -0
  19. package/dsh-memory-local/lib/prod-gate.mjs +121 -0
  20. package/dsh-memory-local/lib/recall-log.mjs +118 -0
  21. package/dsh-memory-local/lib/recall.mjs +192 -0
  22. package/dsh-memory-local/lib/schema-migrate.mjs +365 -0
  23. package/dsh-memory-local/lib/user-message.mjs +32 -0
  24. package/dsh-memory-local/lib/vectors-read.mjs +110 -0
  25. package/dsh-memory-local/package.json +51 -0
  26. package/dsh-memory-local/restore.mjs +259 -0
  27. package/dsh-memory-local/rollback-injector-fix.mjs +166 -0
  28. package/dsh-memory-local/src/bridge.mjs +108 -0
  29. package/dsh-memory-local/src/host-adapter.js +1171 -0
  30. package/dsh-memory-local/src/index.d.ts +128 -0
  31. package/dsh-memory-local/src/index.js +944 -0
  32. package/dsh-memory-local/src/injector.mjs +400 -0
  33. package/dsh-memory-local/src/memory-read.mjs +116 -0
  34. package/dsh-memory-local/src/module-gate.mjs +68 -0
  35. package/dsh-memory-local/src/pointer.mjs +152 -0
  36. package/dsh-memory-local/src/recall-log.mjs +118 -0
  37. package/dsh-memory-local/src/recall.mjs +192 -0
  38. package/dsh-memory-local/src/schema-migrate.mjs +365 -0
  39. package/dsh-memory-local/src/user-message.mjs +32 -0
  40. package/dsh-memory-local/src/vectors-read.mjs +110 -0
  41. package/dsh-memory-local/test/fixtures-s2b.mjs +216 -0
  42. package/dsh-memory-local/test/probe-entity.mjs +170 -0
  43. package/dsh-memory-local/test/regression.mjs +270 -0
  44. package/dsh-memory-local/test/s2b.test.mjs +568 -0
  45. package/dsh-memory-local/test/schema-migrate.test.mjs +334 -0
  46. package/dsh-memory-local/test/test-adapter-converge.mjs +127 -0
  47. package/dsh-memory-local/test/visible-injections.test.mjs +239 -0
  48. package/dsh-memory-local/verify-after-restart.mjs +181 -0
  49. package/lib/deploy-guard.mjs +391 -0
  50. package/lib/guard-http.mjs +93 -0
  51. package/lib/pitfall-form-map.mjs +130 -0
  52. package/lib/pitfall-score.mjs +185 -0
  53. package/lib/session-file-name.mjs +160 -0
  54. package/lib/zstd-frames.mjs +78 -0
  55. package/memory-db-tools/README.md +68 -0
  56. package/memory-db-tools/_test/test-memory-scope.mjs +302 -0
  57. package/memory-db-tools/audit-analyze.mjs +352 -0
  58. package/memory-db-tools/audit-dump-ids.mjs +245 -0
  59. package/memory-db-tools/audit-export.mjs +270 -0
  60. package/memory-db-tools/audit-hit-probe.mjs +227 -0
  61. package/memory-db-tools/audit-ledger-probe.mjs +236 -0
  62. package/memory-db-tools/audit-persona-overlap.mjs +277 -0
  63. package/memory-db-tools/audit-struct-probe.mjs +213 -0
  64. package/memory-db-tools/audit-verify-unchanged.mjs +276 -0
  65. package/memory-db-tools/check-memory-db.mjs +8 -0
  66. package/memory-db-tools/dump-memory-db.mjs +12 -0
  67. package/memory-db-tools/lib/memory-scope.mjs +446 -0
  68. package/memory-db-tools/list-memory-summary.mjs +477 -0
  69. package/memory-db-tools/memory-identity-check.mjs +101 -0
  70. package/memory-db-tools/memory-schema-migrate.mjs +290 -0
  71. package/memory-db-tools/memory-scope-fixture.mjs +197 -0
  72. package/memory-db-tools/read-memories.mjs +26 -0
  73. package/memory-db-tools/read-memory-db.mjs +20 -0
  74. package/memory-db-tools/refine-memories.mjs +247 -0
  75. package/memory-db-tools/show-mem-times.mjs +22 -0
  76. package/memory-engine/README.md +70 -0
  77. package/memory-engine/bench/bench.mjs +221 -0
  78. package/memory-engine/cli/distill-rollback.mjs +86 -0
  79. package/memory-engine/cli/distill.mjs +156 -0
  80. package/memory-engine/cli/doc-index-rebuild.mjs +155 -0
  81. package/memory-engine/cli/doc-index.mjs +48 -0
  82. package/memory-engine/cli/domain-tags.mjs +251 -0
  83. package/memory-engine/cli/pitfall-hook.mjs +91 -0
  84. package/memory-engine/cli/recall.mjs +132 -0
  85. package/memory-engine/cli/selfcheck.mjs +57 -0
  86. package/memory-engine/cli/serve-start.mjs +247 -0
  87. package/memory-engine/cli/sweep.mjs +58 -0
  88. package/memory-engine/cli/sync.mjs +69 -0
  89. package/memory-engine/lib/cli-util.mjs +197 -0
  90. package/memory-engine/lib/config.mjs +135 -0
  91. package/memory-engine/lib/dirty-ledger.mjs +174 -0
  92. package/memory-engine/lib/distill-rollback.mjs +178 -0
  93. package/memory-engine/lib/distill.mjs +898 -0
  94. package/memory-engine/lib/doc-index-store.mjs +325 -0
  95. package/memory-engine/lib/doc-index.mjs +529 -0
  96. package/memory-engine/lib/doc-scan.mjs +271 -0
  97. package/memory-engine/lib/doc-summarize.mjs +305 -0
  98. package/memory-engine/lib/domain-tags.mjs +429 -0
  99. package/memory-engine/lib/embedder.mjs +242 -0
  100. package/memory-engine/lib/hashes.mjs +12 -0
  101. package/memory-engine/lib/memory-source.mjs +127 -0
  102. package/memory-engine/lib/pitfall-source.mjs +170 -0
  103. package/memory-engine/lib/policy-source.mjs +371 -0
  104. package/memory-engine/lib/recall.mjs +331 -0
  105. package/memory-engine/lib/selfcheck.mjs +131 -0
  106. package/memory-engine/lib/sweep.mjs +159 -0
  107. package/memory-engine/lib/sync.mjs +180 -0
  108. package/memory-engine/lib/tick-hook.mjs +218 -0
  109. package/memory-engine/lib/vectors-store.mjs +292 -0
  110. package/memory-engine/package-lock.json +1086 -0
  111. package/memory-engine/package.json +22 -0
  112. package/memory-engine/serve.mjs +171 -0
  113. package/memory-engine/test/01-store.test.mjs +127 -0
  114. package/memory-engine/test/02-sync.test.mjs +196 -0
  115. package/memory-engine/test/03-sweep.test.mjs +145 -0
  116. package/memory-engine/test/04-recall.test.mjs +214 -0
  117. package/memory-engine/test/05-selfcheck.test.mjs +175 -0
  118. package/memory-engine/test/06-doc-index.test.mjs +928 -0
  119. package/memory-engine/test/07-domain-tags.test.mjs +298 -0
  120. package/memory-engine/test/08-distill.test.mjs +686 -0
  121. package/memory-engine/test/09-pitfall-source.test.mjs +365 -0
  122. package/memory-engine/test/10-tick-hook.test.mjs +252 -0
  123. package/memory-engine/test/11-policy-source.test.mjs +224 -0
  124. package/memory-engine/test/_tickhook-worker.mjs +56 -0
  125. package/memory-engine/test/fixtures.mjs +205 -0
  126. package/memory-engine/test/harness.mjs +73 -0
  127. package/memory-engine/test/run.mjs +35 -0
  128. package/memory-engine/tools/bridge-smoke.mjs +80 -0
  129. package/memory-engine/tools/fetch-model.mjs +128 -0
  130. package/memory-engine/tools/smoke-recall.mjs +76 -0
  131. package/package.json +53 -4
  132. package/persona-memory-audit.mjs +217 -0
@@ -0,0 +1,216 @@
1
+ /**
2
+ * FORK-EDIT(2026-09-21): S2b 单测夹具 —— 临时目录里的记忆库/向量库(**绝不落生产路径**)。
3
+ *
4
+ * 口径(家规「测试不得写生产路径」):一切路径来自 `fs.mkdtempSync(os.tmpdir())`,
5
+ * 由调用方显式传给被测模块(config.memoryDb / config.vectorsDb / config.logsDir)。
6
+ */
7
+ import fs from 'node:fs';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import crypto from 'node:crypto';
11
+ import { DatabaseSync } from 'node:sqlite';
12
+ // FORK-EDIT(2026-09-22): U3 adapter 接入 —— 夹具造注入事件用的事件名取自适配层唯一源(值恒等)。
13
+ import { EV } from '../src/host-adapter.js';
14
+
15
+ export const DIM = 512;
16
+
17
+ /** 确定性 stub 向量(char n-gram 哈希 → dim 维 + L2 归一化)。仅供单测,无任何语义质量。 */
18
+ export function stubVector(text, dim = DIM) {
19
+ const v = new Float32Array(dim);
20
+ const s = String(text ?? '').toLowerCase();
21
+ const grams = [];
22
+ for (let n = 1; n <= 3; n += 1) {
23
+ for (let i = 0; i + n <= s.length; i += 1) grams.push(s.slice(i, i + n));
24
+ }
25
+ for (const g of grams) {
26
+ const h = crypto.createHash('md5').update(g).digest();
27
+ const idx = ((h[0] << 8) | h[1]) % dim;
28
+ const sign = h[2] % 2 === 0 ? 1 : -1;
29
+ v[idx] += sign;
30
+ }
31
+ let norm = 0;
32
+ for (let i = 0; i < dim; i += 1) norm += v[i] * v[i];
33
+ norm = Math.sqrt(norm) || 1;
34
+ for (let i = 0; i < dim; i += 1) v[i] /= norm;
35
+ return v;
36
+ }
37
+
38
+ const MEMORY_DDL = `
39
+ CREATE TABLE IF NOT EXISTS memories (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ text TEXT NOT NULL,
42
+ tags TEXT NOT NULL DEFAULT '',
43
+ pinned INTEGER NOT NULL DEFAULT 0,
44
+ created_at INTEGER NOT NULL,
45
+ updated_at INTEGER NOT NULL
46
+ );
47
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
48
+ USING fts5(text, tags, content='memories', content_rowid='id');
49
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
50
+ INSERT INTO memories_fts (rowid, text, tags) VALUES (new.id, new.text, new.tags);
51
+ END;
52
+ `;
53
+
54
+ const VECTORS_DDL = `
55
+ CREATE TABLE IF NOT EXISTS vectors (
56
+ source TEXT NOT NULL, ref_id TEXT NOT NULL, model TEXT NOT NULL, dim INTEGER NOT NULL,
57
+ vec BLOB NOT NULL, text_hash TEXT NOT NULL, updated_at INTEGER NOT NULL,
58
+ PRIMARY KEY (source, ref_id)
59
+ );
60
+ CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL);
61
+ `;
62
+
63
+ /**
64
+ * 造一个隔离环境(临时目录 + 两侧库 + 记忆条目)。
65
+ * @param {object} [opts]
66
+ * @param {Array<{text:string,tags?:string,pinned?:boolean,updated_at?:number}>} [opts.memories]
67
+ * @param {boolean} [opts.makeVectors] 是否建 vectors.db(false = 模拟"向量库不存在")
68
+ * @param {boolean} [opts.makeMemory] 是否建 memory.db
69
+ * @param {boolean} [opts.writeAutonomy] 是否写 butler-autonomy.json
70
+ * @param {boolean} [opts.moduleEnabled] 模块位开关值
71
+ * @param {boolean} [opts.writeLogsDir] 是否预建 logs 目录
72
+ */
73
+ export function makeTempEnv(opts = {}) {
74
+ const {
75
+ memories = [],
76
+ makeVectors = true,
77
+ makeMemory = true,
78
+ writeAutonomy = true,
79
+ moduleEnabled = true,
80
+ writeLogsDir = true,
81
+ modelId = 'stub-ngram',
82
+ } = opts;
83
+
84
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'g24-s2b-'));
85
+ const home = path.join(root, '.dsh');
86
+ const memoryDb = path.join(home, 'memory', 'memory.db');
87
+ const vectorsDb = path.join(home, 'memory-vectors', 'vectors.db');
88
+ const logsDir = path.join(home, 'logs');
89
+ fs.mkdirSync(path.dirname(memoryDb), { recursive: true });
90
+ if (writeLogsDir) fs.mkdirSync(logsDir, { recursive: true });
91
+
92
+ const inserted = [];
93
+ if (makeMemory) {
94
+ const db = new DatabaseSync(memoryDb);
95
+ db.exec(MEMORY_DDL);
96
+ const stmt = db.prepare(
97
+ 'INSERT INTO memories (text, tags, pinned, created_at, updated_at) VALUES (?, ?, ?, ?, ?)',
98
+ );
99
+ memories.forEach((m, i) => {
100
+ const ts = m.updated_at ?? 1700000000000 + i * 86400000;
101
+ const r = stmt.run(String(m.text), String(m.tags ?? ''), m.pinned ? 1 : 0, ts, ts);
102
+ inserted.push({ id: Number(r.lastInsertRowid), text: String(m.text), tags: String(m.tags ?? ''), pinned: Boolean(m.pinned), updated_at: ts });
103
+ });
104
+ db.close();
105
+ }
106
+
107
+ if (makeVectors) {
108
+ fs.mkdirSync(path.dirname(vectorsDb), { recursive: true });
109
+ const vdb = new DatabaseSync(vectorsDb);
110
+ vdb.exec(VECTORS_DDL);
111
+ const ins = vdb.prepare(
112
+ 'INSERT INTO vectors (source, ref_id, model, dim, vec, text_hash, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
113
+ );
114
+ for (const rec of inserted) {
115
+ const vec = stubVector(rec.text);
116
+ ins.run(
117
+ 'memory',
118
+ String(rec.id),
119
+ modelId,
120
+ DIM,
121
+ Buffer.from(vec.buffer, vec.byteOffset, vec.byteLength),
122
+ crypto.createHash('sha256').update(rec.text).digest('hex').slice(0, 16),
123
+ rec.updated_at,
124
+ );
125
+ }
126
+ vdb.prepare('INSERT INTO meta (k, v) VALUES (?, ?)').run('schemaVersion', '1');
127
+ vdb.prepare('INSERT INTO meta (k, v) VALUES (?, ?)').run('modelId', modelId);
128
+ vdb.prepare('INSERT INTO meta (k, v) VALUES (?, ?)').run('dim', String(DIM));
129
+ vdb.close();
130
+ }
131
+
132
+ if (writeAutonomy) {
133
+ fs.writeFileSync(
134
+ path.join(home, 'butler-autonomy.json'),
135
+ `${JSON.stringify({ schemaVersion: 1, modules: { 'memory-recall': { enabled: Boolean(moduleEnabled), since: '2026-09-21T00:00:00.000Z', by: 'test', why: 's2b unittest' } } }, null, 2)}\n`,
136
+ 'utf8',
137
+ );
138
+ }
139
+
140
+ return {
141
+ root,
142
+ home,
143
+ memoryDb,
144
+ vectorsDb,
145
+ logsDir,
146
+ inserted,
147
+ cleanup: () => {
148
+ try {
149
+ fs.rmSync(root, { recursive: true, force: true });
150
+ } catch {
151
+ /* ignore */
152
+ }
153
+ },
154
+ };
155
+ }
156
+
157
+ /** 内存 logger(可注入;不落盘)。 */
158
+ export function memoryLogger() {
159
+ const lines = [];
160
+ return {
161
+ log: (obj) => {
162
+ lines.push(obj);
163
+ return true;
164
+ },
165
+ sweep: () => {},
166
+ path: '(memory)',
167
+ stats: () => ({ written: lines.length, errors: 0 }),
168
+ lines,
169
+ find: (event) => lines.filter((l) => l.event === event),
170
+ };
171
+ }
172
+
173
+ /** 结构化 stub 桥(可注入;可控 ready / 延迟 / 报错)。 */
174
+ export function stubBridge({ ready = true, delayMs = 0, throwOnEncode = null, dim = DIM } = {}) {
175
+ const calls = { probe: 0, encode: 0 };
176
+ return {
177
+ host: '127.0.0.1',
178
+ port: 8201,
179
+ kind: 'stub',
180
+ calls,
181
+ async probe() {
182
+ calls.probe += 1;
183
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
184
+ return ready
185
+ ? { ok: true, port: 8201, dim, model: 'stub-ngram', error: null }
186
+ : { ok: false, port: null, dim: 0, model: null, error: 'ECONNREFUSED' };
187
+ },
188
+ async health() {
189
+ return this.probe();
190
+ },
191
+ async encode(text) {
192
+ calls.encode += 1;
193
+ if (throwOnEncode) throw new Error(throwOnEncode);
194
+ if (delayMs > 0) await new Promise((r) => setTimeout(r, delayMs));
195
+ return stubVector(String(text), dim);
196
+ },
197
+ stats: () => ({ encodeCalls: calls.encode }),
198
+ };
199
+ }
200
+
201
+ /** 假 agent(surface 可见性测试用):nodes/events 结构照 dsh-session 形态。 */
202
+ export function fakeAgent(events) {
203
+ const list = events || [];
204
+ return {
205
+ id: 'test-session',
206
+ session: {
207
+ events: list,
208
+ surface: { nodes: list.map((_, i) => i) },
209
+ },
210
+ };
211
+ }
212
+
213
+ /** 造一条"注入消息"事件(形态与插件注入一致)。 */
214
+ export function injectionEvent(text, ids = [], seq = 0) {
215
+ return { seq, type: EV.USER_MESSAGE, data: { role: 'user', content: [{ type: 'text', text }], source: { kind: 'memory-recall', version: 1, items: ids } } };
216
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * probe-entity.mjs <entityDir> —— 在**部署位置**加载 dsh-memory 实体,用 mock ctx 断言
3
+ * 「插件注册出什么」:三工具(memory_write / memory_search / memory_forget)+ memory:recall 段。
4
+ *
5
+ * 为什么需要它:`node --check` 只查语法、静态扫只查 import 形态,而「插件被 DSH 加载后到底
6
+ * 注册了什么」只有真 import 一次真 apply 才知道。它同时是 deploy.mjs 的门禁③ 与
7
+ * verify-after-restart.mjs 的核心断言(同一支探针,两处口径一致)。
8
+ *
9
+ * 副作用:只在一个临时 SQLite 文件上跑 write/search/forget,跑完删掉;不碰任何 DSH home。
10
+ * 输出:stdout 末行 `PROBE: {json}`;退出码 0 = 全部断言通过,1 = 有断言失败。
11
+ *
12
+ * 用法:node test/probe-entity.mjs <entityDir>
13
+ */
14
+ import fs from 'node:fs'
15
+ import os from 'node:os'
16
+ import path from 'node:path'
17
+ import { pathToFileURL } from 'node:url'
18
+ // FORK-EDIT(2026-09-22): U3 adapter 接入 —— 断言里的服务名取自适配层唯一源;
19
+ // 由于两端同源,本断言变成**真可证伪**:适配层若把 SVC.tools 的值改错,这里立刻红。
20
+ import { SVC } from '../src/host-adapter.js'
21
+
22
+ const entityDir = process.argv[2]
23
+ // 默认断言「这是 fork 实体」;--upstream 用于拿上游原物做对照(回归脚本会用到)。
24
+ const mode = process.argv.includes('--upstream') ? 'upstream' : 'fork'
25
+ if (!entityDir) {
26
+ console.error('usage: node test/probe-entity.mjs <entityDir>')
27
+ process.exit(2)
28
+ }
29
+ const entityAbs = path.resolve(entityDir)
30
+ const entry = path.join(entityAbs, 'lib', 'index.js')
31
+ const failures = []
32
+ const check = (name, ok, detail = '') => {
33
+ if (!ok) failures.push(`${name}${detail ? ` — ${detail}` : ''}`)
34
+ return ok
35
+ }
36
+
37
+ const result = { entityDir: entityAbs, packageName: null, fork: null, tools: [], section: null, behavior: null }
38
+
39
+ try {
40
+ const pkgFile = path.join(entityAbs, 'package.json')
41
+ if (fs.existsSync(pkgFile)) {
42
+ const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8'))
43
+ result.packageName = pkg.name
44
+ result.packageVersion = pkg.version
45
+ result.fork = pkg.fork ?? null
46
+ }
47
+ check('lib/index.js exists', fs.existsSync(entry), entry)
48
+ if (mode === 'fork') {
49
+ check('package.json has a fork section', Boolean(result.fork), JSON.stringify(result.packageName))
50
+ check('fork section records the upstream base',
51
+ Boolean(result.fork?.base) && Boolean(result.fork?.baseSha256) && Boolean(result.fork?.libSha256),
52
+ JSON.stringify(result.fork))
53
+ } else {
54
+ check('entity is the untouched upstream package',
55
+ !result.fork && result.packageName === 'dsh-memory', JSON.stringify(result.packageName))
56
+ }
57
+
58
+ const mod = await import(pathToFileURL(entry).href)
59
+ check('module exports apply', typeof mod.apply === 'function')
60
+ check('module name === "memory"', mod.name === 'memory', String(mod.name))
61
+ check('inject includes tools + systemPrompt',
62
+ Array.isArray(mod.inject) && mod.inject.includes(SVC.tools) && mod.inject.includes(SVC.systemPrompt),
63
+ JSON.stringify(mod.inject))
64
+
65
+ // mock ctx:只实现插件真正用到的那几个面(tools.register / systemPrompt.section / effect / on)
66
+ // FORK-EDIT(2026-09-21 · S2b): 新增 `on`(`agent/pre-step` 监听)与 `memory_get` 断言。
67
+ const registered = []
68
+ const sections = []
69
+ const disposers = []
70
+ const listeners = []
71
+ const ctx = {
72
+ tools: { register: (tool) => { registered.push(tool); return () => {} } },
73
+ systemPrompt: { section: (section) => { sections.push(section); return () => {} } },
74
+ effect: (fn) => { const d = fn(); if (typeof d === 'function') disposers.push(d); return () => {} },
75
+ on: (event, handler, options) => { listeners.push({ event, handler, options }); return () => {} },
76
+ }
77
+
78
+ const dbFile = path.join(os.tmpdir(), `dsh-memory-probe-${process.pid}-${Date.now()}.db`)
79
+ // S2b:配置走插件自己的 schema(拿全量默认值),再把注入面全部指向临时目录
80
+ // ⇒ 探针不读也不写任何 DSH home(家规:测试不得落生产路径)。
81
+ const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-memory-probe-home-'))
82
+ const baseConfig = {
83
+ path: dbFile,
84
+ promptRecentCount: 10,
85
+ promptMaxChars: 2000,
86
+ maxTextChars: 2000,
87
+ searchLimitDefault: 10,
88
+ searchLimitMax: 50,
89
+ promptOrder: 50,
90
+ }
91
+ const config = typeof mod.Config === 'function'
92
+ ? mod.Config({
93
+ ...baseConfig,
94
+ recallEnabled: false, // 探针不跑注入链,只验注册面
95
+ recallHome: tmpHome,
96
+ recallMemoryDb: path.join(tmpHome, 'memory', 'memory.db'),
97
+ recallVectorsDb: path.join(tmpHome, 'memory-vectors', 'vectors.db'),
98
+ })
99
+ : baseConfig
100
+ result.configKeys = Object.keys(config).length
101
+ mod.apply(ctx, config)
102
+
103
+ result.tools = registered.map((t) => ({ name: t?.name, hasExecute: typeof t?.execute === 'function' }))
104
+ const names = result.tools.map((t) => t.name).sort()
105
+ // S2b:fork 版在官方三工具之外自注册 `memory_get`(指针指向的那扇门,方案 §3.4 关 B1);
106
+ // `--upstream` 模式拿上游原物对照,仍是官方三工具、也没有 pre-step 监听。
107
+ const expectedTools = mode === 'fork'
108
+ ? ['memory_forget', 'memory_get', 'memory_search', 'memory_write']
109
+ : ['memory_forget', 'memory_search', 'memory_write']
110
+ check(`registers the expected tools (${mode})`,
111
+ JSON.stringify(names) === JSON.stringify(expectedTools),
112
+ JSON.stringify(names))
113
+ check('every tool has execute()', result.tools.every((t) => t.hasExecute))
114
+ if (mode === 'fork') {
115
+ check('registers the agent/pre-step recall listener',
116
+ listeners.some((l) => l.event === 'agent/pre-step' && typeof l.handler === 'function'),
117
+ JSON.stringify(listeners.map((l) => l.event)))
118
+ check('the pre-step listener is prepended',
119
+ listeners.some((l) => l.event === 'agent/pre-step' && l.options && l.options.prepend === true),
120
+ JSON.stringify(listeners.map((l) => l.options ?? null)))
121
+ }
122
+
123
+ check('registers the memory:recall prompt section', sections.length === 1 && sections[0]?.name === 'memory:recall',
124
+ JSON.stringify(sections.map((s) => s?.name)))
125
+ const section = sections[0]
126
+ check('recall section exposes text()', typeof section?.text === 'function')
127
+
128
+ // 行为冒烟:write → search → get → forget(在临时库上)
129
+ const tool = (n) => registered.find((t) => t?.name === n)
130
+ const written = await tool('memory_write').execute({ text: 'probe: dsh-memory-local entity smoke', tags: ['Probe', 'probe'], pinned: false })
131
+ const found = await tool('memory_search').execute({ query: 'probe dsh-memory-local' })
132
+ // S2b:只读展开(指针的落点)
133
+ const got = tool('memory_get')
134
+ ? await tool('memory_get').execute({ id: written.id }, { agent: { id: 'probe-session' } })
135
+ : null
136
+ const forgotten = await tool('memory_forget').execute({ id: written.id })
137
+ const sectionTextOnEmpty = sections[0].text()
138
+ result.behavior = {
139
+ writeId: written.id, writeTags: written.tags, writePinned: written.pinned,
140
+ searchHits: found.matches.length, searchFirstId: found.matches[0]?.id ?? null,
141
+ gotText: got ? String(got.text).slice(0, 40) : null,
142
+ gotFound: got ? String(got.text).length > 0 : null,
143
+ forgotten: forgotten.forgotten,
144
+ sectionAfterForget: sectionTextOnEmpty === '' ? 'empty' : 'non-empty',
145
+ }
146
+ check('memory_write returned an id + normalized tags', Number.isInteger(written.id) && written.tags === 'probe',
147
+ JSON.stringify(written))
148
+ check('memory_search finds the freshly written fact', found.matches.length === 1 && found.matches[0].id === written.id,
149
+ JSON.stringify(found.matches.map((m) => m.id)))
150
+ if (mode === 'fork') {
151
+ check('memory_get expands the same record (read-only)',
152
+ Boolean(got) && got.id === written.id && String(got.text).includes('entity smoke') && got.truncated === false,
153
+ JSON.stringify(got))
154
+ check('memory_get reports a missing id instead of throwing',
155
+ JSON.stringify(await tool('memory_get').execute({ id: 999999 })) !== null)
156
+ }
157
+ check('memory_forget deletes it', forgotten.forgotten === true, JSON.stringify(forgotten))
158
+ check('recall section renders empty after the delete', sectionTextOnEmpty === '', JSON.stringify(sectionTextOnEmpty.slice(0, 60)))
159
+
160
+ for (const d of disposers) { try { d() } catch { /* dispose best-effort */ } }
161
+ for (const f of [dbFile, `${dbFile}-journal`]) { try { fs.rmSync(f, { force: true }) } catch { /* ignore */ } }
162
+ try { fs.rmSync(tmpHome, { recursive: true, force: true }) } catch { /* ignore */ }
163
+ } catch (error) {
164
+ failures.push(`threw: ${error instanceof Error ? error.message : String(error)}`)
165
+ }
166
+
167
+ result.ok = failures.length === 0
168
+ result.failures = failures
169
+ console.log(`PROBE: ${JSON.stringify(result)}`)
170
+ process.exit(result.ok ? 0 : 1)
@@ -0,0 +1,270 @@
1
+ /**
2
+ * regression.mjs —— fork 回归:**同输入、同输出**地对照上游实体与 fork 实体。
3
+ *
4
+ * 对照对象(各跑一遍完全相同的操作序列,全部在各自的临时 SQLite 上):
5
+ * · 模块面 :name / inject / 三工具的名字、描述、参数 schema、输出 render 文本
6
+ * · 行为面 :write(含 tags 归一化 / pinned)/ search(命中、空结果、非法 limit)/
7
+ * forget(命中、不存在)/ memory:recall 段文本 / 各错误消息
8
+ * · 汇总指纹 :把两次运行的观测序列各算一个 sha256,不相等即 FAIL 并打印首个差异
9
+ *
10
+ * 这是「三个官方工具行为一致」的机器判据(不花 token、不碰任何 DSH home、只用临时库)。
11
+ *
12
+ * 用法:
13
+ * node test/regression.mjs [--upstream <上游实体目录>] [--fork <fork实体或源码目录>]
14
+ * 默认:上游 = _archive 里 deploy 备份的 official-entity(其次本机生产 profile 实体,只读);
15
+ * fork = 本包(未部署时直接用源码 lib/)。
16
+ * 退出码:0 = 完全一致 / 1 = 存在差异 / 2 = 用法错误 / 3 = 找不到上游对照物。
17
+ */
18
+ import fs from 'node:fs'
19
+ import os from 'node:os'
20
+ import path from 'node:path'
21
+ import crypto from 'node:crypto'
22
+ import { pathToFileURL, fileURLToPath } from 'node:url'
23
+
24
+ const here = path.dirname(fileURLToPath(import.meta.url))
25
+ const pkgRoot = path.join(here, '..')
26
+ const args = process.argv.slice(2)
27
+ const argValue = (flag) => {
28
+ const i = args.indexOf(flag)
29
+ return i >= 0 && args[i + 1] ? args[i + 1] : null
30
+ }
31
+
32
+ function findUpstream() {
33
+ const explicit = argValue('--upstream')
34
+ if (explicit) return explicit
35
+ const archive = process.env.DSH_ARCHIVE || path.join(process.cwd(), '_archive')
36
+ if (fs.existsSync(archive)) {
37
+ const snaps = fs.readdirSync(archive)
38
+ .filter((n) => n.startsWith('g24-s2a-memory-deploy-'))
39
+ .map((n) => path.join(archive, n, 'official-entity'))
40
+ .filter((d) => fs.existsSync(path.join(d, 'lib', 'index.js')))
41
+ .sort()
42
+ if (snaps.length) return snaps.at(-1)
43
+ }
44
+ const production = path.join(os.homedir(), '.dsh', 'profiles', 'web', 'node_modules', 'dsh-memory')
45
+ if (fs.existsSync(path.join(production, 'lib', 'index.js'))) return production
46
+ return null
47
+ }
48
+
49
+ /**
50
+ * fork 侧默认取**隔离实例里的部署实体** —— 插件只能在「部署位置」被加载:
51
+ * 它的 peer 依赖(@deepseek-ai/schemastery、@deepseek-ai/dsh-tools)由 profile 的闭包提供,
52
+ * 源码目录(tools/dsh-memory-local)没有 node_modules,直接 import 会 ERR_MODULE_NOT_FOUND。
53
+ */
54
+ function findForkEntity() {
55
+ const explicit = argValue('--fork') ?? process.env.DSH_MEMORY_FORK_ENTITY ?? null
56
+ if (explicit) return explicit
57
+ const candidates = [
58
+ path.join(process.env.DSH_HOME || path.join(os.homedir(), '.dsh'), 'profiles', 'web', 'node_modules', 'dsh-memory'),
59
+ ]
60
+ for (const c of candidates) {
61
+ const pkgFile = path.join(c, 'package.json')
62
+ if (!fs.existsSync(pkgFile)) continue
63
+ try {
64
+ if (JSON.parse(fs.readFileSync(pkgFile, 'utf8')).fork) return c
65
+ } catch { /* not a dsh entity */ }
66
+ }
67
+ return null
68
+ }
69
+
70
+ /**
71
+ * 从 dir 向上找第一个「能解析到宿主闭包」的目录(判据 = node_modules/@deepseek-ai/schemastery 存在)。
72
+ * 插件只能在这样的位置被 import —— 源码目录与 _archive 都不满足,profile 实体满足。
73
+ */
74
+ function resolvableContext(dir) {
75
+ let cur = path.resolve(dir)
76
+ for (;;) {
77
+ if (fs.existsSync(path.join(cur, 'node_modules', '@deepseek-ai', 'schemastery'))) return cur
78
+ const parent = path.dirname(cur)
79
+ if (parent === cur) return null
80
+ cur = parent
81
+ }
82
+ }
83
+
84
+ const forkDir = findForkEntity()
85
+ const upstreamDir = findUpstream()
86
+ if (!forkDir) {
87
+ console.error('[regression] 找不到 fork 部署实体:先把 fork 部署到隔离实例(deploy.mjs --target <DSH_HOME>),')
88
+ console.error(' 或用 --fork <实体目录> / $DSH_MEMORY_FORK_ENTITY 显式指定部署位置。')
89
+ process.exit(2)
90
+ }
91
+ if (!upstreamDir) { console.error('[regression] 找不到上游对照实体:用 --upstream <dir> 显式指定'); process.exit(3) }
92
+
93
+ const fixtureText = 'regression fixture: DSH 记忆引擎 fork 基线(tags 归一化 + 中文)'
94
+ const pinnedText = 'pinned fixture: 每轮都要出现的事实'
95
+
96
+ /** 用 mock ctx 加载一个实体,跑固定序列,返回可比较的观测对象。 */
97
+ async function observe(label, dir) {
98
+ const obs = { label, dir, steps: [] }
99
+ const entry = path.join(dir, 'lib', 'index.js')
100
+ const mod = await import(pathToFileURL(entry).href)
101
+ obs.module = { name: mod.name, inject: mod.inject, hasApply: typeof mod.apply === 'function' }
102
+
103
+ const registered = []
104
+ const sections = []
105
+ const ctx = {
106
+ tools: { register: (t) => { registered.push(t); return () => {} } },
107
+ systemPrompt: { section: (s) => { sections.push(s); return () => {} } },
108
+ effect: (fn) => { const d = fn(); return () => { if (typeof d === 'function') d() } },
109
+ // FORK-EDIT(2026-09-21): S2b 起 fork 会注册 `agent/pre-step` 监听(指针注入)。
110
+ // 夹具给一个空监听口 —— 注入链本身由 s2b.test.mjs 覆盖,本对照只看三工具与段。
111
+ on: () => () => {},
112
+ }
113
+ const dbFile = path.join(os.tmpdir(), `dsh-memory-regression-${label}-${process.pid}-${Date.now()}.db`)
114
+ // FORK-EDIT(2026-09-21): S2b 起 fork 的 Config 多了 recall* 字段(指针注入,默认关)。
115
+ // 它们全部给默认值 ⇒ 两侧行为面不变;上游实体不认识这些键(schemastery 忽略),故可同一份夹具。
116
+ // `recallEnabled:false` = 注入链路不激活,对照的仍是「三工具 + memory:recall 段」本身。
117
+ const recallDefaults = {
118
+ recallEnabled: false, recallHome: '', recallMemoryDb: '', recallVectorsDb: '',
119
+ recallBridgeHost: '127.0.0.1', recallBridgePort: 8201, recallBridgePortScanMax: 10,
120
+ recallTimeoutMs: 300, recallEncodeTimeoutMs: 250, recallMaxItems: 8, recallMaxChars: 800,
121
+ recallMaxTokens: 600, recallMaxPerSession: 3, recallSummaryChars: 80, recallQueryMaxChars: 512,
122
+ recallVectorTopK: 20, recallFtsTopK: 20, recallRrfK: 60, recallResultTopK: 8, recallIndexTtlMs: 60000,
123
+ }
124
+ mod.apply(ctx, {
125
+ path: dbFile, promptRecentCount: 10, promptMaxChars: 2000, maxTextChars: 2000,
126
+ searchLimitDefault: 10, searchLimitMax: 50, promptOrder: 50,
127
+ ...recallDefaults,
128
+ })
129
+
130
+ const capture = async (step, fn) => {
131
+ try {
132
+ const value = await fn()
133
+ obs.steps.push({ step, ok: true, value })
134
+ } catch (error) {
135
+ obs.steps.push({ step, ok: false, error: error instanceof Error ? error.message : String(error) })
136
+ }
137
+ }
138
+
139
+ const tool = (n) => registered.find((t) => t?.name === n)
140
+ const describe = (t) => ({
141
+ name: t?.name,
142
+ description: t?.description,
143
+ parameters: t?.parameters,
144
+ outputSchema: t?.output?.schema,
145
+ hasRender: typeof t?.output?.render === 'function',
146
+ hasPresentCall: typeof t?.presentCall === 'function',
147
+ })
148
+ obs.toolShapes = registered.map(describe)
149
+ obs.sectionShapes = sections.map((s) => ({ name: s?.name, order: s?.order, hasText: typeof s?.text === 'function' }))
150
+
151
+ const render = (name, args, value) => tool(name).output.render(args, value)
152
+
153
+ await capture('write.fixture', () => tool('memory_write').execute({ text: fixtureText, tags: ['Reg', 'reg', '中文 标签'], pinned: false }))
154
+ await capture('write.pinned', () => tool('memory_write').execute({ text: pinnedText, tags: [], pinned: true }))
155
+ await capture('section.afterWrites', () => sections[0].text())
156
+ await capture('search.hit', async () => {
157
+ const value = await tool('memory_search').execute({ query: 'regression fixture' })
158
+ return { value, rendered: render('memory_search', { query: 'regression fixture' }, value) }
159
+ })
160
+ await capture('search.miss', async () => {
161
+ const value = await tool('memory_search').execute({ query: 'zzz-不存在的词-zzz' })
162
+ return { value, rendered: render('memory_search', { query: 'zzz-不存在的词-zzz' }, value) }
163
+ })
164
+ await capture('search.badLimit', () => tool('memory_search').execute({ query: 'fixture', limit: 0 }))
165
+ await capture('search.emptyQuery', () => tool('memory_search').execute({ query: ' ' }))
166
+ await capture('search.limitOverMax', () => tool('memory_search').execute({ query: 'fixture', limit: 999 }))
167
+ await capture('write.blank', () => tool('memory_write').execute({ text: ' ' }))
168
+ await capture('write.tooLong', () => tool('memory_write').execute({ text: 'x'.repeat(2001) }))
169
+ await capture('forget.missing', async () => {
170
+ const value = await tool('memory_forget').execute({ id: 999999 })
171
+ return { value, rendered: render('memory_forget', { id: 999999 }, value) }
172
+ })
173
+ await capture('forget.hit', async () => {
174
+ const value = await tool('memory_forget').execute({ id: 1 })
175
+ return { value, rendered: render('memory_forget', { id: 1 }, value) }
176
+ })
177
+ await capture('section.afterForget', () => sections[0].text())
178
+ await capture('write.render', () => render('memory_write', { text: fixtureText }, { id: 7, tags: 'reg', pinned: false }))
179
+
180
+ for (const f of [dbFile, `${dbFile}-journal`]) { try { fs.rmSync(f, { force: true }) } catch { /* ignore */ } }
181
+ obs.fingerprint = crypto.createHash('sha256').update(JSON.stringify(obs)).digest('hex')
182
+ return obs
183
+ }
184
+
185
+ // 上游对照物也必须落在「能解析到宿主闭包」的位置:_archive 里的备份原物不在任何 profile 的
186
+ // node_modules 树内,直接 import 会 ERR_MODULE_NOT_FOUND(schemastery 由 profile 闭包提供)。
187
+ // → 必要时把它铺进隔离 profile 的隐藏参考目录(跑完清理,--keep-ref 可保留)。
188
+ const keepRef = args.includes('--keep-ref')
189
+ const refRoot = path.join(path.dirname(path.resolve(forkDir)), '.g24-s2a-upstream-ref')
190
+ let upstreamPath = path.resolve(upstreamDir)
191
+ let cleanupRef = null
192
+ if (resolvableContext(upstreamPath) === null) {
193
+ try { fs.rmSync(refRoot, { recursive: true, force: true }) } catch { /* ignore */ }
194
+ fs.mkdirSync(refRoot, { recursive: true })
195
+ upstreamPath = path.join(refRoot, 'dsh-memory')
196
+ fs.cpSync(upstreamDir, upstreamPath, { recursive: true })
197
+ cleanupRef = keepRef ? null : refRoot
198
+ console.log(`[regression] 上游对照物已铺到可解析位置:${upstreamPath}${keepRef ? '(--keep-ref 保留)' : '(跑完清理)'}`)
199
+ }
200
+
201
+ let upstream
202
+ let fork
203
+ try {
204
+ upstream = await observe('upstream', upstreamPath)
205
+ fork = await observe('fork', path.resolve(forkDir))
206
+ } finally {
207
+ if (cleanupRef) { try { fs.rmSync(cleanupRef, { recursive: true, force: true }) } catch { /* ignore */ } }
208
+ }
209
+
210
+ const failures = []
211
+ function compare(label, a, b) {
212
+ const sa = JSON.stringify(a)
213
+ const sb = JSON.stringify(b)
214
+ if (sa !== sb) failures.push({ label, upstream: sa?.slice(0, 400), fork: sb?.slice(0, 400) })
215
+ }
216
+ /**
217
+ * FORK-EDIT(2026-09-21): 「工具集」对照的**显式白名单**(否则这道门永远红 = 等于失效)。
218
+ * 依据:方案 `docs\P2-记忆引擎升级-方案-20260916.md` §3.4 —— `memory_get` 是 fork 自注册的
219
+ * 「指针展开」工具(官方包不提供,属**有意新增**);其余任何工具差异照旧判红。
220
+ * 只放宽"多出"这一个名字,且要求上游工具集是 fork 的**真子集**(少一个/改名/多一个都算差异)。
221
+ */
222
+ const FORK_ONLY_TOOLS = ['memory_get']
223
+ function compareToolShapes(a, b) {
224
+ const names = (list) => (Array.isArray(list) ? list.map((t) => t?.name) : [])
225
+ const up = names(a)
226
+ const fk = names(b)
227
+ const extra = fk.filter((n) => !up.includes(n))
228
+ const missing = up.filter((n) => !fk.includes(n))
229
+ const unexpectedExtra = extra.filter((n) => !FORK_ONLY_TOOLS.includes(n))
230
+ const byName = (list) => Object.fromEntries((list ?? []).map((t) => [t?.name, t]))
231
+ const upMap = byName(a)
232
+ const fkMap = byName(b)
233
+ const shapeDiffs = up.filter((n) => JSON.stringify(upMap[n]) !== JSON.stringify(fkMap[n])).map((n) => n)
234
+ if (missing.length === 0 && unexpectedExtra.length === 0 && shapeDiffs.length === 0) {
235
+ if (extra.length > 0) console.log(`[regression] 白名单放行 fork 独有工具:${extra.join(', ')}(依据:方案 §3.4 / S2b 交付报告)`)
236
+ return
237
+ }
238
+ failures.push({
239
+ label: `tool shapes(缺=${JSON.stringify(missing)} 越界多出=${JSON.stringify(unexpectedExtra)} 形状变化=${JSON.stringify(shapeDiffs)})`,
240
+ upstream: JSON.stringify(a)?.slice(0, 400),
241
+ fork: JSON.stringify(b)?.slice(0, 400),
242
+ })
243
+ }
244
+ compare('module shape', upstream.module, fork.module)
245
+ compareToolShapes(upstream.toolShapes, fork.toolShapes)
246
+ compare('section shapes', upstream.sectionShapes, fork.sectionShapes)
247
+ compare('step count', upstream.steps.length, fork.steps.length)
248
+ for (let i = 0; i < Math.max(upstream.steps.length, fork.steps.length); i++) {
249
+ compare(`step[${i}] ${upstream.steps[i]?.step ?? fork.steps[i]?.step ?? '?'}`, upstream.steps[i], fork.steps[i])
250
+ }
251
+
252
+ console.log(`upstream : ${upstream.dir}`)
253
+ console.log(`fork : ${fork.dir}`)
254
+ console.log(`upstream fingerprint : ${upstream.fingerprint}`)
255
+ console.log(`fork fingerprint : ${fork.fingerprint}`)
256
+ console.log(`steps compared : ${upstream.steps.length}`)
257
+ for (const s of upstream.steps) {
258
+ console.log(` ${s.ok ? 'ok ' : 'err '} ${s.step.padEnd(20)} ${JSON.stringify(s.ok ? s.value : s.error).slice(0, 120)}`)
259
+ }
260
+ if (failures.length === 0) {
261
+ console.log('\nregression: IDENTICAL — 上下游实体在同一序列上逐步骤一致(含工具描述、参数 schema、错误消息、段落文本、render 文本)')
262
+ process.exit(0)
263
+ }
264
+ console.log(`\nregression: ${failures.length} DIFFERENCE(S)`)
265
+ for (const f of failures) {
266
+ console.log(` ✗ ${f.label}`)
267
+ console.log(` upstream: ${f.upstream}`)
268
+ console.log(` fork : ${f.fork}`)
269
+ }
270
+ process.exit(1)