@modusensus/dsh-mneme 0.8.3 → 0.8.5
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/LICENSE +21 -21
- package/README.md +727 -636
- package/bin/cli.mjs +0 -0
- package/bin/dsh-mneme-mcp.mjs +510 -0
- package/cordis.patch.yml +21 -21
- package/lib/api-standalone.js +506 -316
- package/lib/api.js +1403 -1316
- package/lib/bootstrap.js +211 -0
- package/lib/client.js +4657 -4366
- package/lib/commands.js +95 -95
- package/lib/config.js +615 -510
- package/lib/document.js +221 -0
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +658 -658
- package/lib/dream/narratives.js +44 -0
- package/lib/dream/sleep.js +702 -700
- package/lib/dream.js +147 -16
- package/lib/embedding.js +177 -177
- package/lib/entities/extractor.js +249 -249
- package/lib/guide.js +44 -0
- package/lib/heat.js +144 -136
- package/lib/hot-memory.js +46 -46
- package/lib/index.js +515 -515
- package/lib/inject.js +375 -295
- package/lib/lang.js +208 -179
- package/lib/local-embedder.js +320 -320
- package/lib/mirror.js +198 -198
- package/lib/organize.js +450 -0
- package/lib/quality-filter.js +126 -126
- package/lib/recall-stats.js +125 -0
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -235
- package/lib/runtime/loader.js +252 -249
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +204 -191
- package/lib/scope.js +118 -118
- package/lib/search/adaptive.js +60 -22
- package/lib/search/bm25.js +96 -96
- package/lib/service.js +2324 -2087
- package/lib/settings.js +459 -420
- package/lib/store.js +2549 -2367
- package/lib/summarize.js +820 -449
- package/lib/tools.js +702 -574
- package/lib/vector-index.js +110 -110
- package/lib/version-check.js +122 -122
- package/package.json +109 -108
- package/runtime-manifest.json +526 -596
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +199 -199
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -217
- package/scripts/check-sync.js +42 -42
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +67 -61
- package/src/api-standalone.js +506 -316
- package/src/api.js +1403 -1316
- package/src/bootstrap.js +211 -0
- package/src/commands.js +95 -95
- package/src/config.js +615 -510
- package/src/document.js +221 -0
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +658 -658
- package/src/dream/narratives.js +44 -0
- package/src/dream/sleep.js +702 -700
- package/src/dream.js +147 -16
- package/src/embedding.js +177 -177
- package/src/entities/extractor.js +249 -249
- package/src/guide.js +44 -0
- package/src/heat.js +144 -136
- package/src/hot-memory.js +46 -46
- package/src/index.js +515 -515
- package/src/inject.js +375 -295
- package/src/lang.js +208 -179
- package/src/local-embedder.js +320 -320
- package/src/mirror.js +198 -198
- package/src/organize.js +450 -0
- package/src/quality-filter.js +126 -126
- package/src/recall-stats.js +125 -0
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -235
- package/src/runtime/loader.js +252 -249
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +204 -191
- package/src/scope.js +118 -118
- package/src/search/adaptive.js +60 -22
- package/src/search/bm25.js +96 -96
- package/src/service.js +2324 -2087
- package/src/settings.js +459 -420
- package/src/store.js +2549 -2367
- package/src/summarize.js +820 -449
- package/src/tools.js +702 -574
- package/src/vector-index.js +110 -110
- package/src/version-check.js +122 -122
- package/test/adaptive-inject-budget.test.js +85 -0
- package/test/api-routes-gaps.test.js +218 -218
- package/test/api.test.js +1640 -1482
- package/test/audit.test.js +448 -448
- package/test/benchmark.test.js +94 -94
- package/test/bootstrap.test.js +220 -0
- package/test/client.test.js +860 -736
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config.test.js +66 -66
- package/test/conflict-freeze.test.js +300 -300
- package/test/document.test.js +523 -0
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream.test.js +1308 -1254
- package/test/embedding-ready.test.js +196 -196
- package/test/entities.test.js +547 -547
- package/test/entity-recall.test.js +179 -0
- package/test/epistemic.test.js +298 -298
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-api.test.js +175 -175
- package/test/heat.test.js +175 -148
- package/test/helpers/dream-mock.js +82 -82
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/hot-memory.test.js +145 -145
- package/test/inject-content-cap.test.js +91 -0
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +61 -0
- package/test/inject-pools.test.js +170 -0
- package/test/inject-recall.test.js +105 -0
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +283 -249
- package/test/lang.test.js +87 -87
- package/test/lib-smoke.test.js +96 -96
- package/test/llm-audit.test.js +462 -309
- package/test/llm-message-source.test.js +177 -177
- package/test/local-embedder.test.js +249 -249
- package/test/mcp-stdio.test.js +385 -0
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -0
- package/test/organize.test.js +418 -0
- package/test/peer-blockers.test.js +193 -193
- package/test/policy-epoch.test.js +259 -259
- package/test/quality-filter.test.js +186 -186
- package/test/reasoning-effort.test.js +746 -746
- package/test/recall-evals.test.js +235 -235
- package/test/recall-layer.test.js +326 -326
- package/test/recall-stats.test.js +123 -0
- package/test/receipt-chain.test.js +451 -451
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +62 -62
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -0
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -237
- package/test/runtime-manifest.test.js +159 -158
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +279 -214
- package/test/scope-candidates.test.js +239 -239
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +231 -231
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +405 -405
- package/test/settings.test.js +261 -261
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +422 -422
- package/test/sleep-heat.test.js +126 -125
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +410 -373
- package/test/standalone-api.test.js +544 -359
- package/test/store-vector-cache.test.js +98 -98
- package/test/store.test.js +396 -383
- package/test/stress.test.js +209 -209
- package/test/summarize.test.js +1288 -575
- package/test/tools-dto-schema-matrix.test.js +325 -0
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +374 -341
- package/test/updated-at-semantics.test.js +113 -113
- package/test/vector-index.test.js +221 -221
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
// #230:document 型记忆——注册校验 / 摘要+doc_path 落库 / C2 去重 / supersede
|
|
2
|
+
// 记账 / 注入档位与预算 / 写入权分离守卫。口径基线 = #164 设计稿评审线
|
|
3
|
+
// (discussioncomment-18495098 + 维护者对账 18502150)。
|
|
4
|
+
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { createStore } from "../src/store.js";
|
|
11
|
+
import { createService } from "../src/service.js";
|
|
12
|
+
import { createDocumentRegistrar } from "../src/document.js";
|
|
13
|
+
import { MEMORY_ITEM_SCHEMA } from "../src/tools.js";
|
|
14
|
+
import { TYPE_DECAY_DEFAULTS } from "../src/heat.js";
|
|
15
|
+
|
|
16
|
+
function makeService(config = {}) {
|
|
17
|
+
const store = createStore(":memory:");
|
|
18
|
+
const service = createService({ store, mirror: null, config });
|
|
19
|
+
return { store, service, close: () => store.close() };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function makeDocDir() {
|
|
23
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "dsh-mneme-doc-"));
|
|
24
|
+
const writeDoc = async (name, content = "# Doc\n\nbody text") => {
|
|
25
|
+
const p = path.join(dir, name);
|
|
26
|
+
await writeFile(p, content, "utf8");
|
|
27
|
+
return p;
|
|
28
|
+
};
|
|
29
|
+
return { dir, writeDoc, cleanup: () => rm(dir, { recursive: true, force: true }) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 单元级 registrar:注入假 embedQuery,隔离 vector 档行为。 */
|
|
33
|
+
function makeRegistrar(store, config = {}, embedQuery = async () => null) {
|
|
34
|
+
const finalized = [];
|
|
35
|
+
const register = createDocumentRegistrar({
|
|
36
|
+
store,
|
|
37
|
+
config,
|
|
38
|
+
embedQuery,
|
|
39
|
+
// 与 service.js 的 pushContentHistory 同形(FIFO cap 20)
|
|
40
|
+
pushContentHistory: (existing, source) => [
|
|
41
|
+
{ content: existing?.content ?? "", source, updated_at: new Date().toISOString() },
|
|
42
|
+
...(Array.isArray(existing?.content_history) ? existing.content_history : [])
|
|
43
|
+
].slice(0, 20),
|
|
44
|
+
transaction: (fn) => fn(),
|
|
45
|
+
finalize: (rows) => finalized.push(...rows)
|
|
46
|
+
});
|
|
47
|
+
return { register, finalized };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================ 注册校验(DoD 1)
|
|
51
|
+
|
|
52
|
+
test("registration is refused while the feature flag is off", async () => {
|
|
53
|
+
const { service, close } = makeService({});
|
|
54
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
55
|
+
try {
|
|
56
|
+
const p = await writeDoc("a.md");
|
|
57
|
+
await assert.rejects(
|
|
58
|
+
() => service.registerDocument({ path: p, title: "T", summary: "S" }),
|
|
59
|
+
/documentMemoryEnabled/
|
|
60
|
+
);
|
|
61
|
+
} finally {
|
|
62
|
+
cleanup();
|
|
63
|
+
close();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("path validation: required / absolute-only / existing non-empty regular file", async () => {
|
|
68
|
+
const { service, close } = makeService({ documentMemoryEnabled: true });
|
|
69
|
+
const { dir, writeDoc, cleanup } = await makeDocDir();
|
|
70
|
+
try {
|
|
71
|
+
await assert.rejects(
|
|
72
|
+
() => service.registerDocument({ title: "T", summary: "S" }),
|
|
73
|
+
/path is required/
|
|
74
|
+
);
|
|
75
|
+
await assert.rejects(
|
|
76
|
+
() => service.registerDocument({ path: "rel.md", title: "T", summary: "S" }),
|
|
77
|
+
/must be absolute/
|
|
78
|
+
);
|
|
79
|
+
await assert.rejects(
|
|
80
|
+
() => service.registerDocument({ path: path.join(dir, "missing.md"), title: "T", summary: "S" }),
|
|
81
|
+
/not found or empty/
|
|
82
|
+
);
|
|
83
|
+
await assert.rejects(
|
|
84
|
+
() => service.registerDocument({ path: dir, title: "T", summary: "S" }),
|
|
85
|
+
/not found or empty/
|
|
86
|
+
);
|
|
87
|
+
const empty = await writeDoc("empty.md", "");
|
|
88
|
+
await assert.rejects(
|
|
89
|
+
() => service.registerDocument({ path: empty, title: "T", summary: "S" }),
|
|
90
|
+
/not found or empty/
|
|
91
|
+
);
|
|
92
|
+
} finally {
|
|
93
|
+
cleanup();
|
|
94
|
+
close();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("~ paths expand to the real home and register", async () => {
|
|
99
|
+
const { service, close } = makeService({ documentMemoryEnabled: true });
|
|
100
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
101
|
+
try {
|
|
102
|
+
const p = await writeDoc("tilde.md");
|
|
103
|
+
const rel = path.relative(os.homedir(), p);
|
|
104
|
+
const res = await service.registerDocument({
|
|
105
|
+
path: `~/${rel.split(path.sep).join("/")}`,
|
|
106
|
+
title: "TildeDoc",
|
|
107
|
+
summary: "S"
|
|
108
|
+
});
|
|
109
|
+
assert.equal(res.action, "created");
|
|
110
|
+
assert.equal(res.memory.doc_path, p);
|
|
111
|
+
} finally {
|
|
112
|
+
cleanup();
|
|
113
|
+
close();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("evidence intersection: valid subset kept, unknown dropped + degraded tag, all-fabricated rejected", async () => {
|
|
118
|
+
const { service, close } = makeService({ documentMemoryEnabled: true });
|
|
119
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
120
|
+
try {
|
|
121
|
+
const a = service.saveWithDedupe({ type: "preference", title: "P1", content: "c1" }).memory;
|
|
122
|
+
const b = service.saveWithDedupe({ type: "decision", title: "D1", content: "c2" }).memory;
|
|
123
|
+
const p = await writeDoc("r.md");
|
|
124
|
+
|
|
125
|
+
const partial = await service.registerDocument({
|
|
126
|
+
path: p, title: "R", summary: "S", evidence: [a.id, "nope-1", b.id]
|
|
127
|
+
});
|
|
128
|
+
assert.equal(partial.action, "created");
|
|
129
|
+
assert.deepEqual(partial.memory.evidence, [a.id, b.id]);
|
|
130
|
+
assert.equal(partial.evidence_kept, 2);
|
|
131
|
+
assert.equal(partial.evidence_dropped, 1);
|
|
132
|
+
assert.equal(partial.degraded, true);
|
|
133
|
+
assert.ok(partial.memory.tags.includes("evidence_degraded"));
|
|
134
|
+
|
|
135
|
+
await assert.rejects(
|
|
136
|
+
() => service.registerDocument({ path: p, title: "T2", summary: "S", evidence: ["nope-2"] }),
|
|
137
|
+
/fabricated evidence is rejected/
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const clean = await service.registerDocument({ path: await writeDoc("clean.md"), title: "T3", summary: "S" });
|
|
141
|
+
assert.equal(clean.degraded, false);
|
|
142
|
+
assert.deepEqual(clean.memory.evidence, []);
|
|
143
|
+
assert.ok(!clean.memory.tags.includes("evidence_degraded"));
|
|
144
|
+
} finally {
|
|
145
|
+
cleanup();
|
|
146
|
+
close();
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ============================================================ supersede 记账(DoD 2)
|
|
151
|
+
|
|
152
|
+
test("re-registering the same path supersedes: old row archived + traceable, file untouched", async () => {
|
|
153
|
+
const { service, close } = makeService({ documentMemoryEnabled: true });
|
|
154
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
155
|
+
try {
|
|
156
|
+
const p = await writeDoc("v.md");
|
|
157
|
+
const v1 = await service.registerDocument({ path: p, title: "T1", summary: "summary v1", tags: ["doc"] });
|
|
158
|
+
const v2 = await service.registerDocument({ path: p, title: "T2", summary: "summary v2" });
|
|
159
|
+
|
|
160
|
+
assert.equal(v2.action, "superseded");
|
|
161
|
+
assert.equal(v2.superseded.id, v1.memory.id);
|
|
162
|
+
assert.equal(v2.memory.doc_path, p);
|
|
163
|
+
|
|
164
|
+
const old = service.getById(v1.memory.id);
|
|
165
|
+
assert.equal(old.archived, true, "old row archived (no physical delete)");
|
|
166
|
+
assert.ok(old.content.includes(`[superseded by ${v2.memory.id}]`), "loser carries the pointer note");
|
|
167
|
+
assert.equal(old.content_history[0].source, "superseded");
|
|
168
|
+
assert.equal(old.content_history[0].content, "summary v1", "old summary traceable via content_history");
|
|
169
|
+
assert.equal(old.doc_path, p, "old pointer preserved");
|
|
170
|
+
|
|
171
|
+
const fresh = service.getById(v2.memory.id);
|
|
172
|
+
assert.equal(fresh.archived, false);
|
|
173
|
+
assert.equal(fresh.content, "summary v2");
|
|
174
|
+
} finally {
|
|
175
|
+
cleanup();
|
|
176
|
+
close();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("same title on a different path also supersedes (new version of the moved doc)", async () => {
|
|
181
|
+
const { service, close } = makeService({ documentMemoryEnabled: true });
|
|
182
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
183
|
+
try {
|
|
184
|
+
const p1 = await writeDoc("old.md");
|
|
185
|
+
const p2 = await writeDoc("new.md");
|
|
186
|
+
const v1 = await service.registerDocument({ path: p1, title: "Same", summary: "s1" });
|
|
187
|
+
const v2 = await service.registerDocument({ path: p2, title: "Same", summary: "s2" });
|
|
188
|
+
assert.equal(v2.action, "superseded");
|
|
189
|
+
assert.equal(v2.superseded.id, v1.memory.id);
|
|
190
|
+
assert.equal(service.getById(v1.memory.id).archived, true);
|
|
191
|
+
assert.equal(service.getById(v2.memory.id).doc_path, p2);
|
|
192
|
+
} finally {
|
|
193
|
+
cleanup();
|
|
194
|
+
close();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ============================================================ C2 去重(DoD 6 前半)
|
|
199
|
+
|
|
200
|
+
test("vector near-duplicate on a different path/title is rejected (agent decides, no silent supersede)", async () => {
|
|
201
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
202
|
+
const store = createStore(":memory:");
|
|
203
|
+
try {
|
|
204
|
+
store.saveDocument({
|
|
205
|
+
type: "document", title: "Alpha", content: "existing summary",
|
|
206
|
+
doc_path: "X:\\elsewhere\\a.md", embedding: [1, 0, 0], source: "tool"
|
|
207
|
+
});
|
|
208
|
+
const b = await writeDoc("b.md");
|
|
209
|
+
const c = await writeDoc("c.md");
|
|
210
|
+
const { register } = makeRegistrar(store, { documentMemoryEnabled: true }, async () => [1, 0, 0]);
|
|
211
|
+
await assert.rejects(
|
|
212
|
+
() => register({ path: b, title: "Beta", summary: "similar summary" }),
|
|
213
|
+
/near duplicate/
|
|
214
|
+
);
|
|
215
|
+
// 正交向量 = 无近重复 → 正常创建。
|
|
216
|
+
const { register: register2 } = makeRegistrar(store, { documentMemoryEnabled: true }, async () => [0, 1, 0]);
|
|
217
|
+
const res = await register2({ path: c, title: "Gamma", summary: "unrelated summary" });
|
|
218
|
+
assert.equal(res.action, "created");
|
|
219
|
+
} finally {
|
|
220
|
+
cleanup();
|
|
221
|
+
store.close();
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("embedder unavailable: dedupe degrades to path/title tiers, writes still land", async () => {
|
|
226
|
+
const { service, close } = makeService({ documentMemoryEnabled: true });
|
|
227
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
228
|
+
try {
|
|
229
|
+
const p1 = await writeDoc("d1.md");
|
|
230
|
+
const p2 = await writeDoc("d2.md");
|
|
231
|
+
const r1 = await service.registerDocument({ path: p1, title: "A", summary: "identical summary text" });
|
|
232
|
+
const r2 = await service.registerDocument({ path: p2, title: "B", summary: "identical summary text" });
|
|
233
|
+
assert.equal(r1.action, "created");
|
|
234
|
+
assert.equal(r2.action, "created", "no vector signal = no near-dup verdict, write is never blocked");
|
|
235
|
+
} finally {
|
|
236
|
+
cleanup();
|
|
237
|
+
close();
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ============================================================ 写入权分离守卫(DoD 1/3 旁路防护)
|
|
242
|
+
|
|
243
|
+
test("saveWithDedupe refuses to mint document rows", () => {
|
|
244
|
+
const { service, close } = makeService({});
|
|
245
|
+
try {
|
|
246
|
+
assert.throws(
|
|
247
|
+
() => service.saveWithDedupe({ type: "document", title: "x", content: "y" }),
|
|
248
|
+
/registerDocument/
|
|
249
|
+
);
|
|
250
|
+
} finally {
|
|
251
|
+
close();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("updateMemory cannot retype a row into document, but document summary repair passes", async () => {
|
|
256
|
+
const { service, close } = makeService({ documentMemoryEnabled: true });
|
|
257
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
258
|
+
try {
|
|
259
|
+
const pref = service.saveWithDedupe({ type: "preference", title: "P", content: "c" }).memory;
|
|
260
|
+
assert.throws(() => service.update(pref.id, { type: "document" }), /registerDocument/);
|
|
261
|
+
|
|
262
|
+
const p = await writeDoc("fix.md");
|
|
263
|
+
const atomic = service.saveWithDedupe({ type: "decision", title: "D", content: "c" }).memory;
|
|
264
|
+
const doc = (await service.registerDocument({
|
|
265
|
+
path: p, title: "Doc", summary: "s", evidence: [atomic.id, "nope"]
|
|
266
|
+
})).memory;
|
|
267
|
+
const updated = service.update(doc.id, { content: "repaired summary", tags: ["user-tag"] });
|
|
268
|
+
assert.equal(updated.content, "repaired summary");
|
|
269
|
+
assert.equal(updated.doc_path, p, "pointer untouched by summary repair");
|
|
270
|
+
assert.equal(updated.archived, false);
|
|
271
|
+
assert.ok(
|
|
272
|
+
updated.tags.includes("evidence_degraded") && updated.tags.includes("user-tag"),
|
|
273
|
+
"system signal tag survives a tags-replacing update"
|
|
274
|
+
);
|
|
275
|
+
} finally {
|
|
276
|
+
cleanup();
|
|
277
|
+
close();
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ============================================================ 注入档位与预算(DoD 5)
|
|
282
|
+
|
|
283
|
+
function seedInjectionFixtures(service, store) {
|
|
284
|
+
service.saveWithDedupe({ type: "summary", title: "当前项目状态", content: "resident digest", source: "dream", importance: 5, _overwrite: true });
|
|
285
|
+
service.saveWithDedupe({ type: "summary", title: "叙述:topic", content: "narrative bar", source: "narrative", tags: ["topic-n"], importance: 3 });
|
|
286
|
+
// document 行铸造口唯一:测试种子也走 store.saveDocument(saveWithDedupe
|
|
287
|
+
// 有 service 守卫、store.save 有存储层守卫)。
|
|
288
|
+
for (let i = 1; i <= 3; i++) {
|
|
289
|
+
store.saveDocument({ type: "document", title: `Doc${i}`, content: `d${i}`, source: "tool", doc_path: `X:\\${i}.md`, importance: 3 });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
test("injection: document rows at the next-priority tier, capped by documentInjectBudget; narrative tier re-enabled", () => {
|
|
294
|
+
const { store, service, close } = makeService({ documentMemoryEnabled: true, documentInjectBudget: 2, dreamNarrativeEnabled: true });
|
|
295
|
+
try {
|
|
296
|
+
seedInjectionFixtures(service, store);
|
|
297
|
+
const picked = service.injectCandidates({});
|
|
298
|
+
assert.equal(picked[0].type, "summary");
|
|
299
|
+
assert.equal(picked[0].source, "dream", "resident digest keeps tier 0");
|
|
300
|
+
assert.equal(picked.filter((m) => m.type === "document").length, 2, "document budget caps at 2 of 3");
|
|
301
|
+
assert.ok(picked.some((m) => m.source === "narrative"), "narrative rows inject at the next-priority tier (#230 拍板)");
|
|
302
|
+
} finally {
|
|
303
|
+
close();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("injection: flags off → document and narrative rows stay out, behavior unchanged", () => {
|
|
308
|
+
const { store, service, close } = makeService({});
|
|
309
|
+
try {
|
|
310
|
+
seedInjectionFixtures(service, store);
|
|
311
|
+
const picked = service.injectCandidates({});
|
|
312
|
+
assert.ok(picked.every((m) => m.type !== "document"), "document rows excluded while flag off");
|
|
313
|
+
assert.ok(picked.every((m) => m.source !== "narrative"), "narrative rows excluded while dreamNarrativeEnabled off");
|
|
314
|
+
assert.ok(picked.some((m) => m.type === "summary" && m.source === "dream"), "dream digest unaffected");
|
|
315
|
+
} finally {
|
|
316
|
+
close();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ============================================================ DTO / 扩展面(DoD 3)
|
|
321
|
+
|
|
322
|
+
test("toApiList exposes doc_path on pointer rows only, within MEMORY_ITEM_SCHEMA", async () => {
|
|
323
|
+
const { service, close } = makeService({ documentMemoryEnabled: true });
|
|
324
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
325
|
+
try {
|
|
326
|
+
const pref = service.saveWithDedupe({ type: "preference", title: "P", content: "c" }).memory;
|
|
327
|
+
const p = await writeDoc("dto.md");
|
|
328
|
+
const doc = (await service.registerDocument({ path: p, title: "D", summary: "s" })).memory;
|
|
329
|
+
|
|
330
|
+
const prefDto = service.toApiList([pref])[0];
|
|
331
|
+
assert.ok(!("doc_path" in prefDto), "non-pointer rows keep the pre-#230 DTO shape");
|
|
332
|
+
const docDto = service.toApiList([doc])[0];
|
|
333
|
+
assert.equal(docDto.doc_path, p);
|
|
334
|
+
for (const key of Object.keys(docDto)) {
|
|
335
|
+
assert.ok(key in MEMORY_ITEM_SCHEMA.properties, `DTO key "${key}" declared in MEMORY_ITEM_SCHEMA`);
|
|
336
|
+
}
|
|
337
|
+
} finally {
|
|
338
|
+
cleanup();
|
|
339
|
+
close();
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("document rows are heat-immune (pointer rows do not decay)", () => {
|
|
344
|
+
assert.equal(TYPE_DECAY_DEFAULTS.document, 0);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// ============================================================ CodeRabbit 修复回归(PR #256 review)
|
|
348
|
+
|
|
349
|
+
test("path normalization: equivalent absolute spellings supersede instead of duplicating", async () => {
|
|
350
|
+
const { service, close } = makeService({ documentMemoryEnabled: true });
|
|
351
|
+
const { dir, writeDoc, cleanup } = await makeDocDir();
|
|
352
|
+
try {
|
|
353
|
+
const p = await writeDoc("norm.md");
|
|
354
|
+
const first = await service.registerDocument({ path: p, title: "V1", summary: "s1" });
|
|
355
|
+
// path.join 会把 "." 段吃掉,手工拼出等价但未归一化的写法。
|
|
356
|
+
const dotted = `${dir}${path.sep}.${path.sep}norm.md`;
|
|
357
|
+
const second = await service.registerDocument({ path: dotted, title: "V2", summary: "s2" });
|
|
358
|
+
assert.equal(second.action, "superseded", "normalized spelling hits the same-path supersede tier");
|
|
359
|
+
assert.equal(second.superseded.id, first.memory.id);
|
|
360
|
+
} finally {
|
|
361
|
+
cleanup();
|
|
362
|
+
close();
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("exact supersede tiers scan beyond any window (>200 active documents)", async () => {
|
|
367
|
+
const { store, close } = makeService({ documentMemoryEnabled: true });
|
|
368
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
369
|
+
try {
|
|
370
|
+
// 新注册的路径必须过文件校验——目标行(窗口外的旧版)用真实文件,其余
|
|
371
|
+
// 209 行用虚构路径垫量。Bulk0 先种 = updated_at 最早 = list(DESC) 末位。
|
|
372
|
+
const pathA = await writeDoc("a.md");
|
|
373
|
+
const pathB = await writeDoc("b.md");
|
|
374
|
+
store.saveDocument({ type: "document", title: "Bulk0", content: "c0", source: "tool", doc_path: pathA });
|
|
375
|
+
for (let i = 1; i < 210; i++) {
|
|
376
|
+
store.saveDocument({ type: "document", title: `Bulk${i}`, content: `c${i}`, source: "tool", doc_path: `X:\\bulk\\${i}.md` });
|
|
377
|
+
}
|
|
378
|
+
const oldest = store.list({ type: "document", limit: null }).at(-1);
|
|
379
|
+
assert.equal(oldest.title, "Bulk0", "precondition: oldest row is outside the 200 window");
|
|
380
|
+
const { register } = makeRegistrar(store, { documentMemoryEnabled: true });
|
|
381
|
+
// 路径层:窗口外的同路径行必须仍被发现并 supersede。
|
|
382
|
+
const byPath = await register({ path: pathA, title: "Fresh path re-register", summary: "new version" });
|
|
383
|
+
assert.equal(byPath.action, "superseded");
|
|
384
|
+
assert.equal(byPath.superseded.id, oldest.id);
|
|
385
|
+
// 标题层:同款全量语义(Bulk0 已归档,此时窗口外最老是 Bulk1)。
|
|
386
|
+
const next = store.list({ type: "document", limit: null }).at(-1);
|
|
387
|
+
assert.equal(next.title, "Bulk1");
|
|
388
|
+
const byTitle = await register({ path: pathB, title: next.title, summary: "another version" });
|
|
389
|
+
assert.equal(byTitle.action, "superseded");
|
|
390
|
+
assert.equal(byTitle.superseded.id, next.id);
|
|
391
|
+
} finally {
|
|
392
|
+
cleanup();
|
|
393
|
+
close();
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("sensitivity is persisted and participates in the supersede matching key", async () => {
|
|
398
|
+
const { service, close } = makeService({ documentMemoryEnabled: true });
|
|
399
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
400
|
+
try {
|
|
401
|
+
const p = await writeDoc("sens.md");
|
|
402
|
+
const first = await service.registerDocument({ path: p, title: "S", summary: "s1", sensitivity: "personal" });
|
|
403
|
+
assert.equal(first.memory.sensitivity, "personal", "sensitivity must persist (scopeKeyOf third dim)");
|
|
404
|
+
// 同 path + 同 sensitivity = 显式出新版。
|
|
405
|
+
const again = await service.registerDocument({ path: p, title: "S2", summary: "s2", sensitivity: "personal" });
|
|
406
|
+
assert.equal(again.action, "superseded");
|
|
407
|
+
// sensitivity 不同 = 另一 scope 维度,不互判(与 memory_save 的同键分离语义一致)。
|
|
408
|
+
const other = await service.registerDocument({ path: p, title: "S3", summary: "s3", sensitivity: "team" });
|
|
409
|
+
assert.equal(other.action, "created");
|
|
410
|
+
} finally {
|
|
411
|
+
cleanup();
|
|
412
|
+
close();
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("store guards: generic paths cannot mint or retype document rows", () => {
|
|
417
|
+
const { store, close } = makeService({});
|
|
418
|
+
try {
|
|
419
|
+
// save:整类拒绝——doc_path 是任意调用方可捏造的字符串,「必带 doc_path」
|
|
420
|
+
// 挡不住绕过注册校验的直铸(CodeRabbit 复核 #882)。
|
|
421
|
+
assert.throws(
|
|
422
|
+
() => store.save({ type: "document", title: "X", content: "c" }),
|
|
423
|
+
/minted only via store\.saveDocument/
|
|
424
|
+
);
|
|
425
|
+
assert.throws(
|
|
426
|
+
() => store.save({ type: "document", title: "X", content: "c", doc_path: "X:\\x.md" }),
|
|
427
|
+
/minted only via store\.saveDocument/
|
|
428
|
+
);
|
|
429
|
+
// saveDocument:唯一铸造口,且保留指针行结构不变量。
|
|
430
|
+
assert.throws(
|
|
431
|
+
() => store.saveDocument({ type: "document", title: "X", content: "c" }),
|
|
432
|
+
/doc_path is required/
|
|
433
|
+
);
|
|
434
|
+
const pref = store.save({ type: "preference", title: "P", content: "c" });
|
|
435
|
+
const doc = store.saveDocument({ type: "document", title: "D", content: "s", doc_path: "X:\\d.md" });
|
|
436
|
+
// update / CAS:type 不许改入或改出 document(CAS 绕过 service 守卫,存储层兜底)。
|
|
437
|
+
assert.throws(() => store.update(pref.id, { type: "document" }), /cannot be changed to or from/);
|
|
438
|
+
assert.throws(() => store.update(doc.id, { type: "preference" }), /cannot be changed to or from/);
|
|
439
|
+
assert.throws(
|
|
440
|
+
() => store.compareAndUpdate(pref.id, pref.updated_at, { type: "document" }),
|
|
441
|
+
/cannot be changed to or from/
|
|
442
|
+
);
|
|
443
|
+
// 同 type 的摘要修复照常(doc_path 不在 UPDATE SET,天然保持)。
|
|
444
|
+
const repaired = store.update(doc.id, { content: "repaired summary" });
|
|
445
|
+
assert.equal(repaired.content, "repaired summary");
|
|
446
|
+
assert.equal(repaired.doc_path, "X:\\d.md");
|
|
447
|
+
} finally {
|
|
448
|
+
close();
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("sleep cold-scan and the pattern pool exclude document rows at the query layer", () => {
|
|
453
|
+
const { store, close } = makeService({});
|
|
454
|
+
try {
|
|
455
|
+
const proj = store.save({ type: "project", title: "Proj", content: "c" });
|
|
456
|
+
const hist = store.save({ type: "history", title: "Hist", content: "c" });
|
|
457
|
+
// 最新行是 document——不带排除时它会占满 LIMIT 1 的扫描窗(饿池前置条件)。
|
|
458
|
+
store.saveDocument({ type: "document", title: "Doc", content: "s", source: "tool", doc_path: "X:\\d.md" });
|
|
459
|
+
// 冷扫描(getUnrecalledSince,sleep phase 2 的归档降级候选):document 豁免。
|
|
460
|
+
const cold = store.getUnrecalledSince(0);
|
|
461
|
+
assert.ok(cold.some((m) => m.id === proj.id) && cold.some((m) => m.id === hist.id));
|
|
462
|
+
assert.ok(cold.every((m) => m.type !== "document"), "pointer rows never become archival-demotion candidates");
|
|
463
|
+
// 模式扫描池:excludeTypes 在 LIMIT 之前生效。
|
|
464
|
+
const plain = store.list({ limit: 1 });
|
|
465
|
+
assert.equal(plain[0].type, "document", "precondition: newest row is a document");
|
|
466
|
+
const pool = store.list({ limit: 1, excludeTypes: ["document"] });
|
|
467
|
+
assert.notEqual(pool[0].type, "document", "ordinary memories reach the pool even under a tight limit");
|
|
468
|
+
// 无界扫描(注册器精确层):limit null = 全量。
|
|
469
|
+
assert.equal(store.list({ type: "document", limit: null }).length, 1);
|
|
470
|
+
} finally {
|
|
471
|
+
close();
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("hidden evidence ids (strictScope) are treated as nonexistent by the registrar", async () => {
|
|
476
|
+
const { service, close } = makeService({ documentMemoryEnabled: true });
|
|
477
|
+
const { writeDoc, cleanup } = await makeDocDir();
|
|
478
|
+
try {
|
|
479
|
+
const a = service.saveWithDedupe({ type: "preference", title: "P", content: "c1" }).memory;
|
|
480
|
+
const b = service.saveWithDedupe({ type: "decision", title: "D", content: "c2" }).memory;
|
|
481
|
+
const p = await writeDoc("hidden.md");
|
|
482
|
+
// 全部不可见 = 整单拒绝(与捏造同款;先测,避免先落行后又被同路径命中)。
|
|
483
|
+
await assert.rejects(
|
|
484
|
+
() => service.registerDocument({ path: p, title: "H", summary: "s", evidence: [b.id] }, { hiddenEvidenceIds: [b.id] }),
|
|
485
|
+
/unknown, archived or out of scope/
|
|
486
|
+
);
|
|
487
|
+
// 混合:不可见 id 落 dropped、不进持久化 evidence,kept/dropped 计数保持诚实。
|
|
488
|
+
const mixed = await service.registerDocument(
|
|
489
|
+
{ path: p, title: "H", summary: "s", evidence: [a.id, b.id] },
|
|
490
|
+
{ hiddenEvidenceIds: [b.id] }
|
|
491
|
+
);
|
|
492
|
+
assert.equal(mixed.evidence_kept, 1);
|
|
493
|
+
assert.equal(mixed.evidence_dropped, 1);
|
|
494
|
+
assert.equal(mixed.degraded, true);
|
|
495
|
+
assert.deepEqual(mixed.memory.evidence, [a.id]);
|
|
496
|
+
} finally {
|
|
497
|
+
cleanup();
|
|
498
|
+
close();
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("narrative gate covers the semantic paths: BM25 fallback keeps narratives out while the flag is off", () => {
|
|
503
|
+
const { service, close } = makeService({});
|
|
504
|
+
try {
|
|
505
|
+
service.saveWithDedupe({ type: "summary", title: "叙述:zebra-topic", content: "zebra-topic narrative bar", source: "narrative", importance: 5 });
|
|
506
|
+
// 无 embedder 时首轮必走 BM25 兜底——此前的门只锁规则路,语义路会漏。
|
|
507
|
+
const picked = service.injectCandidates({ q: "zebra-topic" });
|
|
508
|
+
assert.ok(picked.every((m) => m.source !== "narrative"), "semantic-path candidates respect dreamNarrativeEnabled");
|
|
509
|
+
} finally {
|
|
510
|
+
close();
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("narrative rows inject via the semantic path once dreamNarrativeEnabled is on", () => {
|
|
515
|
+
const { service, close } = makeService({ dreamNarrativeEnabled: true });
|
|
516
|
+
try {
|
|
517
|
+
service.saveWithDedupe({ type: "summary", title: "叙述:zebra-topic", content: "zebra-topic narrative bar", source: "narrative", importance: 5 });
|
|
518
|
+
const picked = service.injectCandidates({ q: "zebra-topic" });
|
|
519
|
+
assert.ok(picked.some((m) => m.source === "narrative"), "BM25 path admits narratives under the same flag as the rule path");
|
|
520
|
+
} finally {
|
|
521
|
+
close();
|
|
522
|
+
}
|
|
523
|
+
});
|