@kira905/dsh-memory-toolkit 0.1.3 → 0.2.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 (75) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.en.md +225 -2
  3. package/README.md +180 -2
  4. package/lib/deploy-guard.mjs +463 -275
  5. package/lib/guard-http.mjs +59 -62
  6. package/lib/pitfall-form-map.mjs +94 -84
  7. package/lib/pitfall-score.mjs +262 -185
  8. package/lib/session-file-name.mjs +116 -100
  9. package/lib/zstd-frames.mjs +88 -67
  10. package/memory-db-tools/README.md +29 -1
  11. package/memory-db-tools/audit-analyze.mjs +1 -1
  12. package/memory-db-tools/audit-export.mjs +60 -16
  13. package/memory-db-tools/audit-struct-probe.mjs +29 -8
  14. package/memory-db-tools/lib/known-devices.mjs +137 -0
  15. package/memory-engine/README.md +58 -4
  16. package/memory-engine/bench/bench.mjs +1 -1
  17. package/memory-engine/cli/band-sentinel.mjs +164 -0
  18. package/memory-engine/cli/distill-rollback.mjs +112 -44
  19. package/memory-engine/cli/distill-sessions.mjs +237 -0
  20. package/memory-engine/cli/distill.mjs +196 -95
  21. package/memory-engine/cli/doc-index-rebuild.mjs +142 -108
  22. package/memory-engine/cli/doc-index.mjs +47 -38
  23. package/memory-engine/cli/domain-tags.mjs +246 -162
  24. package/memory-engine/cli/layer-migrate.mjs +176 -0
  25. package/memory-engine/cli/layer-move.mjs +189 -0
  26. package/memory-engine/cli/pitfall-hook.mjs +136 -70
  27. package/memory-engine/cli/recall.mjs +243 -110
  28. package/memory-engine/cli/selfcheck.mjs +93 -44
  29. package/memory-engine/cli/serve-start.mjs +221 -140
  30. package/memory-engine/cli/sweep.mjs +83 -44
  31. package/memory-engine/cli/sync.mjs +93 -57
  32. package/memory-engine/config/audit-devices.json +6 -0
  33. package/memory-engine/config/bands.json +7 -0
  34. package/memory-engine/config/layers.json +7 -0
  35. package/memory-engine/dict/LICENSE-/350/257/215/345/205/270.md +53 -0
  36. package/memory-engine/dict/lexicon-config.json +11 -0
  37. package/memory-engine/dict//350/257/215/345/205/270.txt +109750 -0
  38. package/memory-engine/dict//350/257/215/351/242/221.txt +20000 -0
  39. package/memory-engine/lib/band-sentinel.mjs +417 -0
  40. package/memory-engine/lib/cli-strict.mjs +128 -0
  41. package/memory-engine/lib/cli-util.mjs +147 -125
  42. package/memory-engine/lib/config.mjs +83 -96
  43. package/memory-engine/lib/dirty-ledger.mjs +127 -94
  44. package/memory-engine/lib/distill-rollback.mjs +173 -133
  45. package/memory-engine/lib/distill.mjs +878 -585
  46. package/memory-engine/lib/doc-index-store.mjs +208 -235
  47. package/memory-engine/lib/doc-index.mjs +351 -350
  48. package/memory-engine/lib/doc-scan.mjs +178 -156
  49. package/memory-engine/lib/doc-summarize.mjs +220 -205
  50. package/memory-engine/lib/domain-tags.mjs +358 -277
  51. package/memory-engine/lib/embedder.mjs +175 -194
  52. package/memory-engine/lib/hashes.mjs +22 -7
  53. package/memory-engine/lib/layer-migrate.mjs +330 -0
  54. package/memory-engine/lib/layer-move.mjs +307 -0
  55. package/memory-engine/lib/layers.mjs +191 -0
  56. package/memory-engine/lib/lexicon-recall.mjs +473 -0
  57. package/memory-engine/lib/memory-source.mjs +78 -80
  58. package/memory-engine/lib/pitfall-source.mjs +115 -114
  59. package/memory-engine/lib/policy-source.mjs +279 -261
  60. package/memory-engine/lib/recall-log.mjs +285 -0
  61. package/memory-engine/lib/recall.mjs +565 -240
  62. package/memory-engine/lib/selfcheck.mjs +142 -106
  63. package/memory-engine/lib/session-distill.mjs +603 -0
  64. package/memory-engine/lib/session-source.mjs +421 -0
  65. package/memory-engine/lib/sweep.mjs +107 -130
  66. package/memory-engine/lib/sync.mjs +110 -140
  67. package/memory-engine/lib/tick-hook.mjs +123 -148
  68. package/memory-engine/lib/vectors-store.mjs +213 -188
  69. package/memory-engine/test/12-layering.test.mjs +539 -0
  70. package/memory-engine/test/13-session-distill.test.mjs +616 -0
  71. package/memory-engine/tools/bridge-smoke.mjs +94 -55
  72. package/memory-engine/tools/fetch-model.mjs +167 -87
  73. package/memory-engine/tools/smoke-recall.mjs +94 -38
  74. package/memory-engine/tools/verify-recall-chain.mjs +413 -0
  75. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
- // 记忆引擎 v2 · S3c —— 提炼产物回滚(方案 §3.5 回滚行)
1
+ // 提炼产物回滚(方案 §3.5 的回滚行)
2
2
  //
3
- // 口径:**按 `tags LIKE 'distilled%'` 批量列出 / 删除**;默认 dry-run;删除前 `VACUUM INTO` 快照。
4
- // 为什么按 tag 而不是按 id 清单:提炼条是**推断产物**,主人可能整批否定("这批结论不对"),
5
- // 也可能只否定某几组 ⇒ 两种粒度都要能选:`--key <srcKey>`(整组)/ `--id <行 id>`(单条)。
6
- // ⚠️ 删除语句**恒带 `AND tags LIKE 'distilled%'`** —— 即使调用方算错了 id 集合,也不可能误删真实记忆。
3
+ // 口径:按 `tags LIKE 'distilled%'` 批量列、批量删;默认 dry-run;真删之前先用 VACUUM INTO 拍快照。
4
+ // 为什么认 tag 而不认 id 清单:提炼条本身是**推断产物**,主人可能一句话否掉一整批,
5
+ // 也可能只否掉其中某几组 ⇒ 两种粒度都得给:`--key <srcKey>` 整组、`--id <行 id>` 单条。
6
+ // ⚠ 删除语句**恒带 `AND tags LIKE 'distilled%'`**:调用方就算把 id 集合算错,也误伤不到真实记忆。
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import crypto from 'node:crypto';
@@ -11,83 +11,103 @@ import { DatabaseSync } from 'node:sqlite';
11
11
  import { vacuumInto } from './domain-tags.mjs';
12
12
  import { DISTILL_PREFIX } from './distill.mjs';
13
13
 
14
- /** 解析提炼条标签 → {srcKey, srcIds, date}。 */
15
- export function parseDistilledTags(tags) {
16
- const t = String(tags ?? '');
17
- if (!t.startsWith(DISTILL_PREFIX)) return null;
18
- const key = t.match(/(?:^|\s)src=([0-9a-f]+)(?:\s|$)/);
19
- const ids = t.match(/(?:^|\s)srcids=([0-9,\s]*)(?:\s|$)/);
20
- const date = t.match(/(?:^|\s)date=(\d{8})(?:\s|$)/);
21
- return {
22
- srcKey: key ? key[1] : null,
23
- srcIds: ids && ids[1] ? ids[1].split(',').map((x) => Number(x.trim())).filter((x) => Number.isFinite(x)) : [],
24
- date: date ? date[1] : null,
25
- };
26
- }
14
+ // 标签里的三个属性位:来源键 / 来源 id 串 / 提炼日期
15
+ const TAG_SRCKEY_RE = /(?:^|\s)src=([0-9a-f]+)(?:\s|$)/;
16
+ const TAG_SRCIDS_RE = /(?:^|\s)srcids=([0-9,\s]*)(?:\s|$)/;
17
+ const TAG_DATE_RE = /(?:^|\s)date=(\d{8})(?:\s|$)/;
27
18
 
28
- function open(dbPath, { readOnly = false } = {}) {
29
- const db = readOnly ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
30
- if (!readOnly) db.exec('PRAGMA busy_timeout = 5000;');
31
- return db;
19
+ const SQL_LIST = `SELECT id, text, tags, updated_at, created_at%EXTRA% FROM memories WHERE tags LIKE 'distilled%' ORDER BY id`;
20
+ const SQL_TRIGGERS = "SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name='memories'";
21
+ const SQL_DELETE = "DELETE FROM memories WHERE id = ? AND tags LIKE 'distilled%'";
22
+
23
+ // 库里有没有某一列(老库没有 owner_user / visibility 这两个多人口径字段)
24
+ function columnExists(db, name) {
25
+ return db.prepare('PRAGMA table_info(memories)').all().some((row) => row.name === name);
32
26
  }
33
27
 
34
- /**
35
- * 列出提炼条(只读)。
36
- * @param {{dbPath:string, keys?:string[], ids?:number[], limit?:number, headChars?:number}} p
37
- */
38
- export function listDistilled({ dbPath, keys = null, ids = null, limit = 0, headChars = 100 }) {
39
- const db = open(dbPath, { readOnly: true });
28
+ // 统一开关库:只读走 readOnly 连接,写连接才设 busy_timeout;关库一律放 finally
29
+ function withDb(dbPath, { readOnly = false } = {}, body) {
30
+ const conn = readOnly ? new DatabaseSync(dbPath, { readOnly: true }) : new DatabaseSync(dbPath);
40
31
  try {
41
- const rows = db
42
- .prepare(`SELECT id, text, tags, updated_at, created_at${hasCol(db, 'owner_user') ? ', owner_user, visibility' : ''} FROM memories WHERE tags LIKE 'distilled%' ORDER BY id`)
43
- .all();
44
- let items = rows.map((r) => {
45
- const meta = parseDistilledTags(r.tags) ?? { srcKey: null, srcIds: [], date: null };
46
- return {
47
- id: Number(r.id), srcKey: meta.srcKey, srcIds: meta.srcIds, date: meta.date,
48
- chars: String(r.text ?? '').length,
49
- head: String(r.text ?? '').slice(0, headChars).replace(/\s+/g, ' '),
50
- tags: r.tags, createdAt: Number(r.created_at), updatedAt: Number(r.updated_at),
51
- ownerUser: r.owner_user ?? null, visibility: r.visibility ?? null,
52
- };
53
- });
54
- if (Array.isArray(keys) && keys.length) {
55
- const want = new Set(keys.map((k) => String(k)));
56
- items = items.filter((x) => want.has(String(x.srcKey)));
57
- }
58
- if (Array.isArray(ids) && ids.length) {
59
- const want = new Set(ids.map((x) => Number(x)));
60
- items = items.filter((x) => want.has(x.id));
61
- }
62
- if (limit > 0) items = items.slice(0, Number(limit));
63
- return items;
32
+ if (!readOnly) conn.exec('PRAGMA busy_timeout = 5000;');
33
+ return body(conn);
64
34
  } finally {
65
- try { db.close(); } catch { /* ignore */ }
35
+ try { conn.close(); } catch { /* 关不掉也不拖累结果 */ }
66
36
  }
67
37
  }
68
38
 
69
- function hasCol(db, name) {
70
- return db.prepare('PRAGMA table_info(memories)').all().some((r) => r.name === name);
39
+ // 解析提炼条标签 → {srcKey, srcIds, date};不是提炼条就给 null
40
+ export function parseDistilledTags(tags) {
41
+ const raw = String(tags ?? '');
42
+ if (!raw.startsWith(DISTILL_PREFIX)) return null;
43
+
44
+ const pick = (pattern) => {
45
+ const hit = pattern.exec(raw);
46
+ return hit ? hit[1] : '';
47
+ };
48
+ const idText = pick(TAG_SRCIDS_RE);
49
+ const ids = idText
50
+ ? idText.split(',').map((piece) => Number(piece.trim())).filter((n) => Number.isFinite(n))
51
+ : [];
52
+ return {
53
+ srcKey: pick(TAG_SRCKEY_RE) || null,
54
+ srcIds: ids,
55
+ date: pick(TAG_DATE_RE) || null,
56
+ };
71
57
  }
72
58
 
73
- /**
74
- * **内容级摘要**:按 id 排序取 (id, text, tags, pinned, updated_at) 逐行哈希再整体哈希。
75
- * 为什么不用文件 sha256:SQLite 写回后页布局会变(同样的内容也可能字节不同),
76
- * 文件 sha 无法回答"回滚后库是否回到原状";内容摘要可以(本卡演练的判据就靠它)。
77
- */
59
+ // 列出提炼条(只读);keys / ids 是两组独立过滤,limit > 0 才截断
60
+ export function listDistilled({ dbPath, keys = null, ids = null, limit = 0, headChars = 100 }) {
61
+ return withDb(dbPath, { readOnly: true }, (conn) => {
62
+ const extra = columnExists(conn, 'owner_user') ? ', owner_user, visibility' : '';
63
+ const rows = conn.prepare(SQL_LIST.replace('%EXTRA%', extra)).all();
64
+ const keySet = new Set((Array.isArray(keys) ? keys : []).map((k) => String(k)));
65
+ const idSet = new Set((Array.isArray(ids) ? ids : []).map((x) => Number(x)));
66
+
67
+ const listed = [];
68
+ for (const row of rows) {
69
+ const meta = parseDistilledTags(row.tags) ?? { srcKey: null, srcIds: [], date: null };
70
+ if (keySet.size > 0 && !keySet.has(String(meta.srcKey))) continue;
71
+ if (idSet.size > 0 && !idSet.has(Number(row.id))) continue;
72
+ const text = String(row.text ?? '');
73
+ listed.push({
74
+ id: Number(row.id),
75
+ srcKey: meta.srcKey,
76
+ srcIds: meta.srcIds,
77
+ date: meta.date,
78
+ chars: text.length,
79
+ head: text.slice(0, headChars).replace(/\s+/g, ' '),
80
+ tags: row.tags,
81
+ createdAt: Number(row.created_at),
82
+ updatedAt: Number(row.updated_at),
83
+ ownerUser: row.owner_user ?? null,
84
+ visibility: row.visibility ?? null,
85
+ });
86
+ }
87
+ return limit > 0 ? listed.slice(0, Number(limit)) : listed;
88
+ });
89
+ }
90
+
91
+ // **内容级摘要**:按 id 排序取 (id, text, tags, pinned, updated_at),逐行拼再整体哈希。
92
+ // 为什么不看文件 sha256:SQLite 写回后页布局会变(内容一样也可能字节不同),
93
+ // 文件 sha 回答不了"回滚后库是否回到原状",内容摘要能。
78
94
  export function contentDigest(dbPath, { excludeTagsLike = null } = {}) {
79
- const db = open(dbPath, { readOnly: true });
80
- try {
81
- const where = excludeTagsLike ? ` WHERE tags NOT LIKE '${excludeTagsLike}'` : '';
82
- const rows = db.prepare(`SELECT id, text, tags, pinned, updated_at FROM memories${where} ORDER BY id`).all();
83
- const h = crypto.createHash('sha256');
84
- for (const r of rows) h.update(`${r.id}\u0001${r.text}\u0001${r.tags}\u0001${r.pinned}\u0001${r.updated_at}\u0002`, 'utf8');
85
- return { count: rows.length, digest: h.digest('hex'), allCount: Number(db.prepare('SELECT COUNT(*) n FROM memories').get().n) };
86
- } finally {
87
- try { db.close(); } catch { /* ignore */ }
88
- }
95
+ return withDb(dbPath, { readOnly: true }, (conn) => {
96
+ const filter = excludeTagsLike ? ` WHERE tags NOT LIKE '${excludeTagsLike}'` : '';
97
+ const rows = conn.prepare(`SELECT id, text, tags, pinned, updated_at FROM memories${filter} ORDER BY id`).all();
98
+ const hash = crypto.createHash('sha256');
99
+ const FIELD_SEP = '\u0001';
100
+ const ROW_END = '\u0002';
101
+ for (const row of rows) {
102
+ const fields = [row.id, row.text, row.tags, row.pinned, row.updated_at].map((value) => String(value));
103
+ hash.update(`${fields.join(FIELD_SEP)}${ROW_END}`, 'utf8');
104
+ }
105
+ const allCount = Number(conn.prepare('SELECT COUNT(*) n FROM memories').get().n);
106
+ return { count: rows.length, digest: hash.digest('hex'), allCount };
107
+ });
89
108
  }
90
109
 
110
+ // 文件 sha256(读不动就给 null,怎么记由调用方决定)
91
111
  export function fileSha256(file) {
92
112
  try {
93
113
  return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
@@ -96,83 +116,103 @@ export function fileSha256(file) {
96
116
  }
97
117
  }
98
118
 
99
- /**
100
- * 批量回滚。
101
- * @param {{dbPath:string, apply?:boolean, keys?:string[], ids?:number[], snapshotDir?:string,
102
- * now?:number, keepSnapshot?:boolean}} p
103
- * @returns {{ok:boolean, matched:number, deleted:number, deletedIds:number[], snapshot:string|null,
104
- * before:object, after:object, dryRun:boolean, rollbackFile:string|null}}
105
- */
106
- export function rollbackDistilled(p = {}) {
107
- const { dbPath, keys = null, ids = null, snapshotDir = null, now = Date.now(), apply = false } = p;
108
- const before = contentDigest(dbPath);
109
- const items = listDistilled({ dbPath, keys, ids });
110
- const out = {
111
- ok: true, dryRun: !apply, matched: items.length, deleted: 0, deletedIds: [],
112
- snapshot: null, before, after: before, items: items.map((x) => ({ id: x.id, srcKey: x.srcKey, head: x.head })),
113
- rollbackFile: null, warnings: [],
114
- };
115
- if (!items.length) return out;
116
-
117
- if (!apply) return out;
118
-
119
- // 删除前快照(**唯一回滚点**)
119
+ // 删除前快照:这是整条链上唯一的回滚点
120
+ function writeSnapshot(dbPath, snapshotDir, now) {
120
121
  const dir = snapshotDir ?? path.join(path.dirname(dbPath), 'snapshots');
121
122
  fs.mkdirSync(dir, { recursive: true });
122
123
  const stamp = new Date(Number(now)).toISOString().replace(/[:.]/g, '-');
123
124
  const snap = path.join(dir, `memory-before-distill-rollback-${stamp}.db`);
124
125
  vacuumInto(dbPath, snap);
125
- out.snapshot = snap;
126
- out.snapshotSha256 = fileSha256(snap);
126
+ return { dir, stamp, snap };
127
+ }
127
128
 
128
- const db = open(dbPath);
129
- try {
130
- // FTS 同步靠 `memories_ad` 触发器(触发器缺失时 DELETE 不会清 FTS 影子表 ⇒ 显式提示)
131
- const triggers = db.prepare("SELECT name FROM sqlite_master WHERE type='trigger' AND tbl_name='memories'").all().map((r) => r.name);
132
- out.triggers = triggers;
133
- if (!triggers.some((n) => /_ad$|delete/i.test(n))) out.warnings.push('未发现 memories 的 AFTER DELETE 触发器 ⇒ FTS 影子表可能残留(建议核对 memories_fts)');
134
-
135
- const targetIds = items.map((x) => x.id);
136
- const stmt = db.prepare(`DELETE FROM memories WHERE id = ? AND tags LIKE 'distilled%'`);
137
- db.exec('BEGIN IMMEDIATE');
129
+ // 真删:先记触发器现场(FTS 影子表靠 memories_ad 同步),再在一个事务里逐条删
130
+ function deleteDistilledRows(dbPath, items) {
131
+ return withDb(dbPath, {}, (conn) => {
132
+ const triggers = conn.prepare(SQL_TRIGGERS).all().map((row) => row.name);
133
+ const wipe = conn.prepare(SQL_DELETE);
134
+ const removed = [];
135
+ conn.exec('BEGIN IMMEDIATE');
138
136
  try {
139
- for (const id of targetIds) {
140
- const r = stmt.run(id);
141
- if (Number(r.changes || 0) > 0) out.deletedIds.push(id);
137
+ for (const item of items) {
138
+ const res = wipe.run(item.id);
139
+ if (Number(res.changes || 0) > 0) removed.push(item.id);
142
140
  }
143
- db.exec('COMMIT');
144
- } catch (e) {
145
- try { db.exec('ROLLBACK'); } catch { /* ignore */ }
146
- throw e;
141
+ conn.exec('COMMIT');
142
+ } catch (err) {
143
+ try { conn.exec('ROLLBACK'); } catch { /* 忽略 */ }
144
+ throw err;
147
145
  }
148
- out.deleted = out.deletedIds.length;
149
- } finally {
150
- try { db.close(); } catch { /* ignore */ }
146
+ return { triggers, removed };
147
+ });
148
+ }
149
+
150
+ // 生成"把快照写回目标库"的一键恢复脚本(覆盖前先把现有库另存一份)
151
+ function restoreScriptText({ snapshot, target }) {
152
+ const lines = [
153
+ '// 自动生成:把提炼回滚前的快照写回目标记忆库(覆盖式,覆盖前先另存现有库)',
154
+ '// 用法:node <本文件路径>',
155
+ "import fs from 'node:fs';",
156
+ `const SNAPSHOT = ${JSON.stringify(snapshot)};`,
157
+ `const DB_TARGET = ${JSON.stringify(target)};`,
158
+ "const CURRENT_SUFFIXES = ['', '-wal', '-shm'];",
159
+ 'if (!fs.existsSync(SNAPSHOT)) {',
160
+ " console.error('快照不存在:' + SNAPSHOT);",
161
+ ' process.exit(3);',
162
+ '}',
163
+ 'for (const suffix of CURRENT_SUFFIXES) {',
164
+ ' const file = DB_TARGET + suffix;',
165
+ " try { if (fs.existsSync(file)) fs.copyFileSync(file, file + '.before-restore'); } catch { /* 忽略 */ }",
166
+ '}',
167
+ 'fs.copyFileSync(SNAPSHOT, DB_TARGET);',
168
+ "for (const suffix of ['-wal', '-shm']) { try { fs.rmSync(DB_TARGET + suffix, { force: true }); } catch { /* 忽略 */ } }",
169
+ "console.log('已恢复:' + DB_TARGET + ' <- ' + SNAPSHOT);",
170
+ '',
171
+ ];
172
+ return lines.join('\n');
173
+ }
174
+
175
+ // 批量回滚主入口:先摘要 + 列表;没命中或 dry-run 都早退;真删才拍快照
176
+ export function rollbackDistilled(p = {}) {
177
+ const { dbPath, keys = null, ids = null, snapshotDir = null, now = Date.now(), apply = false } = p;
178
+ const before = contentDigest(dbPath);
179
+ const items = listDistilled({ dbPath, keys, ids });
180
+
181
+ const out = {
182
+ ok: true,
183
+ dryRun: !apply,
184
+ matched: items.length,
185
+ deleted: 0,
186
+ deletedIds: [],
187
+ snapshot: null,
188
+ before,
189
+ after: before,
190
+ items: items.map((item) => ({ id: item.id, srcKey: item.srcKey, head: item.head })),
191
+ rollbackFile: null,
192
+ warnings: [],
193
+ };
194
+ if (items.length === 0) return out;
195
+ if (!apply) return out;
196
+
197
+ const shot = writeSnapshot(dbPath, snapshotDir, now);
198
+ out.snapshot = shot.snap;
199
+ out.snapshotSha256 = fileSha256(shot.snap);
200
+
201
+ const wiped = deleteDistilledRows(dbPath, items);
202
+ out.triggers = wiped.triggers;
203
+ if (!wiped.triggers.some((name) => /_ad$|delete/i.test(name))) {
204
+ out.warnings.push('memories 上没有 AFTER DELETE 触发器 ⇒ FTS 影子表可能残留脏行(建议核对 memories_fts)');
151
205
  }
206
+ out.deletedIds = wiped.removed;
207
+ out.deleted = wiped.removed.length;
208
+
152
209
  out.after = contentDigest(dbPath);
153
- // 反向回滚脚本(把快照拷回去 = 一键恢复)
210
+
211
+ // 反向回滚脚本:把快照拷回去 = 一键恢复
154
212
  if (out.snapshot) {
155
- const rbFile = path.join(dir, `restore-memory-${stamp}.mjs`);
156
- fs.writeFileSync(rbFile, restoreScript({ snapshot: snap, target: dbPath }), 'utf8');
213
+ const rbFile = path.join(shot.dir, `restore-memory-${shot.stamp}.mjs`);
214
+ fs.writeFileSync(rbFile, restoreScriptText({ snapshot: shot.snap, target: dbPath }), 'utf8');
157
215
  out.rollbackFile = rbFile;
158
216
  }
159
217
  return out;
160
218
  }
161
-
162
- /** 生成"把快照写回目标库"的一键恢复脚本(纯 ASCII 头 + 覆盖前校验存在性)。 */
163
- function restoreScript({ snapshot, target }) {
164
- return `// 自动生成:把提炼回滚前的快照写回目标记忆库(覆盖式)
165
- // 用法:node ${path.basename('restore-memory.mjs')} 或 node "本文件路径"
166
- import fs from 'node:fs';
167
- const SNAP = ${JSON.stringify(snapshot)};
168
- const TARGET = ${JSON.stringify(target)};
169
- if (!fs.existsSync(SNAP)) { console.error('快照不存在:' + SNAP); process.exit(3); }
170
- for (const suffix of ['', '-wal', '-shm']) {
171
- const f = TARGET + suffix;
172
- try { if (fs.existsSync(f)) fs.copyFileSync(f, f + '.before-restore'); } catch { /* ignore */ }
173
- }
174
- fs.copyFileSync(SNAP, TARGET);
175
- for (const suffix of ['-wal', '-shm']) { try { fs.rmSync(TARGET + suffix, { force: true }); } catch { /* ignore */ } }
176
- console.log('已恢复:' + TARGET + ' <- ' + SNAP);
177
- `;
178
- }