dsh-memu-memory 1.2.1 → 1.2.2
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/AGENTS.md +1 -1
- package/README.md +12 -5
- package/README.zh.md +4 -2
- package/docs/contributing.md +16 -3
- package/docs/host-api.md +3 -1
- package/docs/memory-semantics.md +21 -2
- package/docs/troubleshooting.md +42 -4
- package/lib/backup.js +24 -2
- package/lib/binding-diagnostics.js +2 -2
- package/lib/client.js +1361 -146
- package/lib/consolidation-worker.js +30 -8
- package/lib/curator-evidence.js +131 -0
- package/lib/curator-policy.js +81 -0
- package/lib/curator.js +1252 -91
- package/lib/cycle.js +3 -0
- package/lib/embedding-download.js +39 -4
- package/lib/engine.js +6 -3
- package/lib/global-graph.js +282 -0
- package/lib/graph.js +51 -9
- package/lib/inbound.js +19 -0
- package/lib/inject.js +277 -17
- package/lib/lexicon.js +1115 -0
- package/lib/local-vector-service.js +8 -6
- package/lib/managed-runtime.js +33 -2
- package/lib/memory.js +282 -17
- package/lib/memu.js +2 -2
- package/lib/migrate.js +83 -2
- package/lib/private-links.js +587 -0
- package/lib/private.js +32 -3
- package/lib/prompts/global-curator.md +76 -19
- package/lib/prompts/private-consolidation.coding.md +4 -1
- package/lib/prompts/private-consolidation.md +3 -1
- package/lib/prompts/private-consolidation.mixed.md +3 -1
- package/lib/prompts/private-consolidation.rp.md +3 -1
- package/lib/prompts.js +1 -1
- package/lib/rpc.js +577 -9
- package/lib/state.js +4 -1
- package/lib/text-retrieval.js +661 -0
- package/lib/text-similarity.js +36 -18
- package/lib/tools.js +168 -10
- package/lib/webui/standalone.css +17 -0
- package/lib/webui/standalone.js +2 -0
- package/package.json +1 -1
- package/scripts/acceptance-local.mjs +8 -5
- package/scripts/curator-pair-calibrate.mjs +259 -0
- package/scripts/lexicon-calibrate.mjs +230 -0
- package/scripts/lexicon-dedupe.mjs +318 -0
- package/tests/absorb-review.test.mjs +353 -0
- package/tests/appendix-b2.test.mjs +2 -2
- package/tests/client-export.test.mjs +520 -0
- package/tests/client-render.test.mjs +19 -3
- package/tests/consolidation-fork.test.mjs +14 -12
- package/tests/consolidation-snapshot.test.mjs +39 -0
- package/tests/curator-absorb.test.mjs +443 -0
- package/tests/curator-calibrate.test.mjs +173 -0
- package/tests/curator-clusters.test.mjs +281 -0
- package/tests/curator-commit.test.mjs +52 -5
- package/tests/curator-evidence.test.mjs +102 -0
- package/tests/curator-link.test.mjs +316 -0
- package/tests/curator-pairing.test.mjs +252 -0
- package/tests/curator-probe.test.mjs +15 -6
- package/tests/curator-review-mode.test.mjs +314 -0
- package/tests/disclosure.test.mjs +2 -1
- package/tests/fixtures/curator-pairs/labeled.v1.jsonl +90 -0
- package/tests/fixtures/lexicon-stub-hooks.mjs +15 -0
- package/tests/fixtures/lexicon-stub.mjs +284 -0
- package/tests/graph-cascade.test.mjs +35 -2
- package/tests/graph.test.mjs +96 -0
- package/tests/help-page.test.mjs +415 -0
- package/tests/inbound.test.mjs +6 -3
- package/tests/inject-guide.test.mjs +28 -0
- package/tests/inject.test.mjs +157 -5
- package/tests/lexicon-calibrate.test.mjs +48 -0
- package/tests/lexicon-dedupe.test.mjs +171 -0
- package/tests/lexicon-frequency.test.mjs +179 -0
- package/tests/lexicon-inject.test.mjs +410 -0
- package/tests/lexicon-perf.test.mjs +436 -0
- package/tests/lexicon-ui.test.mjs +547 -0
- package/tests/lexicon.test.mjs +922 -0
- package/tests/managed-runtime.test.mjs +32 -0
- package/tests/private-links.test.mjs +445 -0
- package/tests/prompts.test.mjs +35 -0
- package/tests/recall-dedup.test.mjs +33 -4
- package/tests/tool-schema.test.mjs +1 -1
- package/tests/tools.test.mjs +112 -3
- package/tests/webui-auth.test.mjs +348 -2
- package/tests/webui.test.mjs +188 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { registerHooks } from 'node:module'
|
|
7
|
+
|
|
8
|
+
// Package A (spec §6.1): curator review mode + risk score + input fixes.
|
|
9
|
+
//
|
|
10
|
+
// One entry of `memories` must land in exactly one of three states — active
|
|
11
|
+
// (committed), pending_review (index-only, held), or skipped — and the
|
|
12
|
+
// decision must follow §4.10: hard gates (conflict / missing evidence /
|
|
13
|
+
// invalid rewrite target) are pending in BOTH modes, `auto` passes clean
|
|
14
|
+
// creates and passes rewrites below the 0.70 risk threshold, `review-all`
|
|
15
|
+
// holds every content op. Unknown ops and invalid merge targets are skipped
|
|
16
|
+
// and never degrade into a create (the §0.2 bug).
|
|
17
|
+
//
|
|
18
|
+
// Same hermetic pattern as tests/curator-commit.test.mjs: './memu.js' is
|
|
19
|
+
// replaced before production modules load; everything else is the real module
|
|
20
|
+
// stack against a temp DSH_MEMU_HOME. Default settings keep the vector route
|
|
21
|
+
// lexical, so nothing here talks to an engine.
|
|
22
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-memu-curator-review-'))
|
|
23
|
+
process.env.DSH_MEMU_HOME = home
|
|
24
|
+
process.env.DSH_MEMU_MEMORY_HOME = home
|
|
25
|
+
|
|
26
|
+
globalThis.__curatorReviewTest = {
|
|
27
|
+
async commit() { return { ok: true } },
|
|
28
|
+
async forget() { return { deleted: 1, segments: 1 } },
|
|
29
|
+
async retrieve() { return { files: [], segments: [], resources: [], raw: null } },
|
|
30
|
+
async listFiles() { return { recall_files: [], resources: [], raw: null } },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const memuSource = 'const bridge = globalThis.__curatorReviewTest;'
|
|
34
|
+
+ 'export const commit = (args) => bridge.commit(args), forget = (args) => bridge.forget(args),'
|
|
35
|
+
+ 'retrieve = (args) => bridge.retrieve(args), listFiles = (args) => bridge.listFiles(args);'
|
|
36
|
+
const hooks = registerHooks({
|
|
37
|
+
resolve(specifier, context, nextResolve) {
|
|
38
|
+
if (specifier === './memu.js') return { url: 'data:text/javascript,' + encodeURIComponent(memuSource), shortCircuit: true }
|
|
39
|
+
return nextResolve(specifier, context)
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const curator = await import('../lib/curator.js')
|
|
44
|
+
const state = await import('../lib/state.js')
|
|
45
|
+
const rpc = await import('../lib/rpc.js')
|
|
46
|
+
|
|
47
|
+
test.after(async () => {
|
|
48
|
+
try { hooks.unregister?.() } catch {}
|
|
49
|
+
try { fs.rmSync(home, { recursive: true, force: true }) } catch {}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const TARGET = 'gmem-target'
|
|
53
|
+
const TARGET_ENTRY = {
|
|
54
|
+
track: 'memory', domain: 'general', title: '部署窗口约定',
|
|
55
|
+
content: '每周四凌晨两点到四点是固定的部署窗口,窗口内只允许灰度发布。',
|
|
56
|
+
confidence: 0.9, status: 'active',
|
|
57
|
+
createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
|
|
58
|
+
evidence: [{ sessionId: 'old', quote: 'old quote' }],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function seedGlobalIndex(entries = {}) {
|
|
62
|
+
state.saveGlobalIndex({ version: 1, entries: structuredClone(entries) })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Every test sets the mode it means to exercise; nothing relies on leftover
|
|
66
|
+
// settings from a previous case.
|
|
67
|
+
function setMode(mode) {
|
|
68
|
+
state.updateSettings({ curatorReviewMode: mode })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const cleanCreate = (over = {}) => ({
|
|
72
|
+
op: 'create', domain: 'general', track: 'memory',
|
|
73
|
+
title: '独立的全新事实',
|
|
74
|
+
content: '用户倾向于在正式汇报里先给结论再给细节。',
|
|
75
|
+
confidence: 0.9,
|
|
76
|
+
evidence: [
|
|
77
|
+
{ session_id: 's1', quote: '先说结论再说细节' },
|
|
78
|
+
{ session_id: 's2', quote: '汇报的时候结论在前' },
|
|
79
|
+
],
|
|
80
|
+
...over,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
function entryOf(index, predicate) {
|
|
84
|
+
return Object.values(index.entries).find(predicate) || null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
test('auto + clean create with two evidence lands active with reviewRisk 0', async () => {
|
|
88
|
+
seedGlobalIndex({})
|
|
89
|
+
setMode('auto')
|
|
90
|
+
const result = await curator.commitCuratorOutput({ memories: [cleanCreate()] }, { job: { step: 'committing' } })
|
|
91
|
+
assert.equal(result.created, 1)
|
|
92
|
+
assert.equal(result.pending, 0)
|
|
93
|
+
assert.equal(result.files.length, 1)
|
|
94
|
+
// The risk summary is part of the contract: every written entry is counted.
|
|
95
|
+
assert.deepEqual(result.reviewRisk, { max: 0, flagged: 0, reasons: {} })
|
|
96
|
+
const index = state.loadGlobalIndex()
|
|
97
|
+
const entry = entryOf(index, (e) => e.title === '独立的全新事实')
|
|
98
|
+
assert.ok(entry, 'the create must land in the index')
|
|
99
|
+
assert.equal(entry.status, 'active')
|
|
100
|
+
assert.equal(entry.reviewRisk, 0)
|
|
101
|
+
assert.deepEqual(entry.reviewRiskReasons, [])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('auto + create with needs_review still lands active; the flag only feeds the risk score', async () => {
|
|
105
|
+
seedGlobalIndex({})
|
|
106
|
+
setMode('auto')
|
|
107
|
+
const result = await curator.commitCuratorOutput({
|
|
108
|
+
memories: [cleanCreate({ needs_review: true, review_reason: '一次口误,证据偏薄' })],
|
|
109
|
+
}, { job: { step: 'committing' } })
|
|
110
|
+
assert.equal(result.created, 1, 'auto passes a flagged create by default')
|
|
111
|
+
const entry = entryOf(state.loadGlobalIndex(), (e) => e.title === '独立的全新事实')
|
|
112
|
+
assert.equal(entry.status, 'active')
|
|
113
|
+
// needs_review 0.70 + review_reason 0.20 + single-evidence 0.10 was avoided by
|
|
114
|
+
// passing two evidence items, so 0.70 + 0.20 = 0.90 exactly.
|
|
115
|
+
assert.equal(entry.reviewRisk, 0.9)
|
|
116
|
+
assert.ok(entry.reviewRiskReasons.includes('needs_review'))
|
|
117
|
+
assert.ok(entry.reviewRiskReasons.includes('review_reason'))
|
|
118
|
+
assert.equal(entry.reviewReason, undefined, 'an active entry carries no reviewReason')
|
|
119
|
+
assert.equal(result.reviewRisk.flagged, 1, 'the >=0.70 entry is flagged in the summary')
|
|
120
|
+
assert.equal(result.reviewRisk.max, 0.9)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('auto + create with confidence 0.4 still lands active; low confidence only raises the risk score', async () => {
|
|
124
|
+
seedGlobalIndex({})
|
|
125
|
+
setMode('auto')
|
|
126
|
+
const result = await curator.commitCuratorOutput({
|
|
127
|
+
memories: [cleanCreate({ confidence: 0.4 })],
|
|
128
|
+
}, { job: { step: 'committing' } })
|
|
129
|
+
assert.equal(result.created, 1)
|
|
130
|
+
const entry = entryOf(state.loadGlobalIndex(), (e) => e.title === '独立的全新事实')
|
|
131
|
+
assert.equal(entry.status, 'active')
|
|
132
|
+
assert.equal(entry.reviewRisk, 0.7, 'confidence<0.50 is +0.70; the two evidence items avoid the single-evidence bump')
|
|
133
|
+
assert.ok(entry.reviewRiskReasons.includes('confidence<0.50'))
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('auto + merge with needs_review is held pending', async () => {
|
|
137
|
+
seedGlobalIndex({ [TARGET]: TARGET_ENTRY })
|
|
138
|
+
setMode('auto')
|
|
139
|
+
const result = await curator.commitCuratorOutput({
|
|
140
|
+
memories: [{
|
|
141
|
+
op: 'merge', domain: 'general', track: 'memory', target_id: TARGET,
|
|
142
|
+
title: '部署窗口的合并版本',
|
|
143
|
+
content: '每周四凌晨两点到四点是固定的部署窗口,窗口内只允许灰度发布,全量要等监控确认。',
|
|
144
|
+
confidence: 0.9, needs_review: true, review_reason: '模型标记待复核',
|
|
145
|
+
evidence: [{ session_id: 's3', quote: '部署窗口只在周四凌晨' }],
|
|
146
|
+
}],
|
|
147
|
+
}, { job: { step: 'committing' } })
|
|
148
|
+
assert.equal(result.merged, 0, 'a held merge is not counted as merged')
|
|
149
|
+
assert.equal(result.pending, 1)
|
|
150
|
+
assert.equal(result.files.length, 0, 'pending entries are never committed to the vector store')
|
|
151
|
+
const index = state.loadGlobalIndex()
|
|
152
|
+
const held = entryOf(index, (e) => e.status === 'pending_review')
|
|
153
|
+
assert.ok(held, 'the merge must be held for review')
|
|
154
|
+
assert.equal(held.conflictingWith, TARGET)
|
|
155
|
+
// non-hard pending: the model's own reason wins over the generic fallback.
|
|
156
|
+
assert.equal(held.reviewReason, '模型标记待复核')
|
|
157
|
+
assert.equal(held.reviewRisk, 1.2, 'needs_review 0.70 + merge 0.20 + review_reason 0.20 + single evidence 0.10')
|
|
158
|
+
assert.equal(index.entries[TARGET].status, 'active', 'a held merge never touches the target')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('auto + conflict is pending in both modes with the hard reason', async () => {
|
|
162
|
+
seedGlobalIndex({ [TARGET]: TARGET_ENTRY })
|
|
163
|
+
setMode('auto')
|
|
164
|
+
const result = await curator.commitCuratorOutput({
|
|
165
|
+
memories: [{
|
|
166
|
+
op: 'conflict', domain: 'general', track: 'memory', target_id: TARGET,
|
|
167
|
+
title: '部署窗口的两个说法', content: '两个会话对部署窗口的说法不一致。',
|
|
168
|
+
confidence: 0.9,
|
|
169
|
+
evidence: [{ session_id: 's4', quote: '部署窗口是周四' }, { session_id: 's5', quote: '部署窗口是周五' }],
|
|
170
|
+
}],
|
|
171
|
+
}, { job: { step: 'committing' } })
|
|
172
|
+
assert.equal(result.pending, 1)
|
|
173
|
+
assert.equal(result.created, 0)
|
|
174
|
+
const held = entryOf(state.loadGlobalIndex(), (e) => e.status === 'pending_review')
|
|
175
|
+
assert.ok(held, 'a conflict is always held')
|
|
176
|
+
assert.equal(held.reviewReason, 'conflict')
|
|
177
|
+
assert.equal(held.reviewRisk, 2)
|
|
178
|
+
assert.deepEqual(held.reviewRiskReasons, ['conflict'])
|
|
179
|
+
assert.equal(held.conflictingWith, TARGET)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('auto + missing evidence is pending with the hard reason', async () => {
|
|
183
|
+
seedGlobalIndex({})
|
|
184
|
+
setMode('auto')
|
|
185
|
+
const result = await curator.commitCuratorOutput({
|
|
186
|
+
memories: [{ op: 'create', domain: 'general', track: 'memory', title: '没有证据的说法', content: '模型没给引用。', confidence: 0.9 }],
|
|
187
|
+
}, { job: { step: 'committing' } })
|
|
188
|
+
assert.equal(result.created, 0)
|
|
189
|
+
assert.equal(result.pending, 1)
|
|
190
|
+
const held = entryOf(state.loadGlobalIndex(), (e) => e.status === 'pending_review')
|
|
191
|
+
assert.ok(held)
|
|
192
|
+
assert.equal(held.reviewReason, 'missing evidence')
|
|
193
|
+
assert.equal(held.reviewRisk, 2)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
test('review-all holds even a clean create', async () => {
|
|
197
|
+
seedGlobalIndex({})
|
|
198
|
+
setMode('review-all')
|
|
199
|
+
const result = await curator.commitCuratorOutput({ memories: [cleanCreate()] }, { job: { step: 'committing' } })
|
|
200
|
+
assert.equal(result.created, 0)
|
|
201
|
+
assert.equal(result.pending, 1)
|
|
202
|
+
const held = entryOf(state.loadGlobalIndex(), (e) => e.status === 'pending_review')
|
|
203
|
+
assert.ok(held)
|
|
204
|
+
// Not a hard gate and the model gave no reason: the generic threshold note.
|
|
205
|
+
assert.equal(held.reviewReason, 'risk threshold')
|
|
206
|
+
assert.equal(held.reviewRisk, 0, 'a clean hold carries risk 0, the mode alone caused the hold')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('merge with a nonexistent target is skipped and never degrades into a create', async () => {
|
|
210
|
+
seedGlobalIndex({ [TARGET]: TARGET_ENTRY })
|
|
211
|
+
setMode('auto')
|
|
212
|
+
const result = await curator.commitCuratorOutput({
|
|
213
|
+
memories: [{
|
|
214
|
+
op: 'merge', domain: 'general', track: 'memory', target_id: 'gmem-missing',
|
|
215
|
+
title: '没有落点的合并', content: '这段内容绝不能变成一条新记忆。',
|
|
216
|
+
confidence: 0.9,
|
|
217
|
+
evidence: [{ session_id: 's6', quote: '合并的目标丢了' }],
|
|
218
|
+
}],
|
|
219
|
+
}, { job: { step: 'committing' } })
|
|
220
|
+
assert.equal(result.created, 0, 'the §0.2 bug: a skipped merge must not continue as a create')
|
|
221
|
+
assert.equal(result.merged, 0)
|
|
222
|
+
assert.equal(result.pending, 0)
|
|
223
|
+
assert.equal(result.files.length, 0)
|
|
224
|
+
assert.equal(result.skipped.length, 1)
|
|
225
|
+
assert.match(result.skipped[0].reason, /target not found: gmem-missing/)
|
|
226
|
+
const index = state.loadGlobalIndex()
|
|
227
|
+
assert.equal(entryOf(index, (e) => e.title === '没有落点的合并'), null, 'no new entry was written')
|
|
228
|
+
assert.deepEqual(Object.keys(index.entries), [TARGET], 'the index gained nothing')
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('an unknown op is skipped instead of being forced into a create', async () => {
|
|
232
|
+
seedGlobalIndex({})
|
|
233
|
+
setMode('auto')
|
|
234
|
+
const result = await curator.commitCuratorOutput({
|
|
235
|
+
memories: [
|
|
236
|
+
{ op: 'frobnicate', domain: 'general', track: 'memory', title: '未知操作', content: '模型写错了 op 名。', confidence: 0.9, evidence: [{ session_id: 's7', quote: '未知' }] },
|
|
237
|
+
// link 在 当前版本已进 OPS_ALLOWLIST(F1 落地),不再是 unknown op 的代表;
|
|
238
|
+
// 这里改用真正不在名单里的 op,测试意图不变:未知 op 不得默认 create。
|
|
239
|
+
{ op: 'delete', domain: 'general', track: 'memory', title: '未接线的关联', content: '模型写错了另一个 op 名。', confidence: 0.9, evidence: [{ session_id: 's8', quote: '删除操作的原话记录' }] },
|
|
240
|
+
],
|
|
241
|
+
}, { job: { step: 'committing' } })
|
|
242
|
+
assert.equal(result.created, 0, 'unknown ops must not default to create')
|
|
243
|
+
assert.equal(result.skipped.length, 2)
|
|
244
|
+
assert.ok(result.skipped.every((item) => /unknown op/.test(item.reason)))
|
|
245
|
+
assert.deepEqual(Object.keys(state.loadGlobalIndex().entries), [], 'nothing was written')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test('setSettings rejects a bogus curatorReviewMode and accepts review-all', async () => {
|
|
249
|
+
await assert.rejects(
|
|
250
|
+
() => rpc.dispatch({}, 'setSettings', { patch: { curatorReviewMode: 'bogus' } }),
|
|
251
|
+
/invalid setting/,
|
|
252
|
+
'the enum gate must reject unknown modes',
|
|
253
|
+
)
|
|
254
|
+
const accepted = await rpc.dispatch({}, 'setSettings', { patch: { curatorReviewMode: 'review-all' } })
|
|
255
|
+
assert.equal(accepted.curatorReviewMode, 'review-all', 'the allowlisted mode persists')
|
|
256
|
+
setMode('auto')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
test('entries written before this version carry no reviewRisk and still read cleanly', async () => {
|
|
260
|
+
seedGlobalIndex({ [TARGET]: TARGET_ENTRY })
|
|
261
|
+
setMode('auto')
|
|
262
|
+
const result = await curator.commitCuratorOutput({
|
|
263
|
+
memories: [{
|
|
264
|
+
op: 'supersede', domain: 'general', track: 'memory', target_id: TARGET,
|
|
265
|
+
title: '部署窗口的替代版本', content: '部署窗口调整为周四凌晨的整段灰度时段。',
|
|
266
|
+
confidence: 0.92,
|
|
267
|
+
evidence: [{ session_id: 's9', quote: '窗口改成周四凌晨' }, { session_id: 's10', quote: '整段灰度时段一次性放开' }],
|
|
268
|
+
}],
|
|
269
|
+
}, { job: { step: 'committing' } })
|
|
270
|
+
assert.equal(result.superseded, 1)
|
|
271
|
+
const index = state.loadGlobalIndex()
|
|
272
|
+
const oldEntry = index.entries[TARGET]
|
|
273
|
+
assert.ok(oldEntry, 'the old entry is still there (superseded, never deleted)')
|
|
274
|
+
assert.equal(Object.hasOwn(oldEntry, 'reviewRisk'), false, 'the old entry keeps its old shape')
|
|
275
|
+
assert.equal(Object.hasOwn(oldEntry, 'reviewRiskReasons'), false)
|
|
276
|
+
assert.equal(oldEntry.status, 'superseded')
|
|
277
|
+
// Reading the mixed index through the default-value convention must not throw.
|
|
278
|
+
for (const entry of Object.values(index.entries)) {
|
|
279
|
+
assert.equal(typeof (entry.reviewRisk ?? 0), 'number')
|
|
280
|
+
assert.ok(Array.isArray(entry.reviewRiskReasons ?? []))
|
|
281
|
+
}
|
|
282
|
+
const fresh = entryOf(index, (e) => e.status === 'active')
|
|
283
|
+
assert.ok(fresh, 'the replacement landed active (risk 0.20 < 0.70)')
|
|
284
|
+
assert.equal(fresh.reviewRisk, 0.2, 'supersede adds 0.20; two evidence items avoid the single-evidence bump')
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
test('reviewDecision unit: absorb risk and skip reasons follow §4.10 verbatim', () => {
|
|
288
|
+
const index = { entries: { 'gmem-a': { status: 'active' } } }
|
|
289
|
+
// absorb with >3 sources: +0.20; clean otherwise → auto passes at 0.20.
|
|
290
|
+
const absorb = curator.reviewDecision({
|
|
291
|
+
m: { confidence: 0.9, source_ids: ['a', 'b', 'c', 'd'] },
|
|
292
|
+
op: 'absorb', evidence: [{ session_id: 's', quote: 'q' }, { session_id: 's2', quote: 'q2' }],
|
|
293
|
+
targetId: 'gmem-a', index, mode: 'auto',
|
|
294
|
+
})
|
|
295
|
+
assert.equal(absorb.review, false)
|
|
296
|
+
assert.equal(absorb.risk, 0.2)
|
|
297
|
+
assert.ok(absorb.reasons.includes('absorb>3 sources'))
|
|
298
|
+
// absorb with a missing target is a skip, never a pending.
|
|
299
|
+
const noTarget = curator.reviewDecision({
|
|
300
|
+
m: { confidence: 0.9 }, op: 'absorb',
|
|
301
|
+
evidence: [{ session_id: 's', quote: 'q' }],
|
|
302
|
+
targetId: '', index, mode: 'auto',
|
|
303
|
+
})
|
|
304
|
+
assert.equal(noTarget.skip, true)
|
|
305
|
+
assert.match(noTarget.reason, /target not found: \(empty\)/)
|
|
306
|
+
// Hard gates cap at risk 2 regardless of mode.
|
|
307
|
+
const conflictReviewAll = curator.reviewDecision({
|
|
308
|
+
m: {}, op: 'conflict', evidence: [{ session_id: 's', quote: 'q' }],
|
|
309
|
+
targetId: 'gmem-a', index, mode: 'review-all',
|
|
310
|
+
})
|
|
311
|
+
assert.equal(conflictReviewAll.review, true)
|
|
312
|
+
assert.equal(conflictReviewAll.risk, 2)
|
|
313
|
+
assert.equal(conflictReviewAll.hard, true)
|
|
314
|
+
})
|
|
@@ -90,7 +90,8 @@ test('a secret stays out of the anchor but is still reachable through recall', a
|
|
|
90
90
|
assert.equal(anchor.text.includes('PINNED-SECRET-BODY'), false, 'pinning does not override secrecy')
|
|
91
91
|
assert.equal(anchor.text.includes(secretId), false)
|
|
92
92
|
assert.equal(anchor.text.includes(pinnedSecretId), false)
|
|
93
|
-
assert.ok(anchor.text.includes('
|
|
93
|
+
assert.ok(anchor.text.includes('deploy window'), 'ordinary memories still show up as title cards')
|
|
94
|
+
assert.equal(anchor.text.includes('SHARED-BODY'), false, 'the anchor never carries the body')
|
|
94
95
|
|
|
95
96
|
const recalled = await memory.privateRecall({ exec, query: 'leave company diagnosis deploy window', topK: 5, relevanceThreshold: 0 })
|
|
96
97
|
assert.ok(recalled.files.some((file) => file.name === secretId), 'a secret is a recall target, not a hidden row')
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{"id":"sf-001","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-001a","track":"memory","domain":"general","title":"部署窗口约定","content":"每周四凌晨两点到四点是固定的部署窗口,窗口内只允许灰度发布。","appliesTo":["deploy"]},"b":{"id":"e-sf-001b","track":"memory","domain":"general","title":"部署窗口约定","content":"每周四凌晨两点到四点是固定部署窗口,窗口里只允许灰度发布操作。","appliesTo":["deploy"]}}
|
|
2
|
+
{"id":"sf-002","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-002a","track":"memory","domain":"general","title":"汇报风格","content":"用户在正式汇报里习惯先给结论再给细节。","appliesTo":["work"]},"b":{"id":"e-sf-002b","track":"memory","domain":"general","title":"汇报风格","content":"用户在正式汇报时习惯先给结论再讲细节。","appliesTo":["work"]}}
|
|
3
|
+
{"id":"sf-003","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-003a","track":"memory","domain":"coding","title":"代码评审范围","content":"提交评审前必须跑完整测试,未通过的改动不进入评审。","appliesTo":["coding"]},"b":{"id":"e-sf-003b","track":"memory","domain":"coding","title":"代码评审范围","content":"提交评审前必须跑完整测试,没通过的改动不进评审。","appliesTo":["coding"]}}
|
|
4
|
+
{"id":"sf-004","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-004a","track":"memory","domain":"general","title":"周刊习惯","content":"用户每周五下午写项目周报,习惯列三件事。","appliesTo":[]},"b":{"id":"e-sf-004b","track":"memory","domain":"general","title":"周刊习惯","content":"用户每周五下午写项目周报,习惯列出三件事。","appliesTo":[]}}
|
|
5
|
+
{"id":"sf-005","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-005a","track":"memory","domain":"coding","title":"TypeScript strict","content":"User prefers TypeScript strict mode in every project.","appliesTo":["coding"]},"b":{"id":"e-sf-005b","track":"memory","domain":"coding","title":"TypeScript strict","content":"User prefers strict TypeScript mode in all projects.","appliesTo":["coding"]}}
|
|
6
|
+
{"id":"sf-006","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-006a","track":"memory","domain":"coding","title":"PR review flow","content":"Every PR needs one approval before merging to main.","appliesTo":["coding"]},"b":{"id":"e-sf-006b","track":"memory","domain":"coding","title":"PR review flow","content":"Each PR requires one approval before merging to main.","appliesTo":["coding"]}}
|
|
7
|
+
{"id":"sf-007","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-007a","track":"memory","domain":"general","title":"Standup notes","content":"Standup notes are posted to the team channel every morning.","appliesTo":[]},"b":{"id":"e-sf-007b","track":"memory","domain":"general","title":"Standup notes","content":"Standup notes are posted to the team channel each morning.","appliesTo":[]}}
|
|
8
|
+
{"id":"sf-008","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-008a","track":"memory","domain":"coding","title":"Branch naming","content":"Feature branches are named after the ticket id.","appliesTo":["coding"]},"b":{"id":"e-sf-008b","track":"memory","domain":"coding","title":"Branch naming","content":"Feature branches are always named after the ticket id.","appliesTo":["coding"]}}
|
|
9
|
+
{"id":"sf-009","label":"same-fact","lang":"mixed","tags":["paraphrase"],"a":{"id":"e-sf-009a","track":"memory","domain":"coding","title":"Commit message","content":"Commit message 用英文,第一行不超过 50 字。","appliesTo":["coding"]},"b":{"id":"e-sf-009b","track":"memory","domain":"coding","title":"Commit message","content":"Commit message 写英文,首行不超过 50 字。","appliesTo":["coding"]}}
|
|
10
|
+
{"id":"sf-010","label":"same-fact","lang":"mixed","tags":["paraphrase"],"a":{"id":"e-sf-010a","track":"memory","domain":"general","title":"Debug 习惯","content":"遇到 bug 先写最小复现,再查文档。","appliesTo":[]},"b":{"id":"e-sf-010b","track":"memory","domain":"general","title":"Debug 习惯","content":"遇到 bug 先写最小复现用例,再查文档。","appliesTo":[]}}
|
|
11
|
+
{"id":"sf-011","label":"same-fact","lang":"mixed","tags":["paraphrase"],"a":{"id":"e-sf-011a","track":"memory","domain":"general","title":"部署确认","content":"发版后 user 会手动过一遍核心流程。","appliesTo":["deploy"]},"b":{"id":"e-sf-011b","track":"memory","domain":"general","title":"部署确认","content":"发版后 user 手动过一遍核心流程。","appliesTo":["deploy"]}}
|
|
12
|
+
{"id":"sf-012","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-012a","track":"memory","domain":"general","title":"笔记工具","content":"用户用 Obsidian 管理笔记,按项目分库。","appliesTo":[]},"b":{"id":"e-sf-012b","track":"memory","domain":"general","title":"笔记工具","content":"用户用 Obsidian 管笔记,按项目分库。","appliesTo":[]}}
|
|
13
|
+
{"id":"sf-013","label":"same-fact","lang":"zh","tags":["length-gap","containment"],"a":{"id":"e-sf-013a","track":"memory","domain":"coding","title":"缓存过期","content":"Redis 缓存统一设置 30 秒过期。","appliesTo":["backend"]},"b":{"id":"e-sf-013b","track":"memory","domain":"coding","title":"缓存过期","content":"Redis 缓存统一设置 30 秒过期。过期后直接回源查询,回源结果写回缓存;缓存击穿用互斥锁保护。","appliesTo":["backend"]}}
|
|
14
|
+
{"id":"sf-014","label":"same-fact","lang":"en","tags":["length-gap","containment"],"a":{"id":"e-sf-014a","track":"memory","domain":"coding","title":"Deploy freeze","content":"No deploys on Friday afternoons.","appliesTo":["deploy"]},"b":{"id":"e-sf-014b","track":"memory","domain":"coding","title":"Deploy freeze","content":"No deploys on Friday afternoons. Friday afternoon deploys are frozen; hotfixes need on-call sign-off.","appliesTo":["deploy"]}}
|
|
15
|
+
{"id":"sf-015","label":"same-fact","lang":"mixed","tags":["length-gap","containment"],"a":{"id":"e-sf-015a","track":"memory","domain":"coding","title":"Lint 规则","content":"提交前必须先过 lint。","appliesTo":["coding"]},"b":{"id":"e-sf-015b","track":"memory","domain":"coding","title":"Lint 规则","content":"提交前必须先过 lint;lint 不过不允许提交,格式问题交给 format 自动修复。","appliesTo":["coding"]}}
|
|
16
|
+
{"id":"sf-016","label":"same-fact","lang":"zh","tags":["negation-same"],"a":{"id":"e-sf-016a","track":"memory","domain":"general","title":"会议点名","content":"用户不喜欢在会上被突然点名发言。","appliesTo":["work"]},"b":{"id":"e-sf-016b","track":"memory","domain":"general","title":"会议点名","content":"用户不喜欢在会上被突然点名回答。","appliesTo":["work"]}}
|
|
17
|
+
{"id":"sf-017","label":"same-fact","lang":"en","tags":["negation-same"],"a":{"id":"e-sf-017a","track":"memory","domain":"general","title":"No surprises","content":"User does not like surprise scope changes mid-sprint.","appliesTo":["work"]},"b":{"id":"e-sf-017b","track":"memory","domain":"general","title":"No surprises","content":"User does not like unexpected scope changes mid-sprint.","appliesTo":["work"]}}
|
|
18
|
+
{"id":"sf-018","label":"same-fact","lang":"zh","tags":["dates-same"],"a":{"id":"e-sf-018a","track":"memory","domain":"general","title":"发版节奏","content":"每周三 18:00 准时发版,P0 故障才顺延。","appliesTo":["deploy"]},"b":{"id":"e-sf-018b","track":"memory","domain":"general","title":"发版节奏","content":"每周三 18:00 准时发版,遇到 P0 故障才顺延。","appliesTo":["deploy"]}}
|
|
19
|
+
{"id":"sf-019","label":"same-fact","lang":"zh","tags":["dates-same"],"a":{"id":"e-sf-019a","track":"memory","domain":"general","title":"周会时间","content":"项目周会固定在周二 10:30,时长 45 分钟。","appliesTo":["work"]},"b":{"id":"e-sf-019b","track":"memory","domain":"general","title":"周会时间","content":"周二 10:30 的项目周会固定开,时长 45 分钟。","appliesTo":["work"]}}
|
|
20
|
+
{"id":"sf-020","label":"same-fact","lang":"en","tags":["dates-same"],"a":{"id":"e-sf-020a","track":"memory","domain":"general","title":"Release day","content":"Releases ship every Wednesday at 18:00 UTC.","appliesTo":["deploy"]},"b":{"id":"e-sf-020b","track":"memory","domain":"general","title":"Release day","content":"Releases ship each Wednesday at 18:00 UTC.","appliesTo":["deploy"]}}
|
|
21
|
+
{"id":"sf-021","label":"same-fact","lang":"zh","tags":["alias"],"a":{"id":"e-sf-021a","track":"memory","domain":"general","title":"记忆插件","content":"用户说的记忆插件指 dsh-memu-memory 这个插件。","appliesTo":[]},"b":{"id":"e-sf-021b","track":"memory","domain":"general","title":"记忆插件","content":"用户说的记忆插件就是 dsh-memu-memory 这个插件。","appliesTo":[]}}
|
|
22
|
+
{"id":"sf-022","label":"same-fact","lang":"en","tags":["alias"],"a":{"id":"e-sf-022a","track":"memory","domain":"general","title":"Memory plugin","content":"The user calls dsh-memu-memory the memory plugin.","appliesTo":[]},"b":{"id":"e-sf-022b","track":"memory","domain":"general","title":"Memory plugin","content":"The user always calls dsh-memu-memory the memory plugin.","appliesTo":[]}}
|
|
23
|
+
{"id":"sf-023","label":"same-fact","lang":"mixed","tags":["alias"],"a":{"id":"e-sf-023a","track":"memory","domain":"coding","title":"评测脚本","content":"用户说的 eval 脚本就是 scripts/replay.mjs。","appliesTo":["coding"]},"b":{"id":"e-sf-023b","track":"memory","domain":"coding","title":"评测脚本","content":"用户说的 eval 脚本指的是 scripts/replay.mjs。","appliesTo":["coding"]}}
|
|
24
|
+
{"id":"sf-024","label":"same-fact","lang":"zh","tags":["paraphrase","skill"],"a":{"id":"e-sf-024a","track":"skill","domain":"tooling","title":"发布清单","content":"发版前按清单逐项确认:版本号、changelog、回归标记。","appliesTo":["deploy"]},"b":{"id":"e-sf-024b","track":"skill","domain":"tooling","title":"发布清单","content":"发版前按清单逐项确认版本号、changelog 和回归标记。","appliesTo":["deploy"]}}
|
|
25
|
+
{"id":"sf-025","label":"same-fact","lang":"en","tags":["paraphrase","skill"],"a":{"id":"e-sf-025a","track":"skill","domain":"coding","title":"Code style","content":"Keep functions under 40 lines and extract helpers early.","appliesTo":["coding"]},"b":{"id":"e-sf-025b","track":"skill","domain":"coding","title":"Code style","content":"Keep functions below 40 lines and extract helpers early.","appliesTo":["coding"]}}
|
|
26
|
+
{"id":"sf-026","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-026a","track":"memory","domain":"personal","title":"作息","content":"用户习惯 23:30 前睡觉,早上 7 点起。","appliesTo":[]},"b":{"id":"e-sf-026b","track":"memory","domain":"personal","title":"作息","content":"用户习惯 23:30 前睡觉,早上 7 点起床。","appliesTo":[]}}
|
|
27
|
+
{"id":"sf-027","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-027a","track":"memory","domain":"personal","title":"Coffee rule","content":"No coffee after 15:00, decaf only.","appliesTo":[]},"b":{"id":"e-sf-027b","track":"memory","domain":"personal","title":"Coffee rule","content":"No coffee after 15:00, only decaf.","appliesTo":[]}}
|
|
28
|
+
{"id":"sf-028","label":"same-fact","lang":"zh","tags":["punctuation"],"a":{"id":"e-sf-028a","track":"memory","domain":"general","title":"周报模板","content":"周报固定三段:进展、风险、需要的支持。","appliesTo":["work"]},"b":{"id":"e-sf-028b","track":"memory","domain":"general","title":"周报模板","content":"周报固定三段:进展、风险、需要的支持.","appliesTo":["work"]}}
|
|
29
|
+
{"id":"sf-029","label":"same-fact","lang":"en","tags":["paraphrase"],"a":{"id":"e-sf-029a","track":"memory","domain":"general","title":"Focus hours","content":"Mornings are deep-work time; meetings go after lunch.","appliesTo":["work"]},"b":{"id":"e-sf-029b","track":"memory","domain":"general","title":"Focus hours","content":"Mornings are deep work time; meetings go after lunch.","appliesTo":["work"]}}
|
|
30
|
+
{"id":"sf-030","label":"same-fact","lang":"zh","tags":["paraphrase"],"a":{"id":"e-sf-030a","track":"memory","domain":"general","title":"回复节奏","content":"用户偏好异步沟通,消息不必秒回。","appliesTo":["work"]},"b":{"id":"e-sf-030b","track":"memory","domain":"general","title":"回复节奏","content":"用户偏好异步沟通,消息不用秒回。","appliesTo":["work"]}}
|
|
31
|
+
{"id":"rl-001","label":"related","lang":"zh","tags":["time-shift"],"a":{"id":"e-rl-001a","track":"memory","domain":"general","title":"站会安排","content":"每周一上午十点开站会,时长半小时。","appliesTo":["work"]},"b":{"id":"e-rl-001b","track":"memory","domain":"general","title":"站会安排","content":"每周一上午十点开站会,时长缩短到二十分钟。","appliesTo":["work"]}}
|
|
32
|
+
{"id":"rl-002","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-002a","track":"memory","domain":"coding","title":"部署窗口","content":"部署窗口是每周四凌晨两点到四点,只做灰度发布。","appliesTo":["deploy"]},"b":{"id":"e-rl-002b","track":"memory","domain":"coding","title":"部署窗口","content":"部署窗口是每周四凌晨两点到四点,全量发布要等监控确认。","appliesTo":["deploy"]}}
|
|
33
|
+
{"id":"rl-003","label":"related","lang":"zh","tags":["dates-differ"],"a":{"id":"e-rl-003a","track":"memory","domain":"general","title":"站会时间","content":"站会固定在每周一 10:00 开始,先过昨日进展,再排今日任务。","appliesTo":["work"]},"b":{"id":"e-rl-003b","track":"memory","domain":"general","title":"站会时间","content":"站会固定在每周一 11:00 开始,先过昨日进展,再排今日任务。","appliesTo":["work"]}}
|
|
34
|
+
{"id":"rl-004","label":"related","lang":"en","tags":["same-prefix"],"a":{"id":"e-rl-004a","track":"memory","domain":"coding","title":"Release checklist","content":"Release checklist starts with the changelog entry.","appliesTo":["deploy"]},"b":{"id":"e-rl-004b","track":"memory","domain":"coding","title":"Release checklist","content":"Release checklist ends with the regression sweep.","appliesTo":["deploy"]}}
|
|
35
|
+
{"id":"rl-005","label":"related","lang":"en","tags":["same-topic"],"a":{"id":"e-rl-005a","track":"memory","domain":"general","title":"Editor setup","content":"User edits frontend code in VS Code with vim bindings.","appliesTo":["coding"]},"b":{"id":"e-rl-005b","track":"memory","domain":"general","title":"Editor setup","content":"User edits backend code in VS Code with emacs bindings on Linux.","appliesTo":["coding"]}}
|
|
36
|
+
{"id":"rl-006","label":"related","lang":"mixed","tags":["same-prefix"],"a":{"id":"e-rl-006a","track":"memory","domain":"coding","title":"测试策略","content":"核心模块的测试策略是先写单测再写集成测试。","appliesTo":["coding"]},"b":{"id":"e-rl-006b","track":"memory","domain":"coding","title":"测试策略","content":"边缘模块的测试策略是先写集成测试再补单测。","appliesTo":["coding"]}}
|
|
37
|
+
{"id":"rl-007","label":"related","lang":"zh","tags":["same-topic"],"a":{"id":"e-rl-007a","track":"memory","domain":"general","title":"笔记结构","content":"用户按项目给笔记分库,库内再按主题分区。","appliesTo":[]},"b":{"id":"e-rl-007b","track":"memory","domain":"general","title":"笔记结构","content":"用户按项目给笔记分库,库内按时间归档。","appliesTo":[]}}
|
|
38
|
+
{"id":"rl-008","label":"related","lang":"en","tags":["same-topic"],"a":{"id":"e-rl-008a","track":"memory","domain":"general","title":"Meeting slots","content":"User books deep work before 11:00 every day.","appliesTo":["work"]},"b":{"id":"e-rl-008b","track":"memory","domain":"general","title":"Meeting slots","content":"User books meetings after 14:00 every day.","appliesTo":["work"]}}
|
|
39
|
+
{"id":"rl-009","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-009a","track":"memory","domain":"coding","title":"代码评审","content":"代码评审重点是接口设计与错误处理是否到位。","appliesTo":["coding"]},"b":{"id":"e-rl-009b","track":"memory","domain":"coding","title":"评审重点","content":"评审重点是测试覆盖与错误处理是否到位。","appliesTo":["coding"]}}
|
|
40
|
+
{"id":"rl-010","label":"related","lang":"en","tags":["same-prefix"],"a":{"id":"e-rl-010a","track":"memory","domain":"coding","title":"Error handling","content":"API errors return a code plus a human message.","appliesTo":["coding"]},"b":{"id":"e-rl-010b","track":"memory","domain":"coding","title":"Error handling","content":"API errors always carry a code plus a human-readable message.","appliesTo":["coding"]}}
|
|
41
|
+
{"id":"rl-011","label":"related","lang":"mixed","tags":["same-topic"],"a":{"id":"e-rl-011a","track":"memory","domain":"general","title":"文档习惯","content":"user 写文档喜欢先列 FAQ 再展开细节。","appliesTo":[]},"b":{"id":"e-rl-011b","track":"memory","domain":"general","title":"文档习惯","content":"user 写文档喜欢先画架构图再展开细节。","appliesTo":[]}}
|
|
42
|
+
{"id":"rl-012","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-012a","track":"memory","domain":"personal","title":"健身安排","content":"用户每周二、周四晚上去健身房练力量。","appliesTo":[]},"b":{"id":"e-rl-012b","track":"memory","domain":"personal","title":"健身安排","content":"用户每周二、周四晚上去健身房上团课,周日休息。","appliesTo":[]}}
|
|
43
|
+
{"id":"rl-013","label":"related","lang":"en","tags":["same-topic"],"a":{"id":"e-rl-013a","track":"memory","domain":"personal","title":"Reading list","content":"User reads one technical book per quarter.","appliesTo":[]},"b":{"id":"e-rl-013b","track":"memory","domain":"personal","title":"Reading list","content":"User reads two fiction books per month.","appliesTo":[]}}
|
|
44
|
+
{"id":"rl-014","label":"related","lang":"zh","tags":["dates-differ"],"a":{"id":"e-rl-014a","track":"memory","domain":"general","title":"复盘会","content":"项目复盘会定在每月最后一个周五下午。","appliesTo":["work"]},"b":{"id":"e-rl-014b","track":"memory","domain":"general","title":"复盘会","content":"项目复盘会定在每季度最后一个周五上午十点。","appliesTo":["work"]}}
|
|
45
|
+
{"id":"rl-015","label":"related","lang":"en","tags":["dates-differ"],"a":{"id":"e-rl-015a","track":"memory","domain":"general","title":"1:1 cadence","content":"The 1:1 with the lead happens every Tuesday.","appliesTo":["work"]},"b":{"id":"e-rl-015b","track":"memory","domain":"general","title":"1:1 cadence","content":"The 1:1 with the lead happens on the first Tuesday of the month.","appliesTo":["work"]}}
|
|
46
|
+
{"id":"rl-016","label":"related","lang":"zh","tags":["negation-differ"],"a":{"id":"e-rl-016a","track":"memory","domain":"general","title":"香菜偏好","content":"用户吃香菜,面里可以正常加。","appliesTo":[]},"b":{"id":"e-rl-016b","track":"memory","domain":"general","title":"香菜偏好","content":"用户不吃香菜,面里绝对不要放。","appliesTo":[]}}
|
|
47
|
+
{"id":"rl-017","label":"related","lang":"en","tags":["negation-differ"],"a":{"id":"e-rl-017a","track":"memory","domain":"general","title":"Alerts","content":"User wants alerts for every failed deploy.","appliesTo":["deploy"]},"b":{"id":"e-rl-017b","track":"memory","domain":"general","title":"Alerts","content":"User wants alerts for every failed deploy, but no alerts for nightly job noise.","appliesTo":["deploy"]}}
|
|
48
|
+
{"id":"rl-018","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-018a","track":"memory","domain":"coding","title":"依赖升级","content":"依赖升级每月做一次,先升补丁版本。","appliesTo":["coding"]},"b":{"id":"e-rl-018b","track":"memory","domain":"coding","title":"依赖升级","content":"依赖升级每月做一次,大版本要单独开分支。","appliesTo":["coding"]}}
|
|
49
|
+
{"id":"rl-019","label":"related","lang":"mixed","tags":["same-prefix"],"a":{"id":"e-rl-019a","track":"memory","domain":"coding","title":"日志级别","content":"生产环境日志级别默认 info,trace 要手动开。","appliesTo":["coding"]},"b":{"id":"e-rl-019b","track":"memory","domain":"coding","title":"日志级别","content":"生产环境日志级别默认 info,trace 要手动开,error 单独落盘并保留 7 天。","appliesTo":["coding"]}}
|
|
50
|
+
{"id":"rl-020","label":"related","lang":"en","tags":["same-prefix"],"a":{"id":"e-rl-020a","track":"memory","domain":"general","title":"Backup plan","content":"Backups run nightly and keep 14 days.","appliesTo":["deploy"]},"b":{"id":"e-rl-020b","track":"memory","domain":"general","title":"Backup plan","content":"Backups run nightly and keep a full quarter before rotation.","appliesTo":["deploy"]}}
|
|
51
|
+
{"id":"rl-021","label":"related","lang":"zh","tags":["alias-related"],"a":{"id":"e-rl-021a","track":"memory","domain":"general","title":"存储引擎","content":"项目里的存储引擎指 SQLite 本地库。","appliesTo":["coding"]},"b":{"id":"e-rl-021b","track":"memory","domain":"general","title":"向量引擎","content":"项目里的存储引擎指 MemU 向量库。","appliesTo":["coding"]}}
|
|
52
|
+
{"id":"rl-022","label":"related","lang":"en","tags":["alias-related"],"a":{"id":"e-rl-022a","track":"memory","domain":"coding","title":"The runner","content":"The runner means the nightly replay script.","appliesTo":["coding"]},"b":{"id":"e-rl-022b","track":"memory","domain":"coding","title":"The runner","content":"The runner means the smoke workflow, not the replay script.","appliesTo":["coding"]}}
|
|
53
|
+
{"id":"rl-023","label":"related","lang":"mixed","tags":["same-topic"],"a":{"id":"e-rl-023a","track":"memory","domain":"general","title":"沟通渠道","content":"工作沟通 user 用邮件,私人用微信。","appliesTo":[]},"b":{"id":"e-rl-023b","track":"memory","domain":"general","title":"沟通渠道","content":"工作沟通 user 用钉钉,私人用 telegram。","appliesTo":[]}}
|
|
54
|
+
{"id":"rl-024","label":"related","lang":"zh","tags":["same-topic"],"a":{"id":"e-rl-024a","track":"memory","domain":"general","title":"学习方式","content":"用户学新框架先跑官方示例再读源码。","appliesTo":[]},"b":{"id":"e-rl-024b","track":"memory","domain":"general","title":"学习方式","content":"用户学新框架先跑官方示例,再读文档和源码注释。","appliesTo":[]}}
|
|
55
|
+
{"id":"rl-025","label":"related","lang":"en","tags":["same-topic"],"a":{"id":"e-rl-025a","track":"memory","domain":"coding","title":"Perf budget","content":"Pages must load in under two seconds on 3G.","appliesTo":["coding"]},"b":{"id":"e-rl-025b","track":"memory","domain":"coding","title":"Perf budget","content":"APIs must respond in under 200ms at p95.","appliesTo":["coding"]}}
|
|
56
|
+
{"id":"rl-026","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-026a","track":"skill","domain":"tooling","title":"排障步骤","content":"排障第一步是看活动日志,再看引擎日志。","appliesTo":["coding"]},"b":{"id":"e-rl-026b","track":"skill","domain":"tooling","title":"排障步骤","content":"排障第一步是看活动日志,第二步才复现问题。","appliesTo":["coding"]}}
|
|
57
|
+
{"id":"rl-027","label":"related","lang":"zh","tags":["same-topic"],"a":{"id":"e-rl-027a","track":"memory","domain":"general","title":"咖啡偏好","content":"用户下午的咖啡只喝美式,上午喝拿铁。","appliesTo":[]},"b":{"id":"e-rl-027b","track":"memory","domain":"general","title":"咖啡偏好","content":"用户下午的咖啡只喝美式,但加班时喝双份。","appliesTo":[]}}
|
|
58
|
+
{"id":"rl-028","label":"related","lang":"en","tags":["same-topic"],"a":{"id":"e-rl-028a","track":"memory","domain":"general","title":"Keyboard","content":"User carries a 65 percent mechanical keyboard.","appliesTo":[]},"b":{"id":"e-rl-028b","track":"memory","domain":"general","title":"Keyboard","content":"User carries a 65 percent keyboard; the ergo split stays home.","appliesTo":[]}}
|
|
59
|
+
{"id":"rl-029","label":"related","lang":"mixed","tags":["same-prefix"],"a":{"id":"e-rl-029a","track":"memory","domain":"coding","title":"Code review SLA","content":"PR 要在 24 小时内给出首个 review。","appliesTo":["coding"]},"b":{"id":"e-rl-029b","track":"memory","domain":"coding","title":"Code review SLA","content":"PR 要在 48 小时内给出最终结论。","appliesTo":["coding"]}}
|
|
60
|
+
{"id":"rl-030","label":"related","lang":"zh","tags":["same-prefix"],"a":{"id":"e-rl-030a","track":"memory","domain":"general","title":"值班规则","content":"线上值班期间手机必须保持畅通。","appliesTo":["deploy"]},"b":{"id":"e-rl-030b","track":"memory","domain":"general","title":"值班规则","content":"线上值班期间手机必须保持畅通,两小时巡检一次大盘,交接要写记录。","appliesTo":["deploy"]}}
|
|
61
|
+
{"id":"nn-001","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-001a","track":"memory","domain":"general","title":"部署窗口约定","content":"每周四凌晨两点到四点是固定的部署窗口,窗口内只允许灰度发布。","appliesTo":["deploy"]},"b":{"id":"e-nn-001b","track":"memory","domain":"personal","title":"摄影器材","content":"用户的相机是全画幅微单,常用 35mm 定焦。","appliesTo":[]}}
|
|
62
|
+
{"id":"nn-002","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-002a","track":"memory","domain":"general","title":"汇报风格","content":"用户在正式汇报里习惯先给结论再给细节。","appliesTo":["work"]},"b":{"id":"e-nn-002b","track":"memory","domain":"personal","title":"宠物","content":"用户养了一只橘猫,名叫年糕。","appliesTo":[]}}
|
|
63
|
+
{"id":"nn-003","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-003a","track":"memory","domain":"coding","title":"TypeScript strict","content":"User prefers TypeScript strict mode in every project.","appliesTo":["coding"]},"b":{"id":"e-nn-003b","track":"memory","domain":"general","title":"Hiking","content":"User hikes every other weekend when the weather holds.","appliesTo":[]}}
|
|
64
|
+
{"id":"nn-004","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-004a","track":"memory","domain":"coding","title":"PR review flow","content":"Every PR needs one approval before merging to main.","appliesTo":["coding"]},"b":{"id":"e-nn-004b","track":"memory","domain":"personal","title":"Guitar","content":"User practices fingerstyle guitar for twenty minutes daily.","appliesTo":[]}}
|
|
65
|
+
{"id":"nn-005","label":"none","lang":"zh","tags":["different-domain"],"a":{"id":"e-nn-005a","track":"memory","domain":"rp","title":"叙事视角","content":"RP 跑团里用户偏好第二人称叙事,节奏偏慢。","appliesTo":[]},"b":{"id":"e-nn-005b","track":"memory","domain":"coding","title":"构建工具","content":"项目构建统一用 pnpm,monorepo 走 workspace。","appliesTo":["coding"]}}
|
|
66
|
+
{"id":"nn-006","label":"none","lang":"mixed","tags":["different-topic"],"a":{"id":"e-nn-006a","track":"memory","domain":"general","title":"Commit message","content":"Commit message 用英文,第一行不超过 50 字。","appliesTo":["coding"]},"b":{"id":"e-nn-006b","track":"memory","domain":"personal","title":"旅行偏好","content":"user 旅行时喜欢住民宿,不跟团。","appliesTo":[]}}
|
|
67
|
+
{"id":"nn-007","label":"none","lang":"zh","tags":["cross-track"],"a":{"id":"e-nn-007a","track":"skill","domain":"tooling","title":"发布清单","content":"发版前按清单逐项确认:版本号、changelog、回归标记。","appliesTo":["deploy"]},"b":{"id":"e-nn-007b","track":"memory","domain":"personal","title":"饮食禁忌","content":"用户对花生过敏,零食要避开。","appliesTo":[]}}
|
|
68
|
+
{"id":"nn-008","label":"none","lang":"en","tags":["cross-track"],"a":{"id":"e-nn-008a","track":"skill","domain":"coding","title":"Code style","content":"Keep functions under 40 lines and extract helpers early.","appliesTo":["coding"]},"b":{"id":"e-nn-008b","track":"memory","domain":"general","title":"Podcast habit","content":"User listens to one long podcast episode per week.","appliesTo":[]}}
|
|
69
|
+
{"id":"nn-009","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-009a","track":"memory","domain":"general","title":"周刊习惯","content":"用户每周五下午写项目周报,习惯列三件事。","appliesTo":["work"]},"b":{"id":"e-nn-009b","track":"memory","domain":"general","title":"方言背景","content":"用户是杭州人,会说一点杭州话。","appliesTo":[]}}
|
|
70
|
+
{"id":"nn-010","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-010a","track":"memory","domain":"general","title":"笔记工具","content":"用户用 Obsidian 管理笔记,按项目分库。","appliesTo":[]},"b":{"id":"e-nn-010b","track":"memory","domain":"general","title":"运动损伤","content":"用户右膝有旧伤,深蹲要减重。","appliesTo":[]}}
|
|
71
|
+
{"id":"nn-011","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-011a","track":"memory","domain":"general","title":"Standup notes","content":"Standup notes are posted to the team channel every morning.","appliesTo":["work"]},"b":{"id":"e-nn-011b","track":"memory","domain":"general","title":"Chess hobby","content":"User plays correspondence chess at a slow pace.","appliesTo":[]}}
|
|
72
|
+
{"id":"nn-012","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-012a","track":"memory","domain":"coding","title":"Branch naming","content":"Feature branches are named after the ticket id.","appliesTo":["coding"]},"b":{"id":"e-nn-012b","track":"memory","domain":"personal","title":"Skiing level","content":"User skis blue runs confidently, blacks with a guide.","appliesTo":[]}}
|
|
73
|
+
{"id":"nn-013","label":"none","lang":"mixed","tags":["different-topic"],"a":{"id":"e-nn-013a","track":"memory","domain":"coding","title":"Debug 习惯","content":"遇到 bug 先写最小复现,再查文档。","appliesTo":["coding"]},"b":{"id":"e-nn-013b","track":"memory","domain":"personal","title":"晨跑路线","content":"user 晨跑固定沿河边往返五公里。","appliesTo":[]}}
|
|
74
|
+
{"id":"nn-014","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-014a","track":"memory","domain":"general","title":"回复节奏","content":"用户偏好异步沟通,消息不必秒回。","appliesTo":["work"]},"b":{"id":"e-nn-014b","track":"memory","domain":"general","title":"视力情况","content":"用户近视六百度,戴框架眼镜。","appliesTo":[]}}
|
|
75
|
+
{"id":"nn-015","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-015a","track":"memory","domain":"general","title":"Focus hours","content":"Mornings are deep-work time; meetings go after lunch.","appliesTo":["work"]},"b":{"id":"e-nn-015b","track":"memory","domain":"personal","title":"Baking","content":"User bakes sourdough on Sunday mornings.","appliesTo":[]}}
|
|
76
|
+
{"id":"nn-016","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-016a","track":"memory","domain":"personal","title":"作息","content":"用户习惯 23:30 前睡觉,早上 7 点起床。","appliesTo":[]},"b":{"id":"e-nn-016b","track":"memory","domain":"coding","title":"测试框架","content":"仓库统一用 node:test 跑测试,不引第三方框架。","appliesTo":["coding"]}}
|
|
77
|
+
{"id":"nn-017","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-017a","track":"memory","domain":"personal","title":"Coffee rule","content":"No coffee after 15:00, only decaf.","appliesTo":[]},"b":{"id":"e-nn-017b","track":"memory","domain":"coding","title":"CI policy","content":"CI must stay green before any merge queue entry.","appliesTo":["coding"]}}
|
|
78
|
+
{"id":"nn-018","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-018a","track":"memory","domain":"general","title":"周报模板","content":"周报固定三段:进展、风险、需要的支持。","appliesTo":["work"]},"b":{"id":"e-nn-018b","track":"memory","domain":"general","title":"旧机回收","content":"用户的旧手机折价回收给了平台。","appliesTo":[]}}
|
|
79
|
+
{"id":"nn-019","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-019a","track":"memory","domain":"general","title":"Release day","content":"Releases ship every Wednesday at 18:00 UTC.","appliesTo":["deploy"]},"b":{"id":"e-nn-019b","track":"memory","domain":"personal","title":"Vinyl collection","content":"User collects jazz vinyl records from the 60s.","appliesTo":[]}}
|
|
80
|
+
{"id":"nn-020","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-020a","track":"memory","domain":"coding","title":"缓存过期","content":"Redis 缓存统一设置 30 秒过期。","appliesTo":["backend"]},"b":{"id":"e-nn-020b","track":"memory","domain":"personal","title":"园艺","content":"用户在阳台种了薄荷和迷迭香。","appliesTo":[]}}
|
|
81
|
+
{"id":"nn-021","label":"none","lang":"mixed","tags":["different-topic"],"a":{"id":"e-nn-021a","track":"memory","domain":"coding","title":"Lint 规则","content":"提交前必须先过 lint。","appliesTo":["coding"]},"b":{"id":"e-nn-021b","track":"memory","domain":"personal","title":"Board games","content":"user 每月约朋友玩一次桌游,偏策略类。","appliesTo":[]}}
|
|
82
|
+
{"id":"nn-022","label":"none","lang":"zh","tags":["different-domain"],"a":{"id":"e-nn-022a","track":"memory","domain":"rp","title":"跑团日程","content":"RP 跑团固定周六晚上开团,三小时一场。","appliesTo":[]},"b":{"id":"e-nn-022b","track":"memory","domain":"coding","title":"代码归属","content":"实验性代码统一放 experiments 分支,不合主干。","appliesTo":["coding"]}}
|
|
83
|
+
{"id":"nn-023","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-023a","track":"memory","domain":"general","title":"记忆插件","content":"用户说的记忆插件指 dsh-memu-memory 这个插件。","appliesTo":[]},"b":{"id":"e-nn-023b","track":"memory","domain":"general","title":"手机套餐","content":"用户的手机套餐是八元保号套餐。","appliesTo":[]}}
|
|
84
|
+
{"id":"nn-024","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-024a","track":"memory","domain":"general","title":"Memory plugin","content":"The user always calls dsh-memu-memory the memory plugin.","appliesTo":[]},"b":{"id":"e-nn-024b","track":"memory","domain":"personal","title":"Running shoes","content":"User replaces running shoes every 800 kilometers.","appliesTo":[]}}
|
|
85
|
+
{"id":"nn-025","label":"none","lang":"mixed","tags":["different-topic"],"a":{"id":"e-nn-025a","track":"memory","domain":"coding","title":"评测脚本","content":"用户说的 eval 脚本就是 scripts/replay.mjs。","appliesTo":["coding"]},"b":{"id":"e-nn-025b","track":"memory","domain":"personal","title":"雨具习惯","content":"user 的包里常年放着一把折叠伞。","appliesTo":[]}}
|
|
86
|
+
{"id":"nn-026","label":"none","lang":"zh","tags":["length-gap-none"],"a":{"id":"e-nn-026a","track":"memory","domain":"general","title":"借书证","content":"用户的图书馆借书证到期时间是明年 3 月。","appliesTo":[]},"b":{"id":"e-nn-026b","track":"memory","domain":"coding","title":"接入文档","content":"新服务接入前要先读接入文档,确认鉴权方式与限流阈值,再在沙箱环境跑通示例,最后才允许申请生产密钥。","appliesTo":["coding"]}}
|
|
87
|
+
{"id":"nn-027","label":"none","lang":"en","tags":["length-gap-none"],"a":{"id":"e-nn-027a","track":"memory","domain":"personal","title":"Gym card","content":"The gym membership card expires in March.","appliesTo":[]},"b":{"id":"e-nn-027b","track":"memory","domain":"coding","title":"Onboarding","content":"New services onboard through the sandbox first: read the integration doc, verify the auth flow, run the sample, then request production keys.","appliesTo":["coding"]}}
|
|
88
|
+
{"id":"nn-028","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-028a","track":"memory","domain":"general","title":"站会安排","content":"每周一上午十点开站会,时长半小时。","appliesTo":["work"]},"b":{"id":"e-nn-028b","track":"memory","domain":"general","title":"电池更换","content":"用户的鼠标电池三个月换一次。","appliesTo":[]}}
|
|
89
|
+
{"id":"nn-029","label":"none","lang":"en","tags":["different-topic"],"a":{"id":"e-nn-029a","track":"memory","domain":"general","title":"Editor setup","content":"User edits frontend code in VS Code with vim bindings.","appliesTo":["coding"]},"b":{"id":"e-nn-029b","track":"memory","domain":"personal","title":"Tea stash","content":"User keeps a small stash of oolong from Fujian.","appliesTo":[]}}
|
|
90
|
+
{"id":"nn-030","label":"none","lang":"zh","tags":["different-topic"],"a":{"id":"e-nn-030a","track":"memory","domain":"general","title":"值班规则","content":"线上值班期间手机必须保持畅通。","appliesTo":["deploy"]},"b":{"id":"e-nn-030b","track":"memory","domain":"personal","title":"旧照扫描","content":"用户把家里的旧照片扫成了电子版。","appliesTo":[]}}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// 集成回归时移除:lib/lexicon.js 真身(旧版,另一代理并行实现)落地之前,
|
|
2
|
+
// 把核心层模块解析到同目录的轻量 stub。判定保持保守——真身存在时一律放行原解析。
|
|
3
|
+
// 与 tests/loader.mjs 的 resolve 映射同一机制(node:module register hooks)。
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
|
|
6
|
+
const stub = new URL('./lexicon-stub.mjs', import.meta.url)
|
|
7
|
+
const real = new URL('../../lib/lexicon.js', import.meta.url)
|
|
8
|
+
|
|
9
|
+
export function resolve(specifier, context, next) {
|
|
10
|
+
if (specifier === './lexicon.js' && String(context.parentURL || '').includes('/lib/')) {
|
|
11
|
+
if (fs.existsSync(real)) return next(specifier, context)
|
|
12
|
+
return { url: stub.href, shortCircuit: true, format: 'module' }
|
|
13
|
+
}
|
|
14
|
+
return next(specifier, context)
|
|
15
|
+
}
|