@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,214 @@
1
+ // 04 · 召回(向量为主 + FTS 补位 + RRF 融合 + 排除 pinned + 触发条件)
2
+ import { test, assert, assertEq } from './harness.mjs';
3
+ import { makeTempRoot, makeMemoryDb, setPinned } from './fixtures.mjs';
4
+ import { openVectorsStore } from '../lib/vectors-store.mjs';
5
+ import { openMemorySource } from '../lib/memory-source.mjs';
6
+ import { runIncrementalSync } from '../lib/sync.mjs';
7
+ import { createStubEmbedder } from '../lib/embedder.mjs';
8
+ import {
9
+ shouldTrigger,
10
+ extractFtsTokens,
11
+ buildFtsMatch,
12
+ recall,
13
+ buildVectorIndex,
14
+ rrfFuse,
15
+ cosineTopK,
16
+ } from '../lib/recall.mjs';
17
+
18
+ export const __section = '04 · 召回层';
19
+
20
+ const TS = 1_760_000_000_000;
21
+ const ROWS = [
22
+ { id: 1, text: '跨机同步的挡推裁决口径:先看方向再看闸门,task-board 账本走 Host API 并回', tags: 'task-board, 同步', pinned: 0, updated_at: TS + 1000 },
23
+ { id: 2, text: '守护鱼崩溃链路:120s 内连崩 3 次才唤醒分身,日志在 TEMP', tags: '守护鱼', pinned: 0, updated_at: TS + 2000 },
24
+ { id: 3, text: '生图角色一致性的参考图与提示词模板,锁身份用 U1.5', tags: '生图', pinned: 0, updated_at: TS + 3000 },
25
+ { id: 4, text: '记忆库 FTS5 分词实测:中文连续串只有完全相等才命中', tags: '记忆', pinned: 0, updated_at: TS + 4000 },
26
+ { id: 5, text: '本地 ComfyUI 栈启动与模型切换,端口 8199 是共享桥', tags: '生图, 本地', pinned: 0, updated_at: TS + 5000 },
27
+ { id: 6, text: '跨机同步的挡推裁决口径:先看方向再看闸门,task-board 账本走 Host API 并回(P0 必读)', tags: 'task-board, 同步', pinned: 1, updated_at: TS + 6000 },
28
+ { id: 7, text: '端口 3080 是 DSH 的 web 服务端口,守护鱼负责拉起', tags: 'DSH, 端口', pinned: 0, updated_at: TS + 7000 },
29
+ ];
30
+
31
+ function setup() {
32
+ const t = makeTempRoot();
33
+ makeMemoryDb(t.paths.memoryDb, ROWS);
34
+ const store = openVectorsStore(t.paths.vectorsDb);
35
+ const source = openMemorySource(t.paths.memoryDb);
36
+ const embedder = createStubEmbedder({ dim: 512 });
37
+ return { t, store, source, embedder };
38
+ }
39
+
40
+ test('触发条件:非空即触发;纯符号 / 单字跳过', () => {
41
+ assertEq(shouldTrigger('退下吧').trigger, true, '三字短暗号应触发');
42
+ assertEq(shouldTrigger('整夜干活').trigger, true, '短句应触发');
43
+ assertEq(shouldTrigger('3080').trigger, true, '纯数字应触发');
44
+ assertEq(shouldTrigger('hi').trigger, true, '两字母应触发');
45
+ assertEq(shouldTrigger('').trigger, false, '空串不触发');
46
+ assertEq(shouldTrigger(' ').reason, 'empty', '空白 = empty');
47
+ assertEq(shouldTrigger('!!!').reason, 'symbols-only', '纯符号不触发');
48
+ assertEq(shouldTrigger('?').reason, 'symbols-only', '单符号不触发');
49
+ assertEq(shouldTrigger('啊').reason, 'single-char', '单字不触发');
50
+ assertEq(shouldTrigger('a').reason, 'single-char', '单字母不触发');
51
+ });
52
+
53
+ test('FTS token 抽取:只取英文/数字,长度 ≥2,去重且限量', () => {
54
+ assertEq(extractFtsTokens('跨机同步 task-board 与 3080'), ['task-board', '3080'], '英文+数字');
55
+ assertEq(extractFtsTokens('看板 ledger-v2.json 并回'), ['ledger-v2.json'], '连字符与点');
56
+ assertEq(extractFtsTokens('中文连续串不产出任何东西'), [], '中文不产出 token');
57
+ assertEq(extractFtsTokens('a b c'), [], '单字符全过滤');
58
+ assertEq(buildFtsMatch(['task-board', '3080']), '"task-board" OR "3080"', 'MATCH 表达式');
59
+ assertEq(buildFtsMatch([]), null, '空 token → 不参与');
60
+ });
61
+
62
+ test('RRF 融合(k=60):两路都命中的条目得分最高', () => {
63
+ const fused = rrfFuse(
64
+ [
65
+ { source: 'vector', ids: ['3', '1', '5'] },
66
+ { source: 'fts', ids: ['1', '7'] },
67
+ ],
68
+ 60,
69
+ );
70
+ assertEq(fused[0].id, '1', '两路命中排第一');
71
+ assertEq(fused[0].sources.sort().join('+'), 'fts+vector', '来源合并');
72
+ const expected = 1 / (60 + 2) + 1 / (60 + 1);
73
+ assertEq(Math.abs(fused[0].score - expected) < 1e-12, true, 'RRF 分值公式');
74
+ assertEq(fused[1].id, '3', '仅向量且 rank1 排第二');
75
+ });
76
+
77
+ test('向量召回:查询与某条文本重合时该条排第一(余弦主导)', async () => {
78
+ const { t, store, source, embedder } = setup();
79
+ try {
80
+ await runIncrementalSync({ source, store, embedder, modelId: 'stub-ngram', dim: 512 });
81
+ const r = await recall({
82
+ query: '生图角色一致性的参考图与提示词模板,锁身份用 U1.5',
83
+ store,
84
+ source,
85
+ embedder,
86
+ resultTopK: 5,
87
+ });
88
+ assertEq(r.triggered, true, '已触发');
89
+ assertEq(r.hits[0].id, 3, '完全重合条排第一');
90
+ assert(r.hits[0].sources.includes('vector'), '来自向量路');
91
+ } finally {
92
+ source.close();
93
+ store.db.close();
94
+ t.cleanup();
95
+ }
96
+ });
97
+
98
+ test('排除 pinned:pinned 条即使文本完全一致也不出现在召回结果里', async () => {
99
+ const { t, store, source, embedder } = setup();
100
+ try {
101
+ await runIncrementalSync({ source, store, embedder, modelId: 'stub-ngram', dim: 512 });
102
+ const q = ROWS[5].text; // #6 是 pinned 且文本与 #1 几乎一致
103
+ const r = await recall({ query: q, store, source, embedder, resultTopK: 8 });
104
+ assertEq(r.hits.some((h) => h.id === 6), false, 'pinned 条不得出现');
105
+ assertEq(r.stats.pinnedExcluded, 1, '已排除 1 条 pinned');
106
+ assert(r.hits.length > 0, '仍有非 pinned 命中');
107
+ assert(r.hits.every((h) => h.id !== 6), '所有命中都不是 pinned');
108
+ } finally {
109
+ source.close();
110
+ store.db.close();
111
+ t.cleanup();
112
+ }
113
+ });
114
+
115
+ test('pinned 变化实时生效:把某条改成 pinned 后立即从召回中消失', async () => {
116
+ const { t, store, source, embedder } = setup();
117
+ try {
118
+ await runIncrementalSync({ source, store, embedder, modelId: 'stub-ngram', dim: 512 });
119
+ const q = ROWS[2].text;
120
+ const before = await recall({ query: q, store, source, embedder, resultTopK: 3 });
121
+ assertEq(before.hits[0].id, 3, '未 pinned 时可召回');
122
+ source.close();
123
+ setPinned(t.paths.memoryDb, 3, 1);
124
+ const source2 = openMemorySource(t.paths.memoryDb);
125
+ const after = await recall({ query: q, store, source: source2, embedder, resultTopK: 3 });
126
+ assertEq(after.hits.some((h) => h.id === 3), false, '改成 pinned 后不再召回(无需重编码)');
127
+ source2.close();
128
+ } finally {
129
+ source.close();
130
+ store.db.close();
131
+ t.cleanup();
132
+ }
133
+ });
134
+
135
+ test('FTS 补位:英文 token 命中 tags/正文,且与向量路 RRF 融合', async () => {
136
+ const { t, store, source, embedder } = setup();
137
+ try {
138
+ await runIncrementalSync({ source, store, embedder, modelId: 'stub-ngram', dim: 512 });
139
+ const r = await recall({ query: '跨机同步 task-board', store, source, embedder, resultTopK: 8 });
140
+ assertEq(r.stats.ftsTokens.join(','), 'task-board', '抽出英文 token');
141
+ assert(r.stats.ftsMatched >= 1, `FTS 应命中(实际 ${r.stats.ftsMatched})`);
142
+ assert(r.hits.some((h) => h.sources.includes('fts')), '有命中来自 FTS 路');
143
+ assert(
144
+ r.hits.some((h) => h.sources.includes('fts') && h.sources.includes('vector')),
145
+ '存在两路同时命中的条目(融合生效)',
146
+ );
147
+ } finally {
148
+ source.close();
149
+ store.db.close();
150
+ t.cleanup();
151
+ }
152
+ });
153
+
154
+ test('FTS 对中文不参与(A4 结论):中文查询无 token ⇒ 只走向量路且不报错', async () => {
155
+ const { t, store, source, embedder } = setup();
156
+ try {
157
+ await runIncrementalSync({ source, store, embedder, modelId: 'stub-ngram', dim: 512 });
158
+ const r = await recall({ query: '记忆库分词结论', store, source, embedder, resultTopK: 5 });
159
+ assertEq(r.stats.ftsTokens.length, 0, '无英文 token');
160
+ assertEq(r.stats.ftsMatchExpr, null, '不构造 MATCH');
161
+ assert(r.hits.length > 0, '仍有向量命中');
162
+ assert(r.hits.every((h) => h.sources.length === 1 && h.sources[0] === 'vector'), '全部来自向量路');
163
+ } finally {
164
+ source.close();
165
+ store.db.close();
166
+ t.cleanup();
167
+ }
168
+ });
169
+
170
+ test('不触发时短路:不编码、不检索、不抛错', async () => {
171
+ const { t, store, source, embedder } = setup();
172
+ try {
173
+ await runIncrementalSync({ source, store, embedder, modelId: 'stub-ngram', dim: 512 });
174
+ const callsBefore = embedder.stats().encodeCount;
175
+ const r = await recall({ query: '?', store, source, embedder });
176
+ assertEq(r.triggered, false, '未触发');
177
+ assertEq(r.hits.length, 0, '无命中');
178
+ assertEq(embedder.stats().encodeCount, callsBefore, '未发生编码');
179
+ } finally {
180
+ source.close();
181
+ store.db.close();
182
+ t.cleanup();
183
+ }
184
+ });
185
+
186
+ test('余弦索引:query 与自身向量相似度为 1(归一化正确)', async () => {
187
+ const { t, store, source, embedder } = setup();
188
+ try {
189
+ await runIncrementalSync({ source, store, embedder, modelId: 'stub-ngram', dim: 512 });
190
+ const index = buildVectorIndex(store.db, { source: 'memory' });
191
+ const qvec = await embedder.encode(ROWS[0].text);
192
+ const top = cosineTopK(index, qvec, 1);
193
+ assertEq(top[0].id, 1, '自身排第一');
194
+ assert(Math.abs(top[0].score - 1) < 1e-5, `自相似度应 ≈1,实际 ${top[0].score}`);
195
+ } finally {
196
+ source.close();
197
+ store.db.close();
198
+ t.cleanup();
199
+ }
200
+ });
201
+
202
+ test('空向量库:召回安全返回空而不抛错(fail-open 前置)', async () => {
203
+ const { t, store, source, embedder } = setup();
204
+ try {
205
+ const r = await recall({ query: '跨机同步', store, source, embedder });
206
+ assertEq(r.triggered, true, '仍判定触发');
207
+ assertEq(r.hits.length, 0, '无向量 ⇒ 空结果');
208
+ assertEq(r.stats.indexSize, 0, '索引为空');
209
+ } finally {
210
+ source.close();
211
+ store.db.close();
212
+ t.cleanup();
213
+ }
214
+ });
@@ -0,0 +1,175 @@
1
+ // 05 · 启动自检(模型可用性 / 维度一致 / 表存在性 / hostId 校验)
2
+ import { DatabaseSync } from 'node:sqlite';
3
+ import { test, assert, assertEq } from './harness.mjs';
4
+ import { makeTempRoot, makeMemoryDb, sampleRows } from './fixtures.mjs';
5
+ import { openVectorsStore, META_KEYS, setMeta, getMeta } from '../lib/vectors-store.mjs';
6
+ import { openMemorySource } from '../lib/memory-source.mjs';
7
+ import { runIncrementalSync } from '../lib/sync.mjs';
8
+ import { runSelfcheck } from '../lib/selfcheck.mjs';
9
+ import { createStubEmbedder, createHttpEmbedder } from '../lib/embedder.mjs';
10
+ import { resolveConfig } from '../lib/config.mjs';
11
+
12
+ export const __section = '05 · 启动自检';
13
+
14
+ function cfg(t, extra = {}) {
15
+ return resolveConfig({
16
+ memoryDb: t.paths.memoryDb,
17
+ vectorsDb: t.paths.vectorsDb,
18
+ stateFile: t.paths.stateFile,
19
+ modelDir: t.paths.modelDir,
20
+ dim: 512,
21
+ hostId: 'TEST-HOST',
22
+ clearEnv: true,
23
+ env: {},
24
+ ...extra,
25
+ });
26
+ }
27
+
28
+ function pick(result, name) {
29
+ return result.checks.find((c) => c.name === name);
30
+ }
31
+
32
+ test('全绿:正常库 + 可用编码器 ⇒ 四判据 ok', async () => {
33
+ const t = makeTempRoot();
34
+ try {
35
+ makeMemoryDb(t.paths.memoryDb, sampleRows(4));
36
+ const store = openVectorsStore(t.paths.vectorsDb);
37
+ const source = openMemorySource(t.paths.memoryDb);
38
+ const embedder = createStubEmbedder({ dim: 512, modelId: 'stub-ngram' });
39
+ await runIncrementalSync({ source, store, embedder, modelId: 'stub-ngram', dim: 512, batchSize: 10 });
40
+ const r = await runSelfcheck({ store, source, embedder, config: cfg(t) });
41
+ assertEq(r.ok, true, `应全过:${JSON.stringify(r.checks.map((c) => [c.name, c.status]))}`);
42
+ assertEq(pick(r, '表存在性').status, 'ok');
43
+ assertEq(pick(r, '模型可用性').status, 'ok');
44
+ assertEq(pick(r, '维度一致').status, 'ok');
45
+ assertEq(pick(r, 'hostId 校验').status, 'ok');
46
+ assertEq(pick(r, '向量覆盖度').status, 'info');
47
+ assertEq(pick(r, '向量覆盖度').vecCount, 4, '覆盖度 4/4');
48
+ source.close();
49
+ store.db.close();
50
+ } finally {
51
+ t.cleanup();
52
+ }
53
+ });
54
+
55
+ test('表存在性:表被删掉 ⇒ 自检报告"已重建"', async () => {
56
+ const t = makeTempRoot();
57
+ try {
58
+ makeMemoryDb(t.paths.memoryDb, sampleRows(2));
59
+ const s1 = openVectorsStore(t.paths.vectorsDb);
60
+ setMeta(s1.db, META_KEYS.hostId, 'TEST-HOST');
61
+ s1.db.close();
62
+ // 手工毁表
63
+ const raw = new DatabaseSync(t.paths.vectorsDb);
64
+ raw.exec('DROP TABLE IF EXISTS vectors; DROP TABLE IF EXISTS meta;');
65
+ raw.close();
66
+
67
+ const store = openVectorsStore(t.paths.vectorsDb);
68
+ const source = openMemorySource(t.paths.memoryDb);
69
+ const embedder = createStubEmbedder({ dim: 512 });
70
+ const r = await runSelfcheck({ store, source, embedder, config: cfg(t) });
71
+ const t1 = pick(r, '表存在性');
72
+ assertEq(t1.status, 'ok', '重建成功');
73
+ assert(/已重建/.test(t1.detail), `detail 应写明重建:${t1.detail}`);
74
+ assert(t1.created.includes('vectors') && t1.created.includes('meta'), '两张表都在重建清单里');
75
+ source.close();
76
+ store.db.close();
77
+ } finally {
78
+ t.cleanup();
79
+ }
80
+ });
81
+
82
+ test('维度一致:库内 meta.dim 与实测不符 ⇒ fail', async () => {
83
+ const t = makeTempRoot();
84
+ try {
85
+ makeMemoryDb(t.paths.memoryDb, sampleRows(2));
86
+ const store = openVectorsStore(t.paths.vectorsDb);
87
+ const source = openMemorySource(t.paths.memoryDb);
88
+ const embedder = createStubEmbedder({ dim: 512 });
89
+ setMeta(store.db, META_KEYS.dim, '99');
90
+ setMeta(store.db, META_KEYS.hostId, 'TEST-HOST');
91
+ const r = await runSelfcheck({ store, source, embedder, config: cfg(t) });
92
+ assertEq(pick(r, '维度一致').status, 'fail', '应判 fail');
93
+ assert(/库内 99/.test(pick(r, '维度一致').detail), `detail 应包含库内维度:${pick(r, '维度一致').detail}`);
94
+ assertEq(r.ok, false, '整体不通过');
95
+ source.close();
96
+ store.db.close();
97
+ } finally {
98
+ t.cleanup();
99
+ }
100
+ });
101
+
102
+ test('维度一致:配置与实测不符 ⇒ fail(换模型维度时必须重建)', async () => {
103
+ const t = makeTempRoot();
104
+ try {
105
+ makeMemoryDb(t.paths.memoryDb, sampleRows(2));
106
+ const store = openVectorsStore(t.paths.vectorsDb);
107
+ const source = openMemorySource(t.paths.memoryDb);
108
+ const embedder = createStubEmbedder({ dim: 256 });
109
+ setMeta(store.db, META_KEYS.hostId, 'TEST-HOST');
110
+ const r = await runSelfcheck({ store, source, embedder, config: cfg(t, { dim: 512 }) });
111
+ assertEq(pick(r, '维度一致').status, 'fail', '实测 256 vs 配置 512 ⇒ fail');
112
+ source.close();
113
+ store.db.close();
114
+ } finally {
115
+ t.cleanup();
116
+ }
117
+ });
118
+
119
+ test('hostId:库来自别的机器 ⇒ warn(strict 时 fail)', async () => {
120
+ const t = makeTempRoot();
121
+ try {
122
+ makeMemoryDb(t.paths.memoryDb, sampleRows(2));
123
+ const store = openVectorsStore(t.paths.vectorsDb);
124
+ const source = openMemorySource(t.paths.memoryDb);
125
+ const embedder = createStubEmbedder({ dim: 512 });
126
+ setMeta(store.db, META_KEYS.hostId, 'SOME-OTHER-HOST');
127
+ const r1 = await runSelfcheck({ store, source, embedder, config: cfg(t), allowHostMismatch: true });
128
+ assertEq(pick(r1, 'hostId 校验').status, 'warn', '默认 warn');
129
+ assertEq(r1.ok, true, 'warn 不算失败');
130
+ const r2 = await runSelfcheck({ store, source, embedder, config: cfg(t), allowHostMismatch: false });
131
+ assertEq(pick(r2, 'hostId 校验').status, 'fail', 'strict 时 fail');
132
+ assertEq(r2.ok, false, 'strict 整体不通过');
133
+ source.close();
134
+ store.db.close();
135
+ } finally {
136
+ t.cleanup();
137
+ }
138
+ });
139
+
140
+ test('hostId:首次初始化自动写入当前主机名', async () => {
141
+ const t = makeTempRoot();
142
+ try {
143
+ makeMemoryDb(t.paths.memoryDb, sampleRows(2));
144
+ const store = openVectorsStore(t.paths.vectorsDb);
145
+ const source = openMemorySource(t.paths.memoryDb);
146
+ const embedder = createStubEmbedder({ dim: 512 });
147
+ const r = await runSelfcheck({ store, source, embedder, config: cfg(t) });
148
+ assertEq(pick(r, 'hostId 校验').status, 'ok');
149
+ assert(/首次初始化/.test(pick(r, 'hostId 校验').detail), '应写明首次初始化');
150
+ assertEq(getMeta(store.db, META_KEYS.hostId), 'TEST-HOST', '已落 meta');
151
+ source.close();
152
+ store.db.close();
153
+ } finally {
154
+ t.cleanup();
155
+ }
156
+ });
157
+
158
+ test('模型不可用:桥不在 ⇒ 模型可用性 fail,但自检本身不抛错(fail-open 前置)', async () => {
159
+ const t = makeTempRoot();
160
+ try {
161
+ makeMemoryDb(t.paths.memoryDb, sampleRows(2));
162
+ const store = openVectorsStore(t.paths.vectorsDb);
163
+ const source = openMemorySource(t.paths.memoryDb);
164
+ const embedder = createHttpEmbedder({ port: 1, timeoutMs: 120 }); // 必然连不上
165
+ const r = await runSelfcheck({ store, source, embedder, config: cfg(t) });
166
+ assertEq(pick(r, '模型可用性').status, 'fail', '桥不在 ⇒ fail');
167
+ assertEq(pick(r, '维度一致').status, 'fail', '编码探测也失败');
168
+ assertEq(r.ok, false, '整体不通过');
169
+ assert(r.checks.length >= 5, '仍返回完整判据清单(不抛错)');
170
+ source.close();
171
+ store.db.close();
172
+ } finally {
173
+ t.cleanup();
174
+ }
175
+ });