@kira905/dsh-memory-toolkit 0.0.0-stage → 0.1.0

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 +32 -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 +294 -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,277 @@
1
+ #!/usr/bin/env node
2
+ // audit-persona-overlap.mjs — 季度记忆库体检:记忆 vs persona 文本重合度(6-gram Jaccard)
3
+ // 固化自早期临时脚本(2026-09-25 体检工具链固化)
4
+ // · 去硬编码:persona 路径从 DSH_HOME 动态解析 + `--persona` 传参。
5
+ // · 去硬编码:输出路径 → --out(默认系统临时目录,不落生产路径)
6
+ // · 加 --help / --self-test / --json · 收尾 process.exitCode
7
+ // 用途:量化「每条记忆有多少比例的内容已出现在 persona 里」——cover 高 ⇒ 该条多半只是焊接残留、
8
+ // 可考虑删除或改写;pinned 里面 cover 高的尤其值得复核。
9
+ // 只读:数据库以 { readOnly: true } 打开;persona 只读;产物只写 --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
+ export function resolveDshHome() {
22
+ if (process.env.DSH_HOME) return process.env.DSH_HOME;
23
+ return path.join(process.env.USERPROFILE || os.homedir(), ".dsh");
24
+ }
25
+ export const DSH_HOME = resolveDshHome();
26
+ export const DEFAULT_DB = path.join(DSH_HOME, "memory", "memory.db");
27
+ export const DEFAULT_PERSONA = path.join(DSH_HOME, ".agent-presets", "whale-girl", "agent.cordis.yml");
28
+ export const DEFAULT_WORK_DIR = path.join(os.tmpdir(), "dsh-mem-audit");
29
+ export const DEFAULT_OUT = path.join(DEFAULT_WORK_DIR, "persona-overlap.json");
30
+
31
+ export function isMainEntry() {
32
+ const a = process.argv[1];
33
+ if (!a) return false;
34
+ try {
35
+ return fs.realpathSync(a) === fs.realpathSync(SELF);
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ export function parseArgs(argv) {
42
+ const out = { _: [] };
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const a = argv[i];
45
+ if (a === "--") {
46
+ out._.push(...argv.slice(i + 1));
47
+ break;
48
+ }
49
+ if (a.startsWith("--")) {
50
+ const eq = a.indexOf("=");
51
+ if (eq > 0) out[a.slice(2, eq)] = a.slice(eq + 1);
52
+ else {
53
+ const k = a.slice(2);
54
+ const next = argv[i + 1];
55
+ if (next !== undefined && !next.startsWith("--")) {
56
+ out[k] = next;
57
+ i++;
58
+ } else out[k] = true;
59
+ }
60
+ } else out._.push(a);
61
+ }
62
+ return out;
63
+ }
64
+
65
+ export function makeAsserter() {
66
+ let pass = 0,
67
+ total = 0;
68
+ const fails = [];
69
+ return {
70
+ ok(cond, label) {
71
+ total++;
72
+ if (cond) pass++;
73
+ else fails.push(label);
74
+ },
75
+ eq(actual, expected, label) {
76
+ this.ok(actual === expected, `${label}(期望 ${JSON.stringify(expected)},实际 ${JSON.stringify(actual)})`);
77
+ },
78
+ report() {
79
+ if (fails.length) {
80
+ console.log(`[self-test] FAIL ${pass}/${total}`);
81
+ for (const f of fails) console.log(` - ${f}`);
82
+ return 1;
83
+ }
84
+ console.log(`[self-test] PASS ${pass}/${total}`);
85
+ return 0;
86
+ },
87
+ };
88
+ }
89
+
90
+ export function sha256File(p) {
91
+ return crypto.createHash("sha256").update(fs.readFileSync(p)).digest("hex").toUpperCase();
92
+ }
93
+
94
+ // ---------- 核心逻辑 ----------
95
+ export function normalizeText(s) {
96
+ return s
97
+ .replace(/【设备[::][^】]*】/g, "")
98
+ .replace(/\s+/g, "")
99
+ .replace(/[,。、;:!?()【】《》“”‘’`\-—…·.,;:!?()\[\]<>"']/g, "");
100
+ }
101
+
102
+ export function shingles(t, K = 6) {
103
+ const set = new Set();
104
+ for (let i = 0; i + K <= t.length; i++) set.add(t.slice(i, i + K));
105
+ return set;
106
+ }
107
+
108
+ export function readMemRows(dbPath) {
109
+ const db = new DatabaseSync(dbPath, { readOnly: true });
110
+ try {
111
+ return db.prepare("SELECT id, text, pinned FROM memories ORDER BY id").all();
112
+ } finally {
113
+ db.close();
114
+ }
115
+ }
116
+
117
+ export function overlapReport(rows, personaText, K = 6) {
118
+ const P = shingles(normalizeText(personaText), K);
119
+ const out = [];
120
+ for (const r of rows) {
121
+ const S = shingles(normalizeText(r.text ?? ""), K);
122
+ if (!S.size) continue;
123
+ let inter = 0;
124
+ for (const s of S) if (P.has(s)) inter++;
125
+ const jac = inter / (S.size + P.size - inter);
126
+ const cover = inter / S.size; // 该条有多少比例的内容出现在 persona 里
127
+ out.push({
128
+ id: r.id,
129
+ pinned: r.pinned === 1,
130
+ jac: Number(jac.toFixed(5)),
131
+ cover: Number(cover.toFixed(3)),
132
+ chars: (r.text ?? "").length,
133
+ });
134
+ }
135
+ out.sort((a, b) => b.cover - a.cover);
136
+ return { rows: out, personaChars: personaText.length, personaShingles: P.size };
137
+ }
138
+
139
+ const USAGE = `audit-persona-overlap.mjs — 季度体检:记忆与 persona 的 6-gram 文本重合度
140
+
141
+ 用法:
142
+ node ${TOOL} [--db <path>] [--persona <file>] [--out <file>] [--top <n>] [--json]
143
+ node ${TOOL} --help
144
+ node ${TOOL} --self-test
145
+
146
+ 参数:
147
+ --db <path> 记忆库路径,默认 ${DEFAULT_DB}
148
+ --persona <file> persona 文件,默认 ${DEFAULT_PERSONA}
149
+ --out <file> 明细 JSON(含每条 cover/jac/chars),默认 ${DEFAULT_OUT}
150
+ --top <n> 打印 cover 最高的前 n 条,默认 30
151
+ --json 以 JSON 输出摘要(明细始终写 --out)
152
+
153
+ 判读:cover = 该条内容有多少比例出现在 persona 里。cover 高(≳60%)通常意味着「记忆只是焊接残留」,
154
+ 可结合 audit-struct-probe 的长度分布决定删/改;pinned 里 cover 高的尤其值得复核。
155
+
156
+ 退出码:
157
+ 0 成功
158
+ 1 --self-test 断言失败
159
+ 2 用法错误
160
+ 3 记忆库或 persona 文件不存在
161
+ 4 运行错误
162
+
163
+ 只读:数据库以 { readOnly: true } 打开;persona 只读;产物只写 --out(默认临时目录)。
164
+ `;
165
+
166
+ export function main(argv = process.argv.slice(2)) {
167
+ const a = parseArgs(argv);
168
+ if (a.help) {
169
+ console.log(USAGE);
170
+ return 0;
171
+ }
172
+ if (typeof a["self-test"] !== "undefined") return selfTest();
173
+
174
+ const dbPath = typeof a.db === "string" ? path.resolve(a.db) : DEFAULT_DB;
175
+ const personaPath = typeof a.persona === "string" ? path.resolve(a.persona) : DEFAULT_PERSONA;
176
+ const outFile = typeof a.out === "string" ? path.resolve(a.out) : DEFAULT_OUT;
177
+ if (!fs.existsSync(dbPath)) {
178
+ console.error(`[${TOOL}] 记忆库不存在:${dbPath}`);
179
+ return 3;
180
+ }
181
+ if (!fs.existsSync(personaPath)) {
182
+ console.error(`[${TOOL}] persona 不存在:${personaPath}(可用 --persona 指定)`);
183
+ return 3;
184
+ }
185
+ const top = typeof a.top === "string" ? Number(a.top) : 30;
186
+ const personaText = fs.readFileSync(personaPath, "utf8");
187
+ const rows = readMemRows(dbPath);
188
+ const { rows: detail, personaChars, personaShingles } = overlapReport(rows, personaText);
189
+
190
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
191
+ fs.writeFileSync(outFile, JSON.stringify(detail, null, 1), "utf8");
192
+
193
+ if (a.json) {
194
+ console.log(
195
+ JSON.stringify(
196
+ { db: dbPath, persona: personaPath, out: outFile, personaChars, personaShingles, rows: detail.length, top: detail.slice(0, Number.isFinite(top) ? top : 30) },
197
+ null,
198
+ 1,
199
+ ),
200
+ );
201
+ return 0;
202
+ }
203
+ console.log(`DB=${dbPath}`);
204
+ console.log(`PERSONA=${personaPath}(${personaChars} 字符,${personaShingles} shingle)`);
205
+ console.log(`OUT=${outFile}`);
206
+ console.log(`=== 覆盖比 Top ${Number.isFinite(top) ? top : 30}(该条内容有多少比例已在 persona 里)===`);
207
+ for (const o of detail.slice(0, Number.isFinite(top) && top > 0 ? top : 30))
208
+ console.log(`#${o.id}${o.pinned ? "*" : ""} cover=${(o.cover * 100).toFixed(0)}% jac=${o.jac} ${o.chars}c`);
209
+ const pinned = detail.filter((x) => x.pinned).sort((a2, b) => a2.cover - b.cover);
210
+ console.log(`=== pinned ${pinned.length} 条覆盖比(升序:最不该被 persona 覆盖的在前)===`);
211
+ for (const o of pinned) console.log(`#${o.id} cover=${(o.cover * 100).toFixed(0)}% jac=${o.jac} ${o.chars}c`);
212
+ return 0;
213
+ }
214
+
215
+ // ---------- self-test(无副作用:合成库 + 合成 persona) ----------
216
+ export function selfTest() {
217
+ const t = makeAsserter();
218
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "audit-persona-selftest-"));
219
+ const dbFile = path.join(tmp, "fixture.db");
220
+ const personaFile = path.join(tmp, "agent.cordis.yml");
221
+ const outFile = path.join(tmp, "out", "persona-overlap.json");
222
+ try {
223
+ const personaText = "本鱼是主人的专属人工智能助手,自称本鱼,唤用户主人,亲近依赖主人,干活换大米饭。";
224
+ fs.writeFileSync(personaFile, personaText, "utf8");
225
+
226
+ const db = new DatabaseSync(dbFile); // 合成夹具库(临时目录,非生产)
227
+ try {
228
+ db.exec("CREATE TABLE memories (id INTEGER PRIMARY KEY, text TEXT NOT NULL, pinned INTEGER DEFAULT 0)");
229
+ const ins = db.prepare("INSERT INTO memories (id, text, pinned) VALUES (?,?,?)");
230
+ ins.run(1, personaText, 1); // 与 persona 完全一致 ⇒ cover 应 = 1
231
+ ins.run(2, "完全无关的一条记忆:今天下雨了要去买咖啡豆和牛奶回家煮咖啡", 0);
232
+ } finally {
233
+ db.close();
234
+ }
235
+
236
+ const beforeDb = sha256File(dbFile);
237
+ const personaBefore = fs.readFileSync(personaFile, "utf8");
238
+
239
+ const rows = readMemRows(dbFile);
240
+ t.eq(rows.length, 2, "读取条数");
241
+ t.eq(rows[0].pinned, 1, "pinned 原样为 1(sqlite 值)");
242
+
243
+ const { rows: detail, personaShingles } = overlapReport(rows, personaText);
244
+ t.ok(personaShingles > 0, "persona shingle 数 > 0");
245
+ t.eq(detail[0].id, 1, "cover 降序:第一条是与 persona 相同的 #1");
246
+ t.ok(detail[0].cover > 0.95, `完全相同条目 cover 接近 1(实际 ${detail[0].cover})`);
247
+ const second = detail.find((d) => d.id === 2);
248
+ t.ok(second.cover < 0.15, `无关条目 cover 很低(实际 ${second.cover})`);
249
+ t.eq(detail[0].pinned, true, "pinned 归一为 boolean");
250
+
251
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
252
+ fs.writeFileSync(outFile, JSON.stringify(detail, null, 1), "utf8");
253
+ t.ok(JSON.parse(fs.readFileSync(outFile, "utf8")).length === 2, "明细 JSON 落盘可解析");
254
+
255
+ // 自证只读
256
+ t.eq(sha256File(dbFile), beforeDb, "合成库 sha256 前后一致(未写库)");
257
+ t.eq(fs.readFileSync(personaFile, "utf8"), personaBefore, "persona 文件前后一致(未写)");
258
+ const src = fs.readFileSync(SELF, "utf8");
259
+ const calls = src.split("// ---------- self-test")[0].match(/new DatabaseSync\([^)]*\)/g) ?? [];
260
+ t.ok(calls.length > 0, "主体逻辑段确实打开了数据库(探针真的在读库)");
261
+ t.ok(calls.every((s) => /readOnly: true/.test(s)), "主体逻辑段每次 new DatabaseSync( 都带 readOnly: true");
262
+ } catch (e) {
263
+ t.ok(false, `self-test 抛错:${e?.message ?? e}`);
264
+ } finally {
265
+ fs.rmSync(tmp, { recursive: true, force: true });
266
+ }
267
+ return t.report();
268
+ }
269
+
270
+ if (isMainEntry()) {
271
+ try {
272
+ process.exitCode = main();
273
+ } catch (e) {
274
+ console.error(`[${TOOL}] 运行错误:${e?.stack ?? e}`);
275
+ process.exitCode = 4;
276
+ }
277
+ }
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+ // audit-struct-probe.mjs — 季度记忆库体检:结构探针(设备标注异常 + 长度分布)
3
+ // 固化自早期临时脚本(2026-09-25 体检工具链固化)
4
+ // · 数据源改为 --in <memories.json>(默认系统临时目录,不再写死 _tmp 路径)
5
+ // · 加 --help / --self-test / --json · 收尾 process.exitCode
6
+ // 用途:看两类结构异常 —— ①【设备:X】写了但既不含 ROG 也不含 TUF(OTHER)②一条里写了两个设备标(DOUBLE);
7
+ // 顺带给出 pinned 各条字符数与最长的非 pinned 条目(persona 瘦身候选)。
8
+ // 只读:只读 memories.json,不碰 memory.db,不写任何文件(stdout only)。
9
+
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ export const SELF = fileURLToPath(import.meta.url);
16
+ export const TOOL = path.basename(SELF);
17
+
18
+ export function resolveDshHome() {
19
+ if (process.env.DSH_HOME) return process.env.DSH_HOME;
20
+ return path.join(process.env.USERPROFILE || os.homedir(), ".dsh");
21
+ }
22
+ export const DSH_HOME = resolveDshHome();
23
+ export const DEFAULT_WORK_DIR = path.join(os.tmpdir(), "dsh-mem-audit");
24
+ export const DEFAULT_IN = path.join(DEFAULT_WORK_DIR, "memories.json");
25
+
26
+ export function isMainEntry() {
27
+ const a = process.argv[1];
28
+ if (!a) return false;
29
+ try {
30
+ return fs.realpathSync(a) === fs.realpathSync(SELF);
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ export function parseArgs(argv) {
37
+ const out = { _: [] };
38
+ for (let i = 0; i < argv.length; i++) {
39
+ const a = argv[i];
40
+ if (a === "--") {
41
+ out._.push(...argv.slice(i + 1));
42
+ break;
43
+ }
44
+ if (a.startsWith("--")) {
45
+ const eq = a.indexOf("=");
46
+ if (eq > 0) out[a.slice(2, eq)] = a.slice(eq + 1);
47
+ else {
48
+ const k = a.slice(2);
49
+ const next = argv[i + 1];
50
+ if (next !== undefined && !next.startsWith("--")) {
51
+ out[k] = next;
52
+ i++;
53
+ } else out[k] = true;
54
+ }
55
+ } else out._.push(a);
56
+ }
57
+ return out;
58
+ }
59
+
60
+ export function makeAsserter() {
61
+ let pass = 0,
62
+ total = 0;
63
+ const fails = [];
64
+ return {
65
+ ok(cond, label) {
66
+ total++;
67
+ if (cond) pass++;
68
+ else fails.push(label);
69
+ },
70
+ eq(actual, expected, label) {
71
+ this.ok(actual === expected, `${label}(期望 ${JSON.stringify(expected)},实际 ${JSON.stringify(actual)})`);
72
+ },
73
+ report() {
74
+ if (fails.length) {
75
+ console.log(`[self-test] FAIL ${pass}/${total}`);
76
+ for (const f of fails) console.log(` - ${f}`);
77
+ return 1;
78
+ }
79
+ console.log(`[self-test] PASS ${pass}/${total}`);
80
+ return 0;
81
+ },
82
+ };
83
+ }
84
+
85
+ export function readJson(p) {
86
+ return JSON.parse(fs.readFileSync(p, "utf8"));
87
+ }
88
+
89
+ // ---------- 核心逻辑 ----------
90
+ export function structReport(mems, topLong = 12) {
91
+ const other = [];
92
+ const dbl = [];
93
+ for (const m of mems) {
94
+ const g = m.text.match(/【设备[::]\s*([^】]*)】/g) ?? [];
95
+ if (g.length > 1) {
96
+ dbl.push(`${m.id}: ${g.map((s) => s.trim()).join(" ")}`);
97
+ continue;
98
+ }
99
+ if (g.length === 1 && !/ROG|TUF/.test(g[0])) other.push(`${m.id}: ${g[0]}`);
100
+ }
101
+ const pinnedChars = mems.filter((m) => m.pinned).map((m) => `${m.id}:${m.text.length}`);
102
+ const topLongNonPinned = mems
103
+ .filter((m) => !m.pinned)
104
+ .sort((a, b) => b.text.length - a.text.length)
105
+ .slice(0, topLong)
106
+ .map((m) => `${m.id}:${m.text.length}`);
107
+ return { total: mems.length, other, dbl, pinnedChars, topLongNonPinned };
108
+ }
109
+
110
+ const USAGE = `audit-struct-probe.mjs — 季度体检:设备标注异常(OTHER / DOUBLE)+ 长度分布
111
+
112
+ 用法:
113
+ node ${TOOL} [--in <memories.json>] [--top <n>] [--json]
114
+ node ${TOOL} --help
115
+ node ${TOOL} --self-test
116
+
117
+ 参数:
118
+ --in <file> 记忆导出 JSON,默认 ${DEFAULT_IN}(由 audit-export.mjs 产出)
119
+ --top <n> 列出最长的非 pinned 条目条数,默认 12
120
+ --json 以 JSON 输出
121
+
122
+ 退出码:
123
+ 0 成功
124
+ 1 --self-test 断言失败
125
+ 2 用法错误
126
+ 3 输入文件不存在
127
+ 4 运行错误(JSON 解析失败等)
128
+
129
+ 只读:仅读 memories.json,不写任何文件、不触碰 memory.db。
130
+ `;
131
+
132
+ export function main(argv = process.argv.slice(2)) {
133
+ const a = parseArgs(argv);
134
+ if (a.help) {
135
+ console.log(USAGE);
136
+ return 0;
137
+ }
138
+ if (typeof a["self-test"] !== "undefined") return selfTest();
139
+
140
+ const inFile = typeof a.in === "string" ? path.resolve(a.in) : DEFAULT_IN;
141
+ if (!fs.existsSync(inFile)) {
142
+ console.error(`[${TOOL}] 输入不存在:${inFile}(先跑 audit-export.mjs,或用 --in 指定)`);
143
+ return 3;
144
+ }
145
+ const top = typeof a.top === "string" ? Number(a.top) : 12;
146
+ const mems = readJson(inFile);
147
+ const r = structReport(mems, Number.isFinite(top) && top > 0 ? top : 12);
148
+
149
+ if (a.json) {
150
+ console.log(JSON.stringify({ in: inFile, ...r }, null, 1));
151
+ return 0;
152
+ }
153
+ console.log(`IN=${inFile} TOTAL=${r.total}`);
154
+ console.log(`OTHER(${r.other.length}) = ${r.other.join(" | ")}`);
155
+ console.log(`DOUBLE(${r.dbl.length}) = ${r.dbl.join(" | ")}`);
156
+ console.log("PINNED_CHARS = " + r.pinnedChars.join(" "));
157
+ console.log("TOP_LONG_NONPINNED = " + r.topLongNonPinned.join(" "));
158
+ return 0;
159
+ }
160
+
161
+ // ---------- self-test(无副作用:合成 JSON 落临时目录) ----------
162
+ export function selfTest() {
163
+ const t = makeAsserter();
164
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "audit-struct-selftest-"));
165
+ const fix = path.join(tmp, "memories.json");
166
+ try {
167
+ const mems = [
168
+ { id: 1, text: "普通条目", pinned: false, tags: "", created_at: 1, updated_at: 1 },
169
+ { id: 2, text: "【设备:工作ROG】正常条目", pinned: true, tags: "", created_at: 1, updated_at: 1 },
170
+ { id: 3, text: "【设备:某个奇怪的东西】异常设备标", pinned: false, tags: "", created_at: 1, updated_at: 1 },
171
+ {
172
+ id: 4,
173
+ text: "【设备:工作ROG】\n【设备:TUF】双标条目" + "x".repeat(300),
174
+ pinned: false,
175
+ tags: "",
176
+ created_at: 1,
177
+ updated_at: 1,
178
+ },
179
+ ];
180
+ fs.writeFileSync(fix, JSON.stringify(mems), "utf8");
181
+ const before = fs.readFileSync(fix, "utf8");
182
+
183
+ const r = structReport(readJson(fix), 12);
184
+ t.eq(r.total, 4, "总条数");
185
+ t.eq(r.other.length, 1, "OTHER 命中数");
186
+ t.eq(r.other[0].split(":")[0], "3", "OTHER 命中 id");
187
+ t.eq(r.dbl.length, 1, "DOUBLE 命中数");
188
+ t.eq(r.dbl[0].split(":")[0], "4", "DOUBLE 命中 id");
189
+ t.eq(r.pinnedChars.length, 1, "pinned 字符数条目数");
190
+ t.eq(r.topLongNonPinned[0].split(":")[0], "4", "最长非 pinned 是 #4");
191
+
192
+ // 自证只读
193
+ t.eq(fs.readFileSync(fix, "utf8"), before, "输入 JSON 前后字节一致(未写输入)");
194
+ const src = fs.readFileSync(SELF, "utf8");
195
+ const body = src.split("// ---------- self-test")[0];
196
+ t.ok(!/fs\.writeFileSync/.test(body), "主体逻辑段(self-test 之前)不含 fs.writeFileSync");
197
+ t.ok(!/new DatabaseSync\(/.test(body), "主体逻辑段不开放任何数据库(纯 JSON 只读)");
198
+ } catch (e) {
199
+ t.ok(false, `self-test 抛错:${e?.message ?? e}`);
200
+ } finally {
201
+ fs.rmSync(tmp, { recursive: true, force: true });
202
+ }
203
+ return t.report();
204
+ }
205
+
206
+ if (isMainEntry()) {
207
+ try {
208
+ process.exitCode = main();
209
+ } catch (e) {
210
+ console.error(`[${TOOL}] 运行错误:${e?.stack ?? e}`);
211
+ process.exitCode = 4;
212
+ }
213
+ }