@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,245 @@
1
+ #!/usr/bin/env node
2
+ // audit-dump-ids.mjs — 季度记忆库体检:按 id 取全文(分批精读用)
3
+ // 固化自早期临时脚本(2026-09-25 体检工具链固化)
4
+ // · 去硬编码:输出目录改为 --out <file>(默认系统临时目录),库路径改为 --db(默认动态解析)
5
+ // · 加 --help / --self-test / --stdout · 收尾 process.exitCode
6
+ // 用途:把指定 id 的记忆全文批量写成文本(或打印),规避大段中文走 stdout 的编码/截断坑。
7
+ // 只读:数据库以 { readOnly: true } 打开,绝不写库;产物只写 --out 指定文件。
8
+
9
+ import fs from "node:fs";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import crypto from "node:crypto";
13
+ import { DatabaseSync } from "node:sqlite";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ export const SELF = fileURLToPath(import.meta.url);
17
+ export const TOOL = path.basename(SELF);
18
+
19
+ export function resolveDshHome() {
20
+ if (process.env.DSH_HOME) return process.env.DSH_HOME;
21
+ return path.join(process.env.USERPROFILE || os.homedir(), ".dsh");
22
+ }
23
+ export const DSH_HOME = resolveDshHome();
24
+ export const DEFAULT_DB = path.join(DSH_HOME, "memory", "memory.db");
25
+ export const DEFAULT_WORK_DIR = path.join(os.tmpdir(), "dsh-mem-audit");
26
+
27
+ export function isMainEntry() {
28
+ const a = process.argv[1];
29
+ if (!a) return false;
30
+ try {
31
+ return fs.realpathSync(a) === fs.realpathSync(SELF);
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ export function parseArgs(argv) {
38
+ const out = { _: [] };
39
+ for (let i = 0; i < argv.length; i++) {
40
+ const a = argv[i];
41
+ if (a === "--") {
42
+ out._.push(...argv.slice(i + 1));
43
+ break;
44
+ }
45
+ if (a.startsWith("--")) {
46
+ const eq = a.indexOf("=");
47
+ if (eq > 0) out[a.slice(2, eq)] = a.slice(eq + 1);
48
+ else {
49
+ const k = a.slice(2);
50
+ const next = argv[i + 1];
51
+ if (next !== undefined && !next.startsWith("--")) {
52
+ out[k] = next;
53
+ i++;
54
+ } else out[k] = true;
55
+ }
56
+ } else out._.push(a);
57
+ }
58
+ return out;
59
+ }
60
+
61
+ export function makeAsserter() {
62
+ let pass = 0,
63
+ total = 0;
64
+ const fails = [];
65
+ return {
66
+ ok(cond, label) {
67
+ total++;
68
+ if (cond) pass++;
69
+ else fails.push(label);
70
+ },
71
+ eq(actual, expected, label) {
72
+ this.ok(actual === expected, `${label}(期望 ${JSON.stringify(expected)},实际 ${JSON.stringify(actual)})`);
73
+ },
74
+ report() {
75
+ if (fails.length) {
76
+ console.log(`[self-test] FAIL ${pass}/${total}`);
77
+ for (const f of fails) console.log(` - ${f}`);
78
+ return 1;
79
+ }
80
+ console.log(`[self-test] PASS ${pass}/${total}`);
81
+ return 0;
82
+ },
83
+ };
84
+ }
85
+
86
+ export function sha256File(p) {
87
+ return crypto.createHash("sha256").update(fs.readFileSync(p)).digest("hex").toUpperCase();
88
+ }
89
+
90
+ export function parseIds(raw) {
91
+ const parts = Array.isArray(raw) ? raw : String(raw ?? "").split(/[,\s]+/);
92
+ return parts
93
+ .flatMap((s) => String(s).split(/[,\s]+/))
94
+ .map((s) => s.trim())
95
+ .filter(Boolean)
96
+ .map(Number)
97
+ .filter(Number.isInteger);
98
+ }
99
+
100
+ // ---------- 核心逻辑 ----------
101
+ export function dumpIds(dbPath, ids) {
102
+ const db = new DatabaseSync(dbPath, { readOnly: true });
103
+ try {
104
+ const ph = ids.map(() => "?").join(",");
105
+ const rows = db.prepare(`SELECT id, text, pinned FROM memories WHERE id IN (${ph}) ORDER BY id`).all(...ids);
106
+ const text = rows
107
+ .map((r) => `=== #${r.id} [pinned=${r.pinned === 1 ? "Y" : "-"}] ===\n${r.text}`)
108
+ .join("\n\n");
109
+ return { rows, text, chars: rows.reduce((s, r) => s + r.text.length, 0) };
110
+ } finally {
111
+ db.close();
112
+ }
113
+ }
114
+
115
+ const USAGE = `audit-dump-ids.mjs — 季度体检:按 id 取记忆全文(分批精读)
116
+
117
+ 用法:
118
+ node ${TOOL} --ids 1,2,3 [--out <file>] [--db <path>]
119
+ node ${TOOL} --ids 1 2 3 --stdout
120
+ node ${TOOL} --help
121
+ node ${TOOL} --self-test
122
+
123
+ 参数:
124
+ --ids <a,b,c> 要取全文的记忆 id(逗号或空格分隔;也可用位置参数:node ${TOOL} 1 2 3)
125
+ --out <file> 输出文件,默认 ${DEFAULT_WORK_DIR}\\dump-ids-<时间戳>.txt
126
+ --stdout 直接打印到标准输出(小批量方便;大批量仍建议 --out 防截断)
127
+ --db <path> 记忆库路径,默认 ${DEFAULT_DB}
128
+ --json 以 JSON 输出(含路径与条数)
129
+
130
+ 退出码:
131
+ 0 成功
132
+ 1 --self-test 断言失败
133
+ 2 用法错误(未给任何 id)
134
+ 3 记忆库不存在
135
+ 4 运行错误
136
+
137
+ 只读:数据库以 { readOnly: true } 打开;产物只写 --out(默认临时目录,不落生产路径)。
138
+ `;
139
+
140
+ export function defaultOut(now = new Date()) {
141
+ // 用本地时间命名(不用 toISOString:那是 UTC,本地 01:21 会写成前一天的 17:21,看着像旧文件)
142
+ const p = (n) => String(n).padStart(2, "0");
143
+ const ts = `${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}${p(now.getSeconds())}`;
144
+ return path.join(DEFAULT_WORK_DIR, `dump-ids-${ts}.txt`);
145
+ }
146
+
147
+ export function main(argv = process.argv.slice(2)) {
148
+ const a = parseArgs(argv);
149
+ if (a.help) {
150
+ console.log(USAGE);
151
+ return 0;
152
+ }
153
+ if (typeof a["self-test"] !== "undefined") return selfTest();
154
+
155
+ const ids = parseIds(a.ids ?? a._);
156
+ if (!ids.length) {
157
+ console.error(`[${TOOL}] 未提供 id。用法见 --help(示例:node ${TOOL} --ids 1,2,3)`);
158
+ return 2;
159
+ }
160
+ const dbPath = typeof a.db === "string" ? path.resolve(a.db) : DEFAULT_DB;
161
+ if (!fs.existsSync(dbPath)) {
162
+ console.error(`[${TOOL}] 记忆库不存在:${dbPath}`);
163
+ return 3;
164
+ }
165
+
166
+ const { rows, text, chars } = dumpIds(dbPath, ids);
167
+ if (a.stdout === true) {
168
+ console.log(text);
169
+ console.error(`[${TOOL}] ${rows.length} 条 / ${chars} 字符(stdout)`);
170
+ return 0;
171
+ }
172
+ const outFile = typeof a.out === "string" ? path.resolve(a.out) : defaultOut();
173
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
174
+ fs.writeFileSync(outFile, text, "utf8");
175
+ const missing = ids.filter((id) => !rows.some((r) => r.id === id));
176
+ if (a.json) console.log(JSON.stringify({ db: dbPath, out: outFile, rows: rows.length, chars, missing }, null, 1));
177
+ else {
178
+ console.log(`OUT=${outFile}`);
179
+ console.log(`${rows.length} 条 / ${chars} 字符${missing.length ? "|未找到 id: " + missing.join(",") : ""}`);
180
+ }
181
+ return 0;
182
+ }
183
+
184
+ // ---------- self-test(无副作用:合成临时库) ----------
185
+ export function selfTest() {
186
+ const t = makeAsserter();
187
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "audit-dumpids-selftest-"));
188
+ const dbFile = path.join(tmp, "fixture.db");
189
+ const outFile = path.join(tmp, "out", "ids.txt");
190
+ try {
191
+ const db = new DatabaseSync(dbFile); // 合成夹具库(临时目录,非生产)
192
+ try {
193
+ db.exec(
194
+ "CREATE TABLE memories (id INTEGER PRIMARY KEY, text TEXT NOT NULL, tags TEXT, pinned INTEGER DEFAULT 0, created_at INTEGER, updated_at INTEGER)",
195
+ );
196
+ const ins = db.prepare("INSERT INTO memories (id, text, tags, pinned, created_at, updated_at) VALUES (?,?,?,?,?,?)");
197
+ ins.run(7, "第七条记忆全文·甲乙丙", "t", 1, 1000, 1000);
198
+ ins.run(8, "第八条记忆全文·丁戊己", "", 0, 2000, 2000);
199
+ ins.run(9, "第九条记忆全文·庚辛壬", "", 0, 3000, 3000);
200
+ } finally {
201
+ db.close();
202
+ }
203
+
204
+ const before = sha256File(dbFile);
205
+ t.eq(parseIds("7,8").length, 2, "parseIds 逗号分隔");
206
+ t.eq(parseIds("7 8").length, 2, "parseIds 空格分隔");
207
+ t.eq(parseIds(["7", "8"]).length, 2, "parseIds 数组");
208
+ t.eq(parseIds("abc").length, 0, "parseIds 过滤非数字");
209
+
210
+ const { rows, text, chars } = dumpIds(dbFile, [7, 9]);
211
+ t.eq(rows.length, 2, "取到条数");
212
+ t.eq(chars, "第七条记忆全文·甲乙丙".length + "第九条记忆全文·庚辛壬".length, "字符合计");
213
+ t.ok(text.includes("=== #7 [pinned=Y] ==="), "pinned 标记 Y");
214
+ t.ok(text.includes("第九条记忆全文·庚辛壬"), "全文内容写入");
215
+
216
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
217
+ fs.writeFileSync(outFile, text, "utf8");
218
+ t.eq(fs.readFileSync(outFile, "utf8"), text, "落盘内容与返回值一致");
219
+ t.ok(
220
+ /dump-ids-20260925-010203\.txt$/.test(defaultOut(new Date(2026, 8, 25, 1, 2, 3))),
221
+ "默认文件名用本地时间戳(YYYYMMDD-HHmmss)",
222
+ );
223
+
224
+ // 自证只读
225
+ t.eq(sha256File(dbFile), before, "合成库 sha256 前后一致(未写库)");
226
+ const src = fs.readFileSync(SELF, "utf8");
227
+ const calls = src.split("// ---------- self-test")[0].match(/new DatabaseSync\([^)]*\)/g) ?? [];
228
+ t.ok(calls.length > 0, "主体逻辑段确实打开了数据库(探针真的在读库)");
229
+ t.ok(calls.every((s) => /readOnly: true/.test(s)), "主体逻辑段每次 new DatabaseSync( 都带 readOnly: true");
230
+ } catch (e) {
231
+ t.ok(false, `self-test 抛错:${e?.message ?? e}`);
232
+ } finally {
233
+ fs.rmSync(tmp, { recursive: true, force: true });
234
+ }
235
+ return t.report();
236
+ }
237
+
238
+ if (isMainEntry()) {
239
+ try {
240
+ process.exitCode = main();
241
+ } catch (e) {
242
+ console.error(`[${TOOL}] 运行错误:${e?.stack ?? e}`);
243
+ process.exitCode = 4;
244
+ }
245
+ }
@@ -0,0 +1,270 @@
1
+ #!/usr/bin/env node
2
+ // audit-export.mjs — 季度记忆库体检 · 第 1 站:只读导出
3
+ // 固化自早期临时脚本(2026-09-25 体检工具链固化)
4
+ // · 去硬编码:USERPROFILE / ~\.dsh → DSH_HOME 环境变量或家目录动态解析(见 resolveDshHome)
5
+ // · 输出可传参:--out <file>,默认落系统临时目录(不是生产路径)
6
+ // · 加 --help / --self-test · 收尾一律 process.exitCode(禁 process.exit)
7
+ // 用途:把 memory.db 的 memories 表全量导出为 memories.json —— 下游探针(audit-analyze / audit-struct-probe /
8
+ // audit-hit-probe / audit-verify-unchanged)的唯一数据源。
9
+ // 只读:以 { readOnly: true } 打开生产库,绝不写库;产物只写 --out 指定文件。
10
+
11
+ import fs from "node:fs";
12
+ import os from "node:os";
13
+ import path from "node:path";
14
+ import crypto from "node:crypto";
15
+ import { DatabaseSync } from "node:sqlite";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ export const SELF = fileURLToPath(import.meta.url);
19
+ export const TOOL = path.basename(SELF);
20
+
21
+ // ---------- 路径基准(全部动态,无写死盘符 / 用户名) ----------
22
+ export function resolveDshHome() {
23
+ if (process.env.DSH_HOME) return process.env.DSH_HOME;
24
+ return path.join(process.env.USERPROFILE || os.homedir(), ".dsh");
25
+ }
26
+ export const DSH_HOME = resolveDshHome();
27
+ export const DEFAULT_DB = path.join(DSH_HOME, "memory", "memory.db");
28
+ export const DEFAULT_WORK_DIR = path.join(os.tmpdir(), "dsh-mem-audit");
29
+ export const DEFAULT_OUT = path.join(DEFAULT_WORK_DIR, "memories.json");
30
+
31
+ // ---------- 通用小工具 ----------
32
+ export function isMainEntry() {
33
+ const a = process.argv[1];
34
+ if (!a) return false;
35
+ try {
36
+ return fs.realpathSync(a) === fs.realpathSync(SELF);
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ export function parseArgs(argv) {
43
+ const out = { _: [] };
44
+ for (let i = 0; i < argv.length; i++) {
45
+ const a = argv[i];
46
+ if (a === "--") {
47
+ out._.push(...argv.slice(i + 1));
48
+ break;
49
+ }
50
+ if (a.startsWith("--")) {
51
+ const eq = a.indexOf("=");
52
+ if (eq > 0) out[a.slice(2, eq)] = a.slice(eq + 1);
53
+ else {
54
+ const k = a.slice(2);
55
+ const next = argv[i + 1];
56
+ if (next !== undefined && !next.startsWith("--")) {
57
+ out[k] = next;
58
+ i++;
59
+ } else out[k] = true;
60
+ }
61
+ } else out._.push(a);
62
+ }
63
+ return out;
64
+ }
65
+
66
+ export function sha256File(p) {
67
+ return crypto.createHash("sha256").update(fs.readFileSync(p)).digest("hex").toUpperCase();
68
+ }
69
+
70
+ export function makeAsserter() {
71
+ let pass = 0,
72
+ total = 0;
73
+ const fails = [];
74
+ return {
75
+ ok(cond, label) {
76
+ total++;
77
+ if (cond) pass++;
78
+ else fails.push(label);
79
+ },
80
+ eq(actual, expected, label) {
81
+ this.ok(actual === expected, `${label}(期望 ${JSON.stringify(expected)},实际 ${JSON.stringify(actual)})`);
82
+ },
83
+ skip(label) {
84
+ console.log(` · SKIP ${label}`);
85
+ },
86
+ report() {
87
+ if (fails.length) {
88
+ console.log(`[self-test] FAIL ${pass}/${total}`);
89
+ for (const f of fails) console.log(` - ${f}`);
90
+ return 1;
91
+ }
92
+ console.log(`[self-test] PASS ${pass}/${total}`);
93
+ return 0;
94
+ },
95
+ };
96
+ }
97
+
98
+ // ---------- 核心逻辑(可被 import,供 self-test 复用) ----------
99
+ export function readMemories(dbPath) {
100
+ const db = new DatabaseSync(dbPath, { readOnly: true });
101
+ try {
102
+ const rows = db
103
+ .prepare("SELECT id, text, tags, pinned, created_at, updated_at FROM memories ORDER BY id")
104
+ .all();
105
+ return rows.map((r) => ({
106
+ id: r.id,
107
+ text: r.text ?? "",
108
+ tags: r.tags ?? "",
109
+ pinned: r.pinned === 1,
110
+ created_at: r.created_at,
111
+ updated_at: r.updated_at,
112
+ }));
113
+ } finally {
114
+ db.close();
115
+ }
116
+ }
117
+
118
+ export function deviceStats(rows) {
119
+ const dev = { ROG: 0, TUF: 0, NONE: 0, OTHER: 0, DOUBLE: 0 };
120
+ for (const r of rows) {
121
+ const m = r.text.match(/【设备[::]\s*([^】]*)】/g);
122
+ if (!m) dev.NONE++;
123
+ else if (m.length > 1) dev.DOUBLE++;
124
+ else if (/ROG/.test(m[0])) dev.ROG++;
125
+ else if (/TUF/.test(m[0])) dev.TUF++;
126
+ else dev.OTHER++;
127
+ }
128
+ return dev;
129
+ }
130
+
131
+ export function summarize(rows) {
132
+ return {
133
+ total: rows.length,
134
+ pinned: rows.filter((r) => r.pinned).length,
135
+ chars: rows.reduce((s, r) => s + r.text.length, 0),
136
+ dev: deviceStats(rows),
137
+ pinnedIds: rows.filter((r) => r.pinned).map((r) => r.id),
138
+ tsSampleFirst: rows.slice(0, 2).map((r) => [r.id, r.created_at, r.updated_at]),
139
+ tsSampleLast: rows.slice(-2).map((r) => [r.id, r.created_at, r.updated_at]),
140
+ };
141
+ }
142
+
143
+ export function writeExport(dbPath, outFile) {
144
+ const rows = readMemories(dbPath);
145
+ fs.mkdirSync(path.dirname(path.resolve(outFile)), { recursive: true });
146
+ fs.writeFileSync(outFile, JSON.stringify(rows), "utf8");
147
+ return { rows, stats: summarize(rows) };
148
+ }
149
+
150
+ // ---------- CLI ----------
151
+ const USAGE = `audit-export.mjs — 季度记忆库体检 · 第 1 站:只读导出 memory.db → memories.json
152
+
153
+ 用法:
154
+ node ${TOOL} [--db <path>] [--out <file>] [--force] [--json] [--quiet]
155
+ node ${TOOL} --help
156
+ node ${TOOL} --self-test
157
+
158
+ 参数:
159
+ --db <path> 记忆库路径,默认 ${DEFAULT_DB}
160
+ --out <file> 导出目标文件,默认 ${DEFAULT_OUT}
161
+ (默认落系统临时目录;季度体检建议显式传 --out 到自己建的体检目录)
162
+ --force 目标文件已存在时允许覆盖(默认拒绝,保护基线快照)
163
+ --json 以 JSON 输出统计(含路径)
164
+ --quiet 不打印统计明细
165
+
166
+ 退出码:
167
+ 0 成功
168
+ 1 --self-test 断言失败
169
+ 2 用法错误
170
+ 3 记忆库不存在 / 输出文件已存在且未加 --force
171
+ 4 运行错误(SQLite 读取或 JSON 写出失败)
172
+
173
+ 只读保证:数据库一律以 { readOnly: true } 打开,本工具绝不写入记忆库。
174
+ `;
175
+
176
+ export function main(argv = process.argv.slice(2)) {
177
+ const a = parseArgs(argv);
178
+ if (a.help) {
179
+ console.log(USAGE);
180
+ return 0;
181
+ }
182
+ if (typeof a["self-test"] !== "undefined") return selfTest();
183
+
184
+ const dbPath = typeof a.db === "string" ? path.resolve(a.db) : DEFAULT_DB;
185
+ const outFile = typeof a.out === "string" ? path.resolve(a.out) : DEFAULT_OUT;
186
+ if (!fs.existsSync(dbPath)) {
187
+ console.error(`[${TOOL}] 记忆库不存在:${dbPath}(可用 --db 指定)`);
188
+ return 3;
189
+ }
190
+ if (fs.existsSync(outFile) && a.force !== true) {
191
+ console.error(`[${TOOL}] 输出文件已存在:${outFile}`);
192
+ console.error(" 拒绝覆盖(该文件是 audit-verify-unchanged 的基线快照)——加 --force 覆盖,或用 --out 换路径。");
193
+ return 3;
194
+ }
195
+
196
+ const { stats } = writeExport(dbPath, outFile);
197
+ if (a.json) {
198
+ console.log(JSON.stringify({ db: dbPath, out: outFile, ...stats }, null, 1));
199
+ } else if (a.quiet !== true) {
200
+ console.log(`DB=${dbPath}`);
201
+ console.log(`OUT=${outFile}`);
202
+ console.log(`TOTAL=${stats.total} PINNED=${stats.pinned} CHARS=${stats.chars}`);
203
+ console.log("DEV=" + JSON.stringify(stats.dev));
204
+ console.log("PINNED_IDS=" + stats.pinnedIds.join(","));
205
+ console.log("TS_SAMPLE=" + JSON.stringify(stats.tsSampleFirst));
206
+ console.log("TS_SAMPLE_LAST=" + JSON.stringify(stats.tsSampleLast));
207
+ }
208
+ return 0;
209
+ }
210
+
211
+ // ---------- self-test(无副作用:合成临时库 + 临时目录) ----------
212
+ export function selfTest() {
213
+ const t = makeAsserter();
214
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "audit-export-selftest-"));
215
+ const dbFile = path.join(tmp, "fixture.db");
216
+ const outFile = path.join(tmp, "out", "memories.json");
217
+ try {
218
+ const db = new DatabaseSync(dbFile); // 合成夹具库(临时目录,非生产)
219
+ try {
220
+ db.exec(
221
+ "CREATE TABLE memories (id INTEGER PRIMARY KEY, text TEXT NOT NULL, tags TEXT, pinned INTEGER DEFAULT 0, created_at INTEGER, updated_at INTEGER)",
222
+ );
223
+ const ins = db.prepare(
224
+ "INSERT INTO memories (id, text, tags, pinned, created_at, updated_at) VALUES (?,?,?,?,?,?)",
225
+ );
226
+ ins.run(1, "【设备:本机】第一条测试记忆,路径 <workspace>", "t1", 1, 1000, 1000);
227
+ ins.run(2, "【设备:TUF】第二条测试记忆", "t2", 0, 2000, 2000);
228
+ ins.run(3, "第三条无设备标记忆", "", 0, 3000, 3000);
229
+ } finally {
230
+ db.close();
231
+ }
232
+
233
+ const before = sha256File(dbFile);
234
+ const { rows, stats } = writeExport(dbFile, outFile);
235
+
236
+ t.eq(rows.length, 3, "导出条数");
237
+ t.eq(rows[0].pinned, true, "pinned 归一为 boolean");
238
+ t.eq(stats.pinned, 1, "pinned 计数");
239
+ t.eq(stats.dev.ROG, 1, "设备标 ROG 计数");
240
+ t.eq(stats.dev.TUF, 1, "设备标 TUF 计数");
241
+ t.eq(stats.dev.NONE, 1, "无设备标计数");
242
+ t.ok(fs.existsSync(outFile), "产物文件已生成(含自动 mkdir 父目录)");
243
+ const back = JSON.parse(fs.readFileSync(outFile, "utf8"));
244
+ t.eq(back.length, 3, "产物 JSON 可解析且条数一致");
245
+ t.eq(back[2].text, "第三条无设备标记忆", "产物内容一致");
246
+
247
+ // 自证只读
248
+ t.eq(sha256File(dbFile), before, "合成库 sha256 前后一致(未写库)");
249
+ const src = fs.readFileSync(SELF, "utf8");
250
+ const calls = src.split("// ---------- self-test")[0].match(/new DatabaseSync\([^)]*\)/g) ?? [];
251
+ t.ok(calls.length > 0, "主体逻辑段确实打开了数据库(探针真的在读库)");
252
+ t.ok(calls.every((s) => /readOnly: true/.test(s)), "主体逻辑段每次 new DatabaseSync( 都带 readOnly: true");
253
+ if (fs.existsSync(DEFAULT_DB)) t.ok(true, `生产库可 stat(只读探测):${DEFAULT_DB}`);
254
+ else t.skip(`生产库不存在(换机 / 测试环境正常):${DEFAULT_DB}`);
255
+ } catch (e) {
256
+ t.ok(false, `self-test 抛错:${e?.message ?? e}`);
257
+ } finally {
258
+ fs.rmSync(tmp, { recursive: true, force: true });
259
+ }
260
+ return t.report();
261
+ }
262
+
263
+ if (isMainEntry()) {
264
+ try {
265
+ process.exitCode = main();
266
+ } catch (e) {
267
+ console.error(`[${TOOL}] 运行错误:${e?.stack ?? e}`);
268
+ process.exitCode = 4;
269
+ }
270
+ }