@modusensus/dsh-mneme 0.8.14 → 0.8.16
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 +660 -654
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +691 -689
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +275 -0
- package/docs/CONTINUITY.md +124 -0
- package/docs/DAEMON.md +46 -0
- package/docs/DISTILL.md +162 -0
- package/docs/ENTITIES.md +245 -0
- package/docs/LOCAL_MODEL.md +222 -0
- package/docs/SEMANTIC.md +256 -0
- package/docs/SLEEP.md +164 -0
- package/docs/STORAGE.md +98 -0
- package/lib/api-standalone.js +656 -673
- package/lib/api.js +1605 -1433
- package/lib/bootstrap.js +211 -211
- package/lib/client.js +6354 -5296
- package/lib/commands.js +95 -95
- package/lib/config.js +947 -843
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +220 -220
- package/lib/document-index.js +94 -94
- package/lib/document.js +303 -303
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +694 -674
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +819 -819
- package/lib/dream.js +1503 -1501
- package/lib/embedding.js +177 -177
- package/lib/entities/extractor.js +252 -252
- package/lib/graph/anchoring.js +179 -179
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +108 -46
- package/lib/index.js +552 -503
- package/lib/inject.js +428 -450
- package/lib/lang.js +248 -229
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +162 -142
- package/lib/mirror.js +330 -330
- package/lib/organize.js +450 -450
- package/lib/peak-hours.js +117 -117
- package/lib/placement.js +13 -0
- package/lib/pre-inject-gate.js +340 -0
- package/lib/quality-filter.js +126 -126
- package/lib/recall-stats.js +190 -175
- package/lib/reclaim-http.js +150 -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 -265
- package/lib/runtime/loader.js +252 -252
- 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 +223 -204
- package/lib/safe-slice.js +18 -0
- package/lib/scope.js +146 -146
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +229 -229
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +215 -215
- package/lib/service.js +2688 -2605
- package/lib/settings.js +528 -511
- package/lib/store.js +3196 -3007
- package/lib/summarize.js +1039 -856
- package/lib/tools.js +743 -737
- package/lib/vector-index.js +110 -110
- package/lib/version-check.js +122 -122
- package/lib/write-admission.js +402 -402
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +118 -118
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +36 -36
- 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 +69 -69
- package/scripts/test-count-sync.mjs +44 -44
- package/src/api-standalone.js +656 -673
- package/src/api.js +1605 -1433
- package/src/bootstrap.js +211 -211
- package/src/commands.js +95 -95
- package/src/config.js +947 -843
- package/src/content-hash.js +54 -54
- package/src/continuity.js +220 -220
- package/src/document-index.js +94 -94
- package/src/document.js +303 -303
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +694 -674
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +819 -819
- package/src/dream.js +1503 -1501
- package/src/embedding.js +177 -177
- package/src/entities/extractor.js +252 -252
- package/src/graph/anchoring.js +179 -179
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +108 -46
- package/src/index.js +552 -503
- package/src/inject.js +428 -450
- package/src/lang.js +248 -229
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +162 -142
- package/src/mirror.js +330 -330
- package/src/organize.js +450 -450
- package/src/peak-hours.js +117 -117
- package/src/placement.js +13 -0
- package/src/pre-inject-gate.js +340 -0
- package/src/quality-filter.js +126 -126
- package/src/recall-stats.js +190 -175
- package/src/reclaim-http.js +150 -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 -265
- package/src/runtime/loader.js +252 -252
- 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 +223 -204
- package/src/safe-slice.js +18 -0
- package/src/scope.js +146 -146
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +229 -229
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +215 -215
- package/src/service.js +2688 -2605
- package/src/settings.js +528 -511
- package/src/store.js +3196 -3007
- package/src/summarize.js +1039 -856
- package/src/tools.js +743 -737
- package/src/vector-index.js +110 -110
- package/src/version-check.js +122 -122
- package/src/write-admission.js +402 -402
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-routes-gaps.test.js +899 -218
- package/test/api.test.js +1725 -1690
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bootstrap.test.js +220 -220
- package/test/changelog-prep.test.js +60 -60
- package/test/client.test.js +1582 -1169
- 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/content-hash.test.js +204 -204
- package/test/continuity.test.js +333 -333
- package/test/contrast.test.js +314 -0
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +671 -671
- 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-failure-backoff.test.js +173 -173
- package/test/dream-merge-guardrail.test.js +219 -0
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1385 -1383
- package/test/embedding-ready.test.js +196 -196
- package/test/entities.test.js +627 -547
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +175 -175
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- 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/helpers/write-admission-samples.js +308 -308
- package/test/hot-memory.test.js +211 -145
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +106 -106
- package/test/inject-pools.test.js +194 -194
- package/test/inject-recall.test.js +144 -105
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +443 -420
- package/test/lang.test.js +87 -87
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +96 -96
- package/test/llm-audit.test.js +696 -598
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +501 -391
- package/test/mcp-stdio.test.js +385 -385
- 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-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize.test.js +418 -418
- package/test/peer-blockers.test.js +193 -193
- package/test/placement.test.js +36 -0
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +444 -0
- package/test/quality-filter.test.js +186 -186
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-stats.test.js +274 -189
- package/test/receipt-chain.test.js +451 -451
- package/test/reclaim-http.test.js +209 -0
- 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 -83
- 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 -255
- package/test/runtime-manifest.test.js +159 -159
- 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 +319 -279
- package/test/scope-candidates.test.js +239 -239
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +207 -207
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +196 -196
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +410 -405
- package/test/settings.test.js +269 -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 -126
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +527 -527
- package/test/standalone-api.test.js +751 -751
- package/test/store-vector-cache.test.js +98 -98
- package/test/store.test.js +504 -396
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -0
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +1639 -1465
- package/test/supersede-state-tracking.test.js +394 -0
- package/test/test-count-sync.test.js +60 -60
- package/test/tools-dto-schema-matrix.test.js +325 -325
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +428 -374
- 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
- package/test/write-admission.test.js +668 -668
|
@@ -1,162 +1,162 @@
|
|
|
1
|
-
// 叙述条(#164 对齐,PR-4)测试。
|
|
2
|
-
// 覆盖:evidence 列迁移与透传(save/update/parse 回环)/ clusterByTag 纯函数
|
|
3
|
-
// (≥K 门槛、降序、cap)/ intersectEvidence(捏造 id 剔除、交空回落全簇)/
|
|
4
|
-
// dream 叙述阶段端到端(enabled 时第三次 LLM 调用、求交落库、source=narrative)/
|
|
5
|
-
// 默认关零调用 / 低于门槛零调用 / 注入排除(narrative 不进候选,dream 总览保留)。
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import { createStore } from "../src/store.js";
|
|
9
|
-
import { createService } from "../src/service.js";
|
|
10
|
-
import { createDreamScheduler } from "../src/dream.js";
|
|
11
|
-
import { clusterByTag, intersectEvidence } from "../src/dream/narratives.js";
|
|
12
|
-
|
|
13
|
-
function setup(config = {}) {
|
|
14
|
-
const store = createStore(":memory:");
|
|
15
|
-
const service = createService({ store, mirror: null, config });
|
|
16
|
-
return { store, service };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function mockCtx(sequence, callsRef) {
|
|
20
|
-
return {
|
|
21
|
-
llm: {
|
|
22
|
-
stream: async function* () {
|
|
23
|
-
const step = sequence[callsRef.n] ?? { summary: "空转" };
|
|
24
|
-
callsRef.n++;
|
|
25
|
-
const text = step.decisions !== undefined ? JSON.stringify(step.decisions) : step.summary;
|
|
26
|
-
yield { type: "text-delta", text };
|
|
27
|
-
yield { type: "finish", reason: { kind: "ok" } };
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
logger: { warn: () => {} }
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ============================================================ store evidence 回环
|
|
35
|
-
|
|
36
|
-
test("evidence column roundtrips through save and update", () => {
|
|
37
|
-
const { store, service } = setup();
|
|
38
|
-
const ev = [{ memory_id: "m1", op: "support", at: "2026-09-17T00:00:00.000Z" }];
|
|
39
|
-
const { memory } = service.saveWithDedupe({
|
|
40
|
-
type: "summary", title: "叙述:部署", content: "叙述正文", source: "narrative", evidence: ev
|
|
41
|
-
});
|
|
42
|
-
assert.deepEqual(store.getById(memory.id).evidence, ev);
|
|
43
|
-
const ev2 = [{ memory_id: "m2", op: "weaken", at: "2026-09-18T00:00:00.000Z" }];
|
|
44
|
-
store.update(memory.id, { evidence: ev2 });
|
|
45
|
-
assert.deepEqual(store.getById(memory.id).evidence, ev2);
|
|
46
|
-
store.close();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("rows without evidence parse to an empty array", () => {
|
|
50
|
-
const { store, service } = setup();
|
|
51
|
-
const { memory } = service.saveWithDedupe({ type: "project", title: "普通", content: "无证据" });
|
|
52
|
-
assert.deepEqual(store.getById(memory.id).evidence, []);
|
|
53
|
-
store.close();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// ============================================================ 纯函数
|
|
57
|
-
|
|
58
|
-
test("clusterByTag: threshold, size desc, cap", () => {
|
|
59
|
-
const mems = [
|
|
60
|
-
{ id: "a", tags: ["部署"] }, { id: "b", tags: ["部署"] }, { id: "c", tags: ["部署"] },
|
|
61
|
-
{ id: "d", tags: ["检索"] }, { id: "e", tags: ["检索"] }, { id: "f", tags: ["检索"] },
|
|
62
|
-
{ id: "g", tags: ["检索"] }, { id: "h", tags: ["孤儿"] }, { id: "i", tags: [123] }
|
|
63
|
-
];
|
|
64
|
-
const clusters = clusterByTag(mems, { minCluster: 3, cap: 1 });
|
|
65
|
-
assert.equal(clusters.length, 1);
|
|
66
|
-
assert.equal(clusters[0].tag, "检索");
|
|
67
|
-
assert.equal(clusters[0].members.length, 4);
|
|
68
|
-
const two = clusterByTag(mems, { minCluster: 3, cap: 3 });
|
|
69
|
-
assert.deepEqual(two.map((c) => c.tag), ["检索", "部署"]);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("intersectEvidence drops fabricated ids; empty falls back to all members", () => {
|
|
73
|
-
const members = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
|
74
|
-
assert.deepEqual(intersectEvidence(["a", "fabricated"], members), ["a"]);
|
|
75
|
-
assert.deepEqual(intersectEvidence(["x", "y"], members), ["a", "b", "c"]);
|
|
76
|
-
assert.deepEqual(intersectEvidence(undefined, members), ["a", "b", "c"]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// ============================================================ dream 叙述阶段
|
|
80
|
-
|
|
81
|
-
function seedDeploy(store, service) {
|
|
82
|
-
const mems = [];
|
|
83
|
-
for (const t of ["发布", "回滚", "盯盘"]) {
|
|
84
|
-
mems.push(service.saveWithDedupe({ type: "project", title: t, content: `${t}相关的事实记录。`, tags: ["部署"] }).memory);
|
|
85
|
-
}
|
|
86
|
-
service.saveWithDedupe({ type: "preference", title: "无关", content: "别的话题。", tags: ["其他"] });
|
|
87
|
-
return mems;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
test("narrative phase synthesizes per-topic bars with intersected evidence", async () => {
|
|
91
|
-
const { store, service } = setup();
|
|
92
|
-
const mems = seedDeploy(store, service);
|
|
93
|
-
const callsRef = { n: 0 };
|
|
94
|
-
const narrativeJson = JSON.stringify([
|
|
95
|
-
{ tag: "部署", content: "部署主题已积累三条事实:发布、回滚与盯盘均已闭环。", evidence: [mems[0].id, "fabricated-id"] }
|
|
96
|
-
]);
|
|
97
|
-
const ctx = mockCtx([{ decisions: [] }, { summary: "总览叙述" }, { summary: narrativeJson }], callsRef);
|
|
98
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
99
|
-
const result = await dream.runDream(ctx, service, { dreamProvider: "deepseek", dreamModel: "deepseek-chat", dreamNarrativeEnabled: true });
|
|
100
|
-
assert.equal(result.narratives, 1, "one narrative stored");
|
|
101
|
-
const bar = store.all().find((m) => m.source === "narrative");
|
|
102
|
-
assert.ok(bar, "narrative row exists");
|
|
103
|
-
assert.equal(bar.title, "叙述:部署");
|
|
104
|
-
assert.equal(bar.type, "summary");
|
|
105
|
-
assert.equal(bar.evidence.length, 1, "fabricated id dropped by intersection");
|
|
106
|
-
assert.equal(bar.evidence[0].memory_id, mems[0].id);
|
|
107
|
-
assert.equal(bar.evidence[0].op, "support");
|
|
108
|
-
store.close();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("narrative phase off by default: no extra LLM call", async () => {
|
|
112
|
-
const { store, service } = setup();
|
|
113
|
-
seedDeploy(store, service);
|
|
114
|
-
const callsRef = { n: 0 };
|
|
115
|
-
const ctx = mockCtx([{ decisions: [] }, { summary: "总览叙述" }], callsRef);
|
|
116
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
117
|
-
await dream.runDream(ctx, service, { dreamProvider: "deepseek", dreamModel: "deepseek-chat" });
|
|
118
|
-
assert.equal(callsRef.n, 2, "only consolidation + summary calls");
|
|
119
|
-
assert.equal(store.all().filter((m) => m.source === "narrative").length, 0);
|
|
120
|
-
store.close();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test("below minCluster threshold: no narrative call even when enabled", async () => {
|
|
124
|
-
const { store, service } = setup();
|
|
125
|
-
service.saveWithDedupe({ type: "project", title: "甲", content: "c", tags: ["部署"] });
|
|
126
|
-
service.saveWithDedupe({ type: "project", title: "乙", content: "c", tags: ["部署"] });
|
|
127
|
-
const callsRef = { n: 0 };
|
|
128
|
-
const ctx = mockCtx([{ decisions: [] }, { summary: "总览叙述" }], callsRef);
|
|
129
|
-
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
130
|
-
await dream.runDream(ctx, service, { dreamProvider: "deepseek", dreamModel: "deepseek-chat", dreamNarrativeEnabled: true });
|
|
131
|
-
assert.equal(callsRef.n, 2, "no narrative call without a qualifying cluster");
|
|
132
|
-
store.close();
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// ============================================================ 注入排除
|
|
136
|
-
|
|
137
|
-
test("narrative dedupes by tag across language switches (no duplicate row)", () => {
|
|
138
|
-
const { store, service } = setup();
|
|
139
|
-
service.saveWithDedupe({ type: "summary", title: "叙述:部署", content: "中文叙述", importance: 3, source: "narrative", tags: ["部署"] });
|
|
140
|
-
// generateNarratives 落库恒走 _overwrite(标题=主题键,原地刷新)。
|
|
141
|
-
service.saveWithDedupe({ type: "summary", title: "Narrative: 部署", content: "English narrative", importance: 3, source: "narrative", tags: ["部署"], _overwrite: true });
|
|
142
|
-
const bars = store.all().filter((m) => m.source === "narrative");
|
|
143
|
-
assert.equal(bars.length, 1, "language switch must not create a duplicate narrative row");
|
|
144
|
-
assert.equal(bars[0].title, "Narrative: 部署", "row refreshes to the new language title");
|
|
145
|
-
assert.equal(bars[0].content, "English narrative");
|
|
146
|
-
store.close();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("narrative bars are on-demand: excluded from injection, reachable via search", async () => {
|
|
150
|
-
const { store, service } = setup({ dreamNarrativeEnabled: false });
|
|
151
|
-
service.saveWithDedupe({ type: "summary", title: "记忆库总览", content: "当前状态叙述", importance: 5, source: "dream" });
|
|
152
|
-
service.saveWithDedupe({ type: "summary", title: "叙述:部署", content: "部署主题的事实聚合,含部署关键词。", importance: 3, source: "narrative" });
|
|
153
|
-
const candidates = service.injectCandidates({ query: "", maxItems: 5 });
|
|
154
|
-
const ids = new Set(candidates.map((c) => c.id));
|
|
155
|
-
const overview = store.all().find((m) => m.source === "dream");
|
|
156
|
-
const bar = store.all().find((m) => m.source === "narrative");
|
|
157
|
-
assert.ok(ids.has(overview.id), "dream overview stays resident-injectable");
|
|
158
|
-
assert.ok(!ids.has(bar.id), "per-topic narrative excluded from injection");
|
|
159
|
-
const rows = await service.searchMemories("部署", { mode: "keyword" });
|
|
160
|
-
assert.ok(rows.some((r) => r.id === bar.id), "narrative reachable via search");
|
|
161
|
-
store.close();
|
|
162
|
-
});
|
|
1
|
+
// 叙述条(#164 对齐,PR-4)测试。
|
|
2
|
+
// 覆盖:evidence 列迁移与透传(save/update/parse 回环)/ clusterByTag 纯函数
|
|
3
|
+
// (≥K 门槛、降序、cap)/ intersectEvidence(捏造 id 剔除、交空回落全簇)/
|
|
4
|
+
// dream 叙述阶段端到端(enabled 时第三次 LLM 调用、求交落库、source=narrative)/
|
|
5
|
+
// 默认关零调用 / 低于门槛零调用 / 注入排除(narrative 不进候选,dream 总览保留)。
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import assert from "node:assert/strict";
|
|
8
|
+
import { createStore } from "../src/store.js";
|
|
9
|
+
import { createService } from "../src/service.js";
|
|
10
|
+
import { createDreamScheduler } from "../src/dream.js";
|
|
11
|
+
import { clusterByTag, intersectEvidence } from "../src/dream/narratives.js";
|
|
12
|
+
|
|
13
|
+
function setup(config = {}) {
|
|
14
|
+
const store = createStore(":memory:");
|
|
15
|
+
const service = createService({ store, mirror: null, config });
|
|
16
|
+
return { store, service };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function mockCtx(sequence, callsRef) {
|
|
20
|
+
return {
|
|
21
|
+
llm: {
|
|
22
|
+
stream: async function* () {
|
|
23
|
+
const step = sequence[callsRef.n] ?? { summary: "空转" };
|
|
24
|
+
callsRef.n++;
|
|
25
|
+
const text = step.decisions !== undefined ? JSON.stringify(step.decisions) : step.summary;
|
|
26
|
+
yield { type: "text-delta", text };
|
|
27
|
+
yield { type: "finish", reason: { kind: "ok" } };
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
logger: { warn: () => {} }
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================ store evidence 回环
|
|
35
|
+
|
|
36
|
+
test("evidence column roundtrips through save and update", () => {
|
|
37
|
+
const { store, service } = setup();
|
|
38
|
+
const ev = [{ memory_id: "m1", op: "support", at: "2026-09-17T00:00:00.000Z" }];
|
|
39
|
+
const { memory } = service.saveWithDedupe({
|
|
40
|
+
type: "summary", title: "叙述:部署", content: "叙述正文", source: "narrative", evidence: ev
|
|
41
|
+
});
|
|
42
|
+
assert.deepEqual(store.getById(memory.id).evidence, ev);
|
|
43
|
+
const ev2 = [{ memory_id: "m2", op: "weaken", at: "2026-09-18T00:00:00.000Z" }];
|
|
44
|
+
store.update(memory.id, { evidence: ev2 });
|
|
45
|
+
assert.deepEqual(store.getById(memory.id).evidence, ev2);
|
|
46
|
+
store.close();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("rows without evidence parse to an empty array", () => {
|
|
50
|
+
const { store, service } = setup();
|
|
51
|
+
const { memory } = service.saveWithDedupe({ type: "project", title: "普通", content: "无证据" });
|
|
52
|
+
assert.deepEqual(store.getById(memory.id).evidence, []);
|
|
53
|
+
store.close();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ============================================================ 纯函数
|
|
57
|
+
|
|
58
|
+
test("clusterByTag: threshold, size desc, cap", () => {
|
|
59
|
+
const mems = [
|
|
60
|
+
{ id: "a", tags: ["部署"] }, { id: "b", tags: ["部署"] }, { id: "c", tags: ["部署"] },
|
|
61
|
+
{ id: "d", tags: ["检索"] }, { id: "e", tags: ["检索"] }, { id: "f", tags: ["检索"] },
|
|
62
|
+
{ id: "g", tags: ["检索"] }, { id: "h", tags: ["孤儿"] }, { id: "i", tags: [123] }
|
|
63
|
+
];
|
|
64
|
+
const clusters = clusterByTag(mems, { minCluster: 3, cap: 1 });
|
|
65
|
+
assert.equal(clusters.length, 1);
|
|
66
|
+
assert.equal(clusters[0].tag, "检索");
|
|
67
|
+
assert.equal(clusters[0].members.length, 4);
|
|
68
|
+
const two = clusterByTag(mems, { minCluster: 3, cap: 3 });
|
|
69
|
+
assert.deepEqual(two.map((c) => c.tag), ["检索", "部署"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("intersectEvidence drops fabricated ids; empty falls back to all members", () => {
|
|
73
|
+
const members = [{ id: "a" }, { id: "b" }, { id: "c" }];
|
|
74
|
+
assert.deepEqual(intersectEvidence(["a", "fabricated"], members), ["a"]);
|
|
75
|
+
assert.deepEqual(intersectEvidence(["x", "y"], members), ["a", "b", "c"]);
|
|
76
|
+
assert.deepEqual(intersectEvidence(undefined, members), ["a", "b", "c"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ============================================================ dream 叙述阶段
|
|
80
|
+
|
|
81
|
+
function seedDeploy(store, service) {
|
|
82
|
+
const mems = [];
|
|
83
|
+
for (const t of ["发布", "回滚", "盯盘"]) {
|
|
84
|
+
mems.push(service.saveWithDedupe({ type: "project", title: t, content: `${t}相关的事实记录。`, tags: ["部署"] }).memory);
|
|
85
|
+
}
|
|
86
|
+
service.saveWithDedupe({ type: "preference", title: "无关", content: "别的话题。", tags: ["其他"] });
|
|
87
|
+
return mems;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test("narrative phase synthesizes per-topic bars with intersected evidence", async () => {
|
|
91
|
+
const { store, service } = setup();
|
|
92
|
+
const mems = seedDeploy(store, service);
|
|
93
|
+
const callsRef = { n: 0 };
|
|
94
|
+
const narrativeJson = JSON.stringify([
|
|
95
|
+
{ tag: "部署", content: "部署主题已积累三条事实:发布、回滚与盯盘均已闭环。", evidence: [mems[0].id, "fabricated-id"] }
|
|
96
|
+
]);
|
|
97
|
+
const ctx = mockCtx([{ decisions: [] }, { summary: "总览叙述" }, { summary: narrativeJson }], callsRef);
|
|
98
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
99
|
+
const result = await dream.runDream(ctx, service, { dreamProvider: "deepseek", dreamModel: "deepseek-chat", dreamNarrativeEnabled: true });
|
|
100
|
+
assert.equal(result.narratives, 1, "one narrative stored");
|
|
101
|
+
const bar = store.all().find((m) => m.source === "narrative");
|
|
102
|
+
assert.ok(bar, "narrative row exists");
|
|
103
|
+
assert.equal(bar.title, "叙述:部署");
|
|
104
|
+
assert.equal(bar.type, "summary");
|
|
105
|
+
assert.equal(bar.evidence.length, 1, "fabricated id dropped by intersection");
|
|
106
|
+
assert.equal(bar.evidence[0].memory_id, mems[0].id);
|
|
107
|
+
assert.equal(bar.evidence[0].op, "support");
|
|
108
|
+
store.close();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("narrative phase off by default: no extra LLM call", async () => {
|
|
112
|
+
const { store, service } = setup();
|
|
113
|
+
seedDeploy(store, service);
|
|
114
|
+
const callsRef = { n: 0 };
|
|
115
|
+
const ctx = mockCtx([{ decisions: [] }, { summary: "总览叙述" }], callsRef);
|
|
116
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
117
|
+
await dream.runDream(ctx, service, { dreamProvider: "deepseek", dreamModel: "deepseek-chat" });
|
|
118
|
+
assert.equal(callsRef.n, 2, "only consolidation + summary calls");
|
|
119
|
+
assert.equal(store.all().filter((m) => m.source === "narrative").length, 0);
|
|
120
|
+
store.close();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("below minCluster threshold: no narrative call even when enabled", async () => {
|
|
124
|
+
const { store, service } = setup();
|
|
125
|
+
service.saveWithDedupe({ type: "project", title: "甲", content: "c", tags: ["部署"] });
|
|
126
|
+
service.saveWithDedupe({ type: "project", title: "乙", content: "c", tags: ["部署"] });
|
|
127
|
+
const callsRef = { n: 0 };
|
|
128
|
+
const ctx = mockCtx([{ decisions: [] }, { summary: "总览叙述" }], callsRef);
|
|
129
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
130
|
+
await dream.runDream(ctx, service, { dreamProvider: "deepseek", dreamModel: "deepseek-chat", dreamNarrativeEnabled: true });
|
|
131
|
+
assert.equal(callsRef.n, 2, "no narrative call without a qualifying cluster");
|
|
132
|
+
store.close();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// ============================================================ 注入排除
|
|
136
|
+
|
|
137
|
+
test("narrative dedupes by tag across language switches (no duplicate row)", () => {
|
|
138
|
+
const { store, service } = setup();
|
|
139
|
+
service.saveWithDedupe({ type: "summary", title: "叙述:部署", content: "中文叙述", importance: 3, source: "narrative", tags: ["部署"] });
|
|
140
|
+
// generateNarratives 落库恒走 _overwrite(标题=主题键,原地刷新)。
|
|
141
|
+
service.saveWithDedupe({ type: "summary", title: "Narrative: 部署", content: "English narrative", importance: 3, source: "narrative", tags: ["部署"], _overwrite: true });
|
|
142
|
+
const bars = store.all().filter((m) => m.source === "narrative");
|
|
143
|
+
assert.equal(bars.length, 1, "language switch must not create a duplicate narrative row");
|
|
144
|
+
assert.equal(bars[0].title, "Narrative: 部署", "row refreshes to the new language title");
|
|
145
|
+
assert.equal(bars[0].content, "English narrative");
|
|
146
|
+
store.close();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("narrative bars are on-demand: excluded from injection, reachable via search", async () => {
|
|
150
|
+
const { store, service } = setup({ dreamNarrativeEnabled: false });
|
|
151
|
+
service.saveWithDedupe({ type: "summary", title: "记忆库总览", content: "当前状态叙述", importance: 5, source: "dream" });
|
|
152
|
+
service.saveWithDedupe({ type: "summary", title: "叙述:部署", content: "部署主题的事实聚合,含部署关键词。", importance: 3, source: "narrative" });
|
|
153
|
+
const candidates = service.injectCandidates({ query: "", maxItems: 5 });
|
|
154
|
+
const ids = new Set(candidates.map((c) => c.id));
|
|
155
|
+
const overview = store.all().find((m) => m.source === "dream");
|
|
156
|
+
const bar = store.all().find((m) => m.source === "narrative");
|
|
157
|
+
assert.ok(ids.has(overview.id), "dream overview stays resident-injectable");
|
|
158
|
+
assert.ok(!ids.has(bar.id), "per-topic narrative excluded from injection");
|
|
159
|
+
const rows = await service.searchMemories("部署", { mode: "keyword" });
|
|
160
|
+
assert.ok(rows.some((r) => r.id === bar.id), "narrative reachable via search");
|
|
161
|
+
store.close();
|
|
162
|
+
});
|