@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,334 @@
1
+ /**
2
+ * test/schema-migrate.test.mjs —— 记忆引擎 v2 · S3b 表结构自检单测(零依赖、零生产触碰)
3
+ *
4
+ * 覆盖(卡 §四.1 验收 + 自加项):
5
+ * 01 未加列 ⇒ 自检报"已加列"(两列 + 索引)
6
+ * 02 幂等:连跑两次,第二次 0 语句、0 日志追加
7
+ * 03 列定义落库正确(type / notnull / dflt_value)
8
+ * 04 默认值语义:新插入行 owner_user = 身份表 owner;无身份表 ⇒ 回落常量;visibility = private_owner
9
+ * 05 加列后 `SELECT *` 可用(官方版兼容的库侧前提)
10
+ * 06 失败降级:只读连接 ⇒ ok=false / degraded=true / **不抛** + 日志落一行
11
+ * 07 失败降级:坏库(把表 drop 掉再自检)不抛且降级
12
+ * 08 表不存在 ⇒ skipped(不误报成功)
13
+ * 09 日志:变更写 / 幂等不写 / 日志目录可自行创建
14
+ * 10 **与权威源逐字对照**:tools/memory-db-tools/lib/memory-scope.mjs(⑤-3 卡)的
15
+ * 列定义 / 索引 SQL / buildAddColumnSql 输出 / 回落行为四项全部一致(漂移即红)
16
+ *
17
+ * 用法:node test/schema-migrate.test.mjs
18
+ * 退出码:0 = 全绿 / 1 = 有失败
19
+ */
20
+ import fs from 'node:fs';
21
+ import os from 'node:os';
22
+ import path from 'node:path';
23
+ import { DatabaseSync } from 'node:sqlite';
24
+ import { pathToFileURL } from 'node:url';
25
+ import * as S from '../src/schema-migrate.mjs';
26
+
27
+ let pass = 0;
28
+ const failures = [];
29
+ const ok = (cond, msg) => {
30
+ if (cond) { pass += 1; process.stdout.write(` ✓ ${msg}\n`); }
31
+ else { failures.push(msg); process.stdout.write(` ✗ ${msg}\n`); }
32
+ };
33
+ const eq = (a, b, msg) => ok(JSON.stringify(a) === JSON.stringify(b), `${msg} (got=${JSON.stringify(a)} want=${JSON.stringify(b)})`);
34
+ const section = (t) => process.stdout.write(`\n[${t}]\n`);
35
+
36
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 's3b-schema-'));
37
+ let caseNo = 0;
38
+
39
+ /** 造一个「6 列 + 若干行」的库(= 加列前的生产形态),返回 { dir, home, dbPath }。 */
40
+ function makeLegacyDb({ rows = 2, usersJson = undefined, dropTable = false } = {}) {
41
+ const dir = path.join(tmpRoot, `case-${++caseNo}`);
42
+ const home = path.join(dir, '.dsh');
43
+ fs.mkdirSync(path.join(home, 'memory'), { recursive: true });
44
+ const dbPath = path.join(home, 'memory', 'memory.db');
45
+ if (usersJson !== undefined) fs.writeFileSync(path.join(home, S.IDENTITY_TABLE_NAME), usersJson, 'utf8');
46
+ if (dropTable) { fs.writeFileSync(dbPath, 'not a sqlite file at all', 'utf8'); return { dir, home, dbPath }; }
47
+ const db = new DatabaseSync(dbPath);
48
+ db.exec(`CREATE TABLE memories (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ text TEXT NOT NULL,
51
+ tags TEXT NOT NULL DEFAULT '',
52
+ pinned INTEGER NOT NULL DEFAULT 0,
53
+ created_at INTEGER NOT NULL,
54
+ updated_at INTEGER NOT NULL
55
+ )`);
56
+ db.exec('CREATE INDEX memories_recent ON memories (updated_at DESC, id DESC)');
57
+ const ins = db.prepare('INSERT INTO memories (text, tags, pinned, created_at, updated_at) VALUES (?, ?, 0, ?, ?)');
58
+ for (let i = 0; i < rows; i += 1) ins.run(`fixture 记忆 ${i}`, 'fixture', 1700000000000 + i, 1700000000000 + i);
59
+ db.close();
60
+ return { dir, home, dbPath };
61
+ }
62
+
63
+ const logLines = (home) => {
64
+ const f = path.join(home, 'logs', 'memory-schema.log');
65
+ if (!fs.existsSync(f)) return [];
66
+ return fs.readFileSync(f, 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
67
+ };
68
+
69
+ // ────────────────────────────────────────────── 01 未加列 ⇒ 已加列
70
+ section('01 未加列 ⇒ 自检报"已加列"');
71
+ {
72
+ const { home, dbPath } = makeLegacyDb({});
73
+ const db = new DatabaseSync(dbPath);
74
+ const r = S.ensureMemoryColumns(db, { dbPath });
75
+ ok(r.ok === true, '自检 ok=true');
76
+ ok(r.degraded === false, '未降级');
77
+ eq(r.added.sort(), ['owner_user', 'visibility'], 'added 两列(顺序无关)');
78
+ ok(r.indexCreated === true, '索引 memories_visibility 已创建');
79
+ const cols = S.columnsOf(db);
80
+ eq(cols.slice(6), ['owner_user', 'visibility'], '库内列数 6 → 8');
81
+ const idx = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='memories_visibility'").get();
82
+ ok(!!idx, '索引真实存在(sqlite_master 可查)');
83
+ db.close();
84
+ const lines = logLines(home);
85
+ ok(lines.length === 1 && lines[0].event === 'migrate' && lines[0].ok === true, '变更落日志一行(event=migrate, ok=true)');
86
+ eq(lines[0]?.added?.sort(), ['owner_user', 'visibility'], '日志记了加列清单');
87
+ }
88
+
89
+ // ────────────────────────────────────────────── 02 幂等
90
+ section('02 幂等:连跑两次');
91
+ {
92
+ const { home, dbPath } = makeLegacyDb({});
93
+ const db1 = new DatabaseSync(dbPath);
94
+ const r1 = S.ensureMemoryColumns(db1, { dbPath });
95
+ db1.close();
96
+ const db2 = new DatabaseSync(dbPath);
97
+ const r2 = S.ensureMemoryColumns(db2, { dbPath });
98
+ db2.close();
99
+ ok(r1.added.length === 2, '第一次加了两列');
100
+ eq(r2.added, [], '第二次 added 为空');
101
+ eq(r2.statements, [], '第二次 0 条语句');
102
+ eq(r2.present.sort(), ['owner_user', 'visibility'], '第二次报"列已存在"');
103
+ ok(r2.ok === true && r2.degraded === false, '第二次 ok 且不降级');
104
+ const lines = logLines(home);
105
+ ok(lines.length === 1, `幂等无变化不写日志(仍 1 行,实测 ${lines.length})`);
106
+ const db3 = new DatabaseSync(dbPath, { readOnly: true });
107
+ eq(S.columnsOf(db3).length, 8, '列数稳定在 8(不重复加列)');
108
+ db3.close();
109
+ }
110
+
111
+ // ────────────────────────────────────────────── 03 列定义
112
+ section('03 列定义落库正确');
113
+ {
114
+ const { dbPath } = makeLegacyDb({});
115
+ const db = new DatabaseSync(dbPath);
116
+ S.ensureMemoryColumns(db, { dbPath });
117
+ const info = db.prepare('PRAGMA table_info(memories)').all();
118
+ const byName = Object.fromEntries(info.map((c) => [c.name, c]));
119
+ ok(byName.owner_user?.type === 'TEXT' && byName.owner_user.notnull === 1, 'owner_user TEXT NOT NULL');
120
+ ok(byName.visibility?.type === 'TEXT' && byName.visibility.notnull === 1, 'visibility TEXT NOT NULL');
121
+ eq(byName.visibility?.dflt_value, `'private_owner'`, 'visibility 默认值 private_owner');
122
+ ok(!!byName.owner_user?.dflt_value, `owner_user 有默认值(${byName.owner_user?.dflt_value})`);
123
+ const idxSql = db.prepare("SELECT sql FROM sqlite_master WHERE name='memories_visibility'").get()?.sql ?? '';
124
+ ok(/visibility, updated_at DESC/i.test(idxSql), '索引列为 (visibility, updated_at DESC)');
125
+ db.close();
126
+ }
127
+
128
+ // ────────────────────────────────────────────── 04 默认值语义
129
+ section('04 默认值语义(身份表优先 / 回落常量)');
130
+ {
131
+ // 04a 无身份表 ⇒ 回落常量
132
+ const a = makeLegacyDb({});
133
+ const dba = new DatabaseSync(a.dbPath);
134
+ const ra = S.ensureMemoryColumns(dba, { dbPath: a.dbPath });
135
+ eq(ra.ownerSource, 'fallback', '无身份表 ⇒ ownerSource=fallback');
136
+ eq(ra.ownerUser, S.FALLBACK_OWNER_USER, `回落常量 ${S.FALLBACK_OWNER_USER}`);
137
+ dba.prepare("INSERT INTO memories (text, tags, pinned, created_at, updated_at) VALUES ('n1','',0,1,1)").run();
138
+ const rowA = dba.prepare("SELECT owner_user, visibility FROM memories WHERE text='n1'").get();
139
+ eq(rowA.owner_user, S.FALLBACK_OWNER_USER, '新行 owner_user = 回落常量');
140
+ eq(rowA.visibility, 'private_owner', '新行 visibility = private_owner');
141
+ const allA = dba.prepare('SELECT COUNT(*) n FROM memories WHERE owner_user=? AND visibility=?').get(S.FALLBACK_OWNER_USER, 'private_owner');
142
+ eq(allA.n, 3, '存量 2 行 + 新 1 行全部拿到默认值(无 NULL)');
143
+ dba.close();
144
+
145
+ // 04b 身份表存在(**权威 map 形态**,家人档 §3.3)⇒ 取 role==='owner' 的那个键
146
+ const b = makeLegacyDb({ usersJson: JSON.stringify({ _comment: '注释键应跳过', Wife_01: { role: 'family', name: '家人' }, Owner_X: { role: 'owner', name: '主人' } }) });
147
+ const dbb = new DatabaseSync(b.dbPath);
148
+ const rb = S.ensureMemoryColumns(dbb, { dbPath: b.dbPath });
149
+ eq(rb.ownerSource, 'identity-table', '有身份表 ⇒ ownerSource=identity-table');
150
+ eq(rb.ownerUser, 'Owner_X', 'owner_user 默认值 = 身份表 role=owner 的 userid');
151
+ dbb.close();
152
+ const ddlB = (logLines(b.home)[0]?.ownerUser) ?? null;
153
+ eq(ddlB, 'Owner_X', '日志里记的也是身份表 owner');
154
+ eq(S.readIdentityTable(b.home).userids, ['Wife_01', 'Owner_X'], 'map 形态 userid 集合(`_` 注释键跳过)');
155
+
156
+ // 04b-2 包装形态 `{ users: [...] }`(家人档未定稿期可能出现的形态)也认
157
+ const b2 = makeLegacyDb({ usersJson: JSON.stringify({ users: [{ userid: 'U_1', role: 'family' }, { userid: 'U_owner', role: 'owner' }] }) });
158
+ const dbb2 = new DatabaseSync(b2.dbPath);
159
+ eq(S.ensureMemoryColumns(dbb2, { dbPath: b2.dbPath }).ownerUser, 'U_owner', '`{users:[…]}` 形态也认(宽容扩展)');
160
+ dbb2.close();
161
+
162
+ // 04c 身份表是数组形态 + 没有 owner 条目 ⇒ 回落
163
+ const c = makeLegacyDb({ usersJson: JSON.stringify([{ userid: 'A', role: 'family' }]) });
164
+ const dbc = new DatabaseSync(c.dbPath);
165
+ const rc = S.ensureMemoryColumns(dbc, { dbPath: c.dbPath });
166
+ eq(rc.ownerSource, 'fallback', '身份表无 owner 条目 ⇒ 回落');
167
+ ok(rc.notes.some((n) => /没有任何 role==='owner'/.test(n)), 'notes 说明原因');
168
+ dbc.close();
169
+
170
+ // 04d 身份表坏 JSON ⇒ 回落(不抛)
171
+ const d = makeLegacyDb({ usersJson: '{ this is not json' });
172
+ const dbd = new DatabaseSync(d.dbPath);
173
+ const rd = S.ensureMemoryColumns(dbd, { dbPath: d.dbPath });
174
+ ok(rd.ok === true && rd.ownerSource === 'fallback', '坏身份表 ⇒ 仍迁移成功 + 回落');
175
+ dbd.close();
176
+ }
177
+
178
+ // ────────────────────────────────────────────── 05 SELECT * 可用
179
+ section('05 加列后 SELECT * 可用(官方版兼容的库侧前提)');
180
+ {
181
+ const { dbPath } = makeLegacyDb({});
182
+ const db = new DatabaseSync(dbPath);
183
+ S.ensureMemoryColumns(db, { dbPath });
184
+ const rows = db.prepare('SELECT * FROM memories ORDER BY id').all();
185
+ ok(rows.length === 2, 'SELECT * 返回全部行');
186
+ eq(Object.keys(rows[0]).length, 8, 'SELECT * 返回 8 列');
187
+ ok('owner_user' in rows[0] && 'visibility' in rows[0], '新列在 SELECT * 里可见');
188
+ db.close();
189
+ }
190
+
191
+ // ────────────────────────────────────────────── 06 失败降级:只读库
192
+ section('06 失败降级:只读连接(ALTER 必失败)');
193
+ {
194
+ const { home, dbPath } = makeLegacyDb({});
195
+ const ro = new DatabaseSync(dbPath, { readOnly: true });
196
+ let threw = null;
197
+ let r = null;
198
+ try { r = S.ensureMemoryColumns(ro, { dbPath }); } catch (e) { threw = e; }
199
+ ok(threw === null, '**绝不抛**(启动不被阻断)');
200
+ ok(r && r.ok === false && r.degraded === true, 'ok=false + degraded=true');
201
+ ok(!!(r && r.error), `error 有值:${r?.error?.slice(0, 60)}`);
202
+ ro.close();
203
+ const db = new DatabaseSync(dbPath, { readOnly: true });
204
+ eq(S.columnsOf(db).length, 6, '只读库列数仍为 6(没加列)');
205
+ db.close();
206
+ const lines = logLines(home);
207
+ ok(lines.length === 1 && lines[0].ok === false && lines[0].degraded === true, '失败落日志一行(ok=false, degraded=true)');
208
+ }
209
+
210
+ // ────────────────────────────────────────────── 07 失败降级:坏库
211
+ section('07 失败降级:库打开后表被删 + 只读挂载式场景');
212
+ {
213
+ const { home, dbPath } = makeLegacyDb({});
214
+ const db = new DatabaseSync(dbPath);
215
+ db.exec('DROP TABLE memories');
216
+ const r = S.ensureMemoryColumns(db, { dbPath });
217
+ ok(r.ok === true && r.skipped !== null, '表不存在 ⇒ skipped(不误报"已加列")');
218
+ db.close();
219
+ ok(logLines(home).length === 0, 'skipped 不写日志');
220
+
221
+ // 坏文件:node:sqlite 打开是**惰性**的(不校验内容)⇒ 失败面在**首次访问**(MemoryStore 的 SCHEMA exec)
222
+ const bad = makeLegacyDb({ dropTable: true });
223
+ let openErr = null;
224
+ let handle = null;
225
+ try {
226
+ handle = new DatabaseSync(bad.dbPath);
227
+ handle.exec('SELECT 1');
228
+ } catch (e) { openErr = e; } finally { try { handle?.close(); } catch { /* 忽略 */ } }
229
+ ok(openErr !== null, `坏库在首次访问时抛错(失败面属 MemoryStore 的打开阶段,非本自检;边界见交付报告)`);
230
+ }
231
+
232
+ // ────────────────────────────────────────────── 08 表不存在 / 空库
233
+ section('08 空库(无 memories 表)⇒ skipped');
234
+ {
235
+ const dir = path.join(tmpRoot, `case-${++caseNo}`);
236
+ const home = path.join(dir, '.dsh');
237
+ fs.mkdirSync(home, { recursive: true });
238
+ const dbPath = path.join(home, 'memory', 'empty.db');
239
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
240
+ const db = new DatabaseSync(dbPath);
241
+ const r = S.ensureMemoryColumns(db, { dbPath });
242
+ ok(r.ok === true && r.skipped !== null, '空库 skipped');
243
+ eq(r.added, [], '没有加列');
244
+ db.close();
245
+ }
246
+
247
+ // ────────────────────────────────────────────── 09 日志
248
+ section('09 日志行为');
249
+ {
250
+ const { home, dbPath } = makeLegacyDb({});
251
+ const db = new DatabaseSync(dbPath);
252
+ const collected = [];
253
+ S.ensureMemoryColumns(db, { dbPath, onLog: (e) => collected.push(e) });
254
+ db.close();
255
+ ok(collected.length === 1, 'onLog 回调收到 1 条');
256
+ ok(typeof collected[0].at === 'string' && typeof collected[0].pid === 'number', '日志含 at/pid');
257
+ const f = path.join(home, 'logs', 'memory-schema.log');
258
+ ok(fs.existsSync(f), '日志文件自动创建(目录不存在也能建)');
259
+ const raw = fs.readFileSync(f, 'utf8').trim();
260
+ ok(raw.startsWith('{') && JSON.parse(raw).event === 'migrate', 'JSON 行格式可解析');
261
+ // 指定 logFile
262
+ const other = path.join(home, 'logs', 'custom.log');
263
+ const db2 = new DatabaseSync(dbPath);
264
+ S.ensureMemoryColumns(db2, { dbPath: 'x', logFile: other, home });
265
+ db2.close();
266
+ ok(!fs.existsSync(other), '幂等(无变更)⇒ 自定义日志也不写');
267
+ }
268
+
269
+ // ────────────────────────────────────────────── 10 与权威源逐字对照
270
+ section('10 与权威源 memory-scope.mjs 逐字对照(⑤-3 卡)');
271
+ {
272
+ const authoritative = new URL('../../memory-db-tools/lib/memory-scope.mjs', import.meta.url);
273
+ let A = null;
274
+ try {
275
+ A = await import(authoritative.href);
276
+ } catch (e) {
277
+ ok(false, `无法 import 权威源 ${authoritative}:${e.message}`);
278
+ }
279
+ if (A) {
280
+ eq(A.LEGACY_COLUMNS, S.LEGACY_COLUMNS, 'LEGACY_COLUMNS 一致');
281
+ eq(A.VISIBILITY_VALUES, S.VISIBILITY_VALUES, 'VISIBILITY_VALUES 一致');
282
+ eq(A.DEFAULT_VISIBILITY, S.DEFAULT_VISIBILITY, 'DEFAULT_VISIBILITY 一致');
283
+ eq(A.FALLBACK_OWNER_USER, S.FALLBACK_OWNER_USER, 'FALLBACK_OWNER_USER 一致');
284
+ eq(
285
+ A.NEW_COLUMNS.map((c) => ({ ...c })),
286
+ S.NEW_COLUMNS.map((c) => ({ ...c })),
287
+ 'NEW_COLUMNS 两列定义逐字段一致',
288
+ );
289
+ eq(A.VISIBILITY_INDEX.sql, S.VISIBILITY_INDEX_SQL, '索引 SQL 逐字一致');
290
+ eq(
291
+ A.buildAddColumnSql(A.NEW_COLUMNS[0], 'X'),
292
+ S.buildAddColumnSql(S.NEW_COLUMNS[0], 'X'),
293
+ 'buildAddColumnSql(owner_user) 输出一致',
294
+ );
295
+ eq(
296
+ A.buildAddColumnSql(A.NEW_COLUMNS[1], A.DEFAULT_VISIBILITY),
297
+ S.buildAddColumnSql(S.NEW_COLUMNS[1], S.DEFAULT_VISIBILITY),
298
+ 'buildAddColumnSql(visibility) 输出一致',
299
+ );
300
+ // 回落行为一致(无身份表时)
301
+ eq(A.resolveDefaultOwner(null).ownerUser, S.resolveDefaultOwner(null).ownerUser, '回落 ownerUser 一致');
302
+ eq(A.resolveDefaultOwner(null).source, S.resolveDefaultOwner(null).source, '回落 source 标记一致');
303
+ // 同一份身份表文本(权威 map 形态)→ 同一份 owner + 同一份 userid 集合
304
+ const mapText = JSON.stringify({ _note: '注释键', Z_1: { role: 'family' }, Z_owner: { role: 'owner', name: '主人' } });
305
+ eq(A.parseIdentityTable(mapText).owner, S.parseIdentityTable(mapText).owner, 'map 形态:owner 解析一致');
306
+ eq(A.parseIdentityTable(mapText).userids, S.parseIdentityTable(mapText).userids, 'map 形态:userid 集合一致(`_` 注释键跳过)');
307
+ // fork 的**有意扩展**:数组 / `{users:[…]}` 形态也认(权威源原先不认)
308
+ const arrText = JSON.stringify([{ userid: 'Z_owner', role: 'owner' }]);
309
+ eq(S.parseIdentityTable(arrText).owner, 'Z_owner', 'fork 额外认数组形态(有意扩展,非漂移)');
310
+ // ⚠️ 2026-10-11 订正(卡【翻译官·批4·家人档地基】):权威源**也已认**数组形态,本条由"差异"变成"一致"。
311
+ // 原因 = 方案 §5.6 定稿的新身份表形态是 `{version, default:"deny", users:{<userid>:{…}}}`(**users 是 map**),
312
+ // 而两处实现原先只认"平面 map / users 数组" ⇒ 新表一落盘就会被**读成空表**(`resolveDefaultOwner` 回落常量、
313
+ // `ownerBindingDiff` 把全部 owner 判成未登记)—— 那是**静默失真**,必须双向堵住。
314
+ eq(A.parseIdentityTable(arrText).owner, 'Z_owner', '权威源认数组形态(2026-10-11 起与 fork 一致)');
315
+ // 新形态(users = map)两处必须同解
316
+ const usersMapText = JSON.stringify({ version: 1, default: 'deny', users: { Z_owner: { role: 'owner', name: '主人' }, Z_1: { role: 'family' } } });
317
+ eq(A.parseIdentityTable(usersMapText).owner, S.parseIdentityTable(usersMapText).owner, 'users-map 形态:owner 解析一致');
318
+ eq(A.parseIdentityTable(usersMapText).userids, S.parseIdentityTable(usersMapText).userids, 'users-map 形态:userid 集合一致');
319
+ eq(A.parseIdentityTable(usersMapText).userids, ['Z_owner', 'Z_1'], 'users-map 形态:两个 userid 都认出来(不被当空表)');
320
+ }
321
+ }
322
+
323
+ // ────────────────────────────────────────────── 汇总
324
+ process.stdout.write(`\n${'─'.repeat(60)}\n`);
325
+ if (failures.length === 0) {
326
+ process.stdout.write(`[schema-migrate] 全绿:${pass} 项通过\n`);
327
+ process.exitCode = 0;
328
+ } else {
329
+ process.stdout.write(`[schema-migrate] 失败 ${failures.length} 项 / 通过 ${pass} 项\n`);
330
+ for (const f of failures) process.stdout.write(` ✗ ${f}\n`);
331
+ process.exitCode = 1;
332
+ }
333
+ // 收尾清理(测试目录在 %TEMP%)
334
+ try { fs.rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* 忽略 */ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * test-adapter-converge.mjs —— U3「adapter 接入」收敛点的**可证伪**用例(2026-09-22)。
3
+ *
4
+ * 为什么要有它:收敛(裸字面量 → 适配层常量)本身是"值恒等"的机械替换,最容易出的不是
5
+ * 逻辑错,而是**无声失准**——适配层某条常量被改错值、或 `lib/` 副本过期(build 覆盖 / 同步器没跑),
6
+ * 两者都不会让任何功能测试变红,只会在生产上表现为"判据悄悄变了"。
7
+ * 所以这里断言的是**值本身**,而不是"函数返回了 truthy":
8
+ * ① 适配层副本里 SVC.* / EV.* 的值必须等于主包真名(改错值 → 红)
9
+ * ② fork 真正消费出来的值(`inject` 数组 / injector 的判据常量)必须与主包真名一致(改错取值 → 红)
10
+ * ③ `src/` 与 `lib/` 两份副本的**正文逐字节一致**、且头部记录的 sourceSha256 等于唯一源实际 sha
11
+ * (副本过期 / build 把 src 那份拷进 lib 覆盖掉 lib 头 → 红;2026-09-22 实测踩过这一形态)
12
+ *
13
+ * 跑法:node --test test/test-adapter-converge.mjs
14
+ *
15
+ * ⚠️ 命名形态:`test-*.mjs`(而不是 `*.test.mjs`)—— 本文件断言的就是"主包真名"本身,
16
+ * 字面量是**判据**,无法也不需要收敛(用常量断言常量 = 自证、不可证伪);
17
+ * 而 R4 判定器(方案 §3.5)的白名单只豁免 `_test/` 与 `test-*.mjs` 两种测试夹具形态
18
+ * (`test/` 目录本身不在白名单里)。取名 `test-*` 是**走既有合法形态**,不是改判定口径。
19
+ */
20
+ import test from 'node:test'
21
+ import assert from 'node:assert/strict'
22
+ import fs from 'node:fs'
23
+ import path from 'node:path'
24
+ import { createHash } from 'node:crypto'
25
+ import { pathToFileURL } from 'node:url'
26
+ import { fileURLToPath } from 'node:url'
27
+
28
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
29
+ const ROOT = path.resolve(HERE, '..')
30
+ const SRC_COPY = path.join(ROOT, 'src', 'host-adapter.js')
31
+ const LIB_COPY = path.join(ROOT, 'lib', 'host-adapter.js')
32
+ /** 唯一源:<workspace>\tools\lib\host-adapter.mjs(从包位置反推,跨机不写死盘符)。 */
33
+ const SOURCE = path.resolve(ROOT, '..', 'lib', 'host-adapter.mjs')
34
+
35
+ const sha256 = (buf) => createHash('sha256').update(buf).digest('hex')
36
+ /** 去掉生成头(`══ END HEADER ══` 之后才是正文)。 */
37
+ const bodyOf = (text) => {
38
+ const marker = '══ END HEADER ══'
39
+ const i = text.indexOf(marker)
40
+ return i < 0 ? text : text.slice(i + marker.length)
41
+ }
42
+ /**
43
+ * 剥掉行内注释之后的部分 —— **与 R4 判定器同口径**(`check-decoupling.mjs:129` 也是这么做的)。
44
+ * 不加这一步会把"注释里引述原字面量"误判成违规(本文件首跑就踩到)。
45
+ */
46
+ const stripComments = (text) =>
47
+ text.split('\n').map((l) => { const i = l.indexOf('//'); return i >= 0 ? l.slice(0, i) : l }).join('\n')
48
+ /** 头部里的 `// sourceSha256: <hex>` 行。 */
49
+ const sourceShaOf = (text) => (text.match(/^\/\/ sourceSha256: ([0-9a-f]{64})$/m) ?? [])[1] ?? null
50
+
51
+ const srcText = fs.readFileSync(SRC_COPY, 'utf8')
52
+ const libText = fs.readFileSync(LIB_COPY, 'utf8')
53
+ const srcMod = await import(pathToFileURL(SRC_COPY).href)
54
+ const libMod = await import(pathToFileURL(LIB_COPY).href)
55
+
56
+ // ── ① 适配层常量的值(改错值 → 红) ────────────────────────────────────────────
57
+ test('① SVC.* 的值 == 主包 ctx 服务真名', () => {
58
+ assert.equal(srcMod.SVC.tools, 'tools')
59
+ assert.equal(srcMod.SVC.systemPrompt, 'systemPrompt')
60
+ // fork 只用这两个服务;表里多出来的键不在此断言(唯一源自检 test-host-adapter.mjs 负责全表)
61
+ })
62
+
63
+ test('① EV.* 的值 == 主包事件真名', () => {
64
+ assert.equal(srcMod.EV.USER_MESSAGE, 'user/message')
65
+ })
66
+
67
+ test('① 常量表是冻结的(防运行期被就地改)', () => {
68
+ assert.equal(Object.isFrozen(srcMod.SVC), true)
69
+ assert.equal(Object.isFrozen(srcMod.EV), true)
70
+ })
71
+
72
+ // ── ② fork 真消费出来的值(取值方式写错 → 红) ─────────────────────────────────
73
+ test('② inject 声明的取值来自适配层常量,且两值合起来就是 ["tools","systemPrompt"]', () => {
74
+ // 为什么不 import lib/index.js:本包在 tools\ 下没有 node_modules,宿主依赖
75
+ // (@deepseek-ai/schemastery 等)只有部署位置才解析得到 —— "真加载后 inject 是什么"
76
+ // 由部署侧探针 test/probe-entity.mjs 断言(那是它的本职),这里守源码形状 + 常量真值。
77
+ const text = stripComments(fs.readFileSync(path.join(ROOT, 'src', 'index.js'), 'utf8'))
78
+ assert.match(text, /const inject = \[SVC\.tools, SVC\.systemPrompt\]/)
79
+ assert.deepEqual([srcMod.SVC.tools, srcMod.SVC.systemPrompt], ['tools', 'systemPrompt'])
80
+ })
81
+
82
+ test('② injector 判据消费的事件名 == "user/message"', async () => {
83
+ // 从源码消费点读:injector.mjs 里的判据常量就是 EV.USER_MESSAGE(此处断言其真值)
84
+ const injectorText = fs.readFileSync(path.join(ROOT, 'src', 'injector.mjs'), 'utf8')
85
+ assert.match(injectorText, /ev\.type !== EV\.USER_MESSAGE/, 'injector 判据应走适配层常量')
86
+ assert.equal(srcMod.EV.USER_MESSAGE, 'user/message')
87
+ })
88
+
89
+ test('② 源码里不再有收纳点位的裸字面量(剥注释后)', () => {
90
+ const files = [
91
+ path.join(ROOT, 'src', 'index.js'),
92
+ path.join(ROOT, 'src', 'injector.mjs'),
93
+ path.join(ROOT, 'lib', 'index.js'),
94
+ path.join(ROOT, 'lib', 'injector.mjs'),
95
+ path.join(ROOT, 'test', 'fixtures-s2b.mjs'),
96
+ path.join(ROOT, 'test', 'probe-entity.mjs'),
97
+ ]
98
+ for (const f of files) {
99
+ const t = stripComments(fs.readFileSync(f, 'utf8'))
100
+ assert.doesNotMatch(t, /['"]user\/message['"]/, `${path.basename(f)} 仍有裸事件名字面量`)
101
+ assert.doesNotMatch(t, /\[\s*["']tools["']\s*,\s*["']systemPrompt["']\s*\]/, `${path.basename(f)} 仍有裸 inject 字面量`)
102
+ }
103
+ })
104
+
105
+ // ── ③ 副本新鲜度 / 分叉(副本过期或 build 覆盖 → 红) ───────────────────────────
106
+ test('③ src/ 与 lib/ 两份副本正文逐字节一致(仅生成头不同)', () => {
107
+ assert.equal(bodyOf(srcText), bodyOf(libText))
108
+ })
109
+
110
+ test('③ 两份副本头部记录的 sourceSha256 相互一致', () => {
111
+ const a = sourceShaOf(srcText)
112
+ const b = sourceShaOf(libText)
113
+ assert.ok(a && b, '副本头缺 sourceSha256 行 —— 是否被手工改过/被 build 覆盖?')
114
+ assert.equal(a, b)
115
+ })
116
+
117
+ test('③ 副本与唯一源一致(唯一源缺失时跳过——它随仓 3 同步,本包可被单独拷走)', (t) => {
118
+ if (!fs.existsSync(SOURCE)) return t.skip(`唯一源不可达:${SOURCE}`)
119
+ const srcSha = sha256(fs.readFileSync(SOURCE))
120
+ assert.equal(sourceShaOf(srcText), srcSha, 'src 副本过期 —— 跑宿主侧 sync-host-adapter.mjs')
121
+ assert.equal(sourceShaOf(libText), srcSha, 'lib 副本过期 —— 跑宿主侧 sync-host-adapter.mjs')
122
+ })
123
+
124
+ test('③ 两份副本导出的接口面相同(防只同步其中一份导致行为分叉)', () => {
125
+ const keysOf = (m) => Object.keys(m).sort()
126
+ assert.deepEqual(keysOf(srcMod), keysOf(libMod))
127
+ })