@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.
- package/CHANGELOG.md +92 -0
- package/README.en.md +225 -2
- package/README.md +180 -2
- package/lib/deploy-guard.mjs +463 -275
- package/lib/guard-http.mjs +59 -62
- package/lib/pitfall-form-map.mjs +94 -84
- package/lib/pitfall-score.mjs +262 -185
- package/lib/session-file-name.mjs +116 -100
- package/lib/zstd-frames.mjs +88 -67
- package/memory-db-tools/README.md +29 -1
- package/memory-db-tools/audit-analyze.mjs +1 -1
- package/memory-db-tools/audit-export.mjs +60 -16
- package/memory-db-tools/audit-struct-probe.mjs +29 -8
- package/memory-db-tools/lib/known-devices.mjs +137 -0
- package/memory-engine/README.md +58 -4
- package/memory-engine/bench/bench.mjs +1 -1
- package/memory-engine/cli/band-sentinel.mjs +164 -0
- package/memory-engine/cli/distill-rollback.mjs +112 -44
- package/memory-engine/cli/distill-sessions.mjs +237 -0
- package/memory-engine/cli/distill.mjs +196 -95
- package/memory-engine/cli/doc-index-rebuild.mjs +142 -108
- package/memory-engine/cli/doc-index.mjs +47 -38
- package/memory-engine/cli/domain-tags.mjs +246 -162
- package/memory-engine/cli/layer-migrate.mjs +176 -0
- package/memory-engine/cli/layer-move.mjs +189 -0
- package/memory-engine/cli/pitfall-hook.mjs +136 -70
- package/memory-engine/cli/recall.mjs +243 -110
- package/memory-engine/cli/selfcheck.mjs +93 -44
- package/memory-engine/cli/serve-start.mjs +221 -140
- package/memory-engine/cli/sweep.mjs +83 -44
- package/memory-engine/cli/sync.mjs +93 -57
- package/memory-engine/config/audit-devices.json +6 -0
- package/memory-engine/config/bands.json +7 -0
- package/memory-engine/config/layers.json +7 -0
- package/memory-engine/dict/LICENSE-/350/257/215/345/205/270.md +53 -0
- package/memory-engine/dict/lexicon-config.json +11 -0
- package/memory-engine/dict//350/257/215/345/205/270.txt +109750 -0
- package/memory-engine/dict//350/257/215/351/242/221.txt +20000 -0
- package/memory-engine/lib/band-sentinel.mjs +417 -0
- package/memory-engine/lib/cli-strict.mjs +128 -0
- package/memory-engine/lib/cli-util.mjs +147 -125
- package/memory-engine/lib/config.mjs +83 -96
- package/memory-engine/lib/dirty-ledger.mjs +127 -94
- package/memory-engine/lib/distill-rollback.mjs +173 -133
- package/memory-engine/lib/distill.mjs +878 -585
- package/memory-engine/lib/doc-index-store.mjs +208 -235
- package/memory-engine/lib/doc-index.mjs +351 -350
- package/memory-engine/lib/doc-scan.mjs +178 -156
- package/memory-engine/lib/doc-summarize.mjs +220 -205
- package/memory-engine/lib/domain-tags.mjs +358 -277
- package/memory-engine/lib/embedder.mjs +175 -194
- package/memory-engine/lib/hashes.mjs +22 -7
- package/memory-engine/lib/layer-migrate.mjs +330 -0
- package/memory-engine/lib/layer-move.mjs +307 -0
- package/memory-engine/lib/layers.mjs +191 -0
- package/memory-engine/lib/lexicon-recall.mjs +473 -0
- package/memory-engine/lib/memory-source.mjs +78 -80
- package/memory-engine/lib/pitfall-source.mjs +115 -114
- package/memory-engine/lib/policy-source.mjs +279 -261
- package/memory-engine/lib/recall-log.mjs +285 -0
- package/memory-engine/lib/recall.mjs +565 -240
- package/memory-engine/lib/selfcheck.mjs +142 -106
- package/memory-engine/lib/session-distill.mjs +603 -0
- package/memory-engine/lib/session-source.mjs +421 -0
- package/memory-engine/lib/sweep.mjs +107 -130
- package/memory-engine/lib/sync.mjs +110 -140
- package/memory-engine/lib/tick-hook.mjs +123 -148
- package/memory-engine/lib/vectors-store.mjs +213 -188
- package/memory-engine/test/12-layering.test.mjs +539 -0
- package/memory-engine/test/13-session-distill.test.mjs +616 -0
- package/memory-engine/tools/bridge-smoke.mjs +94 -55
- package/memory-engine/tools/fetch-model.mjs +167 -87
- package/memory-engine/tools/smoke-recall.mjs +94 -38
- package/memory-engine/tools/verify-recall-chain.mjs +413 -0
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// 固化自早期临时脚本(2026-09-25 体检工具链固化)
|
|
4
4
|
// · 去硬编码:USERPROFILE / ~\.dsh → DSH_HOME 环境变量或家目录动态解析(见 resolveDshHome)
|
|
5
5
|
// · 输出可传参:--out <file>,默认落系统临时目录(不是生产路径)
|
|
6
|
+
// · 设备统计的「已知设备名」也不写死:见 lib/known-devices.mjs(配置 + 环境变量 + 中性缺省)
|
|
6
7
|
// · 加 --help / --self-test · 收尾一律 process.exitCode(禁 process.exit)
|
|
7
8
|
// 用途:把 memory.db 的 memories 表全量导出为 memories.json —— 下游探针(audit-analyze / audit-struct-probe /
|
|
8
9
|
// audit-hit-probe / audit-verify-unchanged)的唯一数据源。
|
|
@@ -14,6 +15,7 @@ import path from "node:path";
|
|
|
14
15
|
import crypto from "node:crypto";
|
|
15
16
|
import { DatabaseSync } from "node:sqlite";
|
|
16
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { BUILTIN_DEVICES, knownDeviceKey, loadKnownDevices } from "./lib/known-devices.mjs";
|
|
17
19
|
|
|
18
20
|
export const SELF = fileURLToPath(import.meta.url);
|
|
19
21
|
export const TOOL = path.basename(SELF);
|
|
@@ -115,36 +117,42 @@ export function readMemories(dbPath) {
|
|
|
115
117
|
}
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
export function deviceStats(rows) {
|
|
119
|
-
const dev = {
|
|
120
|
+
export function deviceStats(rows, devices = loadKnownDevices().devices) {
|
|
121
|
+
const dev = {};
|
|
122
|
+
for (const name of devices) dev[name] = 0;
|
|
123
|
+
dev.NONE = 0;
|
|
124
|
+
dev.OTHER = 0;
|
|
125
|
+
dev.DOUBLE = 0;
|
|
120
126
|
for (const r of rows) {
|
|
121
127
|
const m = r.text.match(/【设备[::]\s*([^】]*)】/g);
|
|
122
128
|
if (!m) dev.NONE++;
|
|
123
129
|
else if (m.length > 1) dev.DOUBLE++;
|
|
124
|
-
else
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
else {
|
|
131
|
+
const key = knownDeviceKey(m[0], devices);
|
|
132
|
+
if (key) dev[key]++;
|
|
133
|
+
else dev.OTHER++;
|
|
134
|
+
}
|
|
127
135
|
}
|
|
128
136
|
return dev;
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
export function summarize(rows) {
|
|
139
|
+
export function summarize(rows, devices = loadKnownDevices().devices) {
|
|
132
140
|
return {
|
|
133
141
|
total: rows.length,
|
|
134
142
|
pinned: rows.filter((r) => r.pinned).length,
|
|
135
143
|
chars: rows.reduce((s, r) => s + r.text.length, 0),
|
|
136
|
-
dev: deviceStats(rows),
|
|
144
|
+
dev: deviceStats(rows, devices),
|
|
137
145
|
pinnedIds: rows.filter((r) => r.pinned).map((r) => r.id),
|
|
138
146
|
tsSampleFirst: rows.slice(0, 2).map((r) => [r.id, r.created_at, r.updated_at]),
|
|
139
147
|
tsSampleLast: rows.slice(-2).map((r) => [r.id, r.created_at, r.updated_at]),
|
|
140
148
|
};
|
|
141
149
|
}
|
|
142
150
|
|
|
143
|
-
export function writeExport(dbPath, outFile) {
|
|
151
|
+
export function writeExport(dbPath, outFile, devices = loadKnownDevices().devices) {
|
|
144
152
|
const rows = readMemories(dbPath);
|
|
145
153
|
fs.mkdirSync(path.dirname(path.resolve(outFile)), { recursive: true });
|
|
146
154
|
fs.writeFileSync(outFile, JSON.stringify(rows), "utf8");
|
|
147
|
-
return { rows, stats: summarize(rows) };
|
|
155
|
+
return { rows, stats: summarize(rows, devices) };
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
// ---------- CLI ----------
|
|
@@ -183,6 +191,9 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
183
191
|
|
|
184
192
|
const dbPath = typeof a.db === "string" ? path.resolve(a.db) : DEFAULT_DB;
|
|
185
193
|
const outFile = typeof a.out === "string" ? path.resolve(a.out) : DEFAULT_OUT;
|
|
194
|
+
// 已知设备名:配置 / 环境变量 / 中性缺省(见 lib/known-devices.mjs)。警告走 stderr,不动 stdout 结构。
|
|
195
|
+
const known = loadKnownDevices();
|
|
196
|
+
for (const w of known.warnings) console.error(`[${TOOL}] 警告:${w}`);
|
|
186
197
|
if (!fs.existsSync(dbPath)) {
|
|
187
198
|
console.error(`[${TOOL}] 记忆库不存在:${dbPath}(可用 --db 指定)`);
|
|
188
199
|
return 3;
|
|
@@ -193,7 +204,7 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
193
204
|
return 3;
|
|
194
205
|
}
|
|
195
206
|
|
|
196
|
-
const { stats } = writeExport(dbPath, outFile);
|
|
207
|
+
const { stats } = writeExport(dbPath, outFile, known.devices);
|
|
197
208
|
if (a.json) {
|
|
198
209
|
console.log(JSON.stringify({ db: dbPath, out: outFile, ...stats }, null, 1));
|
|
199
210
|
} else if (a.quiet !== true) {
|
|
@@ -209,6 +220,10 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
209
220
|
}
|
|
210
221
|
|
|
211
222
|
// ---------- self-test(无副作用:合成临时库 + 临时目录) ----------
|
|
223
|
+
// 夹具自带的「已知设备名」是中性示例名,且**显式传给被测函数**:自检不与机器配置耦合
|
|
224
|
+
// (用户把配置改成别的名字,自检不许跟着红)。
|
|
225
|
+
export const FIXTURE_DEVICES = Object.freeze(["alpha", "beta"]);
|
|
226
|
+
|
|
212
227
|
export function selfTest() {
|
|
213
228
|
const t = makeAsserter();
|
|
214
229
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "audit-export-selftest-"));
|
|
@@ -224,26 +239,55 @@ export function selfTest() {
|
|
|
224
239
|
"INSERT INTO memories (id, text, tags, pinned, created_at, updated_at) VALUES (?,?,?,?,?,?)",
|
|
225
240
|
);
|
|
226
241
|
ins.run(1, "【设备:本机】第一条测试记忆,路径 <workspace>", "t1", 1, 1000, 1000);
|
|
227
|
-
ins.run(2, "【设备:
|
|
242
|
+
ins.run(2, "【设备:beta】第二条测试记忆", "t2", 0, 2000, 2000);
|
|
228
243
|
ins.run(3, "第三条无设备标记忆", "", 0, 3000, 3000);
|
|
244
|
+
ins.run(4, "【设备:alpha】第四条带已知设备标", "t4", 0, 4000, 4000);
|
|
229
245
|
} finally {
|
|
230
246
|
db.close();
|
|
231
247
|
}
|
|
232
248
|
|
|
233
249
|
const before = sha256File(dbFile);
|
|
234
|
-
const { rows, stats } = writeExport(dbFile, outFile);
|
|
250
|
+
const { rows, stats } = writeExport(dbFile, outFile, FIXTURE_DEVICES);
|
|
235
251
|
|
|
236
|
-
t.eq(rows.length,
|
|
252
|
+
t.eq(rows.length, 4, "导出条数");
|
|
237
253
|
t.eq(rows[0].pinned, true, "pinned 归一为 boolean");
|
|
238
254
|
t.eq(stats.pinned, 1, "pinned 计数");
|
|
239
|
-
t.eq(stats.dev.
|
|
240
|
-
t.eq(stats.dev.
|
|
255
|
+
t.eq(stats.dev.alpha, 1, "已知设备标 alpha 计数");
|
|
256
|
+
t.eq(stats.dev.beta, 1, "已知设备标 beta 计数");
|
|
241
257
|
t.eq(stats.dev.NONE, 1, "无设备标计数");
|
|
258
|
+
t.eq(stats.dev.OTHER, 1, "名单外的设备标计入 OTHER");
|
|
259
|
+
t.eq(stats.dev.DOUBLE, 0, "没有双标条目 ⇒ DOUBLE 0");
|
|
260
|
+
t.eq(Object.keys(stats.dev).join(","), "alpha,beta,NONE,OTHER,DOUBLE", "设备统计键 = 名单顺序 + 三个固定键");
|
|
242
261
|
t.ok(fs.existsSync(outFile), "产物文件已生成(含自动 mkdir 父目录)");
|
|
243
262
|
const back = JSON.parse(fs.readFileSync(outFile, "utf8"));
|
|
244
|
-
t.eq(back.length,
|
|
263
|
+
t.eq(back.length, 4, "产物 JSON 可解析且条数一致");
|
|
245
264
|
t.eq(back[2].text, "第三条无设备标记忆", "产物内容一致");
|
|
246
265
|
|
|
266
|
+
// 「已知设备名」的来源优先级:环境变量 > 配置文件 > 内置中性缺省
|
|
267
|
+
const cfgFile = path.join(tmp, "audit-devices.json");
|
|
268
|
+
fs.writeFileSync(cfgFile, JSON.stringify({ devices: ["gamma"] }), "utf8");
|
|
269
|
+
t.eq(loadKnownDevices({ env: {}, file: cfgFile }).devices.join(","), "gamma", "配置文件能给名单");
|
|
270
|
+
t.eq(loadKnownDevices({ env: {}, file: cfgFile }).source, "config", "来源标记 = config");
|
|
271
|
+
t.eq(
|
|
272
|
+
loadKnownDevices({ env: { MEMORY_DB_AUDIT_DEVICES: "a1, b2" }, file: cfgFile }).devices.join(","),
|
|
273
|
+
"a1,b2",
|
|
274
|
+
"环境变量覆盖配置文件",
|
|
275
|
+
);
|
|
276
|
+
t.eq(
|
|
277
|
+
loadKnownDevices({ env: {}, file: path.join(tmp, "no-such.json") }).devices.join(","),
|
|
278
|
+
BUILTIN_DEVICES.join(","),
|
|
279
|
+
"配置缺失 ⇒ 内置中性缺省(不报错)",
|
|
280
|
+
);
|
|
281
|
+
t.eq(
|
|
282
|
+
loadKnownDevices({ env: {}, config: { devices: [] } }).devices.join(","),
|
|
283
|
+
BUILTIN_DEVICES.join(","),
|
|
284
|
+
"空名单 ⇒ 退回内置缺省",
|
|
285
|
+
);
|
|
286
|
+
t.ok(
|
|
287
|
+
loadKnownDevices({ env: {}, config: { devices: ["ok", "OTHER"] } }).warnings.length > 0,
|
|
288
|
+
"名单里撞上固定键(OTHER)⇒ 剔掉并给警告",
|
|
289
|
+
);
|
|
290
|
+
|
|
247
291
|
// 自证只读
|
|
248
292
|
t.eq(sha256File(dbFile), before, "合成库 sha256 前后一致(未写库)");
|
|
249
293
|
const src = fs.readFileSync(SELF, "utf8");
|
|
@@ -3,14 +3,18 @@
|
|
|
3
3
|
// 固化自早期临时脚本(2026-09-25 体检工具链固化)
|
|
4
4
|
// · 数据源改为 --in <memories.json>(默认系统临时目录,不再写死 _tmp 路径)
|
|
5
5
|
// · 加 --help / --self-test / --json · 收尾 process.exitCode
|
|
6
|
-
// 用途:看两类结构异常 —— ①【设备:X
|
|
6
|
+
// 用途:看两类结构异常 —— ①【设备:X】写了但 X 不在「已知设备名」清单里(OTHER)
|
|
7
|
+
// ②一条里写了两个设备标(DOUBLE);
|
|
7
8
|
// 顺带给出 pinned 各条字符数与最长的非 pinned 条目(persona 瘦身候选)。
|
|
9
|
+
// 已知设备名不是硬编码:见 lib/known-devices.mjs(配置 memory-engine/config/audit-devices.json
|
|
10
|
+
// / 环境变量 MEMORY_DB_AUDIT_DEVICES / 内置中性示例名 alpha、beta)。
|
|
8
11
|
// 只读:只读 memories.json,不碰 memory.db,不写任何文件(stdout only)。
|
|
9
12
|
|
|
10
13
|
import fs from "node:fs";
|
|
11
14
|
import os from "node:os";
|
|
12
15
|
import path from "node:path";
|
|
13
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { BUILTIN_DEVICES, isKnownDeviceTag, loadKnownDevices } from "./lib/known-devices.mjs";
|
|
14
18
|
|
|
15
19
|
export const SELF = fileURLToPath(import.meta.url);
|
|
16
20
|
export const TOOL = path.basename(SELF);
|
|
@@ -87,7 +91,7 @@ export function readJson(p) {
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
// ---------- 核心逻辑 ----------
|
|
90
|
-
export function structReport(mems, topLong = 12) {
|
|
94
|
+
export function structReport(mems, topLong = 12, devices = loadKnownDevices().devices) {
|
|
91
95
|
const other = [];
|
|
92
96
|
const dbl = [];
|
|
93
97
|
for (const m of mems) {
|
|
@@ -96,7 +100,7 @@ export function structReport(mems, topLong = 12) {
|
|
|
96
100
|
dbl.push(`${m.id}: ${g.map((s) => s.trim()).join(" ")}`);
|
|
97
101
|
continue;
|
|
98
102
|
}
|
|
99
|
-
if (g.length === 1 &&
|
|
103
|
+
if (g.length === 1 && !isKnownDeviceTag(g[0], devices)) other.push(`${m.id}: ${g[0]}`);
|
|
100
104
|
}
|
|
101
105
|
const pinnedChars = mems.filter((m) => m.pinned).map((m) => `${m.id}:${m.text.length}`);
|
|
102
106
|
const topLongNonPinned = mems
|
|
@@ -143,8 +147,11 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
143
147
|
return 3;
|
|
144
148
|
}
|
|
145
149
|
const top = typeof a.top === "string" ? Number(a.top) : 12;
|
|
150
|
+
// 已知设备名:配置 / 环境变量 / 中性缺省(见 lib/known-devices.mjs)。警告走 stderr,不动 stdout 结构。
|
|
151
|
+
const known = loadKnownDevices();
|
|
152
|
+
for (const w of known.warnings) console.error(`[${TOOL}] 警告:${w}`);
|
|
146
153
|
const mems = readJson(inFile);
|
|
147
|
-
const r = structReport(mems, Number.isFinite(top) && top > 0 ? top : 12);
|
|
154
|
+
const r = structReport(mems, Number.isFinite(top) && top > 0 ? top : 12, known.devices);
|
|
148
155
|
|
|
149
156
|
if (a.json) {
|
|
150
157
|
console.log(JSON.stringify({ in: inFile, ...r }, null, 1));
|
|
@@ -159,6 +166,9 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
// ---------- self-test(无副作用:合成 JSON 落临时目录) ----------
|
|
169
|
+
// 夹具自带的「已知设备名」= 中性示例名,且显式传给被测函数:自检不与机器配置耦合。
|
|
170
|
+
export const FIXTURE_DEVICES = Object.freeze(["alpha", "beta"]);
|
|
171
|
+
|
|
162
172
|
export function selfTest() {
|
|
163
173
|
const t = makeAsserter();
|
|
164
174
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "audit-struct-selftest-"));
|
|
@@ -166,11 +176,11 @@ export function selfTest() {
|
|
|
166
176
|
try {
|
|
167
177
|
const mems = [
|
|
168
178
|
{ id: 1, text: "普通条目", pinned: false, tags: "", created_at: 1, updated_at: 1 },
|
|
169
|
-
{ id: 2, text: "【设备:
|
|
170
|
-
{ id: 3, text: "
|
|
179
|
+
{ id: 2, text: "【设备:alpha】正常条目", pinned: true, tags: "", created_at: 1, updated_at: 1 },
|
|
180
|
+
{ id: 3, text: "【设备:某个不在名单里的名字】异常设备标", pinned: false, tags: "", created_at: 1, updated_at: 1 },
|
|
171
181
|
{
|
|
172
182
|
id: 4,
|
|
173
|
-
text: "【设备:
|
|
183
|
+
text: "【设备:alpha】\n【设备:beta】双标条目" + "x".repeat(300),
|
|
174
184
|
pinned: false,
|
|
175
185
|
tags: "",
|
|
176
186
|
created_at: 1,
|
|
@@ -180,7 +190,7 @@ export function selfTest() {
|
|
|
180
190
|
fs.writeFileSync(fix, JSON.stringify(mems), "utf8");
|
|
181
191
|
const before = fs.readFileSync(fix, "utf8");
|
|
182
192
|
|
|
183
|
-
const r = structReport(readJson(fix), 12);
|
|
193
|
+
const r = structReport(readJson(fix), 12, FIXTURE_DEVICES);
|
|
184
194
|
t.eq(r.total, 4, "总条数");
|
|
185
195
|
t.eq(r.other.length, 1, "OTHER 命中数");
|
|
186
196
|
t.eq(r.other[0].split(":")[0], "3", "OTHER 命中 id");
|
|
@@ -195,6 +205,17 @@ export function selfTest() {
|
|
|
195
205
|
const body = src.split("// ---------- self-test")[0];
|
|
196
206
|
t.ok(!/fs\.writeFileSync/.test(body), "主体逻辑段(self-test 之前)不含 fs.writeFileSync");
|
|
197
207
|
t.ok(!/new DatabaseSync\(/.test(body), "主体逻辑段不开放任何数据库(纯 JSON 只读)");
|
|
208
|
+
|
|
209
|
+
// 「已知设备名」接入:判据跟着名单走,不靠改代码
|
|
210
|
+
const widened = structReport(mems, 12, ["某个不在名单里的名字"]);
|
|
211
|
+
t.eq(widened.other.length, 1, "换一套名单 ⇒ OTHER 仍是 1 条");
|
|
212
|
+
t.eq(widened.other[0].split(":")[0], "2", "换名单后轮到 #2 算 OTHER(判据真的跟着名单走,不是写死的)");
|
|
213
|
+
t.eq(widened.dbl.length, 1, "换名单不影响 DOUBLE 判据");
|
|
214
|
+
t.eq(
|
|
215
|
+
loadKnownDevices({ env: {}, config: { devices: [] } }).devices.join(","),
|
|
216
|
+
BUILTIN_DEVICES.join(","),
|
|
217
|
+
"空名单 ⇒ 退回内置中性缺省(不是「全都算 OTHER」)",
|
|
218
|
+
);
|
|
198
219
|
} catch (e) {
|
|
199
220
|
t.ok(false, `self-test 抛错:${e?.message ?? e}`);
|
|
200
221
|
} finally {
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// memory-db-tools/lib/known-devices.mjs — 「已知设备名」的唯一出处
|
|
2
|
+
//
|
|
3
|
+
// 为什么有这个文件:体检工具要靠「设备标里的这个名字认不认识」把记忆分成
|
|
4
|
+
// 已知设备 / 未知设备(OTHER)。这份名单**每台机器都不一样**,所以它绝不属于代码:
|
|
5
|
+
// ① 随包发布一份配置(memory-engine/config/audit-devices.json)—— 换机器改那一份;
|
|
6
|
+
// ② 环境变量 MEMORY_DB_AUDIT_DEVICES 可在不改文件的情况下覆盖(逗号或空白分隔);
|
|
7
|
+
// ③ 两者都没有时退回**中性示例名** alpha / beta —— 只是让工具装完能跑,不代表任何真实机器。
|
|
8
|
+
//
|
|
9
|
+
// 匹配口径(与旧实现同形,别改):
|
|
10
|
+
// · 只看【设备:X】里的 X 那一段,拿名单里的每个名字做**大小写敏感**的子串匹配;
|
|
11
|
+
// · 一条里有两个设备标 ⇒ 那是 DOUBLE,不参与本判据(由调用方先判);
|
|
12
|
+
// · 名单为空的配置视为无效(会让所有设备标都变成 OTHER),退回缺省并给一句警告。
|
|
13
|
+
//
|
|
14
|
+
// 本模块纯读:只读一个 JSON 文件,不写、不联网、不起子进程。
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
/** 内置缺省:中性示例名(不是机器名)。装完就能跑,换机器请改配置或设环境变量。 */
|
|
21
|
+
export const BUILTIN_DEVICES = Object.freeze(['alpha', 'beta']);
|
|
22
|
+
|
|
23
|
+
/** 环境变量名(覆盖配置)。 */
|
|
24
|
+
export const ENV_DEVICES = 'MEMORY_DB_AUDIT_DEVICES';
|
|
25
|
+
|
|
26
|
+
/** 统计结果里固定跟着的三个键,不能被设备名占用。 */
|
|
27
|
+
export const RESERVED_KEYS = Object.freeze(['NONE', 'OTHER', 'DOUBLE']);
|
|
28
|
+
|
|
29
|
+
/** 设备名的合法形态:字母数字下划线连字符,1~32 字符(要能当 JSON 的键用)。 */
|
|
30
|
+
export const DEVICE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,31}$/;
|
|
31
|
+
|
|
32
|
+
/** 设备标在正文里的形态(与各探针一致;只用于从一条命中里取出 X 段)。 */
|
|
33
|
+
export const DEVICE_TAG_RE = /【设备[::]\s*([^】]*)】/g;
|
|
34
|
+
|
|
35
|
+
/** 配置文件的绝对位置:`memory-engine/config/audit-devices.json`(随包发布)。 */
|
|
36
|
+
export function configPath() {
|
|
37
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'memory-engine', 'config', 'audit-devices.json');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 逗号 / 空白分隔的名单串(环境变量与配置文件里的字符串共用)。 */
|
|
41
|
+
export function splitNames(text) {
|
|
42
|
+
return String(text ?? '')
|
|
43
|
+
.split(/[\s,,]+/)
|
|
44
|
+
.map((s) => s.trim())
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 把任意来源的名字串收敛成合法名单:去重(大小写敏感)、剔掉保留键与非法形态。
|
|
50
|
+
* 返回 { devices, rejected },rejected 里每项带原因(自检与警告要用)。
|
|
51
|
+
*/
|
|
52
|
+
export function normalizeDevices(names) {
|
|
53
|
+
const rejected = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
const devices = [];
|
|
56
|
+
for (const raw of names) {
|
|
57
|
+
const name = String(raw);
|
|
58
|
+
if (RESERVED_KEYS.includes(name)) {
|
|
59
|
+
rejected.push({ name, reason: '与统计结果里的固定键重名(NONE / OTHER / DOUBLE)' });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!DEVICE_NAME_RE.test(name)) {
|
|
63
|
+
rejected.push({ name, reason: '形态不合法(只允许字母数字下划线连字符,1~32 字符)' });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (seen.has(name)) continue;
|
|
67
|
+
seen.add(name);
|
|
68
|
+
devices.push(name);
|
|
69
|
+
}
|
|
70
|
+
return { devices, rejected };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 解析「已知设备名」。优先级:环境变量 > 配置文件 > 内置缺省。
|
|
75
|
+
* @param {{ env?: Record<string,string>, file?: string, config?: object }} [opts]
|
|
76
|
+
* @returns {{ devices: string[], source: 'env'|'config'|'builtin', file: string, warnings: string[] }}
|
|
77
|
+
*/
|
|
78
|
+
export function loadKnownDevices(opts = {}) {
|
|
79
|
+
const env = opts.env || process.env;
|
|
80
|
+
const file = opts.file || configPath();
|
|
81
|
+
const warnings = [];
|
|
82
|
+
|
|
83
|
+
// ① 环境变量(显式给了就用它,哪怕配置文件也在)
|
|
84
|
+
const fromEnv = normalizeDevices(splitNames(env[ENV_DEVICES]));
|
|
85
|
+
if (fromEnv.devices.length) {
|
|
86
|
+
for (const r of fromEnv.rejected) warnings.push(`${ENV_DEVICES} 里的 ${JSON.stringify(r.name)} 被忽略:${r.reason}`);
|
|
87
|
+
return { devices: fromEnv.devices, source: 'env', file, warnings };
|
|
88
|
+
}
|
|
89
|
+
if (String(env[ENV_DEVICES] ?? '').trim()) {
|
|
90
|
+
warnings.push(`${ENV_DEVICES} 给了但一个合法名字都没有 ⇒ 退回配置文件 / 内置缺省`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ② 配置文件
|
|
94
|
+
const cfg = opts.config !== undefined ? { ok: true, value: opts.config } : readJsonFile(file);
|
|
95
|
+
if (cfg.ok) {
|
|
96
|
+
const raw = cfg.value && cfg.value.devices;
|
|
97
|
+
if (Array.isArray(raw)) {
|
|
98
|
+
const norm = normalizeDevices(raw.map((x) => (typeof x === 'string' ? x : String(x))));
|
|
99
|
+
for (const r of norm.rejected) warnings.push(`${file} 里的 ${JSON.stringify(r.name)} 被忽略:${r.reason}`);
|
|
100
|
+
if (norm.devices.length) return { devices: norm.devices, source: 'config', file, warnings };
|
|
101
|
+
warnings.push(`${file} 的 devices 为空或全不合法 ⇒ 退回内置缺省`);
|
|
102
|
+
} else {
|
|
103
|
+
warnings.push(`${file} 里没有 devices 数组 ⇒ 退回内置缺省`);
|
|
104
|
+
}
|
|
105
|
+
} else if (cfg.reason !== 'missing') {
|
|
106
|
+
warnings.push(`读不了 ${file}(${cfg.reason})⇒ 退回内置缺省`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ③ 内置缺省
|
|
110
|
+
return { devices: [...BUILTIN_DEVICES], source: 'builtin', file, warnings };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function readJsonFile(file) {
|
|
114
|
+
try {
|
|
115
|
+
return { ok: true, value: JSON.parse(fs.readFileSync(file, 'utf8')) };
|
|
116
|
+
} catch (e) {
|
|
117
|
+
if (e && e.code === 'ENOENT') return { ok: false, reason: 'missing' };
|
|
118
|
+
return { ok: false, reason: `解不开:${e && e.message}` };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 一个设备标(形如 `【设备:X】`)落在名单里的哪个名字上;都不落在就是 null(⇒ OTHER)。
|
|
124
|
+
* 找不到 X 段时退回整串匹配,保证与旧实现「直接测整串」同形。
|
|
125
|
+
*/
|
|
126
|
+
export function knownDeviceKey(tag, devices) {
|
|
127
|
+
if (!devices || !devices.length) return null;
|
|
128
|
+
const m = /【设备[::]\s*([^】]*)】/.exec(String(tag));
|
|
129
|
+
const subject = m ? m[1] : String(tag);
|
|
130
|
+
for (const name of devices) if (subject.includes(name)) return name;
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 与 knownDeviceKey 同义,给「认不认识」这种问法用。 */
|
|
135
|
+
export function isKnownDeviceTag(tag, devices) {
|
|
136
|
+
return knownDeviceKey(tag, devices) !== null;
|
|
137
|
+
}
|
package/memory-engine/README.md
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
| 独立向量库 | `vectors` 表 + `meta` 表(schemaVersion / hostId / modelId / dim / lastIncrementalSyncAt / lastFullHashSweepAt) | §3.1 |
|
|
13
13
|
| 增量同步 | 按 `updated_at ≥ 水位` 只读扫 `memory.db`,批 50,失败保留进度可续跑;换模型/维度自动全量重建 | §3.1 |
|
|
14
14
|
| 兜底校验 | 每周按 id 段轮转做全表 `text_hash` 校验(治「改 text 不动 updated_at」的外部写口,A7) | §3.1 / §6.2 |
|
|
15
|
-
| 召回 | 向量余弦 top-20 为主 + FTS5 仅补标签/英文/数字 + RRF(k=60) 融合 + **排除 pinned
|
|
15
|
+
| 召回 | 向量余弦 top-20 为主 + FTS5 仅补标签/英文/数字 + **词表第二路(BM25,默认关)** + RRF(k=60) 融合 + **排除 pinned**;非空即触发,纯符号/单字跳过;**查询截断 256 字符**;**编码超时/失败降级成固定 Top-K** | §3.3 |
|
|
16
|
+
| 召回留痕 | 旁路 JSONL(query / 命中 id / 分数 / 是否降级 / 时刻),按字节轮转 + 天数清理;默认不写 | 本仓 0.2.0 |
|
|
16
17
|
| 启动自检 | 模型可用性 / 维度一致 / 表存在性(缺则重建)/ hostId 校验 | §8.3 #9 |
|
|
17
18
|
| 编码桥 | `serve.mjs`,**仅绑 127.0.0.1:8201**,`GET /health` + `POST /encode` | §3.1 编码通道契约 |
|
|
18
19
|
|
|
@@ -21,9 +22,15 @@
|
|
|
21
22
|
```
|
|
22
23
|
memory-engine/
|
|
23
24
|
├─ serve.mjs 编码桥(回环 HTTP,模型常驻)
|
|
24
|
-
├─ cli/ sync.mjs · sweep.mjs · recall.mjs · selfcheck.mjs
|
|
25
|
-
|
|
26
|
-
├─
|
|
25
|
+
├─ cli/ sync.mjs · sweep.mjs · recall.mjs · selfcheck.mjs ·
|
|
26
|
+
│ layer-migrate.mjs(加层字段)· layer-move.mjs(整层搬走/搬回)· band-sentinel.mjs(号段哨兵)
|
|
27
|
+
├─ lib/ config / vectors-store / memory-source / embedder / sync / sweep / recall /
|
|
28
|
+
│ lexicon-recall(词表第二路) / recall-log(旁路留痕) / selfcheck / cli-util / cli-strict / hashes /
|
|
29
|
+
│ layers(层配置 + 行指纹) / layer-migrate / layer-move / band-sentinel
|
|
30
|
+
├─ config/ layers.json(层名集合 / 列名 / 缺省层) · bands.json(号段表)
|
|
31
|
+
├─ dict/ 词表素材:词典.txt · 词频.txt(jieba · MIT) · LICENSE-词典.md · lexicon-config.json(开关)
|
|
32
|
+
├─ tools/ fetch-model.mjs(模型权重)· bridge-smoke.mjs · smoke-recall.mjs
|
|
33
|
+
│ verify-recall-chain.mjs(召回链验收夹具,自带判据)
|
|
27
34
|
├─ test/ 单测(夹具全部落系统临时目录)+ run.mjs
|
|
28
35
|
└─ bench/bench.mjs V1 / V2 基准(方案 §9)
|
|
29
36
|
```
|
|
@@ -51,10 +58,57 @@ node cli\sweep.mjs --memory-db <副本> [--rounds N | --all]
|
|
|
51
58
|
# 召回(人工查验用)
|
|
52
59
|
node cli\recall.mjs --query "退下吧" --json
|
|
53
60
|
|
|
61
|
+
# 召回:词表第二路 / 降级条数 / 编码预算 / 旁路留痕(都是新增开关;不写就等于原来的行为)
|
|
62
|
+
node cli\recall.mjs --query "挡推裁决口径" --lexicon on --json
|
|
63
|
+
node cli\recall.mjs --query "..." --fallback-top-k 8 --encode-timeout-ms 1500
|
|
64
|
+
node cli\recall.mjs --query "..." --recall-log .\logs\memory-recall
|
|
65
|
+
|
|
66
|
+
# 召回链验收夹具(自带 15 项判据;给 --baseline <改动前 lib 目录> 就追加与旧版逐字对拍)
|
|
67
|
+
node tools\verify-recall-chain.mjs
|
|
68
|
+
|
|
54
69
|
# 基准(写报告)
|
|
55
70
|
node bench\bench.mjs --memory-db <副本> --out bench-report.json
|
|
71
|
+
|
|
72
|
+
# 分层软淘汰 + 号段哨兵(三件都默认干跑 / 只读,详见仓根 README 同名小节)
|
|
73
|
+
node cli\layer-migrate.mjs [--db <库>] # 干跑:说会加什么列、回填几行
|
|
74
|
+
node cli\layer-migrate.mjs --db <库> --apply --backup <备份文件> # 真加列 + 回填
|
|
75
|
+
node cli\layer-migrate.mjs --db <库> --restore <备份文件> [--apply] # 还原(默认干跑)
|
|
76
|
+
node cli\layer-move.mjs --db <库> --layer <层> [--apply] [--force] # 搬出(主表 → memories_evicted)
|
|
77
|
+
node cli\layer-move.mjs --db <库> --layer <层> --back [--apply] # 搬回(先核逐行指纹)
|
|
78
|
+
node cli\layer-move.mjs --db <库> --dump before.json / --verify before.json # 搬前留凭据 / 搬回后对拍
|
|
79
|
+
node cli\band-sentinel.mjs [--db <库>] [--line] [--apply --out r.json] # 号段哨兵(缺省单机、纯只读)
|
|
56
80
|
```
|
|
57
81
|
|
|
82
|
+
## 召回链:三个开关与设计依据
|
|
83
|
+
|
|
84
|
+
### 三个开关(都只增不改)
|
|
85
|
+
|
|
86
|
+
| 开关 | 取值 | 出厂 | 说明 |
|
|
87
|
+
|---|---|---|---|
|
|
88
|
+
| `dict/lexicon-config.json#enabled` | `true/false` | `false` | 词表第二路总开关;`MEMORY_ENGINE_LEXICON=on/off` 可临时覆盖;`--lexicon on/off` 是命令行覆盖 |
|
|
89
|
+
| `fallbackTopK` / `--fallback-top-k` | 正整数 | `8` | 编码失败时兜底几条 |
|
|
90
|
+
| `encodeTimeoutMs` / `--encode-timeout-ms` | 毫秒 | `1500` | 编码等待预算;`0` = 不设预算(只接抛错) |
|
|
91
|
+
| `--recall-log <目录>` / `DSH_MEMORY_RECALL_LOG_DIR` | 目录 | 无(不写) | 旁路留痕;`DSH_MEMORY_RECALL_LOG=off` 一键关回 |
|
|
92
|
+
| `MEMORY_ENGINE_DICT_DIR` | 目录 | `dict/` | 词表素材位置 |
|
|
93
|
+
|
|
94
|
+
### 为什么是这三件事
|
|
95
|
+
|
|
96
|
+
**① 查询截断 256 字符。** 本仓引擎侧原来**完全不截断**(本地 ONNX 只在**分词后**按 512 token 截,HTTP 桥不截);生产侧那道 `512` 是插件配置 `recallQueryMaxChars`,作用在**注入器**里、不在引擎里。所以这里是引擎侧新增的一道下限:超长 query 拿去编码既是浪费,也会把 FTS 的 `MATCH` 表达式撑大。按**码点**截(不是 UTF-16 码元),免得把 emoji 劈成半个代理对;截没截、原长多少都写进 `stats.queryChars/queryCharsUsed/queryTruncated`。
|
|
97
|
+
|
|
98
|
+
**② 编码失败降级成固定 Top-K。** 原来的行为是异常一路冒到调用方:命令行整条命令 rc 2;插件侧记 `skip/exception` ⇒ **那一轮一条记忆都不注入**。现在编码有预算,超时 / 抛错 / 空向量都记成 `encodeError = { name, ms }`(只存错误名与耗时,**不存 query 原文、不存 message**),然后走兜底:
|
|
99
|
+
`source.recent()` → 元数据按 `updated_at` 降序 → 向量表里 id 最大的几条,取 `fallbackTopK` 条,仍排除 pinned,来源标 `fallback`、名次记 `fallbackRank`。
|
|
100
|
+
为什么按"最近改动":拿不到向量时**语义相关已经无从计算**,此时最可能有用的是刚写进来的东西;而且它顺序确定、可复现、非空。
|
|
101
|
+
|
|
102
|
+
**③ 词表第二路的做法与默认值。** 切词 = 英文/数字串(≥2)+ 汉字按词典最长匹配(窗口 16,词典最长词条 15 字);单字与虚词直接丢(单字几乎不是线索)。打分 = **BM25**:`idf = ln(1 + (N-df+0.5)/(df+0.5))`,`tf 饱和 + 长度归一`(`k1=1.2, b=0.75`),再乘"通用词具体度"(来自 jieba 词频表,0 = 最常见 ⇒ 不当线索;低于 `floor`(0.3) 的词直接不参与)。它输出的候选与向量路同构(`[{id, score, rank}]`),直接进现成的 RRF。
|
|
103
|
+
**默认关**的理由:收益是"精确词多捞回来",代价是"两路融合会重排名次";本仓承诺"默认行为不改",所以出厂关、一行配置打开。索引走**进程内缓存 + 后台预热**(条数/`max(updated_at)`/TTL 三重失效),首次调用拿不到就先只走向量路,绝不把建索引的几百毫秒砸在某一轮对话上。
|
|
104
|
+
**词典缺失**(删了 `dict/` 或 `MEMORY_ENGINE_DICT_DIR` 指到空目录)⇒ `buildLexiconIndex` 返回 `null`、这一路零命中,结果与纯向量**逐字一致**。
|
|
105
|
+
|
|
106
|
+
**④ 留痕为什么默认不写、又怎么封口。** query 可能是隐私内容,库不该在纯查询里偷偷落盘;要判"召回准不准"就显式给目录。写入口全程 try/catch(**写失败不影响召回**),记录里**不存记忆正文**,只存 id/分数/来源/是否降级/耗时。封口三闸:单文件 `maxBytes`(512 KB)→ 按字节轮转留 `keepFiles`(3)代 → 超 `keepDays`(14 天)的旧代清掉;`keepFiles: 0` = 只留当前文件、超限清空重开。文件名不许带路径(构造时拒绝),轮转只在日志目录内派生文件名。
|
|
107
|
+
|
|
108
|
+
### 与生产实现的关系
|
|
109
|
+
|
|
110
|
+
生产侧的词表能力是「按行号锚点重放」的补丁(`patch-*-lexicon.mjs`)+ 直改文件;那套东西搬进独立仓必碎。本仓不搬补丁、不做 `sed` 式移植,而是**按行为重写**成 `lib/lexicon-recall.mjs` 与 `lib/recall-log.mjs`(行级同源度分别 18.5% / 19.5%),并用两件判据证明没跑偏:① 开关关掉时与改动前的 `recall.mjs` **逐字对拍**(`tools/verify-recall-chain.mjs --baseline`);② `test/` 184 项一行未改、全绿。
|
|
111
|
+
|
|
58
112
|
## 边界(硬约束)
|
|
59
113
|
|
|
60
114
|
1. **对 `memory.db` 只读**——所有打开方式都是 `new DatabaseSync(path, { readOnly: true })`,同步只写独立向量库。
|
|
@@ -194,7 +194,7 @@ async function main() {
|
|
|
194
194
|
batchSize: flags.batch ? Number(flags.batch) : undefined,
|
|
195
195
|
});
|
|
196
196
|
if (!fs.existsSync(config.memoryDb)) {
|
|
197
|
-
throw new Error(`记忆库不存在:${config.memoryDb}
|
|
197
|
+
throw new Error(`记忆库不存在:${config.memoryDb}(须用生产库的只读副本,勿指向生产库)`);
|
|
198
198
|
}
|
|
199
199
|
console.log('=== 记忆引擎 v2 · 离线层基准 ===');
|
|
200
200
|
console.log(`主机 : ${os.hostname()} · ${os.cpus().length} 核 ${(os.cpus()[0] || {}).model}`);
|