@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,185 @@
1
+ // dsh-butler / lib/pitfall-score.mjs —— P4 动态权重 + 阶梯升级(纯函数,R 回归 golden 值)
2
+ //
3
+ // 方案 §4 P4:
4
+ // 动态权重:score = count × 0.5^(距上次踩坑天数 / 30)
5
+ // 30 天没再踩 → 权重减半、逐渐沉底,标「疑似已修复」(不删除)
6
+ // 阶梯升级:1 次入库 / 2 次上浮 / ≥3 次且跨 ≥2 会话 → 生成候选
7
+ // 候选三形态:①做成自动检查项(最优)②内联进相关技能 ③铁律级才动 persona
8
+ // count 口径 = 同签名总出现次数;权重天数取末次出现时间(lastSeen)—— 均进 golden dataset
9
+
10
+ import { lookupFormByMap } from './pitfall-form-map.mjs';
11
+
12
+ // 0.2.0(2026-09-12):suggestForm 先查「错误码/签名 → 形态」映射表(lib/pitfall-form-map.mjs),
13
+ // 未命中才回落原文本启发式。起因:AUDIT_NOT_PASSED(真实成因=没走 P5 审计)被猜成「路径存在性预检」。
14
+ export const PITFALL_SCORE_VERSION = '0.2.0';
15
+
16
+ /** 权重半衰期(天)。 */
17
+ export const HALF_LIFE_DAYS = 30;
18
+ /** 「疑似已修复」判定:距末次出现 ≥ 该天数。 */
19
+ export const LIKELY_FIXED_DAYS = 30;
20
+ /** 升级候选门槛:次数 ≥ 3 且跨 ≥ 2 会话。 */
21
+ export const CANDIDATE_MIN_COUNT = 3;
22
+ export const CANDIDATE_MIN_SESSIONS = 2;
23
+ /** 坑库收录门槛:≥ 2 次(方案「门槛 ≥2」)。 */
24
+ export const PITFALL_MIN_COUNT = 2;
25
+ /** seed 条目(历史文字坑)的基准权重:不计 count,只进权重衰减池。 */
26
+ export const SEED_BASE_SCORE = 2;
27
+
28
+ const DAY_MS = 86_400_000;
29
+
30
+ /**
31
+ * 坑的权重:scan 条目 = count × 衰减;seed 条目 = 基准权重 × 衰减(不计 count)。
32
+ * @param {object} p
33
+ * @param {number} nowMs
34
+ */
35
+ export function scoreOf(p, nowMs) {
36
+ if (p?.source === 'seed') {
37
+ const base = Number(p.seedBase ?? SEED_BASE_SCORE) || SEED_BASE_SCORE;
38
+ return Math.round(base * Math.pow(0.5, daysSince(p.lastSeen, nowMs) / HALF_LIFE_DAYS) * 10000) / 10000;
39
+ }
40
+ return decayScore(p?.count, p?.lastSeen, nowMs);
41
+ }
42
+
43
+ /**
44
+ * 动态权重:score = count × 0.5^(距上次踩坑天数 / 30)。
45
+ * @param {number} count 同签名总次数
46
+ * @param {number} lastSeenMs 末次出现时间(epoch ms)
47
+ * @param {number} nowMs
48
+ * @param {number} [halfLifeDays]
49
+ * @returns {number} 保留 4 位小数
50
+ */
51
+ export function decayScore(count, lastSeenMs, nowMs, halfLifeDays = HALF_LIFE_DAYS) {
52
+ const c = Number(count) || 0;
53
+ if (!c) return 0;
54
+ const days = daysSince(lastSeenMs, nowMs);
55
+ const raw = c * Math.pow(0.5, days / halfLifeDays);
56
+ return Math.round(raw * 10000) / 10000;
57
+ }
58
+
59
+ /** 距末次出现的天数(负数按 0 处理;无时间戳按 0)。 */
60
+ export function daysSince(lastSeenMs, nowMs) {
61
+ const t = Number(lastSeenMs);
62
+ if (!Number.isFinite(t) || t <= 0) return 0;
63
+ const d = (Number(nowMs) - t) / DAY_MS;
64
+ return d > 0 ? d : 0;
65
+ }
66
+
67
+ /** 是否「疑似已修复」(30 天没再踩,不删除,只沉底标注)。 */
68
+ export function isLikelyFixed(lastSeenMs, nowMs, days = LIKELY_FIXED_DAYS) {
69
+ return daysSince(lastSeenMs, nowMs) >= days;
70
+ }
71
+
72
+ /**
73
+ * 阶梯形态(方案表)。
74
+ * @returns {'recorded'|'surfaced'|'candidate'}
75
+ */
76
+ export function ladderStage(count, sessionCount) {
77
+ const c = Number(count) || 0;
78
+ const s = Number(sessionCount) || 0;
79
+ if (c >= CANDIDATE_MIN_COUNT && s >= CANDIDATE_MIN_SESSIONS) return 'candidate';
80
+ if (c >= PITFALL_MIN_COUNT) return 'surfaced';
81
+ return 'recorded';
82
+ }
83
+
84
+ /** 该坑是否够格进坑库(门槛 ≥2 次;seed 条目豁免,见 source)。 */
85
+ export function isRecordable(pitfall) {
86
+ if (pitfall?.source === 'seed') return true;
87
+ return (Number(pitfall?.count) || 0) >= PITFALL_MIN_COUNT;
88
+ }
89
+
90
+ /**
91
+ * 排序:score 降序 → count 降序 → lastSeen 降序 → id 升序(稳定、可测)。
92
+ * @param {object[]} pitfalls
93
+ * @param {number} nowMs
94
+ * @returns {object[]} 新数组(附 score / stage / likelyFixed / daysSinceLast)
95
+ */
96
+ export function rankPitfalls(pitfalls, nowMs) {
97
+ const rows = (pitfalls ?? []).map((p) => {
98
+ const sessionCount = Number(p.sessionCount ?? (Array.isArray(p.sessions) ? p.sessions.length : 0));
99
+ return {
100
+ ...p,
101
+ sessionCount,
102
+ score: scoreOf(p, nowMs),
103
+ stage: ladderStage(p.count, sessionCount),
104
+ likelyFixed: isLikelyFixed(p.lastSeen, nowMs),
105
+ daysSinceLast: Math.round(daysSince(p.lastSeen, nowMs) * 10) / 10,
106
+ };
107
+ });
108
+ rows.sort((a, b) => (
109
+ b.score - a.score
110
+ || (Number(b.count) || 0) - (Number(a.count) || 0)
111
+ || (Number(b.lastSeen) || 0) - (Number(a.lastSeen) || 0)
112
+ || String(a.id).localeCompare(String(b.id))
113
+ ));
114
+ return rows;
115
+ }
116
+
117
+ /** 开工 Top N(跳过疑似已修复;若全被跳过则返回原始前 N 并标注)。 */
118
+ export function topN(pitfalls, n, nowMs) {
119
+ const ranked = rankPitfalls(pitfalls, nowMs).filter((p) => !p.excluded);
120
+ const fresh = ranked.filter((p) => !p.likelyFixed);
121
+ const pick = (fresh.length ? fresh : ranked).slice(0, n);
122
+ return pick;
123
+ }
124
+
125
+ /**
126
+ * 建议的升级形态(方案:①自动检查项最优 ②内联技能 ③铁律级才动 persona)。
127
+ *
128
+ * 判定顺序(2026-09-12 起):
129
+ * ① 已知错误码 / 签名 → **映射表**(lib/pitfall-form-map.mjs)优先命中 —— 成因确定的坑不交给启发式猜
130
+ * ② 未命中 → 回落下面的文本启发式(行为与改动前一致)
131
+ *
132
+ * 注:`FS_NOT_OBSERVED / FS_STALE_VERSION` 原在此处被判 `check`,实测方向不对(它们是全局铁律、
133
+ * 已进 persona),2026-09-12 升进映射表判 `persona`;`FS_SANDBOX_DENIED` 等仍走启发式。
134
+ */
135
+ export function suggestForm(pitfall) {
136
+ const mapped = lookupFormByMap(pitfall);
137
+ if (mapped) return { form: mapped.form, why: mapped.why, ruleId: mapped.ruleId, via: mapped.via };
138
+
139
+ const cat = pitfall?.category ?? '其他';
140
+ const sig = String(pitfall?.sigKey ?? '');
141
+ const text = String(pitfall?.normText ?? pitfall?.sigKey ?? '');
142
+ // 可脚本化检测的坑 → 优先「自动检查项」
143
+ if (/FS_SANDBOX_DENIED/.test(sig)) return { form: 'check', why: '沙箱拒绝=权限配置问题,可做成"写生产路径前先验权限"的检查项' };
144
+ if (/FS_NOT_TEXT|FS_NOT_FOUND/.test(sig)) return { form: 'check', why: '读文件前可先验类型/存在性,适合做成检查项' };
145
+ if (cat === '守护鱼') return { form: 'check', why: '守护鱼类坑适合做成只读健康检查项(dsh-selfcheck 同款)' };
146
+ if (cat === '插件') return { form: 'skill', why: '插件类坑适合内联进 plugin-deploy 技能开头' };
147
+ if (cat === '同步') return { form: 'skill', why: '同步类坑适合内联进同步/备份相关技能或脚本注释' };
148
+ if (cat === '会话') return { form: 'skill', why: '会话类坑适合内联进 session-cleanup / dsh-restart 技能' };
149
+ if (cat === 'pwsh') return { form: 'skill', why: 'pwsh 类坑适合内联进相关技能;若能脚本化则优先检查项' };
150
+ if (/路径|path|<PATH>/i.test(text)) return { form: 'check', why: '路径假设类坑可做成"路径存在性预检"检查项' };
151
+ return { form: 'skill', why: '默认建议内联技能;只有反复踩且影响全局的铁律级才动 persona' };
152
+ }
153
+
154
+ /**
155
+ * 生成升级候选(≥3 次且跨 ≥2 会话;**永不自动改 persona / 技能**)。
156
+ * @param {object} pitfall 已 rank 的坑(含 score/stage)
157
+ * @returns {object|null}
158
+ */
159
+ export function buildCandidate(pitfall, nowMs = Date.now()) {
160
+ if (!pitfall || pitfall.source === 'seed') return null;
161
+ if (ladderStage(pitfall.count, pitfall.sessionCount ?? 0) !== 'candidate') return null;
162
+ const sug = suggestForm(pitfall);
163
+ const options = [
164
+ { id: 'check', label: '① 做成自动检查项(最优:坑不可能再犯)', hint: sug.form === 'check' ? sug.why : '若可脚本化判定则首选' },
165
+ { id: 'skill', label: '② 内联进相关技能(加载即见)', hint: sug.form === 'skill' ? sug.why : `建议内联进与「${pitfall.category}」相关的技能开头` },
166
+ { id: 'persona', label: '③ 铁律级才动 persona(需 persona-maintain 流程)', hint: '仅当该坑影响全局且反复踩时才考虑;采纳后必须走 validate-persona 验证' },
167
+ ];
168
+ return {
169
+ id: pitfall.id,
170
+ pitfallId: pitfall.id,
171
+ sigKey: pitfall.sigKey,
172
+ category: pitfall.category,
173
+ count: pitfall.count,
174
+ sessionCount: pitfall.sessionCount,
175
+ score: pitfall.score ?? decayScore(pitfall.count, pitfall.lastSeen, nowMs),
176
+ tools: pitfall.tools ?? {},
177
+ normText: pitfall.normText ?? '',
178
+ suggested: sug.form,
179
+ why: sug.why,
180
+ options,
181
+ status: 'pending',
182
+ createdAt: new Date(nowMs).toISOString(),
183
+ decidedAt: null,
184
+ };
185
+ }
@@ -0,0 +1,160 @@
1
+ // DSH / tools/lib/session-file-name.mjs —— 会话日志文件名判据 · **单一真源**(零依赖)
2
+ //
3
+ // 背景(2026-09-24 · 卡「三机同步步A-V3代际适配」):
4
+ // 主包 0.1.5 起把会话日志改名 `session.jsonl.zstd` → `session.v3.jsonl.zstd`;
5
+ // 主包 0.2.x 起**再换代** `session.v3.jsonl.zstd` → `session.v4.jsonl.zstd`(**现行世代**)。
6
+ // (2026-10-16 · 卡「harness · 成本与路由 · 债 D-1/D-3 收口」把**写入侧常量与文案**一并推到 v4;
7
+ // 读取侧判据本已认任意 vN,未动。真源 = 本文件。)
8
+ // 凡是「按精确文件名比较」的**读取侧**判据都会整条失明(实测 pitfall-scan 的
9
+ // discoverSessionFiles() 活跃树覆盖 0 / 212),而归档里仍有 1040 个旧名文件、
10
+ // 活跃树 212 个全是 v3 名 ⇒ 读取侧必须**认全世代**。
11
+ //
12
+ // 三条口径(写死,别自由发挥):
13
+ // ① 读取侧一律用 isSessionFileName() / sessionFileGeneration()(前缀 + 代际),**禁精确字符串比较**;
14
+ // ② 需要"代际"时从文件名提取(旧名 = 0,`session.vN....` = N),**禁硬编码**;
15
+ // ③ 写入侧(真要写文件名时)用 SESSION_FILE_NAME_CURRENT(当前世代 = v4,0.2.x 起)。
16
+ //
17
+ // ⚠️ PowerShell 侧无法 import 本模块:`tools\dsh-backup-sessions.ps1` 内写等价 PS 判据
18
+ // ($SessionFileFilter / Test-SessionFileName / $SessionFileNameCurrent)。**改一处必须同步另一处。**
19
+ // 已知同类读取侧判据(**★2026-09-24 收尾清账批逐处核实后重写**,原清单两处已修、又漏了一族):
20
+ // ✅ 已消费本模块:tools\lib\attach-refs.mjs · tools\board-ops\session-tail.mjs · tools\dsh-sessions-land.mjs ·
21
+ // tools\dsh-butler\{session-ledger.mjs, lib\pitfall-scan.mjs, lib\session-header.mjs, lib\restart.mjs} ·
22
+ // tools\archive-card-sessions.mjs · tools\archive-inactive-sessions.mjs · tools\dsh-archive-sessions\lib\index.js ·
23
+ // tools\dsh-butler-panel\lib\state.js(★A2:判据 +「取哪份」取法一并统一;checks.mjs 经它复用,无第二份实现)。
24
+ // ⚠️ restart-coordinator.mjs **无自有判据**(经 lib\restart.mjs#scanSessionLogFiles 间接同源)——原清单点名它属误记。
25
+ // ⏳ 仍**只认旧名**的读取侧(低频/一次性工具,待各自 owner 认领;行号 = 2026-09-24 快照):
26
+ // tools\dsh-archive-sessions\{clean-empty-shells.mjs:78, scan-empty.mjs:48, scan-small.mjs:57} ·
27
+ // tools\projcache-restore-archived.mjs:107 · tools\restore-shell-sessions.mjs:76 · tools\test-single-archive.mjs:21 ·
28
+ // tools\{session-tail-for-llm.mjs:81, session-tail-audit.mjs:96, session-impact.mjs:78, session-cot.mjs:77,
29
+ // dsh-session-tail-dump.mjs:23}。**要长期用再改**,改法照本文件(认全世代 + 需要代际时提取)。
30
+ // ⚠️ 已知风险登记(A6 · 2026-09-24 **先登记不修**):「两代并存」目录里新世代文件可能只是**补写壳**
31
+ // (实测 v3 仅 2 事件 / 244 B,旧名才是完整对话 2621 事件 / 2.7 MB)⇒ **需要读内容的消费者必须走候选链**
32
+ // (rankSessionFilesInDir / collectSessionFilesByDir 的 files),只做扫描/移动的取 current 即可。
33
+ // 当前无消费方受影响(面板只扫活跃树:231/231 全 v3、0 并存);若将来把归档并存目录纳入扫描,先接候选链再开。
34
+ // (插件包内为**派生副本** `lib/session-file-name.mjs`,由 tools\sync-session-file-name.mjs 生成,
35
+ // deploy 时 fail-closed 校验副本与真源 sha 一致 —— 改真源必须同批重跑该同步器。)
36
+ import fs from 'node:fs';
37
+ import path from 'node:path';
38
+
39
+ /** 旧世代文件名(≤ 0.1.1;归档树里仍有大量此名)。 */
40
+ export const SESSION_FILE_NAME_LEGACY = 'session.jsonl.zstd';
41
+
42
+ /** v3 世代文件名(0.1.5 起;**已被 0.2.x 换代**,仍须认得)。 */
43
+ export const SESSION_FILE_NAME_V3 = 'session.v3.jsonl.zstd';
44
+
45
+ /** 当前世代文件名(0.2.x 起)。 */
46
+ export const SESSION_FILE_NAME_V4 = 'session.v4.jsonl.zstd';
47
+
48
+ /** 写入侧应使用的当前世代文件名。 */
49
+ export const SESSION_FILE_NAME_CURRENT = SESSION_FILE_NAME_V4;
50
+
51
+ /** 认得的三个世代名(读取侧清单;顺序 = 旧 → 新)。 */
52
+ export const SESSION_FILE_NAMES = [SESSION_FILE_NAME_LEGACY, SESSION_FILE_NAME_V3, SESSION_FILE_NAME_V4];
53
+
54
+ /** 宽匹配模式(shell / robocopy / Get-ChildItem -Filter 用;**读取侧**)。 */
55
+ export const SESSION_FILE_GLOB = 'session*.jsonl.zstd';
56
+
57
+ /**
58
+ * 读取侧判据(前缀 + 代际)。命中的形态:
59
+ * `session.jsonl.zstd`(旧世代)· `session.v3.jsonl.zstd`(0.1.5 世代)· `session.v4.jsonl.zstd`(当前世代)
60
+ * · 更高的 `session.vN.jsonl.zstd`(未来世代)—— **本判据认任意 vN,换代时无需改**。
61
+ * 不命中:`session.jsonl`(明文形态,另有判据)· `session.jsonl.zstd.bak` · 其它任意文件。
62
+ */
63
+ export const SESSION_FILE_RE = /^session(?:\.v(\d+))?\.jsonl\.zstd$/;
64
+
65
+ /** 是否会话日志文件(读取侧唯一判据)。 */
66
+ export function isSessionFileName(name) {
67
+ return SESSION_FILE_RE.test(String(name));
68
+ }
69
+
70
+ /**
71
+ * 从文件名提取代际:旧世代 = 0;`session.vN.jsonl.zstd` = N;非会话文件 = null。
72
+ * @returns {number|null}
73
+ */
74
+ export function sessionFileGeneration(name) {
75
+ const m = SESSION_FILE_RE.exec(String(name));
76
+ if (!m) return null;
77
+ return m[1] ? Number(m[1]) : 0;
78
+ }
79
+
80
+ /**
81
+ * 目录形态的会话文件清单(不递归)。
82
+ * @returns {{file:string,name:string,generation:number,bytes:number,mtimeMs:number}[]}
83
+ */
84
+ export function listSessionFilesInDir(dir, opts = {}) {
85
+ const fsImpl = opts.fs ?? fs;
86
+ let ents;
87
+ try { ents = fsImpl.readdirSync(dir, { withFileTypes: true }); } catch { return []; }
88
+ const out = [];
89
+ for (const e of ents) {
90
+ if (!e.isFile()) continue;
91
+ if (!isSessionFileName(e.name)) continue;
92
+ const full = path.join(dir, e.name);
93
+ let st;
94
+ try { st = fsImpl.statSync(full); } catch { continue; }
95
+ out.push({
96
+ file: full, name: e.name, generation: sessionFileGeneration(e.name),
97
+ bytes: st.size, mtimeMs: Math.round(st.mtimeMs),
98
+ });
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * 同一会话目录内的候选文件,按优先级排序(第一个 = 「当前那份」):
105
+ * mtime 最新优先 → 平局取代际大者 → 再平局取名字典序小者(确定性)。
106
+ *
107
+ * ⚠️ **「当前那份」≠「内容最全的那份」**(2026-09-24 实测,卡「修·V3判据④」):
108
+ * 换代后新世代文件可能只是"补写壳"——归档树 28 个两代并存目录里,`session.v3.jsonl.zstd`
109
+ * 只有 2 个事件(`session` 头 + `session/end-seed`、有效内容 244 B),而旧名文件才是完整对话
110
+ * (2621 个事件 / 2.7 MB 解压文本)。**需要读内容的消费者必须走完候选链**(见下),
111
+ * 只做"扫描/移动"的消费者取第一个即可。
112
+ * @returns {{file:string,name:string,generation:number,bytes:number,mtimeMs:number}[]}
113
+ */
114
+ export function rankSessionFilesInDir(dir, opts = {}) {
115
+ const hits = listSessionFilesInDir(dir, opts);
116
+ hits.sort((a, b) => (b.mtimeMs - a.mtimeMs) || (b.generation - a.generation) || (a.name < b.name ? -1 : 1));
117
+ return hits;
118
+ }
119
+
120
+ /**
121
+ * 在一个会话目录里挑出"当前那份"会话文件(两代并存时的取法):
122
+ * mtime 最新优先 → 平局取代际大者 → 再平局取名字典序小者(确定性)。
123
+ * @returns {string|null} 绝对路径;目录不存在 / 无会话文件 = null
124
+ */
125
+ export function pickSessionFileInDir(dir, opts = {}) {
126
+ const hits = rankSessionFilesInDir(dir, opts);
127
+ return hits.length ? hits[0].file : null;
128
+ }
129
+
130
+ /**
131
+ * 递归收集会话目录(**每个目录聚合一次**,不做逐文件扁平化)。
132
+ * 用途:归档器的扫描面 —— 一个会话目录 = 一个会话,两代并存时 `current` 是"当前那份"、
133
+ * `files` 是该目录全部世代(移动校验/尾部判据/内容回落都要用到全量)。
134
+ * @returns {{dir:string, current:string, files:object[]}[]} 按 dir 字典序(确定性)
135
+ */
136
+ export function collectSessionFilesByDir(root, opts = {}) {
137
+ const fsImpl = opts.fs ?? fs;
138
+ const out = [];
139
+ const stack = [root];
140
+ while (stack.length) {
141
+ const d = stack.pop();
142
+ let ents;
143
+ try { ents = fsImpl.readdirSync(d, { withFileTypes: true }); } catch { continue; }
144
+ let hasSessionFile = false;
145
+ for (const e of ents) {
146
+ if (e.isDirectory()) { stack.push(path.join(d, e.name)); continue; }
147
+ if (e.isFile() && isSessionFileName(e.name)) hasSessionFile = true;
148
+ }
149
+ if (!hasSessionFile) continue;
150
+ const files = rankSessionFilesInDir(d, opts);
151
+ if (files.length) out.push({ dir: d, current: files[0].file, files });
152
+ }
153
+ out.sort((a, b) => (a.dir < b.dir ? -1 : 1));
154
+ return out;
155
+ }
156
+
157
+ /** 递归收集「每目录当前那份」的绝对路径(collectSessionFilesByDir 的扁平形态)。 */
158
+ export function collectSessionFilesInTree(root, opts = {}) {
159
+ return collectSessionFilesByDir(root, opts).map((x) => x.current);
160
+ }
@@ -0,0 +1,78 @@
1
+ // zstd-frames.mjs —— zstd 多帧日志「拆帧」的 **CLI 侧唯一源**(严格结构解析)。
2
+ //
3
+ // 用途:会话日志(`session.jsonl.zstd` / `session.v3.jsonl.zstd`)是**多帧**追加写的
4
+ // —— 一帧 ≈ 一次 flush 的一批事件。要取「尾部 N 帧」或「全部帧」,就得先把多帧缓冲切成逐帧。
5
+ //
6
+ // 算法(严格结构解析,**不吃"魔数搜索"那套启发式**):
7
+ // ZSTD magic(0xFD2FB528 LE)→ frame header descriptor(含 dictionary / content-size 字段宽度)
8
+ // → block 循环(block type 3 = reserved ⇒ 断)→ optional 4 字节 checksum;
9
+ // 遇**不完整帧**返回 `tornStart`(该帧起点),**不猜长度、也不把残字节当完整帧**。
10
+ // 对比"魔数搜索 + 游标推进"启发式的两个弱点:
11
+ // ① 压缩数据里**恰好出现 zstd magic 字节序列**时会把一帧切成两半(本件靠帧结构解析不会);
12
+ // ② 多份实现各自演化 ⇒ 一处修另一处不跟(2026-09-29 的「隔帧取样」缺陷族就是这么来的)。
13
+ //
14
+ // 三条必读:
15
+ // ① **本件是 CLI 侧唯一源**(2026-09-29 卡「归档帧扫描同源化」建立):`tools\archive-card-sessions.mjs`
16
+ // 与 `tools\archive-inactive-sessions.mjs` 的 **6 处**拆帧点(tailTypes / readSessionMeta / framesOf、
17
+ // tailState / allTypes / framesOf)全部消费本件。原魔数搜索实现已删除。
18
+ // ② 与插件侧 `tools\dsh-archive-sessions\lib\zstd-frame-rewrite.mjs#scanZstdFrames` **同源但不是同一
19
+ // 文件**:插件部署时只同步插件目录 ⇒ 插件**不能 import 插件目录外的文件**(CLI 侧也不该反向依赖
20
+ // 插件目录 = 跨包部署边界)。两份实现靠 `tools\_test\test-archive-framescan.mjs` 的**一致性单测**
21
+ // 守住:两文件并排跑同一批样本(含魔数字节样本 / 撕裂帧样本 / 生产形态样本),帧边界必须逐帧相同。
22
+ // ⇒ **禁再抄第三份**:要改算法,先改这两份并让一致性单测过。
23
+ // ③ 与本件同算法的另两份**历史副本**(`tools\archive-meta-backfill.mjs` 约 L169 /
24
+ // `tools\projcache-restore-archived.mjs` 约 L64 各自的内联版:无 tornStart、撕裂即 break)
25
+ // **登记为待收口** —— 它们属其它卡的产物,本件建立时**未动**,留给后续卡(避免与在途改动互相踩)。
26
+
27
+ export const ZSTD_MAGIC = 4247762216 // 0xFD2FB528 LE
28
+
29
+ /**
30
+ * 扫描 zstd 多帧边界(严格帧结构解析;与插件侧 scanZstdFrames 逐行同源)。
31
+ * @param {Buffer} buffer 整个多帧缓冲(调用方负责截断/切片)
32
+ * @returns {{frames: {start:number,end:number}[], tornStart: number|undefined}}
33
+ * frames —— **完整帧**的 [start, end) 区间,顺序即文件内顺序;
34
+ * tornStart —— 第一个**不完整**帧的起点(没有 = undefined)。调用方须"遇撕裂不猜":
35
+ * 撕裂帧一律丢弃,绝不把残字节当完整帧去解。
36
+ */
37
+ export function scanZstdFrames(buffer) {
38
+ const frames = []
39
+ let offset = 0
40
+ while (offset < buffer.length) {
41
+ const start = offset
42
+ if (buffer.length - offset < 4) return { frames, tornStart: start }
43
+ if (buffer.readUInt32LE(offset) !== ZSTD_MAGIC) return { frames, tornStart: start }
44
+ offset += 4
45
+ if (offset === buffer.length) return { frames, tornStart: start }
46
+ const descriptor = buffer.readUInt8(offset)
47
+ offset += 1
48
+ if ((descriptor & 24) !== 0) return { frames, tornStart: start }
49
+ const contentSizeFlag = descriptor >>> 6
50
+ const singleSegment = (descriptor & 32) !== 0
51
+ const checksum = (descriptor & 4) !== 0
52
+ const dictionaryFlag = descriptor & 3
53
+ const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag
54
+ const contentSizeBytes = contentSizeFlag === 0 ? (singleSegment ? 1 : 0) : 1 << contentSizeFlag
55
+ const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes
56
+ if (buffer.length - offset < remainingHeaderBytes) return { frames, tornStart: start }
57
+ offset += remainingHeaderBytes
58
+ for (;;) {
59
+ if (buffer.length - offset < 3) return { frames, tornStart: start }
60
+ const blockHeader = buffer.readUIntLE(offset, 3)
61
+ offset += 3
62
+ const lastBlock = (blockHeader & 1) !== 0
63
+ const blockType = (blockHeader >>> 1) & 3
64
+ const blockSize = blockHeader >>> 3
65
+ if (blockType === 3) return { frames, tornStart: start }
66
+ const payloadBytes = blockType === 1 ? 1 : blockSize
67
+ if (buffer.length - offset < payloadBytes) return { frames, tornStart: start }
68
+ offset += payloadBytes
69
+ if (lastBlock) break
70
+ }
71
+ if (checksum) {
72
+ if (buffer.length - offset < 4) return { frames, tornStart: start }
73
+ offset += 4
74
+ }
75
+ frames.push({ start, end: offset })
76
+ }
77
+ return { frames, tornStart: undefined }
78
+ }
@@ -0,0 +1,68 @@
1
+ # memory-db-tools — 记忆库工具集
2
+
3
+ 本目录内与 `~/.dsh/memory/memory.db`(记忆库)相关的工具集。
4
+ 本 README 维护**用法表**;口径类真源(体检判据、字段白名单)在 `lib\` 与各脚本文件头的常量里。
5
+
6
+ > 下文的 `node <工具>` 均指**在本目录内**执行;工具一律支持 `--help` / `--self-test`。
7
+ > 所有 `audit-*` 工具的 `--out` / `--in` 默认落**系统临时目录**(不落生产路径),
8
+ > 季度体检建议显式传 `--out` 到自己建的体检目录(临时目录会被系统清理)。
9
+
10
+ ---
11
+
12
+ ## 体检工具链(`audit-*`)
13
+
14
+ **来源**:2026-09-25 体检工具链固化,从早期临时脚本沉淀而来,
15
+ 做了三件事:去硬编码(用户名/盘符/写死目录 → `DSH_HOME` 动态解析 + 参数化)、补 `--help` / `--self-test`、
16
+ 收尾改 `process.exitCode`(禁 `process.exit()`,Windows + undici 会概率崩 libuv)。
17
+
18
+ | 工具 | 用途 | 命令 | 主要参数 | 只读/可写 | 输出 |
19
+ |---|---|---|---|---|---|
20
+ | `audit-export.mjs` | 第 1 站:把记忆库全量导成 `memories.json`,给下面所有探针当数据源 | `node audit-export.mjs` | `--db` `<库路径>`、`--out` `<文件>`、`--force`(覆盖已存在)、`--json`、`--quiet` | **只读库**(可写 `--out`) | `memories.json` + 统计(TOTAL/PINNED/CHARS/设备分布) |
21
+ | `audit-analyze.mjs` | 第 2 站:六项分析——首行清单 / 漏标设备候选 / 重复对 / 过期关键词 / pinned 全文 / 统计 | `node audit-analyze.mjs` | `--in` `<memories.json>`、`--out` `<目录>`、`--json`、`--quiet` | 只读输入(可写 6 个产物) | `list-lines.txt`、`list-nodev.txt`、`dup-pairs.txt`、`keyword-hits.txt`、`pinned-full.txt`、`stats.json` |
22
+ | `audit-struct-probe.mjs` | 结构异常:设备标不在已知清单(OTHER)
23
+ | `audit-hit-probe.mjs` | 给 id 列表,报每条命中了哪些机器特征词(自证「漏标设备」判据站得住) | `node audit-hit-probe.mjs --ids 40,46,67` | `--ids`(必填)、`--in`、`--json` | **只读,不写文件** | 标准输出(每行 `#id [命中词] 首行`) |
24
+ | `audit-dump-ids.mjs` | 按 id 取记忆全文(分批精读,规避 stdout 编码/截断坑) | `node audit-dump-ids.mjs --ids 40,46,67` | `--ids`(必填)、`--out`、`--stdout`、`--db`、`--json` | **只读库**(可写 `--out`) | `dump-ids-<本地时间>.txt`(默认临时目录)或 stdout |
25
+ | `audit-persona-overlap.mjs` | 量化每条记忆有多少比例已出现在 persona 里(找「焊接残留」条目) | `node audit-persona-overlap.mjs` | `--db`、`--persona`、`--out`、`--top` `<n>`、`--json` | **只读库与 persona**(可写 `--out`) | `persona-overlap.json` + 覆盖比 Top N / pinned 覆盖比 |
26
+ | `audit-ledger-probe.mjs` | 看板账本只读探针:按关键词筛卡 + 状态分布 | `node audit-ledger-probe.mjs --keyword "体检\|记忆"` | `--ledger`、`--keyword` `<正则>`、`--limit`、`--json` | **只读账本** | 标准输出(命中卡列表 + 状态统计) |
27
+ | `audit-verify-unchanged.mjs` | 收尾站:对照基线快照,证明体检期间记忆库零写入(物理 sha + 逻辑指纹 + 逐条 diff) | `node audit-verify-unchanged.mjs --expect-unchanged` | `--db`、`--baseline`、`--expect-unchanged`、`--json` | **只读,不写文件** | 标准输出(sha / 条数 / 新增·缺失·内容变更) |
28
+
29
+ ### 典型季度体检流程
30
+
31
+ ```powershell
32
+ $T = (Get-Location).Path # 本目录
33
+ $W = Join-Path $HOME 'Documents/mem-audit' # 自建体检目录(示例)
34
+
35
+ node $T\audit-export.mjs --out "$W\memories.json" # ① 冻结基线
36
+ node $T\audit-analyze.mjs --in "$W\memories.json" --out $W # ② 六项分析
37
+ node $T\audit-struct-probe.mjs --in "$W\memories.json" # ③ 结构异常
38
+ node $T\audit-hit-probe.mjs --ids <漏标候选 id> # ③b 判据自证
39
+ node $T\audit-persona-overlap.mjs --out "$W\persona-overlap.json" # ④ 与 persona 的重合度
40
+ node $T\audit-ledger-probe.mjs --keyword "体检|记忆" # 顺带看看板
41
+ node $T\audit-verify-unchanged.mjs --baseline "$W\memories.json" --expect-unchanged # ⑤ 零改动自证
42
+ ```
43
+
44
+ 退出码约定:`0` 成功 · `1` `--self-test` 断言失败 · `2` 用法错误 · `3` 输入/输出条件不满足(文件不存在、`--expect-unchanged` 检出差异等)· `4` 运行错误。
45
+
46
+ ### 口径真源(要改判据改这里,别改产物)
47
+
48
+ - **漏标设备判据(机器特征词)**:`audit-analyze.mjs` 的 `MACHINE_RE` 与 `audit-hit-probe.mjs` 的 `MACHINE_PATTERNS`(两处需同批维护)。
49
+ - **过期关键词表**:`audit-analyze.mjs` 的 `STALE_KEYWORDS`(19 组;过期项增减改这一处)。
50
+ - **重复对阈值**:`audit-analyze.mjs` 的 `analyze(mems, { shingle, minShared, jacThreshold })`(默认 6-gram / 共享 shingle ≥ 6 / Jaccard ≥ 0.15)。
51
+
52
+ ### 自检
53
+
54
+ 每个工具都有 `--help`(用法/参数/退出码)与 `--self-test`(合成夹具跑一遍 + 自证只读:合成库 sha 前后一致、主体逻辑段每次开库都带 `readOnly: true`):
55
+
56
+ ```powershell
57
+ node audit-export.mjs --self-test
58
+ ```
59
+
60
+ ---
61
+
62
+ ## 本目录其余工具(非本次固化范围)
63
+
64
+ `list-memory-summary.mjs`(新会话开工简报:**设备行 + 插电保护口径行** + 全量 id/pinned 列表 + pinned 全文 + **坑库段**)。⚠️ **2026-10-16 起头部多一行「🛡 插电保护」口径行** —— 治「会话看不见保护态」(保护其实一直开着,但 persona 的自主授权全是「整夜模式期间…」条件句、条件不在开工面上 ⇒ 会话无从判断 ⇒ 保守走常规口径,主人只能挨个会话人肉通知)。实现 = 只读 `~\.dsh\overnight-mode-<host 小写>.json` **轻判据**(<10ms)+ **fail-open**(任何异常只打一行提示);⚠️ **刻意不调 `overnight-mode.mjs status`**(本机实测 **15.7s / 17.0s**,塞进简报 = 每个会话开场白等 17 秒);⚠️ 该行给的是**口径不是体检**(不核电源设置是否真还在),健康核对归 `plug-guard`(10 分钟一轮 · 2026-10-16 起)与 `overnight-guard`(每日 15:10)两个 job。真源 = persona【铁律·插电保护禁停与自动交棒】(2026-10-16 由「整夜模式禁停与自动交棒」改名)。⚠️ **2026-10-10 起坑库段 = 向量召回优先 + 固定 Top3 兜底**(记忆引擎 A3 第 8 步):按当前会话上下文(会话日志尾部最近的**真实**用户消息 + projcache 标题/上轮请求,去重截 500 字)召回 `vectors.db` 的 `source='pitfall'` 向量;**fail-open**(无上下文/无向量库/坑源无向量/编码桥不可达/超时/零命中/任何异常 ⇒ 回落原 `topN(pitfalls,3)`」,简报绝不报错、超时硬预算 900ms)。参数(默认不影响生产):`--query` / `--no-recall` / `--top-k` / `--recall-timeout-ms` / `--bridge-port` / `--home` / `--memory-db` / `--vectors-db` / `--pitfalls-file` / `--session` / `--debug`。· `read-memories.mjs` / `read-memory-db.mjs`(读条)·
65
+ `dump-memory-db.mjs`(全量导 stdout)· `check-memory-db.mjs` · `show-mem-times.mjs`(时间戳)·
66
+ `refine-memories.mjs`(记忆整理,**可写**)· `memory-schema-migrate.mjs` /
67
+ `memory-identity-check.mjs` / `memory-scope-fixture.mjs`(schema 与字段口径,`lib/` 为共享口径库)。
68
+ 用法见各自文件头注释;改动前先读文件头。