@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,48 +1,77 @@
1
- // 记忆引擎 v2 · 离线层 —— 独立向量库(方案 §3.1)
1
+ // 独立向量库(方案 §3.1)
2
2
  //
3
- // 为什么独立库(D3 已拍):memory.db 是 DELETE 模式 + busy_timeout=0,
4
- // 任何第三个写者都可能让主人的 memory_write 失败。向量是可重建的派生物。
3
+ // 为什么单独一个 SQLite 文件:主记忆库跑 DELETE 日志模式、busy_timeout 是 0,
4
+ // 再挂一个写者就可能把主人的 memory_write 撞成失败。向量本身可由正文重算,是纯派生物。
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
7
  import { DatabaseSync } from 'node:sqlite';
8
8
  import { SCHEMA_VERSION } from './config.mjs';
9
9
 
10
- const DDL = `
11
- CREATE TABLE IF NOT EXISTS vectors (
12
- source TEXT NOT NULL,
13
- ref_id TEXT NOT NULL,
14
- model TEXT NOT NULL,
15
- dim INTEGER NOT NULL,
16
- vec BLOB NOT NULL,
17
- text_hash TEXT NOT NULL,
18
- updated_at INTEGER NOT NULL,
19
- PRIMARY KEY (source, ref_id)
20
- );
21
- CREATE TABLE IF NOT EXISTS meta (
22
- k TEXT PRIMARY KEY,
23
- v TEXT NOT NULL
24
- );
25
- CREATE INDEX IF NOT EXISTS vectors_source_model ON vectors (source, model);
10
+ // 建表改成「列定义表 → 拼 SQL」:以后加列只动这张表,不再改 SQL 正文
11
+ const VECTOR_COLUMN_SPEC = [
12
+ ['source', 'TEXT NOT NULL'],
13
+ ['ref_id', 'TEXT NOT NULL'],
14
+ ['model', 'TEXT NOT NULL'],
15
+ ['dim', 'INTEGER NOT NULL'],
16
+ ['vec', 'BLOB NOT NULL'],
17
+ ['text_hash', 'TEXT NOT NULL'],
18
+ ['updated_at', 'INTEGER NOT NULL'],
19
+ ];
20
+
21
+ const VECTOR_COLUMN_SQL = VECTOR_COLUMN_SPEC.map(([col, type]) => ` ${col} ${type},`).join('\n');
22
+
23
+ const DDL = [
24
+ `CREATE TABLE IF NOT EXISTS vectors (\n${VECTOR_COLUMN_SQL}\n PRIMARY KEY (source, ref_id)\n);`,
25
+ 'CREATE TABLE IF NOT EXISTS meta (k TEXT PRIMARY KEY, v TEXT NOT NULL);',
26
+ 'CREATE INDEX IF NOT EXISTS vectors_source_model ON vectors (source, model);',
27
+ ].join('\n');
28
+
29
+ const SQL_META_READ = 'SELECT v FROM meta WHERE k = ?';
30
+ const SQL_META_UPSERT = `
31
+ INSERT INTO meta (k, v) VALUES (?, ?)
32
+ ON CONFLICT(k) DO UPDATE SET v = excluded.v
26
33
  `;
34
+ const SQL_META_DUMP = 'SELECT k, v FROM meta ORDER BY k';
35
+ const SQL_TABLE_PROBE = 'SELECT name FROM sqlite_master WHERE type = ? AND name = ?';
36
+ const SQL_VECTOR_UPSERT = `
37
+ INSERT INTO vectors (source, ref_id, model, dim, vec, text_hash, updated_at)
38
+ VALUES (?, ?, ?, ?, ?, ?, ?)
39
+ ON CONFLICT(source, ref_id) DO UPDATE SET
40
+ model = excluded.model,
41
+ dim = excluded.dim,
42
+ vec = excluded.vec,
43
+ text_hash = excluded.text_hash,
44
+ updated_at = excluded.updated_at
45
+ `;
46
+ const SQL_HASH_READ = 'SELECT text_hash FROM vectors WHERE source = ? AND ref_id = ?';
47
+ const SQL_COUNT_ALL = 'SELECT COUNT(*) AS n FROM vectors';
48
+ const SQL_COUNT_BY_SOURCE = 'SELECT COUNT(*) AS n FROM vectors WHERE source = ?';
49
+ const SQL_COUNT_BY_MODEL = 'SELECT COUNT(*) AS n FROM vectors WHERE source = ? AND model = ?';
50
+ const SQL_SOURCE_DELETE = 'DELETE FROM vectors WHERE source = ?';
51
+ const SQL_MATRIX_BASE = 'SELECT ref_id, model, dim, vec, text_hash FROM vectors WHERE source = ?';
52
+ const SQL_MATRIX_ORDER = 'ORDER BY CAST(ref_id AS INTEGER)';
27
53
 
28
- /** meta 键名(方案 §3.1 要求的前 6 项;后两项为兜底轮转的必需运行状态) */
29
- export const META_KEYS = {
30
- schemaVersion: 'schemaVersion',
31
- hostId: 'hostId',
32
- modelId: 'modelId',
33
- dim: 'dim',
34
- lastIncrementalSyncAt: 'lastIncrementalSyncAt',
35
- lastFullHashSweepAt: 'lastFullHashSweepAt',
36
- hashSweepCursor: 'hashSweepCursor',
37
- lastSyncStats: 'lastSyncStats',
38
- };
54
+ const META_KEY_NAMES = [
55
+ 'schemaVersion',
56
+ 'hostId',
57
+ 'modelId',
58
+ 'dim',
59
+ 'lastIncrementalSyncAt',
60
+ 'lastFullHashSweepAt',
61
+ 'hashSweepCursor',
62
+ 'lastSyncStats',
63
+ ];
64
+
65
+ /** meta 键名(方案 §3.1 要的前 6 项;后 2 项是兜底轮转的运行状态) */
66
+ export const META_KEYS = META_KEY_NAMES.reduce((bag, name) => {
67
+ bag[name] = name;
68
+ return bag;
69
+ }, {});
39
70
 
40
71
  /**
41
- * 按「来源」命名空间的 meta 键前缀(记忆 v2 §3 A.4 / 2026-10-10 落地)。
42
- *
43
- * 为什么必须分命名空间:坑库(`source='pitfall'`)与记忆(`source='memory'`)**共用一个 vectors.db**,
44
- * 两者的水位/游标/模型键若同名会互相覆盖 ⇒ 表现为「记忆增量漏编码且不报错」(方案 §1.3-③.1 / §A.6 首要风险)。
45
- * ⇒ 这四个键(+ 两个运行状态键)一律写成 `<key>:<source>`。
72
+ * 需要按来源分命名空间的键(记忆 v2 §3 A.4)。
73
+ * 坑库与记忆共用一个 vectors.db,同名键会互相覆盖 —— 症状是「记忆增量漏编码却不报错」。
74
+ * 因此这些键一律存成 `<key>:<source>`。
46
75
  */
47
76
  export const SOURCE_SCOPED_META_KEYS = [
48
77
  'modelId',
@@ -53,62 +82,69 @@ export const SOURCE_SCOPED_META_KEYS = [
53
82
  'lastSyncStats',
54
83
  ];
55
84
 
56
- /** `<baseKey>:<source>`(来源命名空间的 meta 键名) */
85
+ /** 内部:读一个键(没有就 null) */
86
+ function metaRead(db, key) {
87
+ const row = db.prepare(SQL_META_READ).get(key);
88
+ return row ? row.v : null;
89
+ }
90
+
91
+ /** 内部:写一个键(覆盖式) */
92
+ function metaWrite(db, key, value) {
93
+ db.prepare(SQL_META_UPSERT).run(String(key), String(value));
94
+ }
95
+
96
+ /** `<baseKey>:<source>` 形式的键名 */
57
97
  export function metaKeyFor(baseKey, source) {
58
98
  return `${baseKey}:${source}`;
59
99
  }
60
100
 
61
101
  /**
62
- * 读 meta(按来源):先读命名空间键;读不到且来源是 memory ⇒ **兼容读**无后缀旧键
63
- * (老库尚未迁移的那一次;语义等价,不改变任何默认行为)。
102
+ * 按来源读 meta:先认命名空间键;落空且来源是 memory 时兼容读无后缀旧键
103
+ * (老库还没迁移的那一次,语义与迁移后一致)。
64
104
  */
65
105
  export function getMetaFor(db, baseKey, source) {
66
- const v = getMeta(db, metaKeyFor(baseKey, source));
67
- if (v != null) return v;
68
- if (String(source) === 'memory') return getMeta(db, baseKey);
69
- return null;
106
+ const scoped = metaRead(db, metaKeyFor(baseKey, source));
107
+ if (scoped != null) return scoped;
108
+ return String(source) === 'memory' ? metaRead(db, baseKey) : null;
70
109
  }
71
110
 
72
111
  /**
73
- * 写 meta(按来源)+ **冗余写**(方案 §A.4 的 C4 选项①):
74
- * memory 侧同时把无后缀旧键写成同值 ⇒ 回滚到旧代码时读到的水位仍然正确,
75
- * 不会走「首次全量」分支把整库 DELETE 重建(626 条 ≈ 102 s 的一次数据面事件)。
112
+ * 按来源写 meta,并**冗余**写一份无后缀旧键(方案 §A.4 的 C4 选项①)。
113
+ * 目的:回滚到旧代码时水位仍然读得到,不会走「首次全量」把整库 DELETE 重建。
76
114
  */
77
115
  export function setMetaFor(db, baseKey, source, value) {
78
- setMeta(db, metaKeyFor(baseKey, source), value);
79
- if (String(source) === 'memory') setMeta(db, baseKey, value);
116
+ metaWrite(db, metaKeyFor(baseKey, source), value);
117
+ if (String(source) === 'memory') metaWrite(db, baseKey, value);
80
118
  }
81
119
 
82
120
  /**
83
- * 一次性命名空间迁移(**幂等**):把无后缀旧键的现值播种到 `<key>:memory`。
84
- * 只在「旧键在 且 新键缺」时写 ⇒ 跑多少次都只迁移一次;旧键**保留不动**(= 冗余写)。
85
- * @returns {string[]} 本次实际迁移的键名
121
+ * 一次性命名空间迁移(幂等):把旧键现值播种到 `<key>:memory`。
122
+ * 触发条件 = 旧键在、且新键缺;跑几次都只迁一次,旧键保留不算删。
123
+ * @returns {string[]} 本次真正迁过的键名
86
124
  */
87
125
  export function migrateMetaNamespace(db) {
88
- const migrated = [];
89
- for (const k of SOURCE_SCOPED_META_KEYS) {
90
- const old = getMeta(db, k);
91
- if (old == null) continue;
92
- if (getMeta(db, metaKeyFor(k, 'memory')) != null) continue;
93
- setMeta(db, metaKeyFor(k, 'memory'), old);
94
- migrated.push(k);
126
+ const touched = [];
127
+ for (const base of SOURCE_SCOPED_META_KEYS) {
128
+ const legacyValue = metaRead(db, base);
129
+ if (legacyValue == null) continue;
130
+ const scopedKey = metaKeyFor(base, 'memory');
131
+ if (metaRead(db, scopedKey) != null) continue;
132
+ metaWrite(db, scopedKey, legacyValue);
133
+ touched.push(base);
95
134
  }
96
- return migrated;
135
+ return touched;
97
136
  }
98
137
 
99
- function tableExists(db, name) {
100
- const r = db
101
- .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
102
- .get(name);
103
- return Boolean(r);
138
+ /** 内部:某张表在不在 */
139
+ function hasTable(db, name) {
140
+ const probe = db.prepare(SQL_TABLE_PROBE).get('table', name);
141
+ return probe !== undefined && probe !== null;
104
142
  }
105
143
 
106
144
  export function ensureSchema(db) {
107
- const created = [];
108
- if (!tableExists(db, 'vectors')) created.push('vectors');
109
- if (!tableExists(db, 'meta')) created.push('meta');
145
+ const missing = ['vectors', 'meta'].filter((name) => !hasTable(db, name));
110
146
  db.exec(DDL);
111
- return { created, tables: { vectors: tableExists(db, 'vectors'), meta: tableExists(db, 'meta') } };
147
+ return { created: missing, tables: { vectors: hasTable(db, 'vectors'), meta: hasTable(db, 'meta') } };
112
148
  }
113
149
 
114
150
  /**
@@ -117,176 +153,165 @@ export function ensureSchema(db) {
117
153
  * @param {{create?: boolean}} [opts]
118
154
  */
119
155
  export function openVectorsStore(dbPath, opts = {}) {
120
- const create = opts.create !== false;
121
- if (create) fs.mkdirSync(path.dirname(dbPath), { recursive: true });
122
- if (create === false && !fs.existsSync(dbPath)) {
123
- throw new Error(`向量库不存在: ${dbPath}`);
124
- }
156
+ const mayCreate = opts.create !== false;
157
+ if (mayCreate) fs.mkdirSync(path.dirname(dbPath), { recursive: true });
158
+ else if (!fs.existsSync(dbPath)) throw new Error(`向量库不存在: ${dbPath}`);
159
+
125
160
  const db = new DatabaseSync(dbPath);
126
161
  db.exec('PRAGMA busy_timeout = 5000;');
127
162
  try {
128
163
  db.exec('PRAGMA journal_mode = WAL;');
129
164
  } catch {
130
- /* 只读介质上 WAL 可能不可用,退回默认模式(本机不属此情况) */
165
+ // 只读介质上 WAL 可能起不来:退回默认日志模式即可,不影响读写语义
131
166
  }
167
+
132
168
  const schema = ensureSchema(db);
133
- // 【2026-10-10 · 补坑库联动A】meta 命名空间一次性迁移(幂等;旧键保留 = 冗余写,可回滚)
169
+ // 命名空间迁移放在 schemaVersion 之前:迁移发生过就顺手把版本号推上去(表结构没变,只是键位变宽)
134
170
  const migrated = migrateMetaNamespace(db);
135
- const sv = getMeta(db, META_KEYS.schemaVersion);
136
- if (sv == null) {
137
- setMeta(db, META_KEYS.schemaVersion, SCHEMA_VERSION);
138
- } else if (migrated.length && sv !== SCHEMA_VERSION) {
139
- // 迁移实际发生 ⇒ 升版本号(表结构未变,只是键位扩了命名空间)
140
- setMeta(db, META_KEYS.schemaVersion, SCHEMA_VERSION);
141
- }
171
+ const storedVersion = metaRead(db, META_KEYS.schemaVersion);
172
+ const shouldStamp = storedVersion == null || (migrated.length > 0 && storedVersion !== SCHEMA_VERSION);
173
+ if (shouldStamp) metaWrite(db, META_KEYS.schemaVersion, SCHEMA_VERSION);
174
+
142
175
  return { db, dbPath, schema, migrated };
143
176
  }
144
177
 
145
- export function getMeta(db, k) {
146
- const r = db.prepare('SELECT v FROM meta WHERE k = ?').get(k);
147
- return r ? r.v : null;
148
- }
178
+ export const getMeta = (db, key) => metaRead(db, key);
149
179
 
150
- export function setMeta(db, k, v) {
151
- db.prepare(
152
- 'INSERT INTO meta (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v',
153
- ).run(String(k), String(v));
154
- }
180
+ export const setMeta = (db, key, value) => metaWrite(db, key, value);
155
181
 
156
- export function getMetaAll(db) {
157
- const out = {};
158
- for (const r of db.prepare('SELECT k, v FROM meta ORDER BY k').all()) out[r.k] = r.v;
159
- return out;
160
- }
182
+ export const getMetaAll = (db) => {
183
+ const bag = {};
184
+ for (const row of db.prepare(SQL_META_DUMP).all()) bag[row.k] = row.v;
185
+ return bag;
186
+ };
161
187
 
162
188
  export function vecToBlob(vec) {
163
- const f = vec instanceof Float32Array ? vec : Float32Array.from(vec);
164
- return Buffer.from(f.buffer, f.byteOffset, f.byteLength);
189
+ const floats = vec instanceof Float32Array ? vec : Float32Array.from(vec);
190
+ return Buffer.from(floats.buffer, floats.byteOffset, floats.byteLength);
165
191
  }
166
192
 
167
193
  export function blobToVec(blob) {
168
- // 注意:sqlite 返回的 Buffer 可能是池化分配(byteOffset 不对齐 4 字节)⇒ 必须切片后重建
169
- const ab = blob.buffer.slice(blob.byteOffset, blob.byteOffset + blob.byteLength);
170
- return new Float32Array(ab);
194
+ // sqlite 回来的 Buffer 可能是池化分配的(byteOffset 不保证 4 字节对齐)⇒ 先按窗口切一份再建视图
195
+ const { buffer, byteOffset, byteLength } = blob;
196
+ return new Float32Array(buffer.slice(byteOffset, byteOffset + byteLength));
171
197
  }
172
198
 
173
- /** 批量 upsert(单事务) */
174
- export function upsertVectors(db, items) {
175
- if (!items.length) return 0;
176
- const stmt = db.prepare(`
177
- INSERT INTO vectors (source, ref_id, model, dim, vec, text_hash, updated_at)
178
- VALUES (?, ?, ?, ?, ?, ?, ?)
179
- ON CONFLICT(source, ref_id) DO UPDATE SET
180
- model = excluded.model,
181
- dim = excluded.dim,
182
- vec = excluded.vec,
183
- text_hash = excluded.text_hash,
184
- updated_at = excluded.updated_at
185
- `);
199
+ /** 内部:开事务跑一小段 body(异常先回滚再原样抛出) */
200
+ function inTransaction(db, body) {
186
201
  db.exec('BEGIN');
187
202
  try {
188
- for (const it of items) {
189
- stmt.run(
190
- it.source,
191
- String(it.refId),
192
- it.model,
193
- it.dim,
194
- vecToBlob(it.vec),
195
- it.textHash,
196
- it.updatedAt,
197
- );
198
- }
203
+ const outcome = body();
199
204
  db.exec('COMMIT');
200
- } catch (e) {
205
+ return outcome;
206
+ } catch (err) {
201
207
  try {
202
208
  db.exec('ROLLBACK');
203
209
  } catch {
204
- /* ignore */
210
+ // 回滚自己失败也不掩盖原始错误
205
211
  }
206
- throw e;
212
+ throw err;
207
213
  }
214
+ }
215
+
216
+ /** 批量 upsert(单事务) */
217
+ export function upsertVectors(db, items) {
218
+ if (items.length === 0) return 0;
219
+ const put = db.prepare(SQL_VECTOR_UPSERT);
220
+ inTransaction(db, () => {
221
+ for (const item of items) {
222
+ put.run(
223
+ item.source,
224
+ String(item.refId),
225
+ item.model,
226
+ item.dim,
227
+ vecToBlob(item.vec),
228
+ item.textHash,
229
+ item.updatedAt,
230
+ );
231
+ }
232
+ });
208
233
  return items.length;
209
234
  }
210
235
 
211
- /** 取指定 ref_id 的已有哈希(增量同步的跳过判据) */
236
+ /** 取指定 ref_id 的已有哈希(增量同步据此跳过没变的条目) */
212
237
  export function getHashes(db, source, refIds) {
213
- const out = new Map();
214
- if (!refIds.length) return out;
215
- const stmt = db.prepare('SELECT text_hash FROM vectors WHERE source = ? AND ref_id = ?');
216
- for (const id of refIds) {
217
- const r = stmt.get(source, String(id));
218
- if (r) out.set(String(id), r.text_hash);
238
+ const found = new Map();
239
+ if (refIds.length === 0) return found;
240
+ const probe = db.prepare(SQL_HASH_READ);
241
+ for (const refId of refIds) {
242
+ const row = probe.get(source, String(refId));
243
+ if (row) found.set(String(refId), row.text_hash);
219
244
  }
220
- return out;
245
+ return found;
221
246
  }
222
247
 
223
248
  export function countVectors(db, source) {
224
- const r = source
225
- ? db.prepare('SELECT COUNT(*) AS n FROM vectors WHERE source = ?').get(source)
226
- : db.prepare('SELECT COUNT(*) AS n FROM vectors').get();
227
- return r.n;
249
+ const row = source ? db.prepare(SQL_COUNT_BY_SOURCE).get(source) : db.prepare(SQL_COUNT_ALL).get();
250
+ return row.n;
228
251
  }
229
252
 
230
253
  export function countByModel(db, source, model) {
231
- return db
232
- .prepare('SELECT COUNT(*) AS n FROM vectors WHERE source = ? AND model = ?')
233
- .get(source, model).n;
254
+ return db.prepare(SQL_COUNT_BY_MODEL).get(source, model).n;
234
255
  }
235
256
 
236
257
  export function deleteSource(db, source) {
237
- const r = db.prepare('DELETE FROM vectors WHERE source = ?').run(source);
238
- return Number(r.changes || 0);
258
+ const res = db.prepare(SQL_SOURCE_DELETE).run(source);
259
+ return Number(res.changes || 0);
260
+ }
261
+
262
+ /** 空矩阵(形状与正常返回一致,调用方不用判空) */
263
+ function emptyMatrix() {
264
+ return {
265
+ refIds: [],
266
+ refIdsNum: [],
267
+ dim: 0,
268
+ data: new Float32Array(0),
269
+ count: 0,
270
+ textHashes: [],
271
+ models: [],
272
+ };
239
273
  }
240
274
 
241
275
  /**
242
- * 载入整个矩阵(召回用;万级候选纯内存暴力检索,方案 §3.3)。
243
- * @returns {{refIds: string[], refIdsNum: number[], dim: number, data: Float32Array, count: number, textHashes: string[], models: string[]}}
276
+ * 载入整个矩阵(召回用;万级候选直接内存暴力扫,方案 §3.3)。
277
+ * @returns {{refIds: string[], refIdsNum: number[], dim: number, data: Float32Array, count: number,
278
+ * textHashes: string[], models: string[]}}
244
279
  */
245
280
  export function loadMatrix(db, source, model = null) {
246
- const rows = model
247
- ? db
248
- .prepare(
249
- 'SELECT ref_id, model, dim, vec, text_hash FROM vectors WHERE source = ? AND model = ? ORDER BY CAST(ref_id AS INTEGER)',
250
- )
251
- .all(source, model)
252
- : db
253
- .prepare(
254
- 'SELECT ref_id, model, dim, vec, text_hash FROM vectors WHERE source = ? ORDER BY CAST(ref_id AS INTEGER)',
255
- )
256
- .all(source);
257
- if (!rows.length) {
258
- return {
259
- refIds: [],
260
- refIdsNum: [],
261
- dim: 0,
262
- data: new Float32Array(0),
263
- count: 0,
264
- textHashes: [],
265
- models: [],
266
- };
267
- }
268
- const dim = rows[0].dim;
269
- const data = new Float32Array(rows.length * dim);
270
- rows.forEach((r, i) => {
271
- if (r.dim !== dim) {
281
+ const filterByModel = Boolean(model);
282
+ const sql = `${filterByModel ? `${SQL_MATRIX_BASE} AND model = ?` : SQL_MATRIX_BASE} ${SQL_MATRIX_ORDER}`;
283
+ const recs = filterByModel ? db.prepare(sql).all(source, model) : db.prepare(sql).all(source);
284
+ if (recs.length === 0) return emptyMatrix();
285
+
286
+ const dim = recs[0].dim;
287
+ const count = recs.length;
288
+ const data = new Float32Array(count * dim);
289
+ const refIds = [];
290
+ const refIdsNum = [];
291
+ const textHashes = [];
292
+ const models = [];
293
+ const seenModel = new Set();
294
+
295
+ for (let i = 0; i < count; i += 1) {
296
+ const rec = recs[i];
297
+ if (rec.dim !== dim) {
272
298
  throw new Error(
273
- `向量库维度不一致:ref_id=${r.ref_id} dim=${r.dim},首个为 ${dim}(应全量重建)`,
299
+ `向量库维度不一致:ref_id=${rec.ref_id} dim=${rec.dim},首个为 ${dim}(应全量重建)`,
274
300
  );
275
301
  }
276
- const v = blobToVec(r.vec);
277
- if (v.length !== dim) {
278
- throw new Error(`向量 blob 长度异常:ref_id=${r.ref_id} len=${v.length} 期望 ${dim}`);
302
+ const vec = blobToVec(rec.vec);
303
+ if (vec.length !== dim) {
304
+ throw new Error(`向量 blob 长度异常:ref_id=${rec.ref_id} len=${vec.length} 期望 ${dim}`);
279
305
  }
280
- data.set(v, i * dim);
281
- });
282
- const models = [...new Set(rows.map((r) => r.model))];
283
- return {
284
- refIds: rows.map((r) => String(r.ref_id)),
285
- refIdsNum: rows.map((r) => Number(r.ref_id)),
286
- dim,
287
- data,
288
- count: rows.length,
289
- textHashes: rows.map((r) => r.text_hash),
290
- models,
291
- };
306
+ data.set(vec, i * dim);
307
+ refIds.push(String(rec.ref_id));
308
+ refIdsNum.push(Number(rec.ref_id));
309
+ textHashes.push(rec.text_hash);
310
+ if (!seenModel.has(rec.model)) {
311
+ seenModel.add(rec.model);
312
+ models.push(rec.model);
313
+ }
314
+ }
315
+
316
+ return { refIds, refIdsNum, dim, data, count, textHashes, models };
292
317
  }