recallnest 3.0.0 → 3.0.1

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.
Files changed (184) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +222 -254
  3. package/README_CN.md +206 -243
  4. package/package.json +3 -2
  5. package/src/capture-engine.ts +26 -4
  6. package/src/context-composer-stable.ts +5 -1
  7. package/src/embedder.ts +24 -0
  8. package/src/env-config.ts +18 -0
  9. package/src/ingest.ts +225 -17
  10. package/src/kg-extractor.ts +17 -0
  11. package/src/memory-output.ts +35 -0
  12. package/src/pivot-apply.ts +52 -1
  13. package/src/pivot-evidence.ts +128 -0
  14. package/src/ppr-traversal.ts +25 -1
  15. package/src/runtime-config.ts +9 -0
  16. package/src/session-engine.ts +11 -4
  17. package/src/session-output.ts +9 -1
  18. package/src/session-store.ts +2 -3
  19. package/src/skill-promotion.ts +67 -1
  20. package/src/__tests__/access-tracker-novelty.test.ts +0 -324
  21. package/src/__tests__/activity-counter.test.ts +0 -125
  22. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  23. package/src/__tests__/admission-control.test.ts +0 -206
  24. package/src/__tests__/age-label.test.ts +0 -33
  25. package/src/__tests__/anchor-generator.test.ts +0 -67
  26. package/src/__tests__/api-server-security.test.ts +0 -102
  27. package/src/__tests__/auto-consolidation.test.ts +0 -168
  28. package/src/__tests__/auto-recall.test.ts +0 -131
  29. package/src/__tests__/babel-integration.test.ts +0 -144
  30. package/src/__tests__/batch-dedup.test.ts +0 -65
  31. package/src/__tests__/belief-history.test.ts +0 -220
  32. package/src/__tests__/canary-eval.test.ts +0 -114
  33. package/src/__tests__/capture-engine.test.ts +0 -1328
  34. package/src/__tests__/capture-heuristic.test.ts +0 -171
  35. package/src/__tests__/cascade-forget.test.ts +0 -210
  36. package/src/__tests__/category-min-scores.test.ts +0 -116
  37. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  38. package/src/__tests__/circuit-breaker.test.ts +0 -99
  39. package/src/__tests__/cluster-consolidation.test.ts +0 -1236
  40. package/src/__tests__/confidence-metadata.test.ts +0 -125
  41. package/src/__tests__/confidence-tracker.test.ts +0 -261
  42. package/src/__tests__/conflict-advisor.test.ts +0 -206
  43. package/src/__tests__/conflict-escalation.test.ts +0 -149
  44. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  45. package/src/__tests__/conflict-output.test.ts +0 -85
  46. package/src/__tests__/conflict-store.test.ts +0 -418
  47. package/src/__tests__/connector-v1.test.ts +0 -181
  48. package/src/__tests__/consolidation-engine.test.ts +0 -520
  49. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  50. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  51. package/src/__tests__/context-composer-pins.test.ts +0 -64
  52. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  53. package/src/__tests__/context-reconstructor.test.ts +0 -65
  54. package/src/__tests__/context-renderer.test.ts +0 -91
  55. package/src/__tests__/continuity-eval.test.ts +0 -275
  56. package/src/__tests__/continuity-schema.test.ts +0 -97
  57. package/src/__tests__/contracts.test.ts +0 -262
  58. package/src/__tests__/conversation-importer.test.ts +0 -692
  59. package/src/__tests__/core-recall-eval.test.ts +0 -235
  60. package/src/__tests__/core-summary.test.ts +0 -109
  61. package/src/__tests__/data-checkup.test.ts +0 -304
  62. package/src/__tests__/decay-exemption.test.ts +0 -129
  63. package/src/__tests__/dedup-actions.test.ts +0 -58
  64. package/src/__tests__/dedup-guard.test.ts +0 -39
  65. package/src/__tests__/deterministic-id.test.ts +0 -39
  66. package/src/__tests__/distill-lock.test.ts +0 -305
  67. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  68. package/src/__tests__/doctor.test.ts +0 -319
  69. package/src/__tests__/dream-pipeline.test.ts +0 -820
  70. package/src/__tests__/dream-scope-isolation.test.ts +0 -386
  71. package/src/__tests__/dream-write-batching.test.ts +0 -211
  72. package/src/__tests__/drill-down.test.ts +0 -108
  73. package/src/__tests__/embedder.test.ts +0 -122
  74. package/src/__tests__/emotion-decay.test.ts +0 -46
  75. package/src/__tests__/emotion-detector.test.ts +0 -56
  76. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  77. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  78. package/src/__tests__/entity-resolver.test.ts +0 -58
  79. package/src/__tests__/env-config-invariant.test.ts +0 -80
  80. package/src/__tests__/env-config.test.ts +0 -211
  81. package/src/__tests__/error-signature-boost.test.ts +0 -87
  82. package/src/__tests__/error-signature.test.ts +0 -95
  83. package/src/__tests__/error-taxonomy.test.ts +0 -129
  84. package/src/__tests__/ethics-layer.test.ts +0 -476
  85. package/src/__tests__/event-segmenter.test.ts +0 -153
  86. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  87. package/src/__tests__/frequency-tracker.test.ts +0 -113
  88. package/src/__tests__/freshness.test.ts +0 -186
  89. package/src/__tests__/graph-export.test.ts +0 -489
  90. package/src/__tests__/hotness-blend.test.ts +0 -283
  91. package/src/__tests__/ingest-chunking.test.ts +0 -44
  92. package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
  93. package/src/__tests__/ingest.test.ts +0 -360
  94. package/src/__tests__/interference-detector.test.ts +0 -203
  95. package/src/__tests__/kg-extractor.test.ts +0 -192
  96. package/src/__tests__/kg-store.test.ts +0 -300
  97. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  98. package/src/__tests__/llm-consolidation.test.ts +0 -226
  99. package/src/__tests__/maintenance-locks.test.ts +0 -197
  100. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  101. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  102. package/src/__tests__/memory-boundaries.test.ts +0 -282
  103. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
  104. package/src/__tests__/memory-evolution.test.ts +0 -271
  105. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  106. package/src/__tests__/memory-lint.test.ts +0 -421
  107. package/src/__tests__/memory-output.test.ts +0 -224
  108. package/src/__tests__/memory-promotion.test.ts +0 -315
  109. package/src/__tests__/memory-utility.test.ts +0 -256
  110. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  111. package/src/__tests__/multi-vector.test.ts +0 -242
  112. package/src/__tests__/narrative-architecture.test.ts +0 -456
  113. package/src/__tests__/noise-filter.test.ts +0 -205
  114. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  115. package/src/__tests__/obsidian-connector.test.ts +0 -363
  116. package/src/__tests__/omitted-reporting.test.ts +0 -98
  117. package/src/__tests__/openai-contract.test.ts +0 -513
  118. package/src/__tests__/pending-review.test.ts +0 -77
  119. package/src/__tests__/pii-redact.test.ts +0 -333
  120. package/src/__tests__/pivot-apply.test.ts +0 -373
  121. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  122. package/src/__tests__/pivot-distill.test.ts +0 -1178
  123. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  124. package/src/__tests__/ppr-traversal.test.ts +0 -256
  125. package/src/__tests__/prediction-engine.test.ts +0 -349
  126. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  127. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  128. package/src/__tests__/preference-matcher.test.ts +0 -282
  129. package/src/__tests__/preference-slots.test.ts +0 -205
  130. package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
  131. package/src/__tests__/prospective-memory.test.ts +0 -283
  132. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  133. package/src/__tests__/query-entity-detector.test.ts +0 -171
  134. package/src/__tests__/query-expander-alias.test.ts +0 -146
  135. package/src/__tests__/query-expander.test.ts +0 -34
  136. package/src/__tests__/recall-gate.test.ts +0 -266
  137. package/src/__tests__/recall-governor.test.ts +0 -227
  138. package/src/__tests__/recall-mode.test.ts +0 -64
  139. package/src/__tests__/resolve-db-path.test.ts +0 -130
  140. package/src/__tests__/resolve-tier.test.ts +0 -64
  141. package/src/__tests__/result-synthesizer.test.ts +0 -198
  142. package/src/__tests__/resume-context.test.ts +0 -4898
  143. package/src/__tests__/retrieval-eval.test.ts +0 -163
  144. package/src/__tests__/retrieval-trace.test.ts +0 -213
  145. package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
  146. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  147. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  148. package/src/__tests__/retroactive-boost.test.ts +0 -223
  149. package/src/__tests__/salience-scoring.test.ts +0 -106
  150. package/src/__tests__/scope-inventory.test.ts +0 -200
  151. package/src/__tests__/scope-policy.test.ts +0 -69
  152. package/src/__tests__/scope-suggester.test.ts +0 -80
  153. package/src/__tests__/session-checkpoint.test.ts +0 -291
  154. package/src/__tests__/session-distiller.test.ts +0 -640
  155. package/src/__tests__/session-gc.test.ts +0 -215
  156. package/src/__tests__/skill-engine.test.ts +0 -552
  157. package/src/__tests__/skill-promotion.test.ts +0 -654
  158. package/src/__tests__/skill-verifier.test.ts +0 -122
  159. package/src/__tests__/source-heartbeat.test.ts +0 -254
  160. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  161. package/src/__tests__/store-patch-metadata.test.ts +0 -235
  162. package/src/__tests__/store-read-consistency.test.ts +0 -122
  163. package/src/__tests__/store-schema-migration.test.ts +0 -87
  164. package/src/__tests__/store-scope-audit.test.ts +0 -81
  165. package/src/__tests__/store-upsert.test.ts +0 -201
  166. package/src/__tests__/supersede-chain.test.ts +0 -154
  167. package/src/__tests__/synthesis-contract.test.ts +0 -252
  168. package/src/__tests__/synthesis-promotion.test.ts +0 -369
  169. package/src/__tests__/temporal-parser.test.ts +0 -241
  170. package/src/__tests__/temporal-validity.test.ts +0 -160
  171. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  172. package/src/__tests__/term-registry.test.ts +0 -121
  173. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  174. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  175. package/src/__tests__/topic-tag.test.ts +0 -113
  176. package/src/__tests__/trusted-memory.test.ts +0 -867
  177. package/src/__tests__/ui-server-security.test.ts +0 -93
  178. package/src/__tests__/usage-tracker.test.ts +0 -287
  179. package/src/__tests__/utility-weight.test.ts +0 -57
  180. package/src/__tests__/verbatim-self-recall.test.ts +0 -279
  181. package/src/__tests__/version-manager.test.ts +0 -248
  182. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  183. package/src/__tests__/workflow-observation.test.ts +0 -433
  184. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -1,252 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import {
4
- CLUSTER_INSIGHT_PROMPT,
5
- CLUSTER_PATTERN_PROMPT,
6
- PROMPT_ECHO_MARKERS,
7
- SYNTHESIS_MAX_LEN,
8
- allocateClusterBudget,
9
- numberClusterTexts,
10
- parseSynthesisJson,
11
- validateSynthesis,
12
- } from "../synthesis-contract.js";
13
-
14
- // ---------------------------------------------------------------------------
15
- // 输入预算分配(R3:老路径 slice(0,2000) 让 64.2% 的簇被截,p10 只有 13.8% 的源进得了模型)
16
- // ---------------------------------------------------------------------------
17
-
18
- describe("allocateClusterBudget", () => {
19
- it("总量不超预算", () => {
20
- const texts = Array.from({ length: 20 }, () => "x".repeat(5000));
21
- const out = allocateClusterBudget(texts, 1000);
22
- expect(out.reduce((s, t) => s + t.length, 0)).toBeLessThanOrEqual(1000);
23
- });
24
-
25
- it("短成员整条保留,长成员才被截", () => {
26
- const out = allocateClusterBudget(["短", "y".repeat(999)], 100);
27
- expect(out[0]).toBe("短");
28
- expect(out[1].length).toBeLessThan(999);
29
- });
30
-
31
- it("短成员用不完的份额让给长成员,而不是浪费掉", () => {
32
- // 均分是 50/50;第一条只用 1 个字符,第二条应拿到远超 50 的份额。
33
- const out = allocateClusterBudget(["短", "y".repeat(999)], 100);
34
- expect(out[1].length).toBeGreaterThan(50);
35
- });
36
-
37
- it("靠后的成员不会被整条丢掉——这正是老 slice(0,2000) 的失效形态", () => {
38
- // 老路径把 168 条拼成一条长文本再从头切,排在后面的成员一个字都进不了模型。
39
- const texts = Array.from({ length: 50 }, (_, i) => `第${i}条`.repeat(200));
40
- const out = allocateClusterBudget(texts, 2000);
41
- expect(out.length).toBe(50);
42
- expect(out.every(t => t.length > 0)).toBe(true);
43
- // 最后一条必须真的携带自己的内容,而不是空壳
44
- expect(out[49]).toContain("第49条");
45
- });
46
-
47
- it("空簇返回空数组", () => {
48
- expect(allocateClusterBudget([], 100)).toEqual([]);
49
- });
50
- });
51
-
52
- describe("numberClusterTexts", () => {
53
- it("编号从 1 开始,与 evidence 的语义一致", () => {
54
- expect(numberClusterTexts(["a", "b"])).toBe("[1] a\n[2] b");
55
- });
56
- });
57
-
58
- // ---------------------------------------------------------------------------
59
- // JSON 解析
60
- // ---------------------------------------------------------------------------
61
-
62
- describe("parseSynthesisJson", () => {
63
- it("裸 JSON", () => {
64
- expect(parseSynthesisJson('{"has":false}')).toEqual({ has: false });
65
- });
66
-
67
- it("markdown 围栏", () => {
68
- expect(parseSynthesisJson('```json\n{"has":false}\n```')).toEqual({ has: false });
69
- });
70
-
71
- it("带前言的回复也能抠出对象", () => {
72
- expect(parseSynthesisJson('好的,结果如下:\n{"has":false}\n以上。')).toEqual({ has: false });
73
- });
74
-
75
- it("拿不到 JSON 时返回 null 而不是抛异常", () => {
76
- expect(parseSynthesisJson("完全不是 JSON")).toBeNull();
77
- expect(parseSynthesisJson(null)).toBeNull();
78
- });
79
-
80
- it("顶层是数组不算合法(契约要的是对象)", () => {
81
- expect(parseSynthesisJson("[1,2,3]")).toBeNull();
82
- });
83
- });
84
-
85
- // ---------------------------------------------------------------------------
86
- // 写库前校验 —— 每条 reject 分支都要有一条测试撑着,
87
- // 因为这一层的全部意义就是「LLM 说了什么不算数」。
88
- // ---------------------------------------------------------------------------
89
-
90
- const base = { textField: "conclusion", memberCount: 5, minEvidence: 1 };
91
-
92
- describe("validateSynthesis", () => {
93
- it("合格输出:去重 + 升序", () => {
94
- const v = validateSynthesis({
95
- ...base,
96
- parsed: { has: true, conclusion: "必须先初始化再分派,否则读到的是初始化前的值", evidence: [3, 1, 3] },
97
- });
98
- expect(v.status).toBe("ok");
99
- if (v.status === "ok") {
100
- expect(v.output.evidence).toEqual([1, 3]);
101
- expect(v.output.text).toContain("必须先初始化");
102
- }
103
- });
104
-
105
- it("has:false 是弃权,不是失败——这条路径老 generateL0 根本没有", () => {
106
- expect(validateSynthesis({ ...base, parsed: { has: false } }).status).toBe("abstained");
107
- });
108
-
109
- it("解析不出 JSON → unparsable", () => {
110
- const v = validateSynthesis({ ...base, parsed: null });
111
- expect(v).toEqual({ status: "rejected", reason: "unparsable" });
112
- });
113
-
114
- it("has 不是 boolean → 拒绝(不猜它的意图)", () => {
115
- for (const has of ["true", 1, null, undefined]) {
116
- const v = validateSynthesis({ ...base, parsed: { has, conclusion: "随便什么", evidence: [1] } });
117
- expect(v).toEqual({ status: "rejected", reason: "flag-not-boolean" });
118
- }
119
- });
120
-
121
- it("说有结论却没给正文 → empty-text", () => {
122
- expect(validateSynthesis({ ...base, parsed: { has: true, evidence: [1] } }))
123
- .toEqual({ status: "rejected", reason: "empty-text" });
124
- expect(validateSynthesis({ ...base, parsed: { has: true, conclusion: " ", evidence: [1] } }))
125
- .toEqual({ status: "rejected", reason: "empty-text" });
126
- });
127
-
128
- it("长度越界两侧都拒", () => {
129
- expect(validateSynthesis({ ...base, parsed: { has: true, conclusion: "太短", evidence: [1] } }))
130
- .toEqual({ status: "rejected", reason: "too-short" });
131
- expect(validateSynthesis({
132
- ...base,
133
- parsed: { has: true, conclusion: "长".repeat(SYNTHESIS_MAX_LEN + 1), evidence: [1] },
134
- })).toEqual({ status: "rejected", reason: "too-long" });
135
- });
136
-
137
- it("evidence 缺失或不是数组 → evidence-missing", () => {
138
- expect(validateSynthesis({ ...base, parsed: { has: true, conclusion: "一条长度足够越过下限的结论文本" } }))
139
- .toEqual({ status: "rejected", reason: "evidence-missing" });
140
- expect(validateSynthesis({ ...base, parsed: { has: true, conclusion: "一条长度足够越过下限的结论文本", evidence: "1,2" } }))
141
- .toEqual({ status: "rejected", reason: "evidence-missing" });
142
- });
143
-
144
- it("编号全部越界 → evidence-out-of-range(模型在编号,不是证据不够)", () => {
145
- const v = validateSynthesis({
146
- ...base,
147
- parsed: { has: true, conclusion: "一条长度足够越过下限的结论文本", evidence: [99, 0, -1] },
148
- });
149
- expect(v).toEqual({ status: "rejected", reason: "evidence-out-of-range" });
150
- });
151
-
152
- it("pattern 侧只给 1 个合法编号 → evidence-too-few(跨条目按定义要 2 条)", () => {
153
- const v = validateSynthesis({
154
- parsed: { has: true, pattern: "一条长度足够越过下限的跨条目结论文本", evidence: [2] },
155
- textField: "pattern",
156
- memberCount: 5,
157
- minEvidence: 2,
158
- });
159
- expect(v).toEqual({ status: "rejected", reason: "evidence-too-few" });
160
- });
161
-
162
- it("重复编号不算两条证据([2,2] 去重后只剩 1 个)", () => {
163
- const v = validateSynthesis({
164
- parsed: { has: true, pattern: "一条长度足够越过下限的跨条目结论文本", evidence: [2, 2] },
165
- textField: "pattern",
166
- memberCount: 5,
167
- minEvidence: 2,
168
- });
169
- expect(v).toEqual({ status: "rejected", reason: "evidence-too-few" });
170
- });
171
-
172
- it("数字字符串编号可接受(模型常把编号写成字符串)", () => {
173
- const v = validateSynthesis({
174
- ...base,
175
- parsed: { has: true, conclusion: "一条长度足够越过下限的结论文本", evidence: ["2", 3] },
176
- });
177
- expect(v.status).toBe("ok");
178
- if (v.status === "ok") expect(v.output.evidence).toEqual([2, 3]);
179
- });
180
-
181
- it("非整数编号不算合法", () => {
182
- const v = validateSynthesis({
183
- ...base,
184
- parsed: { has: true, conclusion: "一条长度足够越过下限的结论文本", evidence: [1.5, "abc"] },
185
- });
186
- expect(v).toEqual({ status: "rejected", reason: "evidence-out-of-range" });
187
- });
188
- });
189
-
190
- // ---------------------------------------------------------------------------
191
- // 提示词回声 —— R4 的直接回归:库里曾有 28 条记忆的正文就是系统提示词原文
192
- // ---------------------------------------------------------------------------
193
-
194
- describe("提示词回声拦截(R4 回归)", () => {
195
- it("拦下那 28 条真实泄漏的原文形态", () => {
196
- const leaked = "保真规则:端口号/IP/URL/文件路径/API名称 → 原样保留;函数名/事件名/配置项 → 逐项保留不概括。";
197
- const v = validateSynthesis({ ...base, parsed: { has: true, conclusion: leaked, evidence: [1, 2] } });
198
- expect(v).toEqual({ status: "rejected", reason: "prompt-echo" });
199
- });
200
-
201
- it("新提示词自己的句子被回声也拦得住", () => {
202
- const v = validateSynthesis({
203
- ...base,
204
- parsed: { has: true, conclusion: "提炼不出来就说没有。宁可不产出,也不要硬凑。", evidence: [1] },
205
- });
206
- expect(v).toEqual({ status: "rejected", reason: "prompt-echo" });
207
- });
208
-
209
- it("正常结论不被误杀(标记表宁可漏也不误杀真结论)", () => {
210
- const normal = "批量删记忆要显式关掉 cascade,否则同 scope 内相似度高的条目会被连带降权。";
211
- expect(validateSynthesis({ ...base, parsed: { has: true, conclusion: normal, evidence: [1] } }).status).toBe("ok");
212
- });
213
-
214
- it("标记全部真的出现在提示词里——否则这道闸是摆设", () => {
215
- // 反向断言:防有人改提示词时把某条标记的原文改掉,闸门静默失效而测试照绿。
216
- const corpus = CLUSTER_INSIGHT_PROMPT + "\n" + CLUSTER_PATTERN_PROMPT;
217
- const legacyOnly = new Set(["保真规则", "端口号/IP/URL/文件路径", "记忆索引助手", "记忆模式发现助手"]);
218
- for (const marker of PROMPT_ECHO_MARKERS) {
219
- if (legacyOnly.has(marker)) continue; // 老提示词的标记,本仓已无原文,保留是为了拦历史形态
220
- expect(corpus).toContain(marker);
221
- }
222
- });
223
- });
224
-
225
- // ---------------------------------------------------------------------------
226
- // 提示词本身的契约:三件必须写死的事
227
- // ---------------------------------------------------------------------------
228
-
229
- describe("提示词契约", () => {
230
- it("两个提示词都必须给出弃权出口", () => {
231
- for (const p of [CLUSTER_INSIGHT_PROMPT, CLUSTER_PATTERN_PROMPT]) {
232
- expect(p).toContain('has": false');
233
- }
234
- });
235
-
236
- it("两个提示词都必须显式禁掉性向归因措辞(56.1% 的直接来源)", () => {
237
- for (const p of [CLUSTER_INSIGHT_PROMPT, CLUSTER_PATTERN_PROMPT]) {
238
- expect(p).toContain("倾向于");
239
- expect(p).toContain("性格");
240
- }
241
- });
242
-
243
- it("两个提示词都必须要求 evidence 编号", () => {
244
- for (const p of [CLUSTER_INSIGHT_PROMPT, CLUSTER_PATTERN_PROMPT]) {
245
- expect(p).toContain("evidence");
246
- }
247
- });
248
-
249
- it("pattern 侧必须写死 ≥2 条支撑(旧提示词写了但实现不校验,等于没写)", () => {
250
- expect(CLUSTER_PATTERN_PROMPT).toContain("至少 2 条");
251
- });
252
- });
@@ -1,369 +0,0 @@
1
- /**
2
- * Evidence → durable promotion for dream-synthesized conclusions.
3
- *
4
- * The layering RecallNest works under is "evidence retained → current projection →
5
- * eligible for recall → ranked result", and until now a synthesized conclusion could only
6
- * ever occupy the first of those. `buildDerivedBoundary` stamps every cluster insight and
7
- * cross-memory pattern `layer: "evidence"` on purpose — a model re-reading its own
8
- * memories is a lead to its sources, not authority over them — but
9
- * `shouldUseStableMemoryResult` rejects the evidence layer outright, so there was no path
10
- * from a well-supported conclusion to anything downstream could lean on.
11
- *
12
- * These tests cover the road that closes that gap, and the guardrails on it: the synthesis
13
- * row is never touched, re-running revises instead of duplicating, and anything whose
14
- * support cannot be checked is abstained on rather than waved through.
15
- */
16
-
17
- import { describe, expect, it } from "bun:test";
18
-
19
- import {
20
- DEFAULT_SYNTHESIS_PROMOTE_CONFIG,
21
- MIN_PROMOTABLE_CONTRACT_VERSION,
22
- formatSynthesisPromoteResult,
23
- scanSynthesisPromotions,
24
- type PromoteRequest,
25
- type SynthesisPromoteScanDeps,
26
- } from "../memory-promotion.js";
27
- import { shouldUseStableMemoryResult } from "../memory-boundaries.js";
28
- import type { MemoryEntry } from "../store.js";
29
-
30
- // ---------------------------------------------------------------------------
31
- // Fixture
32
- // ---------------------------------------------------------------------------
33
-
34
- const SCOPE = "memory";
35
-
36
- interface RowSpec {
37
- id: string;
38
- text: string;
39
- category?: string;
40
- importance?: number;
41
- /** Omit for a plain (non-synthesis) row. */
42
- synthesis?: {
43
- contract?: number | null;
44
- evidenceMemories?: unknown;
45
- originalCategory?: string;
46
- };
47
- status?: string;
48
- promotedTo?: string;
49
- }
50
-
51
- function row(spec: RowSpec): MemoryEntry {
52
- const metadata: Record<string, unknown> = {
53
- evolution: { status: spec.status ?? "active", version: 1, accessCount: 0 },
54
- };
55
-
56
- if (spec.synthesis) {
57
- metadata.boundary = {
58
- layer: "evidence",
59
- authority: "distillation",
60
- conflictPolicy: "append-only",
61
- originalCategory: spec.synthesis.originalCategory ?? "cases",
62
- note: "Dream-synthesized derivative: a lead to its sources, never authority over them.",
63
- };
64
- if (spec.synthesis.contract !== null) {
65
- metadata.synthesis_contract = spec.synthesis.contract ?? MIN_PROMOTABLE_CONTRACT_VERSION;
66
- }
67
- if (spec.synthesis.evidenceMemories !== undefined) {
68
- metadata.evidenceMemories = spec.synthesis.evidenceMemories;
69
- }
70
- } else {
71
- metadata.boundary = {
72
- layer: "durable",
73
- authority: "structured-memory",
74
- conflictPolicy: "latest-wins",
75
- };
76
- }
77
-
78
- if (spec.promotedTo) metadata.promotedTo = spec.promotedTo;
79
-
80
- return {
81
- id: spec.id,
82
- text: spec.text,
83
- vector: [],
84
- category: (spec.category ?? "cases") as MemoryEntry["category"],
85
- scope: SCOPE,
86
- importance: spec.importance ?? 0.8,
87
- timestamp: Date.parse("2026-08-20T00:00:00.000Z"),
88
- metadata: JSON.stringify(metadata),
89
- };
90
- }
91
-
92
- /** Two live evidence rows every well-formed synthesis in these tests points at. */
93
- const EVIDENCE = [
94
- row({ id: "ev-alpha", text: "第一次踩坑:换机没 push,第二天分叉" }),
95
- row({ id: "ev-beta", text: "第二次:hooks symlink 化之后工作区常态脏" }),
96
- ];
97
-
98
- function buildDeps(rows: MemoryEntry[]): {
99
- deps: SynthesisPromoteScanDeps;
100
- promoted: PromoteRequest[];
101
- } {
102
- const all = [...EVIDENCE, ...rows];
103
- const byId = new Map(all.map((e) => [e.id, e]));
104
- const promoted: PromoteRequest[] = [];
105
-
106
- return {
107
- promoted,
108
- deps: {
109
- store: {
110
- async list() {
111
- return all;
112
- },
113
- async getById(id: string) {
114
- return byId.get(id) ?? null;
115
- },
116
- } as any,
117
- async promote(req) {
118
- promoted.push(req);
119
- return {
120
- id: `durable-${req.memoryId}`,
121
- text: byId.get(req.memoryId)?.text ?? "",
122
- category: req.category,
123
- importance: req.importance,
124
- scope: req.scope,
125
- resolvedScope: req.scope,
126
- canonicalKey: `key:${req.memoryId}`,
127
- storedAt: new Date().toISOString(),
128
- disposition: "stored",
129
- sourceMemoryId: req.memoryId,
130
- sourceCategory: "cases",
131
- } as any;
132
- },
133
- },
134
- };
135
- }
136
-
137
- function goodSynthesis(over: Partial<RowSpec> = {}): MemoryEntry {
138
- return row({
139
- id: "syn-good",
140
- text: "换机交接的失败都同一个形状:脏工作区被当成临时状态,真相源迁走后它变成常态",
141
- importance: 0.85,
142
- ...over,
143
- synthesis: {
144
- contract: MIN_PROMOTABLE_CONTRACT_VERSION,
145
- evidenceMemories: ["ev-alpha", "ev-beta"],
146
- originalCategory: "patterns",
147
- ...(over.synthesis ?? {}),
148
- },
149
- });
150
- }
151
-
152
- // ---------------------------------------------------------------------------
153
-
154
- describe("synthesis promotion: what qualifies", () => {
155
- it("promotes a contract-compliant conclusion with enough live evidence", async () => {
156
- const { deps, promoted } = buildDeps([goodSynthesis()]);
157
-
158
- const result = await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
159
-
160
- expect(result.candidates).toHaveLength(1);
161
- expect(result.promoted).toBe(1);
162
- expect(result.candidates[0]).toMatchObject({
163
- seedId: "syn-good",
164
- category: "patterns",
165
- contractVersion: MIN_PROMOTABLE_CONTRACT_VERSION,
166
- evidenceIds: ["ev-alpha", "ev-beta"],
167
- });
168
- // Promotion goes through the shared promoteMemory path, not a private writer.
169
- expect(promoted).toEqual([
170
- { memoryId: "syn-good", category: "patterns", scope: SCOPE, importance: 0.85 },
171
- ]);
172
- });
173
-
174
- it("defaults to dry-run and writes nothing", async () => {
175
- const { deps, promoted } = buildDeps([goodSynthesis()]);
176
-
177
- const result = await scanSynthesisPromotions(deps, SCOPE);
178
-
179
- expect(DEFAULT_SYNTHESIS_PROMOTE_CONFIG.dryRun).toBe(true);
180
- expect(result.dryRun).toBe(true);
181
- expect(result.candidates).toHaveLength(1);
182
- expect(result.promoted).toBe(0);
183
- expect(promoted).toEqual([]);
184
- expect(result.candidates[0]?.promoted).toBeNull();
185
- });
186
-
187
- it("ignores rows that are not synthesized derivatives", async () => {
188
- // A hand-written durable memory is already on the durable layer; promoting it would be
189
- // meaningless, and promoteMemory itself rejects non-evidence sources.
190
- const { deps } = buildDeps([
191
- row({ id: "handwritten", text: "Alice 用 Bun,不用 npm" }),
192
- goodSynthesis(),
193
- ]);
194
-
195
- const result = await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
196
-
197
- expect(result.scannedSyntheses).toBe(1);
198
- expect(result.candidates.map((c) => c.seedId)).toEqual(["syn-good"]);
199
- });
200
-
201
- it("takes the durable category from the synthesis boundary", async () => {
202
- const { deps, promoted } = buildDeps([
203
- goodSynthesis({ synthesis: { originalCategory: "preferences" } as any }),
204
- ]);
205
-
206
- await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
207
-
208
- expect(promoted[0]?.category).toBe("preferences");
209
- });
210
- });
211
-
212
- describe("synthesis promotion: what it refuses, and says so", () => {
213
- it("skips derivatives written before the synthesis contract", async () => {
214
- // The 4,749 rows already in the database predate validation entirely — they were never
215
- // checked for abstention, evidence, or prompt echo. Promoting them would launder
216
- // unvalidated output into durable memory.
217
- const { deps, promoted } = buildDeps([
218
- goodSynthesis({ id: "syn-legacy", synthesis: { contract: null } as any }),
219
- goodSynthesis({ id: "syn-v1", synthesis: { contract: 1 } as any }),
220
- ]);
221
-
222
- const result = await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
223
-
224
- expect(result.skipped.pre_contract).toBe(2);
225
- expect(result.candidates).toHaveLength(0);
226
- expect(promoted).toEqual([]);
227
- });
228
-
229
- it("abstains when the declared evidence cannot be resolved at all", async () => {
230
- // Not judging is the right answer when the provenance needed for the judgement is
231
- // missing — the same abstention rule the cross-source promotion check uses.
232
- const { deps, promoted } = buildDeps([
233
- goodSynthesis({ id: "syn-dangling", synthesis: { evidenceMemories: ["gone-1", "gone-2"] } as any }),
234
- ]);
235
-
236
- const result = await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
237
-
238
- expect(result.skipped.evidence_unresolvable).toBe(1);
239
- expect(promoted).toEqual([]);
240
- });
241
-
242
- it("abstains when the evidence field is missing or malformed", async () => {
243
- const { deps } = buildDeps([
244
- goodSynthesis({ id: "syn-noevidence", synthesis: { evidenceMemories: undefined } as any }),
245
- goodSynthesis({ id: "syn-badevidence", synthesis: { evidenceMemories: "not-an-array" } as any }),
246
- ]);
247
-
248
- const result = await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
249
-
250
- expect(result.skipped.evidence_unresolvable).toBe(2);
251
- expect(result.candidates).toHaveLength(0);
252
- });
253
-
254
- it("counts duplicate evidence ids once", async () => {
255
- // "Three similar cases" and "three distinct episodes" differ in dimension, not degree;
256
- // the same source listed twice is one source.
257
- const { deps } = buildDeps([
258
- goodSynthesis({ id: "syn-dup", synthesis: { evidenceMemories: ["ev-alpha", "ev-alpha"] } as any }),
259
- ]);
260
-
261
- const result = await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
262
-
263
- expect(result.skipped.too_few_distinct_evidence).toBe(1);
264
- expect(result.candidates).toHaveLength(0);
265
- });
266
-
267
- it("does not count evidence that is no longer active", async () => {
268
- const superseded = row({ id: "ev-gone", text: "已被推翻的旧结论", status: "superseded" });
269
- const { deps } = buildDeps([
270
- superseded,
271
- goodSynthesis({ id: "syn-stale", synthesis: { evidenceMemories: ["ev-alpha", "ev-gone"] } as any }),
272
- ]);
273
-
274
- const result = await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
275
-
276
- // One live source left, below the floor of two.
277
- expect(result.skipped.too_few_distinct_evidence).toBe(1);
278
- expect(result.candidates).toHaveLength(0);
279
- });
280
-
281
- it("skips a conclusion below the importance floor", async () => {
282
- const { deps } = buildDeps([goodSynthesis({ id: "syn-weak", importance: 0.4 })]);
283
-
284
- const result = await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
285
-
286
- expect(result.skipped.below_importance).toBe(1);
287
- expect(result.candidates).toHaveLength(0);
288
- });
289
-
290
- it("skips a conclusion already promoted", async () => {
291
- const { deps } = buildDeps([goodSynthesis({ id: "syn-done", promotedTo: "durable-syn-done" })]);
292
-
293
- const result = await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
294
-
295
- expect(result.skipped.already_promoted).toBe(1);
296
- expect(result.candidates).toHaveLength(0);
297
- });
298
-
299
- it("reports the rejection breakdown even when nothing is promoted", async () => {
300
- // Otherwise "found nothing" and "filtered everything out" read identically.
301
- const { deps } = buildDeps([
302
- goodSynthesis({ id: "syn-legacy", synthesis: { contract: 1 } as any }),
303
- goodSynthesis({ id: "syn-weak", importance: 0.2 }),
304
- ]);
305
-
306
- const text = formatSynthesisPromoteResult(await scanSynthesisPromotions(deps, SCOPE));
307
-
308
- expect(text).toContain("2 synthesized conclusion(s) examined");
309
- expect(text).toContain("pre_contract=1");
310
- expect(text).toContain("below_importance=1");
311
- expect(text).toContain("No promotion candidates found");
312
- });
313
- });
314
-
315
- describe("synthesis promotion: the guarantees around it", () => {
316
- it("never modifies the synthesized row — evidence stays readable as evidence", async () => {
317
- const synthesis = goodSynthesis();
318
- const before = synthesis.metadata;
319
- const { deps } = buildDeps([synthesis]);
320
-
321
- await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
322
-
323
- // Archive-first: the conclusion keeps its evidence boundary and its support set, so a
324
- // later reader can still tell what the durable copy was derived from.
325
- expect(synthesis.metadata).toBe(before);
326
- expect(JSON.parse(synthesis.metadata!).boundary.layer).toBe("evidence");
327
- });
328
-
329
- it("re-scanning promotes the same seed through the same idempotent path", async () => {
330
- // Duplicate suppression lives in promoteMemory -> writeDurableEntry's canonicalKey
331
- // dedup, so a second scan revises rather than piling up a parallel memory. What this
332
- // asserts is that the scan keeps routing through it instead of growing its own writer.
333
- const { deps, promoted } = buildDeps([goodSynthesis()]);
334
-
335
- await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
336
- await scanSynthesisPromotions(deps, SCOPE, { dryRun: false });
337
-
338
- expect(promoted).toHaveLength(2);
339
- expect(new Set(promoted.map((p) => p.memoryId)).size).toBe(1);
340
- });
341
-
342
- it("makes the promoted copy eligible for stable memory while the source stays out", async () => {
343
- // This is the whole point. The synthesized row is refused by
344
- // shouldUseStableMemoryResult; the durable copy promoteMemory writes is not.
345
- const synthesis = goodSynthesis({ synthesis: { originalCategory: "preferences" } as any });
346
-
347
- expect(
348
- shouldUseStableMemoryResult({
349
- category: "preferences",
350
- scope: SCOPE,
351
- metadata: synthesis.metadata,
352
- }),
353
- ).toBe(false);
354
-
355
- const promotedMetadata = JSON.stringify({
356
- evolution: { status: "active", version: 1 },
357
- boundary: { layer: "durable", authority: "structured-memory", conflictPolicy: "latest-wins" },
358
- promotedFrom: { memoryId: synthesis.id, scope: SCOPE, category: "cases" },
359
- });
360
-
361
- expect(
362
- shouldUseStableMemoryResult({
363
- category: "preferences",
364
- scope: SCOPE,
365
- metadata: promotedMetadata,
366
- }),
367
- ).toBe(true);
368
- });
369
- });