@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
|
@@ -1,127 +1,125 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 记忆库的只读数据源。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// 契约:对 memory.db 只开只读连接;语句在**打开阶段**一次性备好 —— 表结构不对应当是
|
|
4
|
+
// open 当场就炸,而不是拖到某次查询才现形。只读连接在 DELETE journal 下既不挡写者、
|
|
5
|
+
// 也不被写者挡。
|
|
5
6
|
import fs from 'node:fs';
|
|
6
7
|
import { DatabaseSync } from 'node:sqlite';
|
|
7
8
|
|
|
9
|
+
const BUSY_TIMEOUT_MS = 5000;
|
|
10
|
+
|
|
11
|
+
/** 全部预编译语句(键名即内部代号,SQL 文本集中在这里便于比对 schema)。 */
|
|
12
|
+
const STATEMENT_SQL = {
|
|
13
|
+
count: 'SELECT COUNT(*) AS n FROM memories',
|
|
14
|
+
maxUpdated: 'SELECT MAX(updated_at) AS m FROM memories',
|
|
15
|
+
idRange: 'SELECT MIN(id) AS lo, MAX(id) AS hi, COUNT(*) AS n FROM memories',
|
|
16
|
+
pinned: 'SELECT id FROM memories WHERE pinned = 1 ORDER BY id',
|
|
17
|
+
page: `SELECT id, text, tags, pinned, updated_at FROM memories
|
|
18
|
+
WHERE updated_at >= ? AND id > ? ORDER BY id LIMIT ?`,
|
|
19
|
+
segment: `SELECT id, text, tags, pinned, updated_at FROM memories
|
|
20
|
+
WHERE id >= ? AND id <= ? ORDER BY id`,
|
|
21
|
+
byId: 'SELECT id, text, tags, pinned, created_at, updated_at FROM memories WHERE id = ?',
|
|
22
|
+
allTexts: 'SELECT id, text FROM memories ORDER BY id',
|
|
23
|
+
allTags: 'SELECT id, tags, updated_at FROM memories',
|
|
24
|
+
recent: 'SELECT id FROM memories ORDER BY updated_at DESC, id DESC LIMIT ?',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** FTS5 补位:中文连续串几乎不可用,只补标签/英文/数字。 */
|
|
28
|
+
const FTS_SQL = `SELECT m.id AS id, m.pinned AS pinned, bm25(memories_fts) AS score
|
|
29
|
+
FROM memories_fts JOIN memories m ON m.id = memories_fts.rowid
|
|
30
|
+
WHERE memories_fts MATCH ? AND m.pinned = 0
|
|
31
|
+
ORDER BY score LIMIT ?`;
|
|
32
|
+
|
|
33
|
+
function asVecRow(r) {
|
|
34
|
+
return {
|
|
35
|
+
id: Number(r.id),
|
|
36
|
+
text: r.text,
|
|
37
|
+
tags: r.tags,
|
|
38
|
+
pinned: Number(r.pinned),
|
|
39
|
+
updated_at: Number(r.updated_at),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const nullableNumber = (v) => (v == null ? null : Number(v));
|
|
44
|
+
|
|
8
45
|
export function openMemorySource(dbPath) {
|
|
9
46
|
if (!fs.existsSync(dbPath)) {
|
|
10
47
|
throw new Error(`记忆库不存在: ${dbPath}`);
|
|
11
48
|
}
|
|
49
|
+
|
|
12
50
|
const db = new DatabaseSync(dbPath, { readOnly: true });
|
|
13
|
-
db.exec(
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`SELECT id, text, tags, pinned, updated_at FROM memories
|
|
22
|
-
WHERE updated_at >= ? AND id > ? ORDER BY id LIMIT ?`,
|
|
23
|
-
),
|
|
24
|
-
segment: db.prepare(
|
|
25
|
-
`SELECT id, text, tags, pinned, updated_at FROM memories
|
|
26
|
-
WHERE id >= ? AND id <= ? ORDER BY id`,
|
|
27
|
-
),
|
|
28
|
-
byId: db.prepare(
|
|
29
|
-
'SELECT id, text, tags, pinned, created_at, updated_at FROM memories WHERE id = ?',
|
|
30
|
-
),
|
|
31
|
-
allTexts: db.prepare('SELECT id, text FROM memories ORDER BY id'),
|
|
32
|
-
tagsOf: db.prepare('SELECT id, tags, updated_at FROM memories'),
|
|
33
|
-
};
|
|
51
|
+
db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS};`);
|
|
52
|
+
|
|
53
|
+
const q = {};
|
|
54
|
+
for (const [key, sql] of Object.entries(STATEMENT_SQL)) q[key] = db.prepare(sql);
|
|
55
|
+
|
|
56
|
+
let closed = false;
|
|
57
|
+
// FTS 表可能不存在 / 表达式可能非法 ⇒ 语句**懒备**,失败要走「补位路径空」而不是让 open 失败
|
|
58
|
+
let ftsStmt = null;
|
|
34
59
|
|
|
35
60
|
return {
|
|
36
61
|
dbPath,
|
|
37
62
|
readOnly: true,
|
|
38
63
|
|
|
39
|
-
count()
|
|
40
|
-
return Number(S.count.get().n);
|
|
41
|
-
},
|
|
64
|
+
count: () => Number(q.count.get().n),
|
|
42
65
|
|
|
43
|
-
maxUpdatedAt() {
|
|
44
|
-
const v =
|
|
66
|
+
maxUpdatedAt: () => {
|
|
67
|
+
const v = q.maxUpdated.get().m;
|
|
45
68
|
return v == null ? 0 : Number(v);
|
|
46
69
|
},
|
|
47
70
|
|
|
48
|
-
idRange() {
|
|
49
|
-
const r =
|
|
50
|
-
return { lo:
|
|
71
|
+
idRange: () => {
|
|
72
|
+
const r = q.idRange.get();
|
|
73
|
+
return { lo: nullableNumber(r.lo), hi: nullableNumber(r.hi), n: Number(r.n) };
|
|
51
74
|
},
|
|
52
75
|
|
|
53
|
-
/**
|
|
54
|
-
pinnedIds()
|
|
55
|
-
return S.pinned.all().map((r) => Number(r.id));
|
|
56
|
-
},
|
|
76
|
+
/** 召回排除集合(pinned 条目由宿主插件注入,引擎侧不重复给)。 */
|
|
77
|
+
pinnedIds: () => q.pinned.all().map((r) => Number(r.id)),
|
|
57
78
|
|
|
58
|
-
/** 增量页:updated_at >= sinceTs 且 id > afterId,按 id
|
|
59
|
-
incrementalPage(sinceTs, afterId, limit)
|
|
60
|
-
|
|
61
|
-
},
|
|
79
|
+
/** 增量页:updated_at >= sinceTs 且 id > afterId,按 id 升序。 */
|
|
80
|
+
incrementalPage: (sinceTs, afterId, limit) =>
|
|
81
|
+
q.page.all(Number(sinceTs), Number(afterId), Number(limit)).map(asVecRow),
|
|
62
82
|
|
|
63
|
-
/** id
|
|
64
|
-
segment(lo, hi)
|
|
65
|
-
return S.segment.all(Number(lo), Number(hi)).map(mapRow);
|
|
66
|
-
},
|
|
83
|
+
/** id 闭区间段(兜底轮转用)。 */
|
|
84
|
+
segment: (lo, hi) => q.segment.all(Number(lo), Number(hi)).map(asVecRow),
|
|
67
85
|
|
|
68
|
-
getById(id) {
|
|
69
|
-
const r =
|
|
86
|
+
getById: (id) => {
|
|
87
|
+
const r = q.byId.get(Number(id));
|
|
70
88
|
return r ? { ...r, id: Number(r.id) } : null;
|
|
71
89
|
},
|
|
72
90
|
|
|
73
|
-
/** 全表 id +
|
|
74
|
-
allTexts() {
|
|
75
|
-
return S.allTexts.all().map((r) => ({ id: Number(r.id), text: r.text }));
|
|
76
|
-
},
|
|
91
|
+
/** 全表 id + 正文(兜底校验用,体量在内存可承受范围内)。 */
|
|
92
|
+
allTexts: () => q.allTexts.all().map((r) => ({ id: Number(r.id), text: r.text })),
|
|
77
93
|
|
|
78
|
-
allTags() {
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
allTags: () => q.allTags.all().map((r) => ({ id: Number(r.id), tags: r.tags })),
|
|
95
|
+
|
|
96
|
+
/** 最近改动的若干条(降级兜底用:编码失败时按这个顺序给固定 Top-K,不依赖向量)。 */
|
|
97
|
+
recent: (limit) => q.recent.all(Math.max(1, Math.trunc(Number(limit) || 1))).map((r) => ({ id: Number(r.id) })),
|
|
81
98
|
|
|
82
|
-
/**
|
|
83
|
-
* FTS5 补位检索(方案 §3.4 A4:中文连续串几近不可用,只补标签/英文/数字)。
|
|
84
|
-
* @param {string} matchExpr 已构造好的 FTS5 MATCH 表达式
|
|
85
|
-
*/
|
|
86
99
|
ftsSearch(matchExpr, limit) {
|
|
87
100
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`SELECT m.id AS id, m.pinned AS pinned, bm25(memories_fts) AS score
|
|
91
|
-
FROM memories_fts JOIN memories m ON m.id = memories_fts.rowid
|
|
92
|
-
WHERE memories_fts MATCH ? AND m.pinned = 0
|
|
93
|
-
ORDER BY score LIMIT ?`,
|
|
94
|
-
)
|
|
101
|
+
if (!ftsStmt) ftsStmt = db.prepare(FTS_SQL);
|
|
102
|
+
return ftsStmt
|
|
95
103
|
.all(String(matchExpr), Number(limit))
|
|
96
104
|
.map((r) => ({ id: Number(r.id), score: Number(r.score) }));
|
|
97
105
|
} catch (e) {
|
|
98
|
-
//
|
|
106
|
+
// 表达式非法 / 表缺失:补位路径返回空,不做零分占位
|
|
99
107
|
return { error: String(e && e.message ? e.message : e) };
|
|
100
108
|
}
|
|
101
109
|
},
|
|
102
110
|
|
|
103
111
|
close() {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
this._closed = true;
|
|
112
|
+
if (closed) return;
|
|
113
|
+
closed = true;
|
|
107
114
|
try {
|
|
108
115
|
db.close();
|
|
109
116
|
} catch {
|
|
110
|
-
/*
|
|
117
|
+
/* 关闭失败不算错:调用方常在 finally 里重复关 */
|
|
111
118
|
}
|
|
112
119
|
},
|
|
120
|
+
|
|
113
121
|
get closed() {
|
|
114
|
-
return
|
|
122
|
+
return closed;
|
|
115
123
|
},
|
|
116
124
|
};
|
|
117
125
|
}
|
|
118
|
-
|
|
119
|
-
function mapRow(r) {
|
|
120
|
-
return {
|
|
121
|
-
id: Number(r.id),
|
|
122
|
-
text: r.text,
|
|
123
|
-
tags: r.tags,
|
|
124
|
-
pinned: Number(r.pinned),
|
|
125
|
-
updated_at: Number(r.updated_at),
|
|
126
|
-
};
|
|
127
|
-
}
|
|
@@ -1,47 +1,67 @@
|
|
|
1
|
-
//
|
|
1
|
+
// 坑库的只读数据源:把 `butler-pitfalls.json` 摆成与记忆源一模一样的形状。
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// 为什么要有它:开工简报原来固定取"出现最多的三条",跟本轮任务毫无关系;
|
|
4
|
+
// 有了这个源,坑条目也能按语义被召回。
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
// 1.
|
|
8
|
-
// 2.
|
|
9
|
-
//
|
|
10
|
-
// /
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
6
|
+
// 四条不许破的口径:
|
|
7
|
+
// 1. 只读那个 JSON,一个字节都不回写;`readOnly:true` 是自证字段。
|
|
8
|
+
// 2. 方法与记忆源一一对应(下游按名字硬调,少一个当场 is not a function):
|
|
9
|
+
// count / maxUpdatedAt / idRange / pinnedIds / incrementalPage / segment /
|
|
10
|
+
// getById / allTexts / allTags / ftsSearch / close。注意名字是 maxUpdatedAt。
|
|
11
|
+
// 3. 分页与分段吐出来的行必须带 id / text / updated_at —— 上层直接取,
|
|
12
|
+
// 缺了不会报错,只会把 undefined 写进向量行。
|
|
13
|
+
// 4. 坑没有数字 id 段:idRange 给"排序后的下标区间",排序键是 lastSeen 升序、
|
|
14
|
+
// 平局按 id 字典序;分页沿用同一套顺序做游标,所以集合中途变了也只多扫不漏、不重复。
|
|
15
|
+
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
const SEED_MARK = '[seed]';
|
|
19
|
+
const CURSOR_WIDTH = 16;
|
|
20
|
+
|
|
21
|
+
/** 一条坑进向量的文本:seed 标记 + 分类前缀 + 正文 + 工具后缀;建议文本一律不进(会随建议漂移)。 */
|
|
19
22
|
export function pitfallText(row) {
|
|
20
|
-
const
|
|
21
|
-
const
|
|
23
|
+
const seed = String(row.source || '') === 'seed' ? SEED_MARK : '';
|
|
24
|
+
const label = row.category ? `[${row.category}] ` : '';
|
|
22
25
|
const tools = row.tools && typeof row.tools === 'object' ? Object.keys(row.tools).join(',') : '';
|
|
23
|
-
|
|
26
|
+
const suffix = tools ? ` | tools=${tools}` : '';
|
|
27
|
+
return `${seed}${label}${row.normText ?? ''}${suffix}`;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
// 工具字段的规范形态:对象取键名拼串,其它一律空串。
|
|
31
|
+
function toolsText(item) {
|
|
32
|
+
if (!item.tools || typeof item.tools !== 'object') return '';
|
|
33
|
+
return Object.keys(item.tools).join(',');
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
function
|
|
34
|
-
|
|
36
|
+
// 稳定顺序:先 lastSeen,再 id 字典序。平局必须先定死,否则翻页会来回抖。
|
|
37
|
+
function byLastSeenThenId(a, b) {
|
|
38
|
+
if (a.updated_at !== b.updated_at) return a.updated_at - b.updated_at;
|
|
39
|
+
if (a.id === b.id) return 0;
|
|
40
|
+
return a.id < b.id ? -1 : 1;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
};
|
|
43
|
+
// 游标的可比较形态:时间戳左补零到定长,再接 '|' 与 id ⇒ 字典序正好等于上面的顺序。
|
|
44
|
+
function cursorOf(updatedAt, id) {
|
|
45
|
+
return `${String(updatedAt).padStart(CURSOR_WIDTH, '0')}|${id}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 内层行 → 向量层要的形状(pinned 恒 0:坑库没有这个字段)
|
|
49
|
+
function toPageRow(row) {
|
|
50
|
+
return { id: row.id, text: row.text, tags: row.tags, pinned: 0, updated_at: row.updated_at };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 解析文件并取出条目数组:顶层直接是数组、或外面套一层 `pitfalls` 都认。
|
|
54
|
+
function readEntryList(jsonPath) {
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
58
|
+
} catch (e) {
|
|
59
|
+
const why = e && e.message ? e.message : String(e);
|
|
60
|
+
throw new Error(`坑库解析失败: ${jsonPath}(${why})`);
|
|
61
|
+
}
|
|
62
|
+
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed.pitfalls) ? parsed.pitfalls : null;
|
|
63
|
+
if (!list) throw new Error(`坑库结构不认识(应含 pitfalls 数组): ${jsonPath}`);
|
|
64
|
+
return list;
|
|
45
65
|
}
|
|
46
66
|
|
|
47
67
|
/**
|
|
@@ -52,119 +72,100 @@ export function openPitfallSource(jsonPath) {
|
|
|
52
72
|
if (!fs.existsSync(jsonPath)) {
|
|
53
73
|
throw new Error(`坑库不存在: ${jsonPath}`);
|
|
54
74
|
}
|
|
55
|
-
let raw;
|
|
56
|
-
try {
|
|
57
|
-
raw = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
|
|
58
|
-
} catch (e) {
|
|
59
|
-
throw new Error(`坑库解析失败: ${jsonPath}(${String(e && e.message ? e.message : e)})`);
|
|
60
|
-
}
|
|
61
|
-
const list = Array.isArray(raw) ? raw : Array.isArray(raw.pitfalls) ? raw.pitfalls : null;
|
|
62
|
-
if (!list) throw new Error(`坑库结构不认识(应含 pitfalls 数组): ${jsonPath}`);
|
|
63
75
|
|
|
64
|
-
const rows =
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
let closed = false;
|
|
76
|
+
const rows = readEntryList(jsonPath)
|
|
77
|
+
.filter((item) => item && item.id != null)
|
|
78
|
+
.map((item) => ({
|
|
79
|
+
id: String(item.id),
|
|
80
|
+
text: pitfallText(item),
|
|
81
|
+
tags: toolsText(item),
|
|
82
|
+
updated_at: Number(item.lastSeen || 0),
|
|
83
|
+
}))
|
|
84
|
+
.sort(byLastSeenThenId);
|
|
85
|
+
const index = new Map(rows.map((row) => [row.id, row]));
|
|
86
|
+
|
|
87
|
+
let shut = false;
|
|
77
88
|
return {
|
|
78
89
|
dbPath: jsonPath,
|
|
79
90
|
readOnly: true,
|
|
80
|
-
/**
|
|
91
|
+
/** 自证:下游日志/自检按它报"逐来源" */
|
|
81
92
|
sourceName: 'pitfall',
|
|
82
|
-
/**
|
|
93
|
+
/** 条目总数(观测用,等于 rows.length) */
|
|
83
94
|
n: rows.length,
|
|
84
95
|
|
|
85
|
-
count()
|
|
86
|
-
return rows.length;
|
|
87
|
-
},
|
|
96
|
+
count: () => rows.length,
|
|
88
97
|
|
|
89
|
-
maxUpdatedAt()
|
|
90
|
-
let m = 0;
|
|
91
|
-
for (const r of rows) if (r.updated_at > m) m = r.updated_at;
|
|
92
|
-
return m;
|
|
93
|
-
},
|
|
98
|
+
maxUpdatedAt: () => rows.reduce((peak, row) => (row.updated_at > peak ? row.updated_at : peak), 0),
|
|
94
99
|
|
|
95
|
-
/**
|
|
96
|
-
idRange() {
|
|
97
|
-
if (!rows.length) return { lo: null, hi: null, n: 0 };
|
|
98
|
-
return { lo: 0, hi: rows.length - 1, n: rows.length };
|
|
99
|
-
},
|
|
100
|
+
/** 坑没有数字 id ⇒ 给稳定等价量:排序后的下标闭区间。 */
|
|
101
|
+
idRange: () => (rows.length ? { lo: 0, hi: rows.length - 1, n: rows.length } : { lo: null, hi: null, n: 0 }),
|
|
100
102
|
|
|
101
|
-
/**
|
|
102
|
-
pinnedIds()
|
|
103
|
-
return [];
|
|
104
|
-
},
|
|
103
|
+
/** 坑库没有 pinned 概念 ⇒ 永远空集(召回侧据此不排除任何条目)。 */
|
|
104
|
+
pinnedIds: () => [],
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
107
|
+
* 增量页:取 `updated_at >= sinceTs` 的条目,按 (lastSeen, id) 升序往后翻,
|
|
108
|
+
* 游标就是上一页末条的 id。游标按"键比较"而不是按下标 ⇒ 中途删改也不会重也不会漏。
|
|
109
109
|
*/
|
|
110
|
-
incrementalPage(sinceTs, afterId, limit) {
|
|
110
|
+
incrementalPage: (sinceTs, afterId, limit) => {
|
|
111
111
|
const since = Number(sinceTs) || 0;
|
|
112
|
-
const
|
|
113
|
-
if (!rows.length ||
|
|
114
|
-
const
|
|
112
|
+
const cap = Number(limit) || 0;
|
|
113
|
+
if (!rows.length || cap <= 0) return [];
|
|
114
|
+
const cursorId = afterId == null || afterId === '' ? null : String(afterId);
|
|
115
115
|
let floor = '';
|
|
116
|
-
if (
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
if (cursorId != null) {
|
|
117
|
+
const known = index.get(cursorId);
|
|
118
|
+
// 游标条已经被删:退化成"从这个 id 起步",整体仍然单调
|
|
119
|
+
floor = known ? cursorOf(known.updated_at, known.id) : cursorOf(0, cursorId);
|
|
120
120
|
}
|
|
121
|
-
const
|
|
122
|
-
for (const
|
|
123
|
-
if (
|
|
124
|
-
if (floor &&
|
|
125
|
-
|
|
126
|
-
if (
|
|
121
|
+
const page = [];
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
if (row.updated_at < since) continue;
|
|
124
|
+
if (floor && cursorOf(row.updated_at, row.id) <= floor) continue;
|
|
125
|
+
page.push(toPageRow(row));
|
|
126
|
+
if (page.length >= cap) break;
|
|
127
127
|
}
|
|
128
|
-
return
|
|
128
|
+
return page;
|
|
129
129
|
},
|
|
130
130
|
|
|
131
|
-
/**
|
|
132
|
-
segment(lo, hi) {
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
if (!Number.isFinite(
|
|
136
|
-
|
|
131
|
+
/** 段(下标闭区间,顺序 = lastSeen 升序):行里必须带 text 与 updated_at,上层直接取。 */
|
|
132
|
+
segment: (lo, hi) => {
|
|
133
|
+
const begin = Number(lo);
|
|
134
|
+
const end = Number(hi);
|
|
135
|
+
if (!Number.isFinite(begin) || !Number.isFinite(end)) return [];
|
|
136
|
+
const from = Math.max(0, begin);
|
|
137
|
+
return rows.slice(from, Math.max(0, end) + 1).map(toPageRow);
|
|
137
138
|
},
|
|
138
139
|
|
|
139
|
-
getById(id) {
|
|
140
|
-
const
|
|
141
|
-
if (!
|
|
142
|
-
return {
|
|
140
|
+
getById: (id) => {
|
|
141
|
+
const found = index.get(String(id));
|
|
142
|
+
if (!found) return null;
|
|
143
|
+
return {
|
|
144
|
+
id: found.id,
|
|
145
|
+
text: found.text,
|
|
146
|
+
tags: found.tags,
|
|
147
|
+
pinned: 0,
|
|
148
|
+
created_at: found.updated_at,
|
|
149
|
+
updated_at: found.updated_at,
|
|
150
|
+
};
|
|
143
151
|
},
|
|
144
152
|
|
|
145
|
-
allTexts() {
|
|
146
|
-
return rows.map((r) => ({ id: r.id, text: r.text }));
|
|
147
|
-
},
|
|
153
|
+
allTexts: () => rows.map((row) => ({ id: row.id, text: row.text })),
|
|
148
154
|
|
|
149
|
-
allTags() {
|
|
150
|
-
return rows.map((r) => ({ id: r.id, tags: r.tags }));
|
|
151
|
-
},
|
|
155
|
+
allTags: () => rows.map((row) => ({ id: row.id, tags: row.tags })),
|
|
152
156
|
|
|
153
157
|
/**
|
|
154
|
-
*
|
|
155
|
-
*
|
|
158
|
+
* 坑库没有 FTS5 ⇒ 这里必须回一个对象而不是抛错:
|
|
159
|
+
* 召回侧拿对象就走"无关键词补位"那条路,抛错会整条链断掉。
|
|
156
160
|
*/
|
|
157
|
-
ftsSearch() {
|
|
158
|
-
return { error: '坑库无 FTS5(pitfall source 无 FTS 补位路)' };
|
|
159
|
-
},
|
|
161
|
+
ftsSearch: () => ({ error: '坑库无 FTS5(pitfall source 无 FTS 补位路)' }),
|
|
160
162
|
|
|
161
163
|
close() {
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
closed = true;
|
|
164
|
+
// 幂等:调用方常在 finally 里再关一次,重复关不许抛
|
|
165
|
+
shut = true;
|
|
165
166
|
},
|
|
166
167
|
get closed() {
|
|
167
|
-
return
|
|
168
|
+
return shut;
|
|
168
169
|
},
|
|
169
170
|
};
|
|
170
171
|
}
|