recallnest 2.2.1 → 2.5.4

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 (227) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +46 -35
  3. package/README_CN.md +41 -26
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/connectors/examples/email-adapter.ts +73 -0
  9. package/connectors/examples/logs-adapter.ts +103 -0
  10. package/connectors/examples/rss-adapter.ts +95 -0
  11. package/docs/api-reference.md +1291 -0
  12. package/docs/architecture.md +168 -0
  13. package/docs/connector-spec.md +126 -0
  14. package/eval/cases-canary.json +58 -0
  15. package/eval/cases-scope-robustness.json +278 -0
  16. package/eval/cases.json +117 -20
  17. package/package.json +22 -1
  18. package/src/__tests__/access-tracker-novelty.test.ts +238 -0
  19. package/src/__tests__/activity-counter.test.ts +125 -0
  20. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  21. package/src/__tests__/admission-control.test.ts +206 -0
  22. package/src/__tests__/anchor-generator.test.ts +67 -0
  23. package/src/__tests__/auto-consolidation.test.ts +168 -0
  24. package/src/__tests__/auto-recall.test.ts +131 -0
  25. package/src/__tests__/babel-integration.test.ts +144 -0
  26. package/src/__tests__/batch-dedup.test.ts +65 -0
  27. package/src/__tests__/canary-eval.test.ts +114 -0
  28. package/src/__tests__/capture-engine.test.ts +1238 -0
  29. package/src/__tests__/capture-heuristic.test.ts +171 -0
  30. package/src/__tests__/cascade-forget.test.ts +210 -0
  31. package/src/__tests__/category-min-scores.test.ts +33 -0
  32. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  33. package/src/__tests__/circuit-breaker.test.ts +99 -0
  34. package/src/__tests__/cluster-consolidation.test.ts +782 -0
  35. package/src/__tests__/confidence-metadata.test.ts +125 -0
  36. package/src/__tests__/confidence-tracker.test.ts +261 -0
  37. package/src/__tests__/conflict-advisor.test.ts +206 -0
  38. package/src/__tests__/conflict-escalation.test.ts +149 -0
  39. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  40. package/src/__tests__/conflict-output.test.ts +85 -0
  41. package/src/__tests__/conflict-store.test.ts +374 -0
  42. package/src/__tests__/connector-v1.test.ts +181 -0
  43. package/src/__tests__/consolidation-engine.test.ts +333 -0
  44. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  45. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  46. package/src/__tests__/context-composer-pins.test.ts +64 -0
  47. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  48. package/src/__tests__/context-reconstructor.test.ts +65 -0
  49. package/src/__tests__/context-renderer.test.ts +91 -0
  50. package/src/__tests__/continuity-eval.test.ts +275 -0
  51. package/src/__tests__/continuity-schema.test.ts +97 -0
  52. package/src/__tests__/contracts.test.ts +216 -0
  53. package/src/__tests__/conversation-importer.test.ts +692 -0
  54. package/src/__tests__/core-recall-eval.test.ts +235 -0
  55. package/src/__tests__/core-summary.test.ts +109 -0
  56. package/src/__tests__/data-checkup.test.ts +276 -0
  57. package/src/__tests__/decay-exemption.test.ts +84 -0
  58. package/src/__tests__/dedup-actions.test.ts +58 -0
  59. package/src/__tests__/dedup-guard.test.ts +39 -0
  60. package/src/__tests__/deterministic-id.test.ts +39 -0
  61. package/src/__tests__/distill-lock.test.ts +305 -0
  62. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  63. package/src/__tests__/doctor.test.ts +319 -0
  64. package/src/__tests__/dream-pipeline.test.ts +321 -0
  65. package/src/__tests__/drill-down.test.ts +108 -0
  66. package/src/__tests__/embedder.test.ts +122 -0
  67. package/src/__tests__/emotion-decay.test.ts +46 -0
  68. package/src/__tests__/emotion-detector.test.ts +56 -0
  69. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  70. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  71. package/src/__tests__/entity-resolver.test.ts +58 -0
  72. package/src/__tests__/env-config.test.ts +151 -0
  73. package/src/__tests__/error-signature-boost.test.ts +87 -0
  74. package/src/__tests__/error-signature.test.ts +95 -0
  75. package/src/__tests__/error-taxonomy.test.ts +129 -0
  76. package/src/__tests__/ethics-layer.test.ts +476 -0
  77. package/src/__tests__/event-segmenter.test.ts +153 -0
  78. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  79. package/src/__tests__/frequency-tracker.test.ts +113 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +76 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-consolidation.test.ts +226 -0
  88. package/src/__tests__/maintenance-locks.test.ts +174 -0
  89. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  90. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  91. package/src/__tests__/memory-boundaries.test.ts +250 -0
  92. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  93. package/src/__tests__/memory-evolution.test.ts +271 -0
  94. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  95. package/src/__tests__/memory-lint.test.ts +421 -0
  96. package/src/__tests__/memory-output.test.ts +215 -0
  97. package/src/__tests__/memory-promotion.test.ts +315 -0
  98. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  99. package/src/__tests__/multi-vector.test.ts +242 -0
  100. package/src/__tests__/narrative-architecture.test.ts +456 -0
  101. package/src/__tests__/noise-filter.test.ts +205 -0
  102. package/src/__tests__/obsidian-connector.test.ts +363 -0
  103. package/src/__tests__/omitted-reporting.test.ts +98 -0
  104. package/src/__tests__/pending-review.test.ts +77 -0
  105. package/src/__tests__/pii-redact.test.ts +314 -0
  106. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  107. package/src/__tests__/ppr-traversal.test.ts +256 -0
  108. package/src/__tests__/prediction-engine.test.ts +349 -0
  109. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  110. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  111. package/src/__tests__/preference-matcher.test.ts +230 -0
  112. package/src/__tests__/preference-slots.test.ts +205 -0
  113. package/src/__tests__/prospective-memory.test.ts +283 -0
  114. package/src/__tests__/query-entity-detector.test.ts +171 -0
  115. package/src/__tests__/query-expander-alias.test.ts +146 -0
  116. package/src/__tests__/query-expander.test.ts +34 -0
  117. package/src/__tests__/recall-governor.test.ts +227 -0
  118. package/src/__tests__/recall-mode.test.ts +64 -0
  119. package/src/__tests__/result-synthesizer.test.ts +198 -0
  120. package/src/__tests__/resume-context.test.ts +4898 -0
  121. package/src/__tests__/retrieval-trace.test.ts +213 -0
  122. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  123. package/src/__tests__/retroactive-boost.test.ts +223 -0
  124. package/src/__tests__/salience-scoring.test.ts +106 -0
  125. package/src/__tests__/scope-inventory.test.ts +200 -0
  126. package/src/__tests__/scope-policy.test.ts +69 -0
  127. package/src/__tests__/scope-suggester.test.ts +80 -0
  128. package/src/__tests__/session-checkpoint.test.ts +291 -0
  129. package/src/__tests__/session-distiller.test.ts +640 -0
  130. package/src/__tests__/session-gc.test.ts +215 -0
  131. package/src/__tests__/skill-engine.test.ts +552 -0
  132. package/src/__tests__/skill-promotion.test.ts +601 -0
  133. package/src/__tests__/skill-verifier.test.ts +122 -0
  134. package/src/__tests__/source-heartbeat.test.ts +250 -0
  135. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  136. package/src/__tests__/store-scope-audit.test.ts +81 -0
  137. package/src/__tests__/store-upsert.test.ts +184 -0
  138. package/src/__tests__/supersede-chain.test.ts +154 -0
  139. package/src/__tests__/temporal-parser.test.ts +241 -0
  140. package/src/__tests__/temporal-validity.test.ts +160 -0
  141. package/src/__tests__/term-registry.test.ts +121 -0
  142. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  143. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  144. package/src/__tests__/topic-tag.test.ts +113 -0
  145. package/src/__tests__/trusted-memory.test.ts +839 -0
  146. package/src/__tests__/usage-tracker.test.ts +287 -0
  147. package/src/__tests__/version-manager.test.ts +213 -0
  148. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  149. package/src/__tests__/workflow-observation.test.ts +433 -0
  150. package/src/__tests__/write-verifier.test.ts +122 -0
  151. package/src/access-tracker.ts +38 -32
  152. package/src/activity-counter.ts +46 -18
  153. package/src/aliases.ts +133 -0
  154. package/src/api-server.ts +27 -7
  155. package/src/audit-log.ts +4 -2
  156. package/src/auto-consolidation.ts +8 -3
  157. package/src/auto-gc.ts +85 -21
  158. package/src/capture-engine.ts +54 -21
  159. package/src/cli.ts +199 -37
  160. package/src/confidence-tracker.ts +36 -18
  161. package/src/connector-types.ts +57 -0
  162. package/src/consolidation-engine.ts +109 -11
  163. package/src/context-composer.ts +8 -1
  164. package/src/conversation-importer.ts +25 -1
  165. package/src/data-checkup.ts +126 -7
  166. package/src/distill-lock.ts +194 -15
  167. package/src/doctor.ts +38 -2
  168. package/src/dream-pipeline.ts +118 -34
  169. package/src/env-config.ts +77 -0
  170. package/src/error-signature.ts +153 -0
  171. package/src/error-taxonomy.ts +115 -0
  172. package/src/eval.ts +251 -6
  173. package/src/idempotency.ts +8 -0
  174. package/src/ingest.ts +234 -10
  175. package/src/kg-extractor.ts +6 -2
  176. package/src/kg-store.ts +236 -44
  177. package/src/llm-consolidation.ts +2 -1
  178. package/src/mcp-server.ts +85 -1799
  179. package/src/mcp-tool-deps.ts +48 -0
  180. package/src/mcp-tools-advanced.ts +941 -0
  181. package/src/mcp-tools-core.ts +633 -0
  182. package/src/mcp-tools-governance.ts +475 -0
  183. package/src/memory-boundaries.ts +3 -1
  184. package/src/memory-evolution.ts +73 -1
  185. package/src/memory-lint.ts +183 -10
  186. package/src/memory-output.ts +114 -3
  187. package/src/memory-promotion.ts +246 -0
  188. package/src/memory-schema.ts +25 -2
  189. package/src/memory-store-port.ts +43 -0
  190. package/src/multi-vector.ts +2 -1
  191. package/src/narrative-schema.ts +2 -1
  192. package/src/noise-filter.ts +57 -6
  193. package/src/obsidian-connector.ts +282 -0
  194. package/src/pii-detector.ts +157 -5
  195. package/src/query-expander.ts +90 -9
  196. package/src/result-synthesizer.ts +2 -1
  197. package/src/retention-policy.ts +2 -1
  198. package/src/retriever.ts +259 -139
  199. package/src/runtime-config.ts +8 -1
  200. package/src/scope-suggester.ts +113 -0
  201. package/src/session-distiller.ts +23 -2
  202. package/src/session-engine.ts +2 -1
  203. package/src/session-schema.ts +3 -2
  204. package/src/session-store.ts +5 -0
  205. package/src/skill-engine.ts +78 -0
  206. package/src/skill-promotion.ts +247 -47
  207. package/src/skill-schema.ts +16 -3
  208. package/src/skill-verifier.ts +152 -0
  209. package/src/source-heartbeat.ts +108 -0
  210. package/src/store.ts +267 -38
  211. package/src/tool-output-compressor.ts +3 -1
  212. package/src/ui-server.ts +2 -1
  213. package/src/usage-tracker.ts +242 -0
  214. package/src/version-manager.ts +3 -2
  215. package/src/workflow-observation-engine.ts +2 -1
  216. package/src/workflow-observation-schema.ts +2 -1
  217. package/src/workflow-observation-store.ts +6 -2
  218. package/.env +0 -4
  219. package/CLAUDE.md +0 -46
  220. package/config.json +0 -55
  221. package/daily-recall.sh +0 -110
  222. package/logs/daily-recall.error.log +0 -0
  223. package/logs/daily-recall.log +0 -30
  224. package/logs/ingest-2026-04-11.log +0 -110
  225. package/logs/ingest-2026-04-12.log +0 -90
  226. package/logs/launchagent-stderr.log +0 -0
  227. package/logs/launchagent-stdout.log +0 -0
@@ -0,0 +1,151 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import { registerCoreTools } from "../mcp-tools-core.js";
4
+ import type { RetrievalContext, RetrievalResult } from "../retriever.js";
5
+
6
+ function makeResult(id: string, scope: string, text: string): RetrievalResult {
7
+ return {
8
+ entry: {
9
+ id,
10
+ text,
11
+ vector: [1, 0, 0],
12
+ category: "events",
13
+ scope,
14
+ importance: 0.8,
15
+ timestamp: Date.parse("2026-06-26T00:00:00.000Z"),
16
+ metadata: "{}",
17
+ },
18
+ score: 0.9,
19
+ sources: {
20
+ vector: { score: 0.9, rank: 1 },
21
+ fused: { score: 0.9 },
22
+ },
23
+ };
24
+ }
25
+
26
+ function createSearchHarness(scopeRelations: Record<string, string[]> = {}) {
27
+ const handlers = new Map<string, any>();
28
+ const calls: RetrievalContext[] = [];
29
+ const byScope: Record<string, RetrievalResult[]> = {
30
+ "project:alpha": [
31
+ makeResult("alpha-note", "project:alpha", "Alpha owns the default deployment notes."),
32
+ ],
33
+ "project:beta": [
34
+ makeResult("beta-note", "project:beta", "Beta has related deployment follow-up context."),
35
+ ],
36
+ };
37
+
38
+ const store = {
39
+ async vectorSearch() {
40
+ return [];
41
+ },
42
+ async update() {
43
+ return null;
44
+ },
45
+ async getById() {
46
+ return null;
47
+ },
48
+ async stats() {
49
+ return {
50
+ totalCount: 2,
51
+ scopeCounts: {
52
+ "project:alpha": 1,
53
+ "project:beta": 1,
54
+ },
55
+ categoryCounts: {},
56
+ };
57
+ },
58
+ };
59
+
60
+ registerCoreTools({
61
+ registerTool(name: string, _description: string, _schema: unknown, handler: unknown) {
62
+ handlers.set(name, handler);
63
+ },
64
+ getComponents() {
65
+ return {
66
+ store,
67
+ embedder: {
68
+ async embedPassage() {
69
+ return [1, 0, 0];
70
+ },
71
+ },
72
+ retriever: {
73
+ async retrieve(context: RetrievalContext) {
74
+ calls.push(context);
75
+ const scope = context.scopeFilter?.[0] ?? "__all__";
76
+ return byScope[scope] ?? [];
77
+ },
78
+ setKGStore() {},
79
+ setLLMClient() {},
80
+ },
81
+ profile: { name: "default" },
82
+ accessTracker: null,
83
+ frequencyTracker: null,
84
+ llm: null,
85
+ };
86
+ },
87
+ config: {
88
+ dbPath: "data/memory.lance",
89
+ embedding: {
90
+ provider: "openai-compatible",
91
+ apiKey: "test",
92
+ model: "test",
93
+ },
94
+ sources: {},
95
+ scopeRelations,
96
+ },
97
+ checkpointStore: {},
98
+ conflictStore: {},
99
+ workflowObservationStore: {},
100
+ toolDescriptions: new Map(),
101
+ toolTiers: {},
102
+ getKGExtractor: () => null,
103
+ getKGStore: () => null,
104
+ } as any);
105
+
106
+ return {
107
+ calls,
108
+ search: handlers.get("search_memory") as (input: Record<string, unknown>) => Promise<{ content: Array<{ text: string }> }>,
109
+ };
110
+ }
111
+
112
+ describe("search_memory related scope sidecar", () => {
113
+ it("keeps related scopes out of default scoped search", async () => {
114
+ const harness = createSearchHarness({
115
+ "project:alpha": ["project:beta"],
116
+ });
117
+
118
+ const result = await harness.search({
119
+ query: "deployment",
120
+ scope: "project:alpha",
121
+ limit: 5,
122
+ });
123
+
124
+ expect(harness.calls.map(call => call.scopeFilter)).toEqual([["project:alpha"]]);
125
+ expect(result.content[0]?.text).toContain("Alpha owns the default deployment notes.");
126
+ expect(result.content[0]?.text).not.toContain("Beta has related deployment follow-up context.");
127
+ expect(result.content[0]?.text).not.toContain("Related scope results");
128
+ });
129
+
130
+ it("adds related scope results only when explicitly requested", async () => {
131
+ const harness = createSearchHarness({
132
+ "project:alpha": ["project:beta"],
133
+ });
134
+
135
+ const result = await harness.search({
136
+ query: "deployment",
137
+ scope: "project:alpha",
138
+ limit: 5,
139
+ includeRelatedScopes: true,
140
+ });
141
+
142
+ expect(harness.calls.map(call => call.scopeFilter)).toEqual([
143
+ ["project:alpha"],
144
+ ["project:beta"],
145
+ ]);
146
+ expect(result.content[0]?.text).toContain("Alpha owns the default deployment notes.");
147
+ expect(result.content[0]?.text).toContain("Related scope results");
148
+ expect(result.content[0]?.text).toContain("project:beta");
149
+ expect(result.content[0]?.text).toContain("Beta has related deployment follow-up context.");
150
+ });
151
+ });
@@ -0,0 +1,250 @@
1
+ import { describe, expect, it } from "bun:test";
2
+
3
+ import {
4
+ buildStructuredMemoryBoundary,
5
+ buildDefaultCanonicalKey,
6
+ extractBoundaryMetadata,
7
+ extractMemoryProvenance,
8
+ extractPromotedFrom,
9
+ extractProvenanceHistory,
10
+ extractProvenanceHistoryCount,
11
+ resolveIngestBoundary,
12
+ shouldUseStableMemoryResult,
13
+ } from "../memory-boundaries.js";
14
+
15
+ describe("memory boundaries", () => {
16
+ it("downgrades transcript-derived profile facts into evidence events", () => {
17
+ const resolved = resolveIngestBoundary({
18
+ source: "cc",
19
+ scope: "cc:abc123",
20
+ category: "profile",
21
+ });
22
+
23
+ expect(resolved.category).toBe("events");
24
+ expect(resolved.boundary.layer).toBe("evidence");
25
+ expect(resolved.boundary.authority).toBe("transcript-ingest");
26
+ expect(resolved.boundary.downgradedFrom).toBe("profile");
27
+ });
28
+
29
+ it("keeps transcript cases searchable but marks them as evidence", () => {
30
+ const resolved = resolveIngestBoundary({
31
+ source: "codex",
32
+ scope: "codex:session1",
33
+ category: "cases",
34
+ });
35
+
36
+ expect(resolved.category).toBe("cases");
37
+ expect(resolved.boundary.layer).toBe("evidence");
38
+ expect(resolved.boundary.authority).toBe("transcript-ingest");
39
+ expect(resolved.boundary.originalCategory).toBe("cases");
40
+ });
41
+
42
+ it("marks structured memory as durable authority", () => {
43
+ const boundary = buildStructuredMemoryBoundary("preferences");
44
+
45
+ expect(boundary).toEqual({
46
+ layer: "durable",
47
+ authority: "structured-memory",
48
+ conflictPolicy: "latest-wins",
49
+ originalCategory: "preferences",
50
+ note: "Structured memory writes are the durable source inside RecallNest.",
51
+ });
52
+ });
53
+
54
+ it("builds slot-aware canonical keys for atomic brand-item preferences", () => {
55
+ expect(buildDefaultCanonicalKey({
56
+ category: "preferences",
57
+ text: "我喜欢吃麦当劳的麦辣鸡翅",
58
+ })).toBe("preferences:brand-item:麦当劳:麦辣鸡翅");
59
+
60
+ expect(buildDefaultCanonicalKey({
61
+ category: "preferences",
62
+ text: "User prefers concise, direct replies.",
63
+ })).toBe("preferences:reply-style:concise:direct");
64
+
65
+ expect(buildDefaultCanonicalKey({
66
+ category: "preferences",
67
+ text: "Uses Bun over Node.",
68
+ })).toBe("preferences:tool-choice:bun:over:node");
69
+
70
+ expect(buildDefaultCanonicalKey({
71
+ category: "preferences",
72
+ text: "我喜欢吃麦当劳的麦旋风、板烧鸡腿堡和麦辣鸡翅",
73
+ })).toBe("preferences:我喜欢吃麦当劳的麦旋风-板烧鸡腿堡和麦辣鸡翅");
74
+ });
75
+
76
+ it("builds distinct canonical keys for different implicit-usage preferences (#6 regression)", () => {
77
+ // 修复前 implicit-usage slot 无专属分支,落到 tool-choice 三元支、preferredTool/avoidedTool
78
+ // 均 undefined → 全部坍缩成 'preferences:implicit-usage:over',不同隐式偏好在 latest-wins
79
+ // 下互相覆盖丢数据。现在用 slot.subject 区分。
80
+ const figma = buildDefaultCanonicalKey({ category: "preferences", text: "I use Figma for design" });
81
+ const python = buildDefaultCanonicalKey({ category: "preferences", text: "I use Python for scripting" });
82
+ const sony = buildDefaultCanonicalKey({ category: "preferences", text: "我有一台索尼相机" });
83
+
84
+ expect(figma).toBe("preferences:implicit-usage:figma");
85
+ expect(python).toBe("preferences:implicit-usage:python");
86
+ expect(new Set([figma, python, sony]).size).toBe(3);
87
+ });
88
+
89
+ it("rejects transcript/evidence stable recall and keeps durable stable recall", () => {
90
+ expect(shouldUseStableMemoryResult({
91
+ category: "preferences",
92
+ scope: "cc:session",
93
+ metadata: JSON.stringify({
94
+ boundary: {
95
+ layer: "evidence",
96
+ authority: "transcript-ingest",
97
+ conflictPolicy: "append-only",
98
+ },
99
+ }),
100
+ })).toBe(false);
101
+
102
+ expect(shouldUseStableMemoryResult({
103
+ category: "preferences",
104
+ scope: "memory:agent",
105
+ metadata: JSON.stringify({
106
+ boundary: buildStructuredMemoryBoundary("preferences"),
107
+ }),
108
+ })).toBe(true);
109
+ });
110
+
111
+ it("parses valid boundary metadata and ignores malformed payloads", () => {
112
+ expect(extractBoundaryMetadata(JSON.stringify({
113
+ boundary: buildStructuredMemoryBoundary("patterns"),
114
+ }))).toEqual(buildStructuredMemoryBoundary("patterns"));
115
+
116
+ expect(extractBoundaryMetadata("{not-json")).toBeNull();
117
+ expect(extractBoundaryMetadata(JSON.stringify({
118
+ boundary: { layer: "durable", authority: "oops" },
119
+ }))).toBeNull();
120
+ });
121
+
122
+ it("extracts promotedFrom provenance and canonical keys", () => {
123
+ const metadata = JSON.stringify({
124
+ canonicalKey: "user-reply-style",
125
+ boundary: buildStructuredMemoryBoundary("preferences"),
126
+ promotedFrom: {
127
+ memoryId: "12345678-1234-1234-1234-123456789abc",
128
+ scope: "cc:session1",
129
+ category: "events",
130
+ source: "cc",
131
+ boundary: {
132
+ layer: "evidence",
133
+ authority: "transcript-ingest",
134
+ conflictPolicy: "append-only",
135
+ originalCategory: "preferences",
136
+ },
137
+ },
138
+ });
139
+
140
+ expect(extractPromotedFrom(metadata)).toEqual({
141
+ memoryId: "12345678-1234-1234-1234-123456789abc",
142
+ scope: "cc:session1",
143
+ category: "events",
144
+ source: "cc",
145
+ boundary: {
146
+ layer: "evidence",
147
+ authority: "transcript-ingest",
148
+ conflictPolicy: "append-only",
149
+ originalCategory: "preferences",
150
+ },
151
+ });
152
+
153
+ expect(extractProvenanceHistory(metadata)).toEqual([{
154
+ memoryId: "12345678-1234-1234-1234-123456789abc",
155
+ scope: "cc:session1",
156
+ category: "events",
157
+ source: "cc",
158
+ boundary: {
159
+ layer: "evidence",
160
+ authority: "transcript-ingest",
161
+ conflictPolicy: "append-only",
162
+ originalCategory: "preferences",
163
+ },
164
+ }]);
165
+ expect(extractProvenanceHistoryCount(metadata)).toBe(1);
166
+
167
+ expect(extractMemoryProvenance({
168
+ scope: "memory:agent",
169
+ metadata,
170
+ })).toEqual({
171
+ boundary: buildStructuredMemoryBoundary("preferences"),
172
+ canonicalKey: "user-reply-style",
173
+ promotedFrom: {
174
+ memoryId: "12345678-1234-1234-1234-123456789abc",
175
+ scope: "cc:session1",
176
+ category: "events",
177
+ source: "cc",
178
+ boundary: {
179
+ layer: "evidence",
180
+ authority: "transcript-ingest",
181
+ conflictPolicy: "append-only",
182
+ originalCategory: "preferences",
183
+ },
184
+ },
185
+ provenanceHistory: [{
186
+ memoryId: "12345678-1234-1234-1234-123456789abc",
187
+ scope: "cc:session1",
188
+ category: "events",
189
+ source: "cc",
190
+ boundary: {
191
+ layer: "evidence",
192
+ authority: "transcript-ingest",
193
+ conflictPolicy: "append-only",
194
+ originalCategory: "preferences",
195
+ },
196
+ }],
197
+ provenanceHistoryCount: 1,
198
+ });
199
+ });
200
+
201
+ it("prefers explicit provenance history over fallback promotedFrom metadata", () => {
202
+ const metadata = JSON.stringify({
203
+ canonicalKey: "preferences:brand-item:麦当劳:麦辣鸡翅",
204
+ boundary: buildStructuredMemoryBoundary("preferences"),
205
+ promotedFrom: {
206
+ memoryId: "12345678-1234-1234-1234-123456789abc",
207
+ scope: "cc:session1",
208
+ category: "events",
209
+ source: "cc",
210
+ },
211
+ provenanceHistory: [
212
+ {
213
+ memoryId: "aaaaaaaa-1234-1234-1234-123456789abc",
214
+ scope: "cc:session-food-1",
215
+ category: "events",
216
+ source: "cc",
217
+ observedAt: "2026-03-17T04:00:00.000Z",
218
+ },
219
+ {
220
+ memoryId: "bbbbbbbb-1234-1234-1234-123456789abc",
221
+ scope: "cc:session-food-2",
222
+ category: "events",
223
+ source: "codex",
224
+ observedAt: "2026-03-17T05:00:00.000Z",
225
+ },
226
+ ],
227
+ provenanceHistoryCount: 2,
228
+ });
229
+
230
+ expect(extractProvenanceHistory(metadata)).toEqual([
231
+ {
232
+ memoryId: "aaaaaaaa-1234-1234-1234-123456789abc",
233
+ scope: "cc:session-food-1",
234
+ category: "events",
235
+ source: "cc",
236
+ observedAt: "2026-03-17T04:00:00.000Z",
237
+ boundary: null,
238
+ },
239
+ {
240
+ memoryId: "bbbbbbbb-1234-1234-1234-123456789abc",
241
+ scope: "cc:session-food-2",
242
+ category: "events",
243
+ source: "codex",
244
+ observedAt: "2026-03-17T05:00:00.000Z",
245
+ boundary: null,
246
+ },
247
+ ]);
248
+ expect(extractProvenanceHistoryCount(metadata)).toBe(2);
249
+ });
250
+ });