@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,128 @@
1
+ // 记忆引擎 v2 · 离线层 —— 模型权重获取器(node 版,不用 curl)
2
+ //
3
+ // 为什么要有这个脚本(Windows + schannel 环境实测坑):
4
+ // Windows 自带 curl.exe 走 schannel;HF 的 LFS 大文件会 302 到 Xet CAS 桥
5
+ // (cas-bridge.xethub.hf.co),部分 Windows 主机上 schannel 对该域名 TLS 握手直接失败
6
+ // (curl: (35) SEC_E_ILLEGAL_MESSAGE),而小文件(走 hf-mirror 直连)没问题。
7
+ // node 自带 TLS 栈不受 schannel 影响 ⇒ 用本脚本取权重。
8
+ //
9
+ // 用法:
10
+ // node tools/fetch-model.mjs --out <模型目录> [--base https://hf-mirror.com]
11
+ // [--repo Xenova/bge-small-zh-v1.5] [--variant fp32|int8] [--force]
12
+ // 幂等:已存在且大小与远端一致 → 跳过(--force 强制重下)。
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import crypto from 'node:crypto';
16
+ import { Readable } from 'node:stream';
17
+ import { pipeline } from 'node:stream/promises';
18
+ import { parseArgs, isMainEntry } from '../lib/cli-util.mjs';
19
+
20
+ const COMMON = [
21
+ 'config.json',
22
+ 'tokenizer.json',
23
+ 'tokenizer_config.json',
24
+ 'special_tokens_map.json',
25
+ 'vocab.txt',
26
+ ];
27
+ const VARIANTS = {
28
+ fp32: 'onnx/model.onnx',
29
+ int8: 'onnx/model_quantized.onnx',
30
+ fp16: 'onnx/model_fp16.onnx',
31
+ };
32
+
33
+ async function fetchOne({ url, dest, force, log }) {
34
+ const head = await fetch(url, { method: 'GET', redirect: 'follow' });
35
+ if (!head.ok) throw new Error(`HTTP ${head.status} ${head.statusText} (${url})`);
36
+ const total = Number(head.headers.get('content-length') || 0);
37
+ const name = path.basename(dest);
38
+ if (!force && fs.existsSync(dest) && total > 0 && fs.statSync(dest).size === total) {
39
+ await head.body?.cancel?.();
40
+ return { file: name, status: 'skip', size: total, sha: shortSha(dest) };
41
+ }
42
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
43
+ const part = `${dest}.part`;
44
+ let got = 0;
45
+ let lastPct = -1;
46
+ const src = Readable.fromWeb(head.body);
47
+ src.on('data', (c) => {
48
+ got += c.length;
49
+ if (total) {
50
+ const pct = Math.floor((got / total) * 100);
51
+ if (pct >= lastPct + 10) {
52
+ lastPct = pct;
53
+ log(` ${name}: ${pct}%(${(got / 1048576).toFixed(1)}/${(total / 1048576).toFixed(1)} MB)`);
54
+ }
55
+ }
56
+ });
57
+ await pipeline(src, fs.createWriteStream(part));
58
+ if (total && fs.statSync(part).size !== total) {
59
+ throw new Error(`${name} 大小不符:期望 ${total},实际 ${fs.statSync(part).size}`);
60
+ }
61
+ fs.renameSync(part, dest);
62
+ return { file: name, status: 'fetched', size: fs.statSync(dest).size, sha: shortSha(dest) };
63
+ }
64
+
65
+ function shortSha(p) {
66
+ return crypto.createHash('sha256').update(fs.readFileSync(p)).digest('hex').slice(0, 32);
67
+ }
68
+
69
+ export async function fetchModel({ out, base, repo, variant, force, log, retries = 3 }) {
70
+ const files = [...COMMON, VARIANTS[variant]];
71
+ const results = [];
72
+ for (const f of files) {
73
+ const url = `${base}/${repo}/resolve/main/${f}`;
74
+ let lastErr = null;
75
+ for (let attempt = 1; attempt <= retries; attempt += 1) {
76
+ try {
77
+ log(` → ${f}${attempt > 1 ? `(第 ${attempt} 次尝试)` : ''}`);
78
+ results.push(await fetchOne({ url, dest: path.join(out, f.replace(/\//g, path.sep)), force, log }));
79
+ lastErr = null;
80
+ break;
81
+ } catch (e) {
82
+ lastErr = e;
83
+ log(` ⚠ ${String(e && e.message ? e.message : e)}`);
84
+ await new Promise((r) => setTimeout(r, 1500 * attempt));
85
+ }
86
+ }
87
+ if (lastErr) {
88
+ results.push({ file: f, status: 'FAILED', error: String(lastErr.message || lastErr) });
89
+ }
90
+ }
91
+ return results;
92
+ }
93
+
94
+ async function main() {
95
+ const { flags } = parseArgs();
96
+ if (flags.help) {
97
+ console.log(`用法: node tools/fetch-model.mjs --out <dir> [--base <url>] [--repo <owner/name>] [--variant fp32|int8|fp16] [--force]`);
98
+ return;
99
+ }
100
+ const out = flags.out || process.env.MEMORY_ENGINE_MODEL_DIR;
101
+ if (!out) throw new Error('必须指定 --out(模型落盘目录)');
102
+ const opts = {
103
+ out: path.resolve(out),
104
+ base: flags.base || 'https://hf-mirror.com',
105
+ repo: flags.repo || 'Xenova/bge-small-zh-v1.5',
106
+ variant: flags.variant || 'fp32',
107
+ force: Boolean(flags.force),
108
+ };
109
+ console.log(`模型获取器:${opts.repo}(${opts.variant})`);
110
+ console.log(`镜像 :${opts.base}`);
111
+ console.log(`目标目录 :${opts.out}`);
112
+ if (!VARIANTS[opts.variant]) throw new Error(`未知 variant=${opts.variant}(可选 ${Object.keys(VARIANTS).join('/')})`);
113
+ const results = await fetchModel({ ...opts, log: (m) => console.log(m) });
114
+ console.log('\n=== 结果 ===');
115
+ let failed = 0;
116
+ for (const r of results) {
117
+ if (r.status === 'FAILED') {
118
+ failed += 1;
119
+ console.log(`❌ ${r.file}:${r.error}`);
120
+ } else {
121
+ console.log(`${r.status === 'skip' ? '⏭️ ' : '✅'} ${r.file.padEnd(28)} ${(r.size / 1048576).toFixed(2)} MB sha256=${r.sha}`);
122
+ }
123
+ }
124
+ console.log(failed ? `\n❌ ${failed} 个文件失败` : '\n✅ 全部就绪');
125
+ process.exitCode = failed ? 1 : 0;
126
+ }
127
+
128
+ if (isMainEntry(import.meta.url)) await main();
@@ -0,0 +1,76 @@
1
+ // 记忆引擎 v2 · 离线层 —— 真实模型召回冒烟(人读证据)
2
+ //
3
+ // 为什么单独一个脚本:命令行传中文经 cmd/ssh 会乱码,这里把查询集写死在文件里,
4
+ // 在目标机上直接跑,得到「真实模型 + 真实语料」的可读召回证据(含短暗号)。
5
+ import fs from 'node:fs';
6
+ import { parseArgs, isMainEntry } from '../lib/cli-util.mjs';
7
+ import { resolveConfig } from '../lib/config.mjs';
8
+ import { openVectorsStore, countVectors, getMetaAll } from '../lib/vectors-store.mjs';
9
+ import { openMemorySource } from '../lib/memory-source.mjs';
10
+ import { createLocalEmbedder } from '../lib/embedder.mjs';
11
+ import { recall, buildVectorIndex, shouldTrigger } from '../lib/recall.mjs';
12
+
13
+ const QUERIES = [
14
+ '退下吧',
15
+ '整夜干活',
16
+ '去深水区',
17
+ '跨机同步挡推',
18
+ '看板卡怎么验收',
19
+ '守护鱼崩溃了怎么办',
20
+ '生图角色一致性',
21
+ 'FTS5 中文分词',
22
+ 'task-board 并回账本',
23
+ '3080 端口',
24
+ ];
25
+
26
+ async function main() {
27
+ const { flags } = parseArgs();
28
+ const config = resolveConfig({
29
+ memoryDb: flags['memory-db'] || undefined,
30
+ vectorsDb: flags['vectors-db'] || undefined,
31
+ modelDir: flags['model-dir'] || undefined,
32
+ modelId: flags.model || undefined,
33
+ dim: flags.dim ? Number(flags.dim) : undefined,
34
+ });
35
+ if (!fs.existsSync(config.memoryDb)) throw new Error(`记忆库不存在:${config.memoryDb}`);
36
+
37
+ const embedder = await createLocalEmbedder({
38
+ modelDir: config.modelDir,
39
+ modelId: config.modelId,
40
+ dim: config.dim,
41
+ });
42
+ const store = openVectorsStore(config.vectorsDb);
43
+ const source = openMemorySource(config.memoryDb);
44
+ const index = buildVectorIndex(store.db, { source: 'memory' });
45
+
46
+ console.log('=== 真实模型召回冒烟 ===');
47
+ console.log(`主机 : ${process.env.COMPUTERNAME || '?'}`);
48
+ console.log(`记忆库 : ${config.memoryDb}(${source.count()} 条 / pinned ${source.pinnedIds().length} 条)`);
49
+ console.log(`向量库 : ${config.vectorsDb}(${countVectors(store.db, 'memory')} 条向量)`);
50
+ console.log(`meta : ${JSON.stringify(getMetaAll(store.db))}`);
51
+ console.log(`模型 : ${config.modelId} dim=${embedder.dim}`);
52
+ console.log('');
53
+
54
+ const pinned = new Set(source.pinnedIds().map(String));
55
+ let pinnedLeak = 0;
56
+ for (const q of QUERIES) {
57
+ const gate = shouldTrigger(q);
58
+ const t0 = performance.now();
59
+ const r = await recall({ query: q, store, source, embedder, index, resultTopK: 5 });
60
+ const ms = performance.now() - t0;
61
+ console.log(`【${q}】触发=${gate.trigger ? '是' : `否(${gate.reason})`} 命中 ${r.hits.length} 条 · ${ms.toFixed(1)}ms · FTS命中=${r.stats.ftsMatched ?? 0}`);
62
+ for (const h of r.hits) {
63
+ if (pinned.has(String(h.refId))) pinnedLeak += 1;
64
+ const row = source.getById(h.id);
65
+ const brief = (row ? row.text : '(已删除)').replace(/\s+/g, ' ').slice(0, 58);
66
+ console.log(` #${h.refId} score=${h.score.toFixed(5)} [${h.sources.join('+')}] pinned=${row ? row.pinned : '?'} · ${brief}…`);
67
+ }
68
+ console.log('');
69
+ }
70
+ console.log(`pinned 泄漏检查:${pinnedLeak === 0 ? '✅ 0 条(排除生效)' : `❌ ${pinnedLeak} 条`}`);
71
+ source.close();
72
+ store.db.close();
73
+ process.exitCode = pinnedLeak === 0 ? 0 : 4;
74
+ }
75
+
76
+ if (isMainEntry(import.meta.url)) await main();
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@kira905/dsh-memory-toolkit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "description": "DSH 记忆面工具集:独立向量记忆引擎(增量同步 / 混合召回 / 文档索引)+ 记忆库只读体检工具链 + 本地记忆插件 fork + persona 记忆体检",
9
+ "keywords": [
10
+ "dsh",
11
+ "deepseek-harness",
12
+ "memory",
13
+ "vector-search",
14
+ "embeddings",
15
+ "sqlite",
16
+ "fts5",
17
+ "plugin"
18
+ ],
19
+ "type": "module",
20
+ "license": "AGPL-3.0-only",
21
+ "author": "kira905",
22
+ "homepage": "https://github.com/kira905/dsh-memory-toolkit#readme",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/kira905/dsh-memory-toolkit.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/kira905/dsh-memory-toolkit/issues"
29
+ },
30
+ "engines": {
31
+ "node": ">=22"
32
+ },
33
+ "bin": {
34
+ "dsh-memory-audit": "./persona-memory-audit.mjs",
35
+ "dsh-memory-summary": "./memory-db-tools/list-memory-summary.mjs",
36
+ "dsh-memory-export": "./memory-db-tools/audit-export.mjs"
37
+ },
38
+ "files": [
39
+ "memory-engine/",
40
+ "memory-db-tools/",
41
+ "dsh-memory-local/",
42
+ "lib/",
43
+ "persona-memory-audit.mjs",
44
+ "README.md",
45
+ "README.en.md",
46
+ "CHANGELOG.md",
47
+ "LICENSE"
48
+ ],
49
+ "scripts": {
50
+ "help": "node memory-db-tools/audit-export.mjs --help",
51
+ "audit": "node persona-memory-audit.mjs",
52
+ "test:engine": "node memory-engine/test/run.mjs",
53
+ "test:plugin": "node dsh-memory-local/test/regression.mjs"
54
+ }
55
+ }
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+ // persona ↔ 记忆 防反弹闸门(体检 · 零依赖 · 默认只读)
3
+ //
4
+ // 用途:把「persona 正文体积」与「记忆可见率」两项指标固定下来,供日报/面板/手工调用。
5
+ // 判据来源(只读参考,不改动):
6
+ // · persona 正文 = <preset>/agent.cordis.yml 的 `prefix: |-` 块(每会话 100% 注入)
7
+ // · 记忆注入 = dsh-memory 的 forPrompt + renderPrompt(lib/index.js L124-128 / L185-203)
8
+ // · 生效预算 = <profile>/cordis.patch.yml 的 `- id: memory` 行
9
+ //
10
+ // 阈值:persona > 20000 字符 ⇒ 🔴;记忆可见率 < 5% ⇒ 🔴
11
+ // 退出码:0 = 全绿/仅黄;3 = 存在 🔴;2 = 关键读数失败
12
+ //
13
+ // 用法:
14
+ // node persona-memory-audit.mjs [--json] [--quiet]
15
+ // [--persona-max 20000] [--visible-min 5] [--preset whale-girl] [--profile web]
16
+ //
17
+ // 变更:2026-09-28 首版
18
+
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { DatabaseSync } from 'node:sqlite';
22
+
23
+ const ARGV = process.argv.slice(2);
24
+ const flag = (name, dflt) => {
25
+ const i = ARGV.indexOf(name);
26
+ return i === -1 ? dflt : ARGV[i + 1];
27
+ };
28
+ const has = (name) => ARGV.includes(name);
29
+
30
+ const PERSONA_MAX = Number(flag('--persona-max', 20000));
31
+ const VISIBLE_MIN = Number(flag('--visible-min', 5));
32
+ const PRESET = flag('--preset', 'whale-girl');
33
+ const PROFILE = flag('--profile', 'web');
34
+ const DH = process.env.DSH_HOME || path.join(process.env.USERPROFILE ?? '.', '.dsh');
35
+
36
+ function readPersonaBody(file) {
37
+ const raw = fs.readFileSync(file, 'utf8');
38
+ const lines = raw.split(/\r?\n/);
39
+ let start = -1;
40
+ for (let i = 0; i < lines.length; i++) if (/^ {4}prefix:\s*\|-?\s*$/.test(lines[i])) { start = i; break; }
41
+ if (start === -1) return null;
42
+ let end = lines.length;
43
+ for (let i = start + 1; i < lines.length; i++) {
44
+ const l = lines[i];
45
+ if (l.trim() === '') continue;
46
+ if (l.length - l.trimStart().length < 6) { end = i; break; }
47
+ }
48
+ const bodyLines = lines.slice(start + 1, end);
49
+ // YAML 块标量的 6 空格缩进是语法、不进注入文本 ⇒ 口径以「剥离缩进后」为准
50
+ // (与 `tools\validate-persona.mjs` 报的 persona text 长度一致)。
51
+ const stripped = bodyLines.map((l) => (l.startsWith(' ') ? l.slice(6) : l));
52
+ return {
53
+ text: stripped.join('\n'),
54
+ indentedChars: bodyLines.join('\n').length,
55
+ lines: bodyLines.length,
56
+ fileChars: raw.length,
57
+ };
58
+ }
59
+
60
+ function readBudget(patchFile) {
61
+ if (!fs.existsSync(patchFile)) return { found: false, promptRecentCount: 10, promptMaxChars: 2000 };
62
+ const txt = fs.readFileSync(patchFile, 'utf8');
63
+ const m = /- id: memory\s*\n\s*config:\s*\n([\s\S]*?)(?=\n- |\n# |\s*$)/.exec(txt);
64
+ const seg = m ? m[1] : '';
65
+ const num = (k, d) => {
66
+ const r = new RegExp(`${k}:\\s*(\\d+)`).exec(seg);
67
+ return r ? Number(r[1]) : d;
68
+ };
69
+ return { found: Boolean(m), promptRecentCount: num('promptRecentCount', 10), promptMaxChars: num('promptMaxChars', 2000) };
70
+ }
71
+
72
+ function safeTags(raw) {
73
+ try { const v = JSON.parse(raw); return Array.isArray(v) ? v : String(v).split(/[,\s]+/).filter(Boolean); }
74
+ catch { return String(raw).split(/[,\s]+/).filter(Boolean); }
75
+ }
76
+
77
+ function measureMemory(dbPath, budget) {
78
+ if (!fs.existsSync(dbPath)) return { ok: false, error: 'memory.db 不存在' };
79
+ let db;
80
+ try { db = new DatabaseSync(dbPath, { readOnly: true }); }
81
+ catch (e) { return { ok: false, error: `打不开 memory.db: ${e.message}` }; }
82
+ try {
83
+ const total = db.prepare('SELECT COUNT(*) n FROM memories').get().n;
84
+ const pinnedN = db.prepare('SELECT COUNT(*) n FROM memories WHERE pinned = 1').get().n;
85
+ const totalChars = db.prepare('SELECT SUM(LENGTH(text)) c FROM memories').get().c ?? 0;
86
+ const records = [
87
+ ...db.prepare('SELECT * FROM memories WHERE pinned = 1 ORDER BY updated_at DESC, id DESC').all(),
88
+ ...db.prepare('SELECT * FROM memories WHERE pinned = 0 ORDER BY updated_at DESC, id DESC LIMIT ?').all(budget.promptRecentCount),
89
+ ].map((r) => ({ id: r.id, pinned: Boolean(r.pinned), tags: r.tags ? safeTags(r.tags) : [], text: r.text }));
90
+
91
+ const header = 'Memories you previously stored (use memory_search for anything not listed):\n';
92
+ let used = 76;
93
+ let dropped = 0;
94
+ const kept = [];
95
+ for (const rec of records) {
96
+ const tags = rec.tags.length ? ` [${rec.tags.join(',')}]` : '';
97
+ const line = `- (#${rec.id}${rec.pinned ? ', pinned' : ''})${tags} ${rec.text}`;
98
+ if (used + line.length + 1 > budget.promptMaxChars) { dropped++; continue; }
99
+ kept.push({ id: rec.id, pinned: rec.pinned, chars: line.length });
100
+ used += line.length + 1;
101
+ }
102
+ const tail = dropped > 0 ? `\n(${dropped} more memories not shown; use memory_search)` : '';
103
+ const injectedChars = kept.length ? 76 + kept.reduce((a, k) => a + k.chars, 0) + tail.length : 0;
104
+ return {
105
+ ok: true,
106
+ total, pinnedN, totalChars,
107
+ candidateCount: records.length,
108
+ injectedCount: kept.length,
109
+ injectedChars,
110
+ dropped,
111
+ visiblePercent: totalChars > 0 ? injectedChars / totalChars * 100 : 0,
112
+ kept,
113
+ };
114
+ } finally { db.close(); }
115
+ }
116
+
117
+ function usage() {
118
+ return `persona ↔ 记忆 体检(只读 · 零依赖)
119
+
120
+ 用法:node persona-memory-audit.mjs [选项]
121
+
122
+ --json 机读输出(JSON)
123
+ --quiet 只输出一行结论
124
+ --persona-max <n> persona 正文字符阈值(默认 20000)
125
+ --visible-min <p> 记忆可见率阈值 %(默认 5)
126
+ --preset <name> preset 名(默认 whale-girl;读 <DSH_HOME>/.agent-presets/<name>/agent.cordis.yml)
127
+ --profile <name> profile 名(默认 web;读 <DSH_HOME>/profiles/<name>/cordis.patch.yml)
128
+ -h, --help 本帮助
129
+
130
+ 环境变量:DSH_HOME(缺省 ~/.dsh)
131
+ 退出码:0 = 全绿/仅黄 · 3 = 存在 🔴 · 2 = 关键读数失败`;
132
+ }
133
+
134
+ function main() {
135
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
136
+ console.log(usage());
137
+ return 0;
138
+ }
139
+ const presetFile = path.join(DH, '.agent-presets', PRESET, 'agent.cordis.yml');
140
+ const patchFile = path.join(DH, 'profiles', PROFILE, 'cordis.patch.yml');
141
+ const dbPath = path.join(DH, 'memory', 'memory.db');
142
+
143
+ const persona = fs.existsSync(presetFile) ? readPersonaBody(presetFile) : null;
144
+ const budget = readBudget(patchFile);
145
+ const mem = measureMemory(dbPath, budget);
146
+
147
+ const findings = [];
148
+ if (!persona) {
149
+ findings.push({ level: 'red', key: 'persona', text: `persona 正文读不到(${presetFile})` });
150
+ } else if (persona.text.length > PERSONA_MAX) {
151
+ findings.push({
152
+ level: 'red', key: 'persona',
153
+ text: `persona 正文 ${persona.text.length} 字符 > 阈值 ${PERSONA_MAX} ⇒ 每会话多注入 ${persona.text.length - PERSONA_MAX} 字符`,
154
+ });
155
+ }
156
+ if (!mem.ok) {
157
+ findings.push({ level: 'red', key: 'memory', text: `记忆库读数失败:${mem.error}` });
158
+ } else if (mem.visiblePercent < VISIBLE_MIN) {
159
+ findings.push({
160
+ level: 'red', key: 'visible',
161
+ text: `记忆可见率 ${mem.visiblePercent.toFixed(2)}% < ${VISIBLE_MIN}% ⇒ 注入 ${mem.injectedCount}/${mem.total} 条(${mem.injectedChars} 字符);pinned ${mem.pinnedN} 条/字符占用高是常见主因`,
162
+ });
163
+ } else if (mem.visiblePercent < VISIBLE_MIN * 2) {
164
+ findings.push({ level: 'yellow', key: 'visible', text: `记忆可见率 ${mem.visiblePercent.toFixed(2)}% 偏低(阈值 ${VISIBLE_MIN}%)` });
165
+ }
166
+
167
+ const report = {
168
+ generatedAt: new Date().toISOString(),
169
+ host: process.env.COMPUTERNAME ?? 'unknown',
170
+ dshHome: DH,
171
+ thresholds: { personaMaxChars: PERSONA_MAX, visibleMinPercent: VISIBLE_MIN },
172
+ persona: persona ? { file: presetFile, bodyChars: persona.text.length, bodyCharsIndented: persona.indentedChars, bodyLines: persona.lines, fileChars: persona.fileChars } : { file: presetFile, error: '未找到 prefix 块' },
173
+ memoryBudget: budget,
174
+ memory: mem.ok ? {
175
+ db: dbPath,
176
+ total: mem.total, pinned: mem.pinnedN, totalChars: mem.totalChars,
177
+ candidateCount: mem.candidateCount,
178
+ injectedCount: mem.injectedCount, injectedChars: mem.injectedChars, dropped: mem.dropped,
179
+ visiblePercent: Number(mem.visiblePercent.toFixed(2)),
180
+ kept: mem.kept,
181
+ } : { error: mem.error },
182
+ findings,
183
+ worst: findings.some((f) => f.level === 'red') ? 'red' : findings.length ? 'yellow' : 'green',
184
+ };
185
+
186
+ if (has('--json')) {
187
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
188
+ return report.worst === 'red' ? 3 : 0;
189
+ }
190
+
191
+ const icon = { red: '🔴', yellow: '🟡', green: '🟢' }[report.worst];
192
+ if (!has('--quiet')) {
193
+ console.log(`${icon} persona ↔ 记忆 体检(${report.host})`);
194
+ if (persona) console.log(` persona 正文: ${persona.text.length} 字符 / ${persona.lines} 行(阈值 ${PERSONA_MAX};含 YAML 缩进 ${persona.indentedChars})`);
195
+ else console.log(' persona 正文: 读不到');
196
+ if (mem.ok) {
197
+ console.log(` 记忆库: ${mem.total} 条 / ${mem.totalChars} 字符(pinned ${mem.pinnedN})`);
198
+ console.log(` 注入预算: promptRecentCount=${budget.promptRecentCount} / promptMaxChars=${budget.promptMaxChars}(profile 覆盖 ${budget.found ? '在位' : '未找到'})`);
199
+ console.log(` 本轮注入: ${mem.injectedCount} 条 / ${mem.injectedChars} 字符(丢弃 ${mem.dropped})`);
200
+ console.log(` 记忆可见率: ${mem.visiblePercent.toFixed(2)}%(阈值 ${VISIBLE_MIN}%)`);
201
+ } else {
202
+ console.log(` 记忆库: ${mem.error}`);
203
+ }
204
+ for (const f of findings) console.log(` ${icon === '🟢' ? '·' : (f.level === 'red' ? '🔴' : '🟡')} ${f.text}`);
205
+ if (!findings.length) console.log(' · 全绿:persona 未超阈值,记忆可见率达标');
206
+ } else {
207
+ console.log(`${icon} persona ${persona ? persona.text.length : '?'} 字符 | 记忆可见率 ${mem.ok ? mem.visiblePercent.toFixed(2) + '%' : '?'} | 注入 ${mem.ok ? mem.injectedCount : '?'} 条`);
208
+ }
209
+ return report.worst === 'red' ? 3 : 0;
210
+ }
211
+
212
+ try {
213
+ process.exitCode = main();
214
+ } catch (e) {
215
+ console.error('体检失败:', e.message);
216
+ process.exitCode = 2;
217
+ }