recallnest 1.4.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 (261) hide show
  1. package/.claude-plugin/CLAUDE.md +29 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.env.example +3 -0
  4. package/.github/workflows/ci.yml +28 -0
  5. package/.mcp.json +9 -0
  6. package/CHANGELOG.md +51 -0
  7. package/CLAUDE.md +42 -0
  8. package/DEMO.md +94 -0
  9. package/EVAL.md +89 -0
  10. package/FAILURES.md +75 -0
  11. package/LICENSE +21 -0
  12. package/OPERATIONS.md +59 -0
  13. package/README.md +427 -0
  14. package/README_CN.md +425 -0
  15. package/ROADMAP.md +198 -0
  16. package/assets/recallnest-workbench.png +0 -0
  17. package/assets/ui/app.js +807 -0
  18. package/assets/ui/index.html +154 -0
  19. package/assets/ui/styles.css +605 -0
  20. package/assets/wechat-qr.jpg +0 -0
  21. package/config.json.example +38 -0
  22. package/daily-recall.sh +87 -0
  23. package/daily-reflections/2026-03-05.md +15 -0
  24. package/daily-reflections/2026-03-06.md +15 -0
  25. package/daily-reflections/2026-03-07.md +7 -0
  26. package/daily-reflections/2026-03-08.md +34 -0
  27. package/daily-reflections/2026-03-09.md +35 -0
  28. package/daily-reflections/2026-03-10.md +36 -0
  29. package/daily-reflections/2026-03-11.md +35 -0
  30. package/daily-reflections/2026-03-12.md +36 -0
  31. package/daily-reflections/2026-03-13.md +35 -0
  32. package/daily-reflections/2026-03-14.md +35 -0
  33. package/daily-reflections/2026-03-15.md +35 -0
  34. package/daily-reflections/2026-03-16.md +41 -0
  35. package/daily-reflections/2026-03-17.md +10 -0
  36. package/daily-reflections/2026-03-18.md +10 -0
  37. package/daily-reflections/2026-03-19.md +10 -0
  38. package/daily-reflections/2026-03-20.md +10 -0
  39. package/daily-reflections/2026-03-21.md +10 -0
  40. package/daily-reflections/2026-03-22.md +10 -0
  41. package/daily-reflections/2026-03-23.md +10 -0
  42. package/daily-reflections/2026-03-24.md +10 -0
  43. package/daily-reflections/2026-03-25.md +32 -0
  44. package/daily-reflections/2026-03-26.md +37 -0
  45. package/daily-reflections/2026-03-27.md +35 -0
  46. package/daily-reflections/2026-03-28.md +35 -0
  47. package/daily-reflections/2026-03-29.md +35 -0
  48. package/daily-reflections/2026-03-30.md +36 -0
  49. package/daily-reflections/2026-03-31.md +35 -0
  50. package/daily-reflections/2026-04-01.md +35 -0
  51. package/docs/api-reference.md +1291 -0
  52. package/docs/architecture.md +168 -0
  53. package/docs/memory-boundary-contract.md +226 -0
  54. package/docs/memory-categories.md +119 -0
  55. package/docs/pipeline-audit-ultramemory.md +177 -0
  56. package/docs/plans/2026-03-08-query-expansion.md +270 -0
  57. package/docs/plans/2026-03-16-continuity-layer.md +196 -0
  58. package/docs/plans/mcp-tiering-plan.md +126 -0
  59. package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
  60. package/docs/self-evolution.md +198 -0
  61. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
  62. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
  63. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
  64. package/eval/cases.json +85 -0
  65. package/eval/continuity/case-seeds.json +90 -0
  66. package/eval/continuity/cases.json +302 -0
  67. package/eval/continuity/memory-seeds.json +26 -0
  68. package/eval/continuity/pattern-seeds.json +100 -0
  69. package/eval/continuity/results-2026-03-16.md +117 -0
  70. package/eval/continuity/results-2026-03-19.md +453 -0
  71. package/eval/continuity/runbook.md +110 -0
  72. package/eval/continuity/seed-cases.md +54 -0
  73. package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
  74. package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
  75. package/eval/reports/2026-03-06-baseline.md +82 -0
  76. package/eval/reports/query-expansion.md +138 -0
  77. package/integrations/README.md +57 -0
  78. package/integrations/claude-code/README.md +133 -0
  79. package/integrations/claude-code/claude-md-snippet.md +31 -0
  80. package/integrations/claude-code/setup.sh +66 -0
  81. package/integrations/claude-code/smoke-continuity.sh +452 -0
  82. package/integrations/codex/README.md +49 -0
  83. package/integrations/codex/agents-md-snippet.md +31 -0
  84. package/integrations/codex/setup.sh +58 -0
  85. package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
  86. package/integrations/examples/langchain/memory-chain.py +111 -0
  87. package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
  88. package/integrations/gemini-cli/README.md +57 -0
  89. package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
  90. package/integrations/gemini-cli/setup.sh +64 -0
  91. package/integrations/shared/install-managed-block.sh +51 -0
  92. package/lm +19 -0
  93. package/package.json +50 -0
  94. package/plan-v1.2-distributable.md +105 -0
  95. package/scripts/archive-distilled-junk.ts +116 -0
  96. package/scripts/backfill-l0l1l2.ts +98 -0
  97. package/scripts/blind-scan.ts +588 -0
  98. package/scripts/cleanup-step1-exact-dups.ts +117 -0
  99. package/scripts/cleanup-step2-template-dups.ts +113 -0
  100. package/scripts/cleanup-step3-stale-content.ts +149 -0
  101. package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
  102. package/scripts/delete-distilled-junk.ts +88 -0
  103. package/scripts/distill-facts.ts +305 -0
  104. package/scripts/health-check.ts +343 -0
  105. package/scripts/incremental-ingest.sh +71 -0
  106. package/scripts/mlp-health-check.ts +382 -0
  107. package/scripts/pull-claude-desktop.ts +515 -0
  108. package/scripts/re-ingest-cc.sh +66 -0
  109. package/scripts/rebalance-memory-health.ts +181 -0
  110. package/scripts/run-desktop-ingest.sh +11 -0
  111. package/scripts/start-server.sh +12 -0
  112. package/scripts/viz-dashboard.html +423 -0
  113. package/scripts/viz-export.ts +703 -0
  114. package/src/__tests__/access-tracker-novelty.test.ts +228 -0
  115. package/src/__tests__/auto-consolidation.test.ts +168 -0
  116. package/src/__tests__/auto-recall.test.ts +131 -0
  117. package/src/__tests__/batch-dedup.test.ts +65 -0
  118. package/src/__tests__/capture-engine.test.ts +1142 -0
  119. package/src/__tests__/capture-heuristic.test.ts +171 -0
  120. package/src/__tests__/cascade-forget.test.ts +210 -0
  121. package/src/__tests__/category-min-scores.test.ts +33 -0
  122. package/src/__tests__/confidence-tracker.test.ts +196 -0
  123. package/src/__tests__/conflict-advisor.test.ts +206 -0
  124. package/src/__tests__/conflict-escalation.test.ts +149 -0
  125. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  126. package/src/__tests__/conflict-output.test.ts +85 -0
  127. package/src/__tests__/conflict-store.test.ts +374 -0
  128. package/src/__tests__/consolidation-engine.test.ts +181 -0
  129. package/src/__tests__/context-composer-pins.test.ts +64 -0
  130. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  131. package/src/__tests__/context-renderer.test.ts +91 -0
  132. package/src/__tests__/continuity-eval.test.ts +275 -0
  133. package/src/__tests__/continuity-schema.test.ts +97 -0
  134. package/src/__tests__/core-recall-eval.test.ts +235 -0
  135. package/src/__tests__/core-summary.test.ts +109 -0
  136. package/src/__tests__/dedup-actions.test.ts +58 -0
  137. package/src/__tests__/deterministic-id.test.ts +39 -0
  138. package/src/__tests__/doctor.test.ts +303 -0
  139. package/src/__tests__/drill-down.test.ts +108 -0
  140. package/src/__tests__/embedder.test.ts +122 -0
  141. package/src/__tests__/entity-resolver.test.ts +58 -0
  142. package/src/__tests__/event-segmenter.test.ts +153 -0
  143. package/src/__tests__/hotness-blend.test.ts +76 -0
  144. package/src/__tests__/ingest.test.ts +360 -0
  145. package/src/__tests__/kg-extractor.test.ts +184 -0
  146. package/src/__tests__/kg-store.test.ts +198 -0
  147. package/src/__tests__/llm-consolidation.test.ts +226 -0
  148. package/src/__tests__/memory-boundaries.test.ts +237 -0
  149. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  150. package/src/__tests__/memory-output.test.ts +215 -0
  151. package/src/__tests__/multi-vector.test.ts +242 -0
  152. package/src/__tests__/ppr-traversal.test.ts +256 -0
  153. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  154. package/src/__tests__/preference-matcher.test.ts +230 -0
  155. package/src/__tests__/preference-slots.test.ts +106 -0
  156. package/src/__tests__/prospective-memory.test.ts +283 -0
  157. package/src/__tests__/query-entity-detector.test.ts +171 -0
  158. package/src/__tests__/query-expander.test.ts +34 -0
  159. package/src/__tests__/recall-mode.test.ts +64 -0
  160. package/src/__tests__/result-synthesizer.test.ts +198 -0
  161. package/src/__tests__/resume-context.test.ts +4743 -0
  162. package/src/__tests__/retrieval-trace.test.ts +213 -0
  163. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  164. package/src/__tests__/retroactive-boost.test.ts +223 -0
  165. package/src/__tests__/scope-inventory.test.ts +200 -0
  166. package/src/__tests__/scope-policy.test.ts +69 -0
  167. package/src/__tests__/session-checkpoint.test.ts +259 -0
  168. package/src/__tests__/session-gc.test.ts +215 -0
  169. package/src/__tests__/store-scope-audit.test.ts +81 -0
  170. package/src/__tests__/temporal-parser.test.ts +241 -0
  171. package/src/__tests__/term-registry.test.ts +121 -0
  172. package/src/__tests__/tool-output-compressor.test.ts +133 -0
  173. package/src/__tests__/version-manager.test.ts +213 -0
  174. package/src/__tests__/workflow-observation.test.ts +400 -0
  175. package/src/access-tracker.ts +316 -0
  176. package/src/adaptive-retrieval.ts +64 -0
  177. package/src/api-server.ts +667 -0
  178. package/src/asset-sync.ts +79 -0
  179. package/src/auto-consolidation.ts +133 -0
  180. package/src/auto-gc.ts +124 -0
  181. package/src/auto-recall.ts +95 -0
  182. package/src/capture-engine.ts +931 -0
  183. package/src/capture-heuristic.ts +177 -0
  184. package/src/cascade-forget.ts +135 -0
  185. package/src/chunker.ts +253 -0
  186. package/src/cli.ts +1917 -0
  187. package/src/confidence-tracker.ts +166 -0
  188. package/src/conflict-advisor.ts +433 -0
  189. package/src/conflict-engine.ts +181 -0
  190. package/src/conflict-escalation.ts +138 -0
  191. package/src/conflict-lifecycle.ts +67 -0
  192. package/src/conflict-output.ts +218 -0
  193. package/src/conflict-schema.ts +130 -0
  194. package/src/conflict-store.ts +127 -0
  195. package/src/consolidation-engine.ts +256 -0
  196. package/src/context-composer-pins.ts +183 -0
  197. package/src/context-composer-queries.ts +121 -0
  198. package/src/context-composer-scope.ts +56 -0
  199. package/src/context-composer-stable-selection.ts +200 -0
  200. package/src/context-composer-stable.ts +217 -0
  201. package/src/context-composer-task-fallbacks.ts +36 -0
  202. package/src/context-composer-task-ranking.ts +197 -0
  203. package/src/context-composer-task-results.ts +137 -0
  204. package/src/context-composer-task-selection.ts +342 -0
  205. package/src/context-composer-text.ts +57 -0
  206. package/src/context-composer.ts +300 -0
  207. package/src/context-renderer.ts +179 -0
  208. package/src/decay-engine.ts +189 -0
  209. package/src/doctor.ts +543 -0
  210. package/src/embedder.ts +557 -0
  211. package/src/entity-resolver.ts +131 -0
  212. package/src/eval.ts +632 -0
  213. package/src/event-segmenter.ts +216 -0
  214. package/src/ingest.ts +1822 -0
  215. package/src/kg-extractor.ts +217 -0
  216. package/src/kg-store.ts +353 -0
  217. package/src/legacy-scope-review.ts +53 -0
  218. package/src/llm-client.ts +629 -0
  219. package/src/llm-consolidation.ts +168 -0
  220. package/src/mcp-server.ts +1337 -0
  221. package/src/memory-assets.ts +475 -0
  222. package/src/memory-boundaries.ts +354 -0
  223. package/src/memory-health-rebalance.ts +220 -0
  224. package/src/memory-output.ts +474 -0
  225. package/src/memory-schema.ts +184 -0
  226. package/src/multi-vector.ts +234 -0
  227. package/src/noise-filter.ts +155 -0
  228. package/src/ppr-traversal.ts +279 -0
  229. package/src/preference-matcher.ts +174 -0
  230. package/src/preference-slots.ts +259 -0
  231. package/src/prospective-memory.ts +198 -0
  232. package/src/query-entity-detector.ts +207 -0
  233. package/src/query-expander.ts +98 -0
  234. package/src/result-synthesizer.ts +114 -0
  235. package/src/retrieval-profiles.ts +101 -0
  236. package/src/retrieval-trace.ts +94 -0
  237. package/src/retriever.ts +1338 -0
  238. package/src/retroactive-boost.ts +154 -0
  239. package/src/rif.ts +73 -0
  240. package/src/runtime-config.ts +172 -0
  241. package/src/schema-utils.ts +56 -0
  242. package/src/scope-inventory.ts +310 -0
  243. package/src/scope-policy.ts +142 -0
  244. package/src/session-engine.ts +168 -0
  245. package/src/session-output.ts +96 -0
  246. package/src/session-schema.ts +60 -0
  247. package/src/session-store.ts +293 -0
  248. package/src/stderr-log.ts +16 -0
  249. package/src/store.ts +903 -0
  250. package/src/temporal-parser.ts +318 -0
  251. package/src/term-registry.ts +791 -0
  252. package/src/tool-output-compressor.ts +115 -0
  253. package/src/tracker.ts +57 -0
  254. package/src/ui-server.ts +430 -0
  255. package/src/version-manager.ts +172 -0
  256. package/src/workflow-observation-engine.ts +316 -0
  257. package/src/workflow-observation-managed.ts +83 -0
  258. package/src/workflow-observation-output.ts +86 -0
  259. package/src/workflow-observation-schema.ts +87 -0
  260. package/src/workflow-observation-scope-review.ts +409 -0
  261. package/src/workflow-observation-store.ts +69 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Confidence Tracker — Viewpoint confidence scoring.
3
+ *
4
+ * Agent-First reasoning: users correct AI all the time ("not Python,
5
+ * TypeScript"). The original wrong memory and the correction both exist
6
+ * in the store. Without confidence tracking, both compete equally in
7
+ * retrieval. Confidence scoring ensures corrections outrank originals,
8
+ * and confirmed facts outrank unverified ones.
9
+ *
10
+ * Brain-science label: "Hindsight bias" — later information reshapes
11
+ * how earlier memories are weighted. We keep the name for branding;
12
+ * the real mechanism is a float field + simple update rules.
13
+ *
14
+ * Confidence semantics:
15
+ * - 1.0 = explicitly confirmed by user
16
+ * - 0.7 = default (unverified)
17
+ * - 0.3 = superseded/corrected
18
+ * - 0.0 = explicitly contradicted
19
+ *
20
+ * Retrieval integration: confidence multiplies into the score pipeline
21
+ * as `score *= (0.5 + 0.5 * confidence)` — same pattern as importance.
22
+ */
23
+
24
+ import type { MemoryStore, MemoryEntry } from "./store.js";
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Constants
28
+ // ---------------------------------------------------------------------------
29
+
30
+ export const CONFIDENCE_DEFAULT = 0.7;
31
+ export const CONFIDENCE_CONFIRMED = 1.0;
32
+ export const CONFIDENCE_CORRECTED = 0.3;
33
+ export const CONFIDENCE_CONTRADICTED = 0.0;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Helpers
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function parseMeta(entry: MemoryEntry): Record<string, any> {
40
+ try { return JSON.parse(entry.metadata || "{}"); } catch { return {}; }
41
+ }
42
+
43
+ /**
44
+ * Read confidence from metadata. Returns default (0.7) if not set.
45
+ */
46
+ export function getConfidence(entry: MemoryEntry): number {
47
+ const meta = parseMeta(entry);
48
+ return typeof meta.confidence === "number" ? meta.confidence : CONFIDENCE_DEFAULT;
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Core operations
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export interface ConfidenceUpdate {
56
+ entryId: string;
57
+ oldConfidence: number;
58
+ newConfidence: number;
59
+ }
60
+
61
+ /**
62
+ * Mark a memory as confirmed (user validated it).
63
+ * Bumps confidence to CONFIRMED (1.0).
64
+ */
65
+ export async function confirmMemory(
66
+ store: MemoryStore,
67
+ entryId: string,
68
+ scope: string,
69
+ ): Promise<ConfidenceUpdate | null> {
70
+ const entry = await store.getById(entryId);
71
+ if (!entry) return null;
72
+
73
+ const meta = parseMeta(entry);
74
+ const oldConfidence = typeof meta.confidence === "number" ? meta.confidence : CONFIDENCE_DEFAULT;
75
+
76
+ meta.confidence = CONFIDENCE_CONFIRMED;
77
+ if (!Array.isArray(meta.confidence_history)) meta.confidence_history = [];
78
+ meta.confidence_history.push({
79
+ action: "confirmed",
80
+ from: oldConfidence,
81
+ to: CONFIDENCE_CONFIRMED,
82
+ date: new Date().toISOString().slice(0, 10),
83
+ });
84
+
85
+ await store.update(entryId, { metadata: JSON.stringify(meta) }, [scope]);
86
+
87
+ return { entryId, oldConfidence, newConfidence: CONFIDENCE_CONFIRMED };
88
+ }
89
+
90
+ /**
91
+ * Mark a memory as corrected (user provided updated info).
92
+ * Drops confidence to CORRECTED (0.3).
93
+ */
94
+ export async function correctMemory(
95
+ store: MemoryStore,
96
+ entryId: string,
97
+ scope: string,
98
+ correctedById?: string,
99
+ ): Promise<ConfidenceUpdate | null> {
100
+ const entry = await store.getById(entryId);
101
+ if (!entry) return null;
102
+
103
+ const meta = parseMeta(entry);
104
+ const oldConfidence = typeof meta.confidence === "number" ? meta.confidence : CONFIDENCE_DEFAULT;
105
+
106
+ meta.confidence = CONFIDENCE_CORRECTED;
107
+ if (correctedById) meta.corrected_by = correctedById;
108
+ if (!Array.isArray(meta.confidence_history)) meta.confidence_history = [];
109
+ meta.confidence_history.push({
110
+ action: "corrected",
111
+ from: oldConfidence,
112
+ to: CONFIDENCE_CORRECTED,
113
+ ...(correctedById ? { correctedBy: correctedById.slice(0, 8) } : {}),
114
+ date: new Date().toISOString().slice(0, 10),
115
+ });
116
+
117
+ await store.update(entryId, { metadata: JSON.stringify(meta) }, [scope]);
118
+
119
+ return { entryId, oldConfidence, newConfidence: CONFIDENCE_CORRECTED };
120
+ }
121
+
122
+ /**
123
+ * Mark a memory as contradicted (explicitly wrong).
124
+ * Drops confidence to CONTRADICTED (0.0).
125
+ */
126
+ export async function contradictMemory(
127
+ store: MemoryStore,
128
+ entryId: string,
129
+ scope: string,
130
+ ): Promise<ConfidenceUpdate | null> {
131
+ const entry = await store.getById(entryId);
132
+ if (!entry) return null;
133
+
134
+ const meta = parseMeta(entry);
135
+ const oldConfidence = typeof meta.confidence === "number" ? meta.confidence : CONFIDENCE_DEFAULT;
136
+
137
+ meta.confidence = CONFIDENCE_CONTRADICTED;
138
+ if (!Array.isArray(meta.confidence_history)) meta.confidence_history = [];
139
+ meta.confidence_history.push({
140
+ action: "contradicted",
141
+ from: oldConfidence,
142
+ to: CONFIDENCE_CONTRADICTED,
143
+ date: new Date().toISOString().slice(0, 10),
144
+ });
145
+
146
+ await store.update(entryId, { metadata: JSON.stringify(meta) }, [scope]);
147
+
148
+ return { entryId, oldConfidence, newConfidence: CONFIDENCE_CONTRADICTED };
149
+ }
150
+
151
+ /**
152
+ * Apply confidence weighting to retrieval scores.
153
+ * Formula: score *= (0.5 + 0.5 * confidence)
154
+ *
155
+ * At confidence=1.0 → ×1.0 (no penalty)
156
+ * At confidence=0.7 → ×0.85 (slight penalty)
157
+ * At confidence=0.3 → ×0.65 (significant penalty)
158
+ * At confidence=0.0 → ×0.50 (heavy penalty)
159
+ */
160
+ export function applyConfidenceWeight(
161
+ score: number,
162
+ entry: MemoryEntry,
163
+ ): number {
164
+ const confidence = getConfidence(entry);
165
+ return score * (0.5 + 0.5 * confidence);
166
+ }
@@ -0,0 +1,433 @@
1
+ import type { ConflictCandidateRecord, ConflictReason } from "./conflict-schema.js";
2
+ import { summarizeConflictLifecycle, type ConflictAttention, CONFLICT_ATTENTION_LEVELS } from "./conflict-lifecycle.js";
3
+ import { normalizeCanonicalKey } from "./memory-boundaries.js";
4
+
5
+ export const CONFLICT_SUGGESTED_RESOLUTIONS = [
6
+ "keep_existing",
7
+ "accept_incoming",
8
+ "manual_review",
9
+ ] as const;
10
+
11
+ export const CONFLICT_ADVICE_CONFIDENCES = [
12
+ "low",
13
+ "medium",
14
+ "high",
15
+ ] as const;
16
+
17
+ export type ConflictSuggestedResolution = (typeof CONFLICT_SUGGESTED_RESOLUTIONS)[number];
18
+ export type ConflictAdviceConfidence = (typeof CONFLICT_ADVICE_CONFIDENCES)[number];
19
+
20
+ export interface ConflictAdviceSummary {
21
+ suggestedResolution: ConflictSuggestedResolution;
22
+ confidence: ConflictAdviceConfidence;
23
+ similarity: number;
24
+ clusterKey: string;
25
+ clusterLabel: string;
26
+ mergeSuggestion?: string;
27
+ reasons: string[];
28
+ }
29
+
30
+ export interface ConflictClusterSummary {
31
+ clusterKey: string;
32
+ clusterLabel: string;
33
+ canonicalKey: string;
34
+ category: string;
35
+ reason: ConflictReason;
36
+ totalCount: number;
37
+ openCount: number;
38
+ latestUpdatedAt: string;
39
+ latestConflictId: string;
40
+ attention: ConflictAttention;
41
+ attentionCounts: Record<ConflictAttention, number>;
42
+ suggestedResolution: ConflictSuggestedResolution;
43
+ confidence: ConflictAdviceConfidence;
44
+ }
45
+
46
+ export interface ConflictAuditSummary {
47
+ totalConflicts: number;
48
+ totalClusters: number;
49
+ openConflicts: number;
50
+ openClusters: number;
51
+ attentionCounts: Record<ConflictAttention, number>;
52
+ priorityClusters: ConflictClusterSummary[];
53
+ suggestedActions: string[];
54
+ }
55
+
56
+ function compactConflictText(text: string): string {
57
+ return text
58
+ .normalize("NFKC")
59
+ .toLowerCase()
60
+ .replace(/[\p{P}\p{S}\s]+/gu, "");
61
+ }
62
+
63
+ function buildCharacterNgrams(text: string): Set<string> {
64
+ if (text.length <= 3) {
65
+ return new Set(text ? [text] : []);
66
+ }
67
+
68
+ const grams = new Set<string>();
69
+ for (let i = 0; i <= text.length - 3; i += 1) {
70
+ grams.add(text.slice(i, i + 3));
71
+ }
72
+ return grams;
73
+ }
74
+
75
+ function calculateTextSimilarity(existingText: string, incomingText: string): number {
76
+ if (!existingText || !incomingText) return 0;
77
+ if (existingText === incomingText) return 1;
78
+
79
+ const existing = buildCharacterNgrams(existingText);
80
+ const incoming = buildCharacterNgrams(incomingText);
81
+ if (existing.size === 0 || incoming.size === 0) return 0;
82
+
83
+ let intersection = 0;
84
+ for (const gram of existing) {
85
+ if (incoming.has(gram)) {
86
+ intersection += 1;
87
+ }
88
+ }
89
+
90
+ const union = new Set([...existing, ...incoming]).size;
91
+ return union === 0 ? 0 : intersection / union;
92
+ }
93
+
94
+ function roundSimilarity(value: number): number {
95
+ return Math.round(value * 100) / 100;
96
+ }
97
+
98
+ function stripTrailingClausePunctuation(text: string): string {
99
+ return text.trim().replace(/[。!?!?;;,,]+$/gu, "").trim();
100
+ }
101
+
102
+ function splitConflictClauses(text: string): string[] {
103
+ const primary = text
104
+ .split(/[。!?!?;;\n]+/u)
105
+ .map(stripTrailingClausePunctuation)
106
+ .filter(Boolean);
107
+ if (primary.length > 1) return primary;
108
+
109
+ return text
110
+ .split(/[,,]+/u)
111
+ .map(stripTrailingClausePunctuation)
112
+ .filter(Boolean);
113
+ }
114
+
115
+ function buildMergedClauseList(existingText: string, incomingText: string): string[] {
116
+ const merged: Array<{ raw: string; normalized: string }> = [];
117
+
118
+ const pushClause = (clause: string) => {
119
+ const raw = stripTrailingClausePunctuation(clause);
120
+ const normalized = compactConflictText(raw);
121
+ if (!normalized) return;
122
+
123
+ const overlappingIndex = merged.findIndex((item) =>
124
+ item.normalized === normalized
125
+ || item.normalized.includes(normalized)
126
+ || normalized.includes(item.normalized),
127
+ );
128
+
129
+ if (overlappingIndex >= 0) {
130
+ const existing = merged[overlappingIndex];
131
+ if (existing && raw.length > existing.raw.length) {
132
+ merged[overlappingIndex] = { raw, normalized };
133
+ }
134
+ return;
135
+ }
136
+
137
+ merged.push({ raw, normalized });
138
+ };
139
+
140
+ for (const clause of splitConflictClauses(existingText)) {
141
+ pushClause(clause);
142
+ }
143
+ for (const clause of splitConflictClauses(incomingText)) {
144
+ pushClause(clause);
145
+ }
146
+
147
+ return merged.map((item) => item.raw);
148
+ }
149
+
150
+ function hasChinese(text: string): boolean {
151
+ return /[\u3400-\u9fff]/u.test(text);
152
+ }
153
+
154
+ function joinMergedClauses(clauses: string[], preferChinese: boolean): string | undefined {
155
+ if (clauses.length === 0) return undefined;
156
+ if (clauses.length === 1) return clauses[0];
157
+
158
+ if (preferChinese) {
159
+ return `${clauses.join(";")}。`;
160
+ }
161
+
162
+ return `${clauses.join(". ")}.`;
163
+ }
164
+
165
+ function buildMergeSuggestion(
166
+ record: ConflictCandidateRecord,
167
+ similarity: number,
168
+ suggestedResolution: ConflictSuggestedResolution,
169
+ ): string | undefined {
170
+ if (record.reason !== "promotion_conflicts_with_existing_durable") return undefined;
171
+ if (record.existing.category !== record.incoming.category) return undefined;
172
+ if (suggestedResolution !== "manual_review") return undefined;
173
+ if (similarity < 0.35 || similarity >= 0.98) return undefined;
174
+
175
+ const clauses = buildMergedClauseList(record.existing.text, record.incoming.text);
176
+ const merged = joinMergedClauses(
177
+ clauses,
178
+ hasChinese(record.existing.text) || hasChinese(record.incoming.text),
179
+ );
180
+ if (!merged) return undefined;
181
+
182
+ const mergedCompact = compactConflictText(merged);
183
+ const existingCompact = compactConflictText(record.existing.text);
184
+ const incomingCompact = compactConflictText(record.incoming.text);
185
+ if (!mergedCompact || mergedCompact === existingCompact || mergedCompact === incomingCompact) {
186
+ return undefined;
187
+ }
188
+
189
+ const maxSourceLength = Math.max(record.existing.text.length, record.incoming.text.length);
190
+ if (merged.length > maxSourceLength + 180) {
191
+ return undefined;
192
+ }
193
+
194
+ return merged;
195
+ }
196
+
197
+ export function buildConflictClusterKey(record: ConflictCandidateRecord): string {
198
+ const canonicalKey = normalizeCanonicalKey(record.canonicalKey) || "na";
199
+ return [
200
+ canonicalKey,
201
+ record.reason,
202
+ record.category,
203
+ ].join("::").slice(0, 240);
204
+ }
205
+
206
+ function pickClusterAttention(
207
+ attentionCounts: Record<ConflictAttention, number>,
208
+ openCount: number,
209
+ ): ConflictAttention {
210
+ if (openCount <= 0) return "resolved";
211
+ if (attentionCounts.escalated > 0) return "escalated";
212
+ if (attentionCounts.stale > 0) return "stale";
213
+ if (attentionCounts.aging > 0) return "aging";
214
+ return "fresh";
215
+ }
216
+
217
+ function attentionRank(attention: ConflictAttention): number {
218
+ switch (attention) {
219
+ case "escalated":
220
+ return 5;
221
+ case "stale":
222
+ return 4;
223
+ case "aging":
224
+ return 3;
225
+ case "fresh":
226
+ return 2;
227
+ case "resolved":
228
+ default:
229
+ return 1;
230
+ }
231
+ }
232
+
233
+ export function summarizeConflictAdvice(record: ConflictCandidateRecord): ConflictAdviceSummary {
234
+ const lifecycle = summarizeConflictLifecycle(record);
235
+ const existingCompact = compactConflictText(record.existing.text);
236
+ const incomingCompact = compactConflictText(record.incoming.text);
237
+ const similarity = roundSimilarity(calculateTextSimilarity(existingCompact, incomingCompact));
238
+ const clusterKey = buildConflictClusterKey(record);
239
+ const clusterLabel = `${record.category} / ${record.canonicalKey}`;
240
+ const reasons: string[] = [];
241
+
242
+ let suggestedResolution: ConflictSuggestedResolution = "manual_review";
243
+ let confidence: ConflictAdviceConfidence = "low";
244
+
245
+ if (existingCompact && incomingCompact && existingCompact === incomingCompact) {
246
+ suggestedResolution = "keep_existing";
247
+ confidence = "high";
248
+ reasons.push("Incoming text is equivalent to the existing durable memory.");
249
+ } else if (
250
+ record.reason === "canonical_key_conflicts_with_existing_durable" &&
251
+ record.existing.category !== record.incoming.category
252
+ ) {
253
+ suggestedResolution = "keep_existing";
254
+ confidence = lifecycle.reopenCount > 0 ? "high" : "medium";
255
+ reasons.push("The canonical key is already owned by another durable category.");
256
+ } else if (
257
+ record.reason === "promotion_conflicts_with_existing_durable" &&
258
+ record.existing.category === record.incoming.category &&
259
+ similarity >= 0.72
260
+ ) {
261
+ if (incomingCompact.length + 2 < existingCompact.length) {
262
+ suggestedResolution = "accept_incoming";
263
+ confidence = "medium";
264
+ reasons.push("Incoming text looks like a tighter rewrite of the same durable memory.");
265
+ } else if (existingCompact.length + 2 < incomingCompact.length) {
266
+ suggestedResolution = "keep_existing";
267
+ confidence = "medium";
268
+ reasons.push("Existing durable text already looks like the tighter rewrite.");
269
+ } else {
270
+ suggestedResolution = "manual_review";
271
+ confidence = "low";
272
+ reasons.push("Both texts are very similar, but neither is clearly the better durable phrasing.");
273
+ }
274
+ } else if (similarity < 0.35) {
275
+ suggestedResolution = "manual_review";
276
+ confidence = lifecycle.needsAttention ? "high" : "medium";
277
+ reasons.push("Existing and incoming texts diverge materially and need human review.");
278
+ } else {
279
+ reasons.push("Conflict needs an operator decision before updating the durable owner.");
280
+ }
281
+
282
+ const mergeSuggestion = buildMergeSuggestion(record, similarity, suggestedResolution);
283
+ if (mergeSuggestion) {
284
+ reasons.push("A merged durable wording suggestion is available for manual review.");
285
+ }
286
+
287
+ if (lifecycle.attention === "stale" || lifecycle.attention === "escalated") {
288
+ reasons.push(`Conflict is ${lifecycle.attention} and should be reviewed soon.`);
289
+ } else if (lifecycle.reopenCount > 0) {
290
+ reasons.push("This conflict has reopened before; verify the prior decision still holds.");
291
+ }
292
+
293
+ return {
294
+ suggestedResolution,
295
+ confidence,
296
+ similarity,
297
+ clusterKey,
298
+ clusterLabel,
299
+ ...(mergeSuggestion ? { mergeSuggestion } : {}),
300
+ reasons,
301
+ };
302
+ }
303
+
304
+ export function clusterConflicts(records: ConflictCandidateRecord[]): ConflictClusterSummary[] {
305
+ const grouped = new Map<string, {
306
+ latest: ConflictCandidateRecord;
307
+ totalCount: number;
308
+ openCount: number;
309
+ attentionCounts: Record<ConflictAttention, number>;
310
+ }>();
311
+
312
+ for (const record of records) {
313
+ const key = buildConflictClusterKey(record);
314
+ const existing = grouped.get(key);
315
+ const lifecycle = summarizeConflictLifecycle(record);
316
+ if (!existing) {
317
+ grouped.set(key, {
318
+ latest: record,
319
+ totalCount: 1,
320
+ openCount: record.status === "open" ? 1 : 0,
321
+ attentionCounts: Object.fromEntries(
322
+ CONFLICT_ATTENTION_LEVELS.map((attention) => [attention, attention === lifecycle.attention ? 1 : 0]),
323
+ ) as Record<ConflictAttention, number>,
324
+ });
325
+ continue;
326
+ }
327
+
328
+ existing.totalCount += 1;
329
+ if (record.status === "open") {
330
+ existing.openCount += 1;
331
+ }
332
+ existing.attentionCounts[lifecycle.attention] += 1;
333
+ if (Date.parse(record.updatedAt) >= Date.parse(existing.latest.updatedAt)) {
334
+ existing.latest = record;
335
+ }
336
+ }
337
+
338
+ return [...grouped.entries()]
339
+ .map(([clusterKey, group]) => {
340
+ const advice = summarizeConflictAdvice(group.latest);
341
+ return {
342
+ clusterKey,
343
+ clusterLabel: advice.clusterLabel,
344
+ canonicalKey: group.latest.canonicalKey,
345
+ category: group.latest.category,
346
+ reason: group.latest.reason,
347
+ totalCount: group.totalCount,
348
+ openCount: group.openCount,
349
+ latestUpdatedAt: group.latest.updatedAt,
350
+ latestConflictId: group.latest.conflictId,
351
+ attention: pickClusterAttention(group.attentionCounts, group.openCount),
352
+ attentionCounts: group.attentionCounts,
353
+ suggestedResolution: advice.suggestedResolution,
354
+ confidence: advice.confidence,
355
+ };
356
+ })
357
+ .sort((a, b) => {
358
+ const attentionDiff = attentionRank(b.attention) - attentionRank(a.attention);
359
+ if (attentionDiff !== 0) return attentionDiff;
360
+ if (b.openCount !== a.openCount) return b.openCount - a.openCount;
361
+ const timeDiff = Date.parse(b.latestUpdatedAt) - Date.parse(a.latestUpdatedAt);
362
+ if (timeDiff !== 0) return timeDiff;
363
+ return a.clusterKey.localeCompare(b.clusterKey);
364
+ });
365
+ }
366
+
367
+ export function buildConflictAuditSummary(
368
+ records: ConflictCandidateRecord[],
369
+ top = 5,
370
+ ): ConflictAuditSummary {
371
+ const clusters = clusterConflicts(records);
372
+ const attentionCounts = Object.fromEntries(
373
+ CONFLICT_ATTENTION_LEVELS.map((attention) => [attention, 0]),
374
+ ) as Record<ConflictAttention, number>;
375
+
376
+ for (const record of records) {
377
+ const lifecycle = summarizeConflictLifecycle(record);
378
+ attentionCounts[lifecycle.attention] += 1;
379
+ }
380
+
381
+ const openClusters = clusters.filter((cluster) => cluster.openCount > 0);
382
+ const priorityClusters = openClusters
383
+ .slice()
384
+ .sort((a, b) => {
385
+ const attentionDiff = attentionRank(b.attention) - attentionRank(a.attention);
386
+ if (attentionDiff !== 0) return attentionDiff;
387
+ if (b.openCount !== a.openCount) return b.openCount - a.openCount;
388
+ const timeDiff = Date.parse(b.latestUpdatedAt) - Date.parse(a.latestUpdatedAt);
389
+ if (timeDiff !== 0) return timeDiff;
390
+ return a.clusterKey.localeCompare(b.clusterKey);
391
+ })
392
+ .slice(0, Math.max(1, top));
393
+
394
+ const suggestedActions: string[] = [];
395
+ const escalatedClusters = openClusters.filter((cluster) => cluster.attention === "escalated");
396
+ const staleClusters = openClusters.filter((cluster) => cluster.attention === "stale");
397
+ const highConfidenceKeeps = openClusters.filter(
398
+ (cluster) => cluster.confidence === "high" && cluster.suggestedResolution === "keep_existing",
399
+ );
400
+ const highConfidenceAccepts = openClusters.filter(
401
+ (cluster) => cluster.confidence === "high" && cluster.suggestedResolution === "accept_incoming",
402
+ );
403
+
404
+ if (openClusters.length === 0) {
405
+ suggestedActions.push("No open conflicts need review.");
406
+ } else {
407
+ if (escalatedClusters.length > 0) {
408
+ suggestedActions.push(`Resolve ${escalatedClusters.length} escalated cluster(s) first.`);
409
+ }
410
+ if (staleClusters.length > 0) {
411
+ suggestedActions.push(`Review ${staleClusters.length} stale cluster(s) before they keep reopening.`);
412
+ }
413
+ if (highConfidenceKeeps.length > 0) {
414
+ suggestedActions.push(`${highConfidenceKeeps.length} open cluster(s) look safe to keep_existing.`);
415
+ }
416
+ if (highConfidenceAccepts.length > 0) {
417
+ suggestedActions.push(`${highConfidenceAccepts.length} open cluster(s) look safe to accept_incoming.`);
418
+ }
419
+ if (suggestedActions.length === 0) {
420
+ suggestedActions.push("Open conflicts remain, but they still need manual review.");
421
+ }
422
+ }
423
+
424
+ return {
425
+ totalConflicts: records.length,
426
+ totalClusters: clusters.length,
427
+ openConflicts: records.filter((record) => record.status === "open").length,
428
+ openClusters: openClusters.length,
429
+ attentionCounts,
430
+ priorityClusters,
431
+ suggestedActions,
432
+ };
433
+ }