recallnest 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +304 -0
  2. package/README.md +251 -258
  3. package/README_CN.md +226 -247
  4. package/assets/ui/app.js +6 -1
  5. package/bin/recallnest.mjs +20 -37
  6. package/docs/api-reference.md +8 -0
  7. package/docs/architecture.md +18 -13
  8. package/eval/cases-canary.json +107 -9
  9. package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
  10. package/integrations/examples/langchain/memory-chain.py +4 -0
  11. package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
  12. package/package.json +12 -3
  13. package/src/activity-counter.ts +1 -0
  14. package/src/adaptive-retrieval.ts +23 -0
  15. package/src/api-server.ts +13 -11
  16. package/src/asset-sync.ts +26 -2
  17. package/src/audit-log.ts +2 -0
  18. package/src/auto-gc.ts +4 -3
  19. package/src/belief-history.ts +137 -31
  20. package/src/capture-engine.ts +459 -71
  21. package/src/cascade-forget.ts +7 -3
  22. package/src/checkpoint-mirror.ts +417 -0
  23. package/src/cli.ts +574 -54
  24. package/src/conflict-engine.ts +43 -15
  25. package/src/consolidation-engine.ts +24 -3
  26. package/src/context-collapse-renderer.ts +8 -1
  27. package/src/context-composer-stable.ts +5 -1
  28. package/src/context-composer.ts +11 -2
  29. package/src/distill-lock.ts +246 -74
  30. package/src/dream-pipeline.ts +3 -1
  31. package/src/embedder.ts +24 -0
  32. package/src/env-config.ts +136 -0
  33. package/src/flock.ts +97 -0
  34. package/src/forget-engine.ts +342 -58
  35. package/src/gateway-server.ts +4 -0
  36. package/src/ingest-plan.ts +56 -0
  37. package/src/ingest.ts +294 -121
  38. package/src/kg-contradiction.ts +753 -0
  39. package/src/kg-extractor.ts +17 -0
  40. package/src/kg-store.ts +1 -1
  41. package/src/known-item-eval.ts +455 -0
  42. package/src/llm-client.ts +13 -26
  43. package/src/llm-consolidation.ts +2 -2
  44. package/src/mcp-tool-deps.ts +6 -0
  45. package/src/mcp-tools-advanced.ts +80 -45
  46. package/src/mcp-tools-core.ts +23 -17
  47. package/src/mcp-tools-governance.ts +13 -7
  48. package/src/memory-assets.ts +144 -8
  49. package/src/memory-boundaries.ts +19 -0
  50. package/src/memory-evolution.ts +9 -0
  51. package/src/memory-lint.ts +1 -1
  52. package/src/memory-output.ts +311 -14
  53. package/src/memory-reconcile.ts +1437 -0
  54. package/src/memory-schema.ts +11 -0
  55. package/src/memory-store-port.ts +1 -0
  56. package/src/minis-archive.ts +151 -0
  57. package/src/noise-filter.ts +15 -2
  58. package/src/pii-detector.ts +31 -1
  59. package/src/pivot-apply.ts +55 -5
  60. package/src/pivot-evidence.ts +128 -0
  61. package/src/ppr-traversal.ts +25 -1
  62. package/src/preference-matcher.ts +40 -28
  63. package/src/prospective-memory.ts +7 -4
  64. package/src/retriever.ts +297 -57
  65. package/src/retroactive-boost.ts +3 -2
  66. package/src/runtime-config.ts +15 -1
  67. package/src/scope-policy.ts +1 -0
  68. package/src/session-distiller.ts +6 -2
  69. package/src/session-engine.ts +11 -4
  70. package/src/session-output.ts +20 -1
  71. package/src/session-schema.ts +5 -2
  72. package/src/session-store.ts +59 -12
  73. package/src/skill-promotion.ts +67 -1
  74. package/src/store-receipts.ts +109 -0
  75. package/src/store-write-budget.ts +37 -0
  76. package/src/store.ts +298 -57
  77. package/src/text-fingerprint.ts +25 -0
  78. package/src/trigger-store.ts +443 -0
  79. package/src/ui-server.ts +22 -21
  80. package/src/version-manager.ts +19 -5
  81. package/src/write-verifier.ts +16 -11
  82. package/src/__tests__/access-tracker-novelty.test.ts +0 -324
  83. package/src/__tests__/activity-counter.test.ts +0 -125
  84. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  85. package/src/__tests__/admission-control.test.ts +0 -206
  86. package/src/__tests__/age-label.test.ts +0 -33
  87. package/src/__tests__/anchor-generator.test.ts +0 -67
  88. package/src/__tests__/api-server-security.test.ts +0 -102
  89. package/src/__tests__/auto-consolidation.test.ts +0 -168
  90. package/src/__tests__/auto-recall.test.ts +0 -131
  91. package/src/__tests__/babel-integration.test.ts +0 -144
  92. package/src/__tests__/batch-dedup.test.ts +0 -65
  93. package/src/__tests__/belief-history.test.ts +0 -220
  94. package/src/__tests__/canary-eval.test.ts +0 -114
  95. package/src/__tests__/capture-engine.test.ts +0 -1328
  96. package/src/__tests__/capture-heuristic.test.ts +0 -171
  97. package/src/__tests__/cascade-forget.test.ts +0 -210
  98. package/src/__tests__/category-min-scores.test.ts +0 -116
  99. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  100. package/src/__tests__/circuit-breaker.test.ts +0 -99
  101. package/src/__tests__/cluster-consolidation.test.ts +0 -1236
  102. package/src/__tests__/confidence-metadata.test.ts +0 -125
  103. package/src/__tests__/confidence-tracker.test.ts +0 -261
  104. package/src/__tests__/conflict-advisor.test.ts +0 -206
  105. package/src/__tests__/conflict-escalation.test.ts +0 -149
  106. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  107. package/src/__tests__/conflict-output.test.ts +0 -85
  108. package/src/__tests__/conflict-store.test.ts +0 -418
  109. package/src/__tests__/connector-v1.test.ts +0 -181
  110. package/src/__tests__/consolidation-engine.test.ts +0 -520
  111. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  112. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  113. package/src/__tests__/context-composer-pins.test.ts +0 -64
  114. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  115. package/src/__tests__/context-reconstructor.test.ts +0 -65
  116. package/src/__tests__/context-renderer.test.ts +0 -91
  117. package/src/__tests__/continuity-eval.test.ts +0 -275
  118. package/src/__tests__/continuity-schema.test.ts +0 -97
  119. package/src/__tests__/contracts.test.ts +0 -262
  120. package/src/__tests__/conversation-importer.test.ts +0 -692
  121. package/src/__tests__/core-recall-eval.test.ts +0 -235
  122. package/src/__tests__/core-summary.test.ts +0 -109
  123. package/src/__tests__/data-checkup.test.ts +0 -304
  124. package/src/__tests__/decay-exemption.test.ts +0 -129
  125. package/src/__tests__/dedup-actions.test.ts +0 -58
  126. package/src/__tests__/dedup-guard.test.ts +0 -39
  127. package/src/__tests__/deterministic-id.test.ts +0 -39
  128. package/src/__tests__/distill-lock.test.ts +0 -305
  129. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  130. package/src/__tests__/doctor.test.ts +0 -319
  131. package/src/__tests__/dream-pipeline.test.ts +0 -820
  132. package/src/__tests__/dream-scope-isolation.test.ts +0 -386
  133. package/src/__tests__/dream-write-batching.test.ts +0 -211
  134. package/src/__tests__/drill-down.test.ts +0 -108
  135. package/src/__tests__/embedder.test.ts +0 -122
  136. package/src/__tests__/emotion-decay.test.ts +0 -46
  137. package/src/__tests__/emotion-detector.test.ts +0 -56
  138. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  139. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  140. package/src/__tests__/entity-resolver.test.ts +0 -58
  141. package/src/__tests__/env-config-invariant.test.ts +0 -80
  142. package/src/__tests__/env-config.test.ts +0 -211
  143. package/src/__tests__/error-signature-boost.test.ts +0 -87
  144. package/src/__tests__/error-signature.test.ts +0 -95
  145. package/src/__tests__/error-taxonomy.test.ts +0 -129
  146. package/src/__tests__/ethics-layer.test.ts +0 -476
  147. package/src/__tests__/event-segmenter.test.ts +0 -153
  148. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  149. package/src/__tests__/frequency-tracker.test.ts +0 -113
  150. package/src/__tests__/freshness.test.ts +0 -186
  151. package/src/__tests__/graph-export.test.ts +0 -489
  152. package/src/__tests__/hotness-blend.test.ts +0 -283
  153. package/src/__tests__/ingest-chunking.test.ts +0 -44
  154. package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
  155. package/src/__tests__/ingest.test.ts +0 -360
  156. package/src/__tests__/interference-detector.test.ts +0 -203
  157. package/src/__tests__/kg-extractor.test.ts +0 -192
  158. package/src/__tests__/kg-store.test.ts +0 -300
  159. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  160. package/src/__tests__/llm-consolidation.test.ts +0 -226
  161. package/src/__tests__/maintenance-locks.test.ts +0 -197
  162. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  163. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  164. package/src/__tests__/memory-boundaries.test.ts +0 -282
  165. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
  166. package/src/__tests__/memory-evolution.test.ts +0 -271
  167. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  168. package/src/__tests__/memory-lint.test.ts +0 -421
  169. package/src/__tests__/memory-output.test.ts +0 -224
  170. package/src/__tests__/memory-promotion.test.ts +0 -315
  171. package/src/__tests__/memory-utility.test.ts +0 -256
  172. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  173. package/src/__tests__/multi-vector.test.ts +0 -242
  174. package/src/__tests__/narrative-architecture.test.ts +0 -456
  175. package/src/__tests__/noise-filter.test.ts +0 -205
  176. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  177. package/src/__tests__/obsidian-connector.test.ts +0 -363
  178. package/src/__tests__/omitted-reporting.test.ts +0 -98
  179. package/src/__tests__/openai-contract.test.ts +0 -513
  180. package/src/__tests__/pending-review.test.ts +0 -77
  181. package/src/__tests__/pii-redact.test.ts +0 -333
  182. package/src/__tests__/pivot-apply.test.ts +0 -373
  183. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  184. package/src/__tests__/pivot-distill.test.ts +0 -1178
  185. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  186. package/src/__tests__/ppr-traversal.test.ts +0 -256
  187. package/src/__tests__/prediction-engine.test.ts +0 -349
  188. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  189. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  190. package/src/__tests__/preference-matcher.test.ts +0 -282
  191. package/src/__tests__/preference-slots.test.ts +0 -205
  192. package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
  193. package/src/__tests__/prospective-memory.test.ts +0 -283
  194. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  195. package/src/__tests__/query-entity-detector.test.ts +0 -171
  196. package/src/__tests__/query-expander-alias.test.ts +0 -146
  197. package/src/__tests__/query-expander.test.ts +0 -34
  198. package/src/__tests__/recall-gate.test.ts +0 -266
  199. package/src/__tests__/recall-governor.test.ts +0 -227
  200. package/src/__tests__/recall-mode.test.ts +0 -64
  201. package/src/__tests__/resolve-db-path.test.ts +0 -130
  202. package/src/__tests__/resolve-tier.test.ts +0 -64
  203. package/src/__tests__/result-synthesizer.test.ts +0 -198
  204. package/src/__tests__/resume-context.test.ts +0 -4898
  205. package/src/__tests__/retrieval-eval.test.ts +0 -163
  206. package/src/__tests__/retrieval-trace.test.ts +0 -213
  207. package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
  208. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  209. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  210. package/src/__tests__/retroactive-boost.test.ts +0 -223
  211. package/src/__tests__/salience-scoring.test.ts +0 -106
  212. package/src/__tests__/scope-inventory.test.ts +0 -200
  213. package/src/__tests__/scope-policy.test.ts +0 -69
  214. package/src/__tests__/scope-suggester.test.ts +0 -80
  215. package/src/__tests__/session-checkpoint.test.ts +0 -291
  216. package/src/__tests__/session-distiller.test.ts +0 -640
  217. package/src/__tests__/session-gc.test.ts +0 -215
  218. package/src/__tests__/skill-engine.test.ts +0 -552
  219. package/src/__tests__/skill-promotion.test.ts +0 -654
  220. package/src/__tests__/skill-verifier.test.ts +0 -122
  221. package/src/__tests__/source-heartbeat.test.ts +0 -254
  222. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  223. package/src/__tests__/store-patch-metadata.test.ts +0 -235
  224. package/src/__tests__/store-read-consistency.test.ts +0 -122
  225. package/src/__tests__/store-schema-migration.test.ts +0 -87
  226. package/src/__tests__/store-scope-audit.test.ts +0 -81
  227. package/src/__tests__/store-upsert.test.ts +0 -201
  228. package/src/__tests__/supersede-chain.test.ts +0 -154
  229. package/src/__tests__/synthesis-contract.test.ts +0 -252
  230. package/src/__tests__/synthesis-promotion.test.ts +0 -369
  231. package/src/__tests__/temporal-parser.test.ts +0 -241
  232. package/src/__tests__/temporal-validity.test.ts +0 -160
  233. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  234. package/src/__tests__/term-registry.test.ts +0 -121
  235. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  236. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  237. package/src/__tests__/topic-tag.test.ts +0 -113
  238. package/src/__tests__/trusted-memory.test.ts +0 -867
  239. package/src/__tests__/ui-server-security.test.ts +0 -93
  240. package/src/__tests__/usage-tracker.test.ts +0 -287
  241. package/src/__tests__/utility-weight.test.ts +0 -57
  242. package/src/__tests__/verbatim-self-recall.test.ts +0 -279
  243. package/src/__tests__/version-manager.test.ts +0 -248
  244. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  245. package/src/__tests__/workflow-observation.test.ts +0 -433
  246. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -1,373 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import {
4
- PIVOT_APPLY_CAPTURE,
5
- PIVOT_APPLY_SOURCE,
6
- PIVOT_SCOPE,
7
- PivotCandidateInputSchema,
8
- persistPivotBatch,
9
- persistPivotCandidate,
10
- } from "../pivot-apply.js";
11
-
12
- const REPORT_ID = "00b609834309b7b44d1e36fc7c93169e083abb66487073bfd64d8c5bafc337fa";
13
-
14
- function candHash(seed: string): string {
15
- // Deterministic 64-hex filler for tests — content-addressing is the runner's job.
16
- return seed.repeat(64).slice(0, 64);
17
- }
18
-
19
- function makeCandidate(overrides: Record<string, unknown> = {}): Record<string, unknown> {
20
- return {
21
- kind: "preference_rule",
22
- text: "用户确立了外部项目学习的标准化工作流:分析链接后必须记录到 learnings 并同步更新借鉴审计目录。",
23
- anchor: "你要记住了,以后都是,甩链接,学习,记录。",
24
- canonicalKey: "pivot-preference-rule-learning-audit-workflow",
25
- evidence: ["你要记住了,以后都是,甩链接,学习,记录,如果借鉴了就要记到审计这里。"],
26
- proposedScope: PIVOT_SCOPE,
27
- proposedCategory: "preferences",
28
- tags: ["src:bb8af0b7", "date:2026-04-12", "harness:claude-code", "raw:claude"],
29
- sessionId: "bb8af0b7-cc58-4bb2-8e35-9744537e5854",
30
- reportId: REPORT_ID,
31
- candidateHash: candHash("a"),
32
- batchId: "test-batch-1",
33
- importance: 0.7,
34
- ...overrides,
35
- };
36
- }
37
-
38
- function createDeps() {
39
- const storedEntries: Array<Record<string, unknown>> = [];
40
- const auditLines: Array<Record<string, unknown>> = [];
41
- let seq = 1;
42
-
43
- const store = {
44
- async store(entry: Record<string, unknown>) {
45
- const stored = {
46
- ...entry,
47
- id: `00000000-0000-0000-0000-${String(seq).padStart(12, "0")}`,
48
- timestamp: 1_700_000_000_000 + seq,
49
- };
50
- seq += 1;
51
- storedEntries.push(stored);
52
- return stored;
53
- },
54
- async list(_scopeFilter?: string[], category?: string, limit = 20, offset = 0) {
55
- return storedEntries
56
- .filter((entry) => !category || entry.category === category)
57
- .slice(offset, offset + limit);
58
- },
59
- async update(id: string, updates: Record<string, unknown>) {
60
- const index = storedEntries.findIndex((entry) => entry.id === id);
61
- if (index < 0) return null;
62
- storedEntries[index] = { ...storedEntries[index], ...updates };
63
- return storedEntries[index];
64
- },
65
- async upsert(entry: Record<string, unknown>) {
66
- const index = storedEntries.findIndex((item) => item.id === entry.id);
67
- if (index >= 0) storedEntries[index] = { ...entry };
68
- else storedEntries.push({ ...entry });
69
- return entry;
70
- },
71
- async get(id: string) {
72
- return storedEntries.find((entry) => entry.id === id) ?? null;
73
- },
74
- };
75
-
76
- return {
77
- storedEntries,
78
- auditLines,
79
- deps: {
80
- store,
81
- embedder: {
82
- async embedPassage(text: string) {
83
- return [text.length, 1, 0];
84
- },
85
- },
86
- auditLogger: {
87
- log(line: Record<string, unknown>) {
88
- auditLines.push(line);
89
- },
90
- },
91
- },
92
- };
93
- }
94
-
95
- describe("PivotCandidateInputSchema", () => {
96
- it("accepts a corpus-shaped candidate", () => {
97
- expect(() => PivotCandidateInputSchema.parse(makeCandidate())).not.toThrow();
98
- });
99
-
100
- it("rejects kind/category mismatch (corpus mapping violated)", () => {
101
- expect(() =>
102
- PivotCandidateInputSchema.parse(makeCandidate({ kind: "case", proposedCategory: "events" })),
103
- ).toThrow();
104
- });
105
-
106
- it("rejects a wrong scope, missing identity keys, and malformed hashes", () => {
107
- expect(() => PivotCandidateInputSchema.parse(makeCandidate({ proposedScope: "memory:other" }))).toThrow();
108
- const missing = makeCandidate();
109
- delete missing.candidateHash;
110
- expect(() => PivotCandidateInputSchema.parse(missing)).toThrow();
111
- expect(() => PivotCandidateInputSchema.parse(makeCandidate({ reportId: "not-hex" }))).toThrow();
112
- });
113
- });
114
-
115
- describe("persistPivotCandidate", () => {
116
- it("stores a clean candidate with session_distill identity and full pivot metadata", async () => {
117
- const { deps, storedEntries, auditLines } = createDeps();
118
- const outcome = await persistPivotCandidate(deps as never, makeCandidate());
119
-
120
- expect(outcome.disposition).toBe("stored");
121
- expect(outcome.memoryId).not.toBeNull();
122
- expect(outcome.verified).toBe(true);
123
-
124
- expect(storedEntries.length).toBe(1);
125
- const meta = JSON.parse(String(storedEntries[0].metadata)) as Record<string, unknown>;
126
- expect(meta.source).toBe(PIVOT_APPLY_SOURCE);
127
- expect(meta.capture).toBe(PIVOT_APPLY_CAPTURE);
128
- expect(meta.anchor).toBe("你要记住了,以后都是,甩链接,学习,记录。");
129
- expect((meta.confidence as Record<string, unknown>).score).toBe(0.6);
130
- expect((meta.confidence as Record<string, unknown>).reliability).toBe("inferred");
131
- const pivotApply = meta.pivotApply as Record<string, unknown>;
132
- expect(pivotApply.kind).toBe("preference_rule");
133
- expect(pivotApply.reportId).toBe(REPORT_ID);
134
- expect(pivotApply.candidateHash).toBe(candHash("a"));
135
- expect(pivotApply.batchId).toBe("test-batch-1");
136
- expect(pivotApply.sessionId).toBe("bb8af0b7-cc58-4bb2-8e35-9744537e5854");
137
- expect(pivotApply.evidence).toEqual([
138
- "你要记住了,以后都是,甩链接,学习,记录,如果借鉴了就要记到审计这里。",
139
- ]);
140
- const tags = meta.tags as string[];
141
- expect(tags).toContain("pivot-apply");
142
- expect(tags).toContain("batch:test-batch-1");
143
- expect((meta.evolution as Record<string, unknown>).status).toBe("active");
144
- expect((meta.boundary as Record<string, unknown>).layer).toBe("durable");
145
-
146
- expect(auditLines.some((line) => line.operation === "store")).toBe(true);
147
- });
148
-
149
- it("runs without any LLM in deps (the channel must be LLM-free)", async () => {
150
- const { deps } = createDeps();
151
- // deps deliberately has no llm / kgExtractor / rateLimiter keys at all.
152
- const outcome = await persistPivotCandidate(deps as never, makeCandidate());
153
- expect(outcome.disposition).toBe("stored");
154
- });
155
-
156
- it("dedupes an identical re-apply (same key, same normalized text) without writing twice", async () => {
157
- const { deps, storedEntries } = createDeps();
158
- const first = await persistPivotCandidate(deps as never, makeCandidate());
159
- const second = await persistPivotCandidate(deps as never, makeCandidate({ candidateHash: candHash("b") }));
160
-
161
- expect(first.disposition).toBe("stored");
162
- expect(second.disposition).toBe("deduped");
163
- expect(second.memoryId).toBe(first.memoryId);
164
- expect(storedEntries.length).toBe(1);
165
- });
166
-
167
- it("supersedes on same-key different-text for preferences (latest-wins revision)", async () => {
168
- const { deps, storedEntries } = createDeps();
169
- const first = await persistPivotCandidate(deps as never, makeCandidate());
170
- const second = await persistPivotCandidate(
171
- deps as never,
172
- makeCandidate({
173
- candidateHash: candHash("c"),
174
- text: "修订版:外部项目学习工作流新增 hippo-wiki 归档要求,三写缺一不可。",
175
- }),
176
- );
177
-
178
- expect(first.disposition).toBe("stored");
179
- expect(second.disposition).toBe("updated");
180
- // latest-wins updates the canonical row in place: same id stays live with V2
181
- // text, while V1 is copied out into a separate superseded history row.
182
- expect(second.memoryId).toBe(first.memoryId);
183
- const canonicalRow = storedEntries.find((entry) => entry.id === first.memoryId);
184
- expect(String(canonicalRow?.text)).toContain("修订版");
185
- const historyRow = storedEntries.find((entry) => {
186
- if (entry.id === first.memoryId) return false;
187
- try {
188
- const meta = JSON.parse(String(entry.metadata)) as Record<string, unknown>;
189
- return (meta.evolution as Record<string, unknown> | undefined)?.status === "superseded";
190
- } catch {
191
- return false;
192
- }
193
- });
194
- expect(historyRow).toBeDefined();
195
- });
196
-
197
- it("appends (stores) on same-key different-text for cases — append-only category", async () => {
198
- const { deps, storedEntries } = createDeps();
199
- const base = {
200
- kind: "case",
201
- proposedCategory: "cases",
202
- canonicalKey: "cases-append-only-probe",
203
- };
204
- const first = await persistPivotCandidate(
205
- deps as never,
206
- makeCandidate({ ...base, candidateHash: candHash("d"), text: "Case: 第一次踩坑的完整记录,包含问题与解法细节。" }),
207
- );
208
- const second = await persistPivotCandidate(
209
- deps as never,
210
- makeCandidate({ ...base, candidateHash: candHash("e"), text: "Case: 第二次同主题踩坑,但情境不同,应作为新行追加保存。" }),
211
- );
212
-
213
- expect(first.disposition).toBe("stored");
214
- expect(second.disposition).toBe("stored");
215
- expect(storedEntries.filter((e) => e.category === "cases").length).toBe(2);
216
- });
217
-
218
- it("reports conflict (and writes nothing) when the key collides across categories", async () => {
219
- const conflictRecords: Array<Record<string, unknown>> = [];
220
- const { deps, storedEntries } = createDeps();
221
- const depsWithConflict = {
222
- ...deps,
223
- conflictStore: {
224
- async save(record: Record<string, unknown>) {
225
- conflictRecords.push(record);
226
- return record;
227
- },
228
- async getOpenByFingerprint() {
229
- return null;
230
- },
231
- async getLatestByFingerprint() {
232
- return null;
233
- },
234
- async replace(record: Record<string, unknown>) {
235
- conflictRecords.push(record);
236
- return record;
237
- },
238
- },
239
- };
240
- const first = await persistPivotCandidate(
241
- depsWithConflict as never,
242
- makeCandidate({ canonicalKey: "pivot-cross-category-probe" }),
243
- );
244
- const crossCategory = await persistPivotCandidate(
245
- depsWithConflict as never,
246
- makeCandidate({
247
- kind: "decision",
248
- proposedCategory: "events",
249
- canonicalKey: "pivot-cross-category-probe",
250
- candidateHash: candHash("f"),
251
- text: "决策:同一个 canonicalKey 被另一类目引用,应触发 conflict 而非写入。",
252
- }),
253
- );
254
-
255
- expect(first.disposition).toBe("stored");
256
- expect(crossCategory.disposition).toBe("conflict");
257
- expect(crossCategory.conflictId).toBeDefined();
258
- expect(storedEntries.length).toBe(1);
259
- });
260
-
261
- it("retries transient 429 embedding errors and succeeds", async () => {
262
- const { deps } = createDeps();
263
- let calls = 0;
264
- const flakyDeps = {
265
- ...deps,
266
- embedder: {
267
- async embedPassage(text: string) {
268
- calls++;
269
- if (calls === 1) throw new Error("Request failed with status 429: rate limit exceeded");
270
- return [text.length, 1, 0];
271
- },
272
- },
273
- };
274
- const outcome = await persistPivotCandidate(flakyDeps as never, makeCandidate());
275
- expect(outcome.disposition).toBe("stored");
276
- expect(calls).toBe(2);
277
- });
278
-
279
- it("fails closed when the embedder returns an empty vector (store never validates shape)", async () => {
280
- const { deps, storedEntries } = createDeps();
281
- const emptyDeps = {
282
- ...deps,
283
- embedder: {
284
- async embedPassage() {
285
- return [] as number[];
286
- },
287
- },
288
- };
289
- await expect(persistPivotCandidate(emptyDeps as never, makeCandidate())).rejects.toThrow(/empty vector/);
290
- expect(storedEntries.length).toBe(0);
291
- });
292
-
293
- it("rejects noise via rule-based admission with a null memoryId and an audit line", async () => {
294
- const { deps, storedEntries, auditLines } = createDeps();
295
- const outcome = await persistPivotCandidate(
296
- deps as never,
297
- makeCandidate({ text: "好的好的,收到收到。", candidateHash: candHash("9") }),
298
- );
299
-
300
- expect(outcome.disposition).toBe("rejected");
301
- expect(outcome.memoryId).toBeNull();
302
- expect(outcome.rejectionReason).toBeDefined();
303
- expect(storedEntries.length).toBe(0);
304
- expect(auditLines.some((line) => line.operation === "reject")).toBe(true);
305
- });
306
- });
307
-
308
- describe("persistPivotBatch", () => {
309
- it("writes a clean batch fully and reports written = total", async () => {
310
- const { deps } = createDeps();
311
- const batch = [
312
- makeCandidate({ candidateHash: candHash("1"), canonicalKey: "pivot-batch-key-1" }),
313
- makeCandidate({
314
- candidateHash: candHash("2"),
315
- canonicalKey: "pivot-batch-key-2",
316
- text: "第二条独立候选:批内不同 key 不同文本,应正常写入。",
317
- }),
318
- ];
319
- const report = await persistPivotBatch(deps as never, "test-batch-1", batch);
320
-
321
- expect(report.halted).toBe(false);
322
- expect(report.total).toBe(2);
323
- expect(report.written).toBe(2);
324
- expect(report.outcomes.every((o) => o.disposition === "stored")).toBe(true);
325
- });
326
-
327
- it("halts fail-closed on the first unexpected disposition and keeps prior outcomes", async () => {
328
- const { deps, storedEntries } = createDeps();
329
- const batch = [
330
- makeCandidate({ candidateHash: candHash("1"), canonicalKey: "pivot-batch-key-1" }),
331
- // Same key + same text as row 1 → deduped → unexpected → halt.
332
- makeCandidate({ candidateHash: candHash("2"), canonicalKey: "pivot-batch-key-1" }),
333
- makeCandidate({ candidateHash: candHash("3"), canonicalKey: "pivot-batch-key-3" }),
334
- ];
335
- const report = await persistPivotBatch(deps as never, "test-batch-1", batch);
336
-
337
- expect(report.halted).toBe(true);
338
- expect(report.haltReason).toContain("deduped");
339
- expect(report.outcomes.length).toBe(2);
340
- expect(report.written).toBe(1);
341
- // Third candidate must never have been attempted.
342
- expect(storedEntries.length).toBe(1);
343
- });
344
-
345
- it("halts before writing when a candidate claims a different batchId", async () => {
346
- const { deps, storedEntries } = createDeps();
347
- const report = await persistPivotBatch(deps as never, "test-batch-1", [
348
- makeCandidate({ batchId: "other-batch" }),
349
- ]);
350
-
351
- expect(report.halted).toBe(true);
352
- expect(report.haltReason).toContain("other-batch");
353
- expect(report.outcomes.length).toBe(0);
354
- expect(storedEntries.length).toBe(0);
355
- });
356
-
357
- it("halts when post-write verification fails (store.get returns null)", async () => {
358
- const { deps } = createDeps();
359
- const brokenDeps = {
360
- ...deps,
361
- store: {
362
- ...deps.store,
363
- async get() {
364
- return null;
365
- },
366
- },
367
- };
368
- const report = await persistPivotBatch(brokenDeps as never, "test-batch-1", [makeCandidate()]);
369
-
370
- expect(report.halted).toBe(true);
371
- expect(report.haltReason).toContain("verification failed");
372
- });
373
- });