@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,290 @@
1
+ #!/usr/bin/env node
2
+ // tools/memory-db-tools/memory-schema-migrate.mjs
3
+ //
4
+ // ⑤ 包 §3.3 / P2 §3.8「两列」迁移 CLI(施工卡 ⑤-3 交付)。
5
+ //
6
+ // 做什么:给 `memories` 表补 `owner_user` + `visibility` 两列(+ visibility 索引)。
7
+ // 怎么保证安全:
8
+ // · 默认 **dry-run**(只读规划,零写入);
9
+ // · `--apply` 打到**生产库**必须显式 `--allow-production`,否则拒绝(exit 2);
10
+ // · 打生产还必须过**停机窗口判据**(3080 无监听 + 无非空 -wal 残留),否则拒绝(fail-closed);
11
+ // · 打生产必须显式给 `--snapshot <dir>`(先 VACUUM INTO 快照 + 落 snapshot.json),否则拒绝;
12
+ // · 幂等:列已存在就跳过(靠 `PRAGMA table_info`,不用 `CREATE TABLE IF NOT EXISTS` —— §3.8 明文)。
13
+ //
14
+ // 用法:
15
+ // node memory-schema-migrate.mjs # dry-run(默认生产路径,只读)
16
+ // node memory-schema-migrate.mjs --db <path> # dry-run 指定库(沙箱)
17
+ // node memory-schema-migrate.mjs --db <path> --apply # 真改(非生产路径无需额外开关)
18
+ // node memory-schema-migrate.mjs --allow-production --apply --snapshot <dir> # 生产(需停机窗口)
19
+ // node memory-schema-migrate.mjs --db <path> --json # 机器可读(stdout 只有 JSON)
20
+ //
21
+ // `--home <dir>` = DSH home(即 `.dsh` 目录),默认 `~/.dsh`;库 = `<home>/memory/memory.db`。
22
+ // `--users <file>` 显式指定身份表(缺省 `<home>/secretary-users.json`)。
23
+ // 退出码:0 = 就绪 / 已迁移(幂等);1 = 有异常(被拒或复核未过);2 = 用法错;3 = 环境不可用。
24
+
25
+ import fs from "node:fs";
26
+ import path from "node:path";
27
+ import net from "node:net";
28
+ import { DatabaseSync } from "node:sqlite";
29
+ import {
30
+ MEMORIES_TABLE,
31
+ NEW_COLUMNS,
32
+ applyMigration,
33
+ describeDb,
34
+ isProductionDb,
35
+ migrationPlan,
36
+ productionDbPath,
37
+ readIdentityTable,
38
+ resolveDefaultOwner,
39
+ sha256File,
40
+ } from "./lib/memory-scope.mjs";
41
+
42
+ const argv = process.argv.slice(2);
43
+ function argRaw(name) {
44
+ const i = argv.indexOf(name);
45
+ if (i >= 0) {
46
+ const v = argv[i + 1];
47
+ return v && !v.startsWith("--") ? v : true;
48
+ }
49
+ const kv = argv.find((a) => a.startsWith(`${name}=`));
50
+ return kv ? kv.slice(name.length + 1) : null;
51
+ }
52
+ const argOf = (name, def = null) => {
53
+ const v = argRaw(name);
54
+ return v === null ? def : v;
55
+ };
56
+ const flag = (name) => argv.includes(name);
57
+ const JSON_OUT = flag("--json");
58
+ const log = (...a) => {
59
+ if (!JSON_OUT) console.log(...a);
60
+ };
61
+
62
+ if (flag("--help") || flag("-h")) {
63
+ console.log("用法见本文件头部注释;关键开关:--db --home --users --apply --allow-production --snapshot <dir> --json");
64
+ process.exitCode = 0;
65
+ } else {
66
+ main().catch((e) => {
67
+ if (JSON_OUT) console.log(JSON.stringify({ ok: false, exitCode: 1, message: `未捕获异常:${e && e.stack ? e.stack : e}` }, null, 2));
68
+ else console.error(`未捕获异常:${e && e.stack ? e.stack : e}`);
69
+ process.exitCode = 1;
70
+ });
71
+ }
72
+
73
+ async function main() {
74
+ const homeOpt = argOf("--home", null); // = .dsh 目录
75
+ const dbOpt = argRaw("--db");
76
+ const usersOpt = argRaw("--users");
77
+ const apply = flag("--apply");
78
+ const allowProd = flag("--allow-production");
79
+ const snapshotDir = argRaw("--snapshot");
80
+
81
+ const result = {
82
+ at: new Date().toISOString(),
83
+ mode: apply ? "apply" : "dry-run",
84
+ ok: false,
85
+ exitCode: 0,
86
+ steps: [],
87
+ warnings: [],
88
+ };
89
+ if (homeOpt === true || dbOpt === true || usersOpt === true || snapshotDir === true) {
90
+ return finish(result, 2, "参数错:--home / --db / --users / --snapshot 都需要一个值");
91
+ }
92
+
93
+ const dshDir = homeOpt ? path.resolve(String(homeOpt)) : path.dirname(path.dirname(productionDbPath()));
94
+ const dbPath = dbOpt ? path.resolve(String(dbOpt)) : path.join(dshDir, "memory", "memory.db");
95
+ const usersFile = usersOpt ? path.resolve(String(usersOpt)) : path.join(dshDir, "secretary-users.json");
96
+ const isProd = isProductionDb(dbPath);
97
+ result.db = dbPath;
98
+ result.isProduction = isProd;
99
+
100
+ if (!fs.existsSync(dbPath)) return finish(result, 3, `环境不可用:库不存在 ${dbPath}`);
101
+
102
+ // ---- 身份表(§3.3-3:默认值取自身份表;取不到回落常量并在自检里报一行)
103
+ const identity = readIdentityTable(usersFile);
104
+ const resolved = resolveDefaultOwner(identity);
105
+ result.identity = { path: identity.path, exists: identity.exists, ok: identity.ok, owner: identity.owner, userids: identity.userids, error: identity.error };
106
+ result.ownerDefault = { user: resolved.ownerUser, source: resolved.source };
107
+ for (const n of resolved.notes) result.warnings.push(n);
108
+ if (identity.ok) log(`✓ 身份表:${identity.userids.length} 个 userid · owner = ${resolved.ownerUser}(${identity.path})`);
109
+ else log(`⚠️ 身份表不可用(${identity.exists ? identity.error : "不存在"}:${identity.path})→ owner_user 默认值回落 '${resolved.ownerUser}'`);
110
+
111
+ // ---- 只读规划
112
+ const ro = new DatabaseSync(dbPath, { readOnly: true });
113
+ let plan;
114
+ let before;
115
+ try {
116
+ before = describeDb(ro);
117
+ plan = migrationPlan(ro, { identity });
118
+ } finally {
119
+ ro.close();
120
+ }
121
+ result.before = { columns: before.columns, count: before.count, visibilityIndex: before.visibilityIndex };
122
+ result.plan = { missing: plan.missing, present: plan.present, statements: plan.statements };
123
+
124
+ log(`库:${dbPath}${isProd ? "(**生产库**)" : "(沙箱/副本)"}`);
125
+ log(`现状:${before.columns.length} 列 · ${before.count} 条 · visibility 索引 ${before.visibilityIndex ? "在" : "缺"}`);
126
+ log(`待办:缺列 [${plan.missing.join(", ") || "无"}] · 已有 [${plan.present.join(", ") || "无"}] · 语句 ${plan.statements.length} 条`);
127
+ for (const s of plan.statements) log(` · ${s}`);
128
+
129
+ if (plan.statements.length === 0) {
130
+ result.ok = true;
131
+ result.steps.push("幂等:两列与索引均已在位,无需任何写入");
132
+ return finish(result, 0, "已是目标结构(幂等)");
133
+ }
134
+ if (!apply) {
135
+ result.ok = true;
136
+ result.steps.push("dry-run:未写入任何内容(源库只读打开)");
137
+ return finish(result, 0, "计划已列出(dry-run)");
138
+ }
139
+
140
+ // ---- 真改前的闸门
141
+ if (isProd && !allowProd) {
142
+ return finish(result, 2, "拒绝:目标是生产库,必须显式 --allow-production(未获停机窗口时不许跑)");
143
+ }
144
+ if (isProd) {
145
+ if (typeof snapshotDir !== "string") {
146
+ return finish(result, 2, "拒绝:打生产必须先给 --snapshot <dir>(VACUUM INTO 快照落回滚点)");
147
+ }
148
+ const win = await checkStopWindow([3080]);
149
+ result.stopWindow = win;
150
+ if (!win.ok) return finish(result, 1, `拒绝:停机窗口判据未过(${win.reasons.join(";")})—— 请先停 DSH 再跑`);
151
+ log(`✓ 停机窗口:3080 未监听 · -wal ${win.walBytes} 字节`);
152
+ }
153
+
154
+ // ---- 快照(先 VACUUM INTO,再动手)
155
+ if (typeof snapshotDir === "string") {
156
+ const snapDir = path.resolve(snapshotDir);
157
+ const snapPath = path.join(snapDir, "memory.db");
158
+ fs.mkdirSync(snapDir, { recursive: true });
159
+ if (fs.existsSync(snapPath)) fs.rmSync(snapPath);
160
+ const src = new DatabaseSync(dbPath, { readOnly: true });
161
+ try {
162
+ src.exec(`VACUUM INTO '${snapPath.replace(/\\/g, "/")}'`);
163
+ } finally {
164
+ src.close();
165
+ }
166
+ const snapDb = new DatabaseSync(snapPath, { readOnly: true });
167
+ let snapInfo;
168
+ try {
169
+ snapInfo = describeDb(snapDb);
170
+ } finally {
171
+ snapDb.close();
172
+ }
173
+ const meta = {
174
+ at: new Date().toISOString(),
175
+ source: dbPath,
176
+ sourceSha256: sha256File(dbPath),
177
+ sourceCount: before.count,
178
+ snapshot: snapPath,
179
+ snapshotSha256: sha256File(snapPath),
180
+ snapshotCount: snapInfo.count,
181
+ snapshotColumns: snapInfo.columns,
182
+ restoresTo: `${before.columns.length} 列(改前结构)`,
183
+ };
184
+ fs.writeFileSync(path.join(snapDir, "snapshot.json"), JSON.stringify(meta, null, 2), "utf8");
185
+ result.snapshot = meta;
186
+ if (snapInfo.count !== before.count) return finish(result, 1, `拒绝:快照条数 ${snapInfo.count} ≠ 源库 ${before.count}(快照不可信)`);
187
+ log(`✓ 快照:${snapPath} · ${snapInfo.count} 条 · sha ${String(meta.snapshotSha256).slice(0, 16)}`);
188
+ result.steps.push("VACUUM INTO 快照已落(含 snapshot.json)");
189
+ } else {
190
+ result.warnings.push("未给 --snapshot:本次未留 VACUUM INTO 快照(沙箱演练可接受,生产不允许)");
191
+ }
192
+
193
+ // ---- 执行迁移
194
+ const rw = new DatabaseSync(dbPath);
195
+ try {
196
+ const ran = applyMigration(rw, plan);
197
+ result.statementsRan = ran;
198
+ result.steps.push(`已执行 ${ran.length} 条 DDL(事务内)`);
199
+ } catch (e) {
200
+ try {
201
+ rw.close();
202
+ } catch {
203
+ /* ignore */
204
+ }
205
+ return finish(result, 1, `迁移失败:${e.message}`);
206
+ }
207
+ try {
208
+ rw.close();
209
+ } catch {
210
+ /* ignore */
211
+ }
212
+
213
+ // ---- 复核
214
+ const chk = new DatabaseSync(dbPath, { readOnly: true });
215
+ let after;
216
+ let dist;
217
+ try {
218
+ after = describeDb(chk);
219
+ dist = chk
220
+ .prepare(`SELECT owner_user AS owner, visibility AS vis, COUNT(*) AS c FROM ${MEMORIES_TABLE} GROUP BY 1, 2 ORDER BY 1, 2`)
221
+ .all();
222
+ } finally {
223
+ chk.close();
224
+ }
225
+ result.after = { columns: after.columns, count: after.count, visibilityIndex: after.visibilityIndex, distribution: dist };
226
+ const missingAfter = NEW_COLUMNS.filter((c) => !after.columns.includes(c.name)).map((c) => c.name);
227
+ const countOk = after.count === before.count;
228
+ log(`复核:${after.columns.length} 列 · ${after.count} 条 · 索引 ${after.visibilityIndex ? "在" : "缺"}`);
229
+ for (const d of dist) log(` · ${d.owner} / ${d.vis} = ${d.c} 条`);
230
+ if (missingAfter.length || !countOk || !after.visibilityIndex) {
231
+ return finish(result, 1, `复核未过:缺列 [${missingAfter.join(",")}] · 条数一致 ${countOk} · 索引 ${after.visibilityIndex}`);
232
+ }
233
+ result.ok = true;
234
+ result.steps.push("复核通过:两列 + 索引在位、条数未变");
235
+ return finish(result, 0, "迁移完成");
236
+ }
237
+
238
+ /** 停机窗口判据:3080(及调用方给的端口)无监听 **且** `memory.db-wal` 无非空残留。 */
239
+ export async function checkStopWindow(ports = [3080]) {
240
+ const reasons = [];
241
+ const walPath = `${productionDbPath()}-wal`;
242
+ let walBytes = 0;
243
+ if (fs.existsSync(walPath)) {
244
+ try {
245
+ walBytes = fs.statSync(walPath).size;
246
+ } catch {
247
+ walBytes = -1;
248
+ }
249
+ }
250
+ if (walBytes > 0) reasons.push(`memory.db-wal 非空(${walBytes} 字节,疑似有未落盘事务)`);
251
+ const live = [];
252
+ for (const p of ports) if (await probePort(p)) live.push(p);
253
+ if (live.length) reasons.push(`端口 ${live.join(",")} 在监听(DSH 在跑)`);
254
+ return { ok: reasons.length === 0, reasons, walBytes, listening: live, checkedPorts: ports };
255
+ }
256
+
257
+ /** TCP 探活(loopback)。 */
258
+ export function probePort(port, host = "127.0.0.1", timeout = 800) {
259
+ return new Promise((resolve) => {
260
+ const sock = net.createConnection({ port, host });
261
+ let done = false;
262
+ const end = (v) => {
263
+ if (done) return;
264
+ done = true;
265
+ try {
266
+ sock.destroy();
267
+ } catch {
268
+ /* ignore */
269
+ }
270
+ resolve(v);
271
+ };
272
+ sock.setTimeout(timeout);
273
+ sock.on("connect", () => end(true));
274
+ sock.on("timeout", () => end(false));
275
+ sock.on("error", () => end(false));
276
+ });
277
+ }
278
+
279
+ function finish(result, code, message) {
280
+ result.exitCode = code;
281
+ result.message = message;
282
+ if (JSON_OUT) {
283
+ console.log(JSON.stringify(result, null, 2));
284
+ } else {
285
+ console.log(`\n[${code === 0 ? "OK" : "STOP"}] ${message}`);
286
+ for (const s of result.steps) console.log(` ✓ ${s}`);
287
+ for (const w of result.warnings) console.log(` ⚠️ ${w}`);
288
+ }
289
+ process.exitCode = code;
290
+ }
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ // tools/memory-db-tools/memory-scope-fixture.mjs
3
+ //
4
+ // ⑤ 包 §5 判据 5「检索隔离可证伪」夹具(施工卡 ⑤-3 交付)。
5
+ //
6
+ // 判据(§5-5 / §3.3-4):造「主人私有条 + 家人条」→ 家人侧真跑过滤 SQL ⇒
7
+ // · 家人侧**不得**命中主人私有条;· 主人注入层**不得**注入家人条。
8
+ // 本卡补充(卡面:「造超域条 ⇒ 必须被过滤掉」):再验一条 **owner 未登记**的 `visibility='family'` 条
9
+ // ⇒ 严格模式(带 `registeredOwners` 护栏)必须把它挡掉。
10
+ //
11
+ // ⚠️ 本夹具**会写库**(造条)⇒ **硬拒绝生产路径**(不给 `--allow-production` 逃生舱:对生产做这事无意义)。
12
+ // 默认在沙箱副本(`VACUUM INTO` 出来的那份,或 `_archive/.../sandbox/` 工作副本)上跑。
13
+ //
14
+ // 用法:
15
+ // node memory-scope-fixture.mjs --db <沙箱库> [--json] [--cleanup]
16
+ // 退出码:0 = 全部断言通过;1 = 有断言失败;2 = 用法错或路径被拒;3 = 环境不可用
17
+
18
+ import fs from "node:fs";
19
+ import path from "node:path";
20
+ import { DatabaseSync } from "node:sqlite";
21
+ import {
22
+ MEMORIES_TABLE,
23
+ familyScopeSql,
24
+ isProductionDb,
25
+ ownerScopeSql,
26
+ productionDbPath,
27
+ selectMemories,
28
+ } from "./lib/memory-scope.mjs";
29
+
30
+ const MARK = "card053-fixture";
31
+ const argv = process.argv.slice(2);
32
+ function argRaw(name) {
33
+ const i = argv.indexOf(name);
34
+ if (i >= 0) {
35
+ const v = argv[i + 1];
36
+ return v && !v.startsWith("--") ? v : true;
37
+ }
38
+ const kv = argv.find((a) => a.startsWith(`${name}=`));
39
+ return kv ? kv.slice(name.length + 1) : null;
40
+ }
41
+ const JSON_OUT = argv.includes("--json");
42
+ const log = (...a) => {
43
+ if (!JSON_OUT) console.log(...a);
44
+ };
45
+
46
+ /** 夹具身份表(wife_test / husband_test 已登记;stranger_unregistered **故意不登记**)。 */
47
+ const FIXTURE_IDENTITY = Object.freeze({
48
+ LiJianXiao: { name: "主人", role: "owner", caps: ["*"] },
49
+ wife_test: { name: "家人A(夹具)", role: "member", caps: [] },
50
+ husband_test: { name: "家人B(夹具)", role: "member", caps: [] },
51
+ });
52
+
53
+ /** 夹具里"来自身份表"的已登记集合(= 严格模式护栏的输入)。 */
54
+ const REGISTERED = Object.freeze(Object.keys(FIXTURE_IDENTITY));
55
+
56
+ const CASES = Object.freeze([
57
+ { key: "owner_private", owner: "LiJianXiao", visibility: "private_owner", expectFamily: false, expectOwner: true, why: "主人私有条(对家人侧超域)" },
58
+ { key: "owner_family", owner: "LiJianXiao", visibility: "family", expectFamily: true, expectOwner: true, why: "主人显式共享条" },
59
+ { key: "owner_public", owner: "LiJianXiao", visibility: "public", expectFamily: true, expectOwner: true, why: "公开条" },
60
+ { key: "wife_private", owner: "wife_test", visibility: "private_owner", expectFamily: true, expectOwner: false, why: "家人A自己的私有条(本人可见)" },
61
+ { key: "husband_private", owner: "husband_test", visibility: "private_owner", expectFamily: false, expectOwner: false, why: "家人B私有条(对家人A超域)" },
62
+ { key: "stranger_family", owner: "stranger_unregistered", visibility: "family", expectFamily: false, expectOwner: false, why: "**超域条**:owner 未登记却标了 family" },
63
+ ]);
64
+
65
+ function main() {
66
+ const dbOpt = argRaw("--db");
67
+ const cleanup = argv.includes("--cleanup");
68
+ const out = { at: new Date().toISOString(), check: "判据5-检索隔离", ok: false, exitCode: 0, assertions: [], knownBoundaries: [] };
69
+ if (!dbOpt || dbOpt === true) return finish(out, 2, "参数错:必须给 --db <沙箱库路径>(本夹具会写库,不接受默认生产路径)");
70
+ const dbPath = path.resolve(String(dbOpt));
71
+ out.db = dbPath;
72
+
73
+ if (isProductionDb(dbPath)) {
74
+ return finish(out, 2, `拒绝:${dbPath} 是生产库 —— 本夹具会造条写库,只允许打沙箱副本(生产库路径 = ${productionDbPath()})`);
75
+ }
76
+ if (!fs.existsSync(dbPath)) return finish(out, 3, `环境不可用:库不存在 ${dbPath}`);
77
+
78
+ const db = new DatabaseSync(dbPath);
79
+ const now = Date.now();
80
+ const insert = db.prepare(`INSERT INTO ${MEMORIES_TABLE} (text, tags, pinned, created_at, updated_at, owner_user, visibility) VALUES (?, ?, 0, ?, ?, ?, ?)`);
81
+ const ids = {};
82
+
83
+ try {
84
+ // ---- 前置:两列必须在位(迁移跑过)
85
+ const cols = db.prepare(`PRAGMA table_info(${MEMORIES_TABLE})`).all().map((r) => r.name);
86
+ const missing = ["owner_user", "visibility"].filter((c) => !cols.includes(c));
87
+ if (missing.length) return finish(out, 2, `不可用:缺列 [${missing.join(", ")}] —— 先跑 memory-schema-migrate.mjs`);
88
+
89
+ // ---- 清掉上一轮夹具条(幂等重跑)
90
+ const pre = db.prepare(`DELETE FROM ${MEMORIES_TABLE} WHERE tags LIKE ?`).run(`%${MARK}%`);
91
+ if (pre.changes) log(`(清掉上一轮夹具条 ${pre.changes} 条)`);
92
+
93
+ // ---- 造条
94
+ for (const c of CASES) {
95
+ const r = insert.run(
96
+ `[夹具 ${MARK}] ${c.key} —— ${c.why}`,
97
+ `${MARK} ${c.key}`,
98
+ now,
99
+ now,
100
+ c.owner,
101
+ c.visibility,
102
+ );
103
+ ids[c.key] = Number(r.lastInsertRowid);
104
+ }
105
+ out.fixtureIds = ids;
106
+ log(`造条:${CASES.length} 条(tags 含 ${MARK})`);
107
+ for (const c of CASES) log(` · #${ids[c.key]} ${c.owner} / ${c.visibility} —— ${c.why}`);
108
+
109
+ // ---- 家人侧(self = wife_test)· 基础模式(§3.3-4 原文)
110
+ // ⚠️ 所有断言都只针对**本次造的夹具条**(`tags LIKE '<MARK>%'`):真实库有近 600 条数据,
111
+ // 不限定就会被 LIMIT 截断 ⇒ 断言假绿/假红(2026-09-20 本卡实测踩过,见 lib.selectMemories 注释)。
112
+ const fx = (scope) => selectMemories(db, scope, { limit: 10000, tagLike: `%${MARK}%` });
113
+ const base = familyScopeSql("wife_test");
114
+ const famBaseKeys = keysOf(fx(base), ids);
115
+
116
+ // ---- 家人侧 · 严格模式(带已登记护栏)
117
+ const strict = familyScopeSql("wife_test", { registeredOwners: REGISTERED });
118
+ const famStrictKeys = keysOf(fx(strict), ids);
119
+
120
+ // ---- 主人侧注入层
121
+ const ownerScope = ownerScopeSql("LiJianXiao");
122
+ const ownerKeys = keysOf(fx(ownerScope), ids);
123
+
124
+ // ---- 身份表不可用 ⇒ fail-closed(只留本人条)
125
+ const failClosed = familyScopeSql("wife_test", { registeredOwners: [] });
126
+ const fcKeys = keysOf(fx(failClosed), ids);
127
+
128
+ out.scopes = {
129
+ familyBase: { mode: base.mode, where: base.where, hit: famBaseKeys },
130
+ familyStrict: { mode: strict.mode, where: strict.where, hit: famStrictKeys },
131
+ ownerInject: { mode: ownerScope.mode, where: ownerScope.where, hit: ownerKeys },
132
+ familyFailClosed: { mode: failClosed.mode, where: failClosed.where, hit: fcKeys },
133
+ };
134
+
135
+ // ---- 断言
136
+ const A = (key, pass, detail) => out.assertions.push({ key, pass: Boolean(pass), detail });
137
+
138
+ A("家人侧基础模式不命中主人私有条", !famBaseKeys.includes("owner_private"), `命中=[${famBaseKeys.join(", ")}]`);
139
+ A("家人侧基础模式不命中另一家人私有条", !famBaseKeys.includes("husband_private"), `命中=[${famBaseKeys.join(", ")}]`);
140
+ A("家人侧基础模式命中 family/public 条", famBaseKeys.includes("owner_family") && famBaseKeys.includes("owner_public"), `命中=[${famBaseKeys.join(", ")}]`);
141
+ A("家人侧基础模式命中自己的私有条", famBaseKeys.includes("wife_private"), `命中=[${famBaseKeys.join(", ")}]`);
142
+ A("家人侧严格模式不命中超域条", !famStrictKeys.includes("stranger_family"), `命中=[${famStrictKeys.join(", ")}]`);
143
+ A("家人侧严格模式不命中主人私有条", !famStrictKeys.includes("owner_private"), `命中=[${famStrictKeys.join(", ")}]`);
144
+ A("主人注入层不注入家人条", !ownerKeys.includes("wife_private") && !ownerKeys.includes("husband_private"), `命中=[${ownerKeys.join(", ")}]`);
145
+ A("主人注入层命中主人自己的条", ownerKeys.includes("owner_private") && ownerKeys.includes("owner_family"), `命中=[${ownerKeys.join(", ")}]`);
146
+ A("主人注入层不注入超域条", !ownerKeys.includes("stranger_family"), `命中=[${ownerKeys.join(", ")}]`);
147
+ A("身份表不可用 ⇒ fail-closed 只返回本人条", fcKeys.length === 1 && fcKeys[0] === "wife_private", `命中=[${fcKeys.join(", ")}]`);
148
+
149
+ // ---- 已知边界(如实记录,不计失败)
150
+ out.knownBoundaries.push({
151
+ key: "基础模式会命中超域条",
152
+ detail: famBaseKeys.includes("stranger_family")
153
+ ? "基础句(§3.3-4 原文:visibility IN ('family','public') OR owner_user=:self)**会**命中 owner 未登记却标 family 的条 ⇒ 实现层应使用严格模式(带 registeredOwners 护栏),且判据 4 会同时判红"
154
+ : "基础句本轮未命中超域条(身份/数据组合变了,需复核)",
155
+ });
156
+
157
+ const failed = out.assertions.filter((a) => !a.pass);
158
+ for (const a of out.assertions) log(` ${a.pass ? "✓" : "✗"} ${a.key} —— ${a.detail}`);
159
+ log(`已知边界:${out.knownBoundaries[0].detail}`);
160
+
161
+ if (cleanup) {
162
+ const del = db.prepare(`DELETE FROM ${MEMORIES_TABLE} WHERE tags LIKE ?`).run(`%${MARK}%`);
163
+ out.cleanedUp = del.changes;
164
+ log(`清理:删除夹具条 ${del.changes} 条`);
165
+ }
166
+ return finish(out, failed.length ? 1 : 0, failed.length ? `${failed.length} 条断言失败` : "判据 5 全部断言通过(家人侧不越域 · 主人侧不注入他人条 · 超域条被严格模式挡掉)");
167
+ } finally {
168
+ try {
169
+ db.close();
170
+ } catch {
171
+ /* ignore */
172
+ }
173
+ }
174
+ }
175
+
176
+ function keysOf(rows, ids) {
177
+ const byId = new Map(Object.entries(ids).map(([k, v]) => [Number(v), k]));
178
+ return rows.map((r) => byId.get(Number(r.id))).filter(Boolean).sort();
179
+ }
180
+
181
+ function finish(out, code, message) {
182
+ out.exitCode = code;
183
+ out.ok = code === 0;
184
+ out.message = message;
185
+ out.failed = out.assertions.filter((a) => !a.pass).map((a) => a.key);
186
+ if (JSON_OUT) console.log(JSON.stringify(out, null, 2));
187
+ else console.log(`\n[${code === 0 ? "PASS" : code === 2 || code === 3 ? "UNAVAILABLE" : "FAIL"}] ${message}`);
188
+ process.exitCode = code;
189
+ }
190
+
191
+ // ---- 入口(放文件末尾:main 依赖的 const 表都在上方声明,避免 TDZ)
192
+ if (argv.includes("--help") || argv.includes("-h")) {
193
+ console.log("用法见本文件头部注释;开关:--db <沙箱库> [--json] [--cleanup]");
194
+ process.exitCode = 0;
195
+ } else {
196
+ main();
197
+ }
@@ -0,0 +1,26 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+
3
+ // 只读读指定记忆全文(替代 PowerShell 内联 node -e 读库,避免引号/转义地狱,见记忆 #46 第 2 条)。
4
+ // 用法:
5
+ // node read-memories.mjs 9 27 读 #9、#27 全文
6
+ // node read-memories.mjs --all 输出全部记忆全文
7
+ // (dsh 运行中也可安全执行,WAL 模式只读无冲突)
8
+ const db = new DatabaseSync(`${process.env.USERPROFILE.replaceAll("\\", "/")}/.dsh/memory/memory.db`, { readOnly: true });
9
+ const args = process.argv.slice(2);
10
+ let rows;
11
+ if (args.length === 0 || args.includes("--all")) {
12
+ rows = db.prepare("SELECT id, text, pinned FROM memories ORDER BY id").all();
13
+ } else {
14
+ const ids = args.map(Number).filter(Number.isInteger);
15
+ if (!ids.length) {
16
+ console.error("用法:node read-memories.mjs <id> [id ...] 或 --all");
17
+ process.exit(1);
18
+ }
19
+ const placeholders = ids.map(() => "?").join(",");
20
+ rows = db.prepare(`SELECT id, text, pinned FROM memories WHERE id IN (${placeholders}) ORDER BY id`).all(...ids);
21
+ }
22
+ for (const r of rows) {
23
+ console.log(`=== #${r.id} [pinned=${r.pinned === 1 ? "Y" : "-"}] ===`);
24
+ console.log(r.text + "\n");
25
+ }
26
+ db.close();
@@ -0,0 +1,20 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+
3
+ const db = new DatabaseSync(`${process.env.USERPROFILE.replaceAll("\\", "/")}/.dsh/memory/memory.db`, { readOnly: true });
4
+
5
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
6
+ console.log("=== TABLES ===");
7
+ console.log(JSON.stringify(tables, null, 2));
8
+
9
+ for (const t of tables) {
10
+ const name = t.name;
11
+ try {
12
+ const cols = db.prepare(`PRAGMA table_info(${name})`).all();
13
+ console.log(`\n=== TABLE ${name} columns ===`);
14
+ console.log(JSON.stringify(cols, null, 2));
15
+ } catch (e) {
16
+ console.log(`\n=== TABLE ${name} === error: ${e.message}`);
17
+ }
18
+ }
19
+
20
+ db.close();