@danielmarbach/mnemonic-mcp 0.27.2 → 0.29.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 (250) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +7 -0
  3. package/build/auto-relate.d.ts.map +1 -1
  4. package/build/auto-relate.js +11 -5
  5. package/build/auto-relate.js.map +1 -1
  6. package/build/brands.d.ts +38 -0
  7. package/build/brands.d.ts.map +1 -0
  8. package/build/brands.js +51 -0
  9. package/build/brands.js.map +1 -0
  10. package/build/cache.d.ts.map +1 -1
  11. package/build/cache.js +10 -12
  12. package/build/cache.js.map +1 -1
  13. package/build/cli/import-claude-memory.d.ts +4 -0
  14. package/build/cli/import-claude-memory.d.ts.map +1 -0
  15. package/build/cli/import-claude-memory.js +146 -0
  16. package/build/cli/import-claude-memory.js.map +1 -0
  17. package/build/cli/migrate.d.ts +2 -0
  18. package/build/cli/migrate.d.ts.map +1 -0
  19. package/build/cli/migrate.js +104 -0
  20. package/build/cli/migrate.js.map +1 -0
  21. package/build/config.d.ts +2 -0
  22. package/build/config.d.ts.map +1 -1
  23. package/build/config.js +52 -23
  24. package/build/config.js.map +1 -1
  25. package/build/consolidate.d.ts +2 -1
  26. package/build/consolidate.d.ts.map +1 -1
  27. package/build/consolidate.js +3 -6
  28. package/build/consolidate.js.map +1 -1
  29. package/build/context.d.ts +5 -0
  30. package/build/context.d.ts.map +1 -0
  31. package/build/context.js +47 -0
  32. package/build/context.js.map +1 -0
  33. package/build/date-utils.d.ts +3 -0
  34. package/build/date-utils.d.ts.map +1 -0
  35. package/build/date-utils.js +10 -0
  36. package/build/date-utils.js.map +1 -0
  37. package/build/domain-errors.d.ts +49 -0
  38. package/build/domain-errors.d.ts.map +1 -0
  39. package/build/domain-errors.js +99 -0
  40. package/build/domain-errors.js.map +1 -0
  41. package/build/embeddings.d.ts +2 -1
  42. package/build/embeddings.d.ts.map +1 -1
  43. package/build/embeddings.js +39 -9
  44. package/build/embeddings.js.map +1 -1
  45. package/build/error-utils.d.ts +33 -0
  46. package/build/error-utils.d.ts.map +1 -0
  47. package/build/error-utils.js +65 -0
  48. package/build/error-utils.js.map +1 -0
  49. package/build/git-constants.d.ts +2 -0
  50. package/build/git-constants.d.ts.map +1 -0
  51. package/build/git-constants.js +2 -0
  52. package/build/git-constants.js.map +1 -0
  53. package/build/git.d.ts +27 -18
  54. package/build/git.d.ts.map +1 -1
  55. package/build/git.js +45 -24
  56. package/build/git.js.map +1 -1
  57. package/build/helpers/embed.d.ts +13 -0
  58. package/build/helpers/embed.d.ts.map +1 -0
  59. package/build/helpers/embed.js +68 -0
  60. package/build/helpers/embed.js.map +1 -0
  61. package/build/helpers/git-commit.d.ts +43 -0
  62. package/build/helpers/git-commit.d.ts.map +1 -0
  63. package/build/helpers/git-commit.js +129 -0
  64. package/build/helpers/git-commit.js.map +1 -0
  65. package/build/helpers/index.d.ts +19 -0
  66. package/build/helpers/index.d.ts.map +1 -0
  67. package/build/helpers/index.js +83 -0
  68. package/build/helpers/index.js.map +1 -0
  69. package/build/helpers/persistence.d.ts +34 -0
  70. package/build/helpers/persistence.d.ts.map +1 -0
  71. package/build/helpers/persistence.js +177 -0
  72. package/build/helpers/persistence.js.map +1 -0
  73. package/build/helpers/project.d.ts +21 -0
  74. package/build/helpers/project.d.ts.map +1 -0
  75. package/build/helpers/project.js +75 -0
  76. package/build/helpers/project.js.map +1 -0
  77. package/build/helpers/vault.d.ts +50 -0
  78. package/build/helpers/vault.d.ts.map +1 -0
  79. package/build/helpers/vault.js +196 -0
  80. package/build/helpers/vault.js.map +1 -0
  81. package/build/index.js +12 -5467
  82. package/build/index.js.map +1 -1
  83. package/build/lexical.d.ts +1 -1
  84. package/build/lexical.d.ts.map +1 -1
  85. package/build/lexical.js +2 -3
  86. package/build/lexical.js.map +1 -1
  87. package/build/markdown-ast.d.ts.map +1 -1
  88. package/build/markdown-ast.js +4 -2
  89. package/build/markdown-ast.js.map +1 -1
  90. package/build/migration.d.ts.map +1 -1
  91. package/build/migration.js +12 -8
  92. package/build/migration.js.map +1 -1
  93. package/build/project-introspection.d.ts +4 -4
  94. package/build/project-introspection.d.ts.map +1 -1
  95. package/build/project-introspection.js +71 -27
  96. package/build/project-introspection.js.map +1 -1
  97. package/build/project-memory-policy.d.ts.map +1 -1
  98. package/build/project-memory-policy.js +38 -3
  99. package/build/project-memory-policy.js.map +1 -1
  100. package/build/project.d.ts +2 -1
  101. package/build/project.d.ts.map +1 -1
  102. package/build/project.js +29 -41
  103. package/build/project.js.map +1 -1
  104. package/build/projections.d.ts.map +1 -1
  105. package/build/projections.js +3 -2
  106. package/build/projections.js.map +1 -1
  107. package/build/prompts.d.ts +3 -0
  108. package/build/prompts.d.ts.map +1 -0
  109. package/build/prompts.js +138 -0
  110. package/build/prompts.js.map +1 -0
  111. package/build/provenance.d.ts +9 -3
  112. package/build/provenance.d.ts.map +1 -1
  113. package/build/provenance.js +46 -15
  114. package/build/provenance.js.map +1 -1
  115. package/build/recall.d.ts +2 -1
  116. package/build/recall.d.ts.map +1 -1
  117. package/build/recall.js +30 -14
  118. package/build/recall.js.map +1 -1
  119. package/build/relationships.d.ts +2 -2
  120. package/build/relationships.d.ts.map +1 -1
  121. package/build/relationships.js +39 -43
  122. package/build/relationships.js.map +1 -1
  123. package/build/semantic-patch.d.ts.map +1 -1
  124. package/build/semantic-patch.js +16 -9
  125. package/build/semantic-patch.js.map +1 -1
  126. package/build/server-context.d.ts +18 -0
  127. package/build/server-context.d.ts.map +1 -0
  128. package/build/server-context.js +2 -0
  129. package/build/server-context.js.map +1 -0
  130. package/build/startup.d.ts +5 -0
  131. package/build/startup.d.ts.map +1 -0
  132. package/build/startup.js +37 -0
  133. package/build/startup.js.map +1 -0
  134. package/build/storage.d.ts +17 -15
  135. package/build/storage.d.ts.map +1 -1
  136. package/build/storage.js +67 -93
  137. package/build/storage.js.map +1 -1
  138. package/build/structured-content.d.ts +115 -74
  139. package/build/structured-content.d.ts.map +1 -1
  140. package/build/structured-content.js +41 -17
  141. package/build/structured-content.js.map +1 -1
  142. package/build/temporal-interpretation.d.ts +2 -1
  143. package/build/temporal-interpretation.d.ts.map +1 -1
  144. package/build/temporal-interpretation.js +15 -8
  145. package/build/temporal-interpretation.js.map +1 -1
  146. package/build/tools/consolidate-helpers.d.ts +55 -0
  147. package/build/tools/consolidate-helpers.d.ts.map +1 -0
  148. package/build/tools/consolidate-helpers.js +815 -0
  149. package/build/tools/consolidate-helpers.js.map +1 -0
  150. package/build/tools/consolidate.d.ts +4 -0
  151. package/build/tools/consolidate.d.ts.map +1 -0
  152. package/build/tools/consolidate.js +127 -0
  153. package/build/tools/consolidate.js.map +1 -0
  154. package/build/tools/detect-project.d.ts +6 -0
  155. package/build/tools/detect-project.d.ts.map +1 -0
  156. package/build/tools/detect-project.js +79 -0
  157. package/build/tools/detect-project.js.map +1 -0
  158. package/build/tools/discover-tags.d.ts +4 -0
  159. package/build/tools/discover-tags.d.ts.map +1 -0
  160. package/build/tools/discover-tags.js +236 -0
  161. package/build/tools/discover-tags.js.map +1 -0
  162. package/build/tools/forget.d.ts +4 -0
  163. package/build/tools/forget.d.ts.map +1 -0
  164. package/build/tools/forget.js +123 -0
  165. package/build/tools/forget.js.map +1 -0
  166. package/build/tools/get-project-identity.d.ts +4 -0
  167. package/build/tools/get-project-identity.d.ts.map +1 -0
  168. package/build/tools/get-project-identity.js +59 -0
  169. package/build/tools/get-project-identity.js.map +1 -0
  170. package/build/tools/get.d.ts +4 -0
  171. package/build/tools/get.d.ts.map +1 -0
  172. package/build/tools/get.js +115 -0
  173. package/build/tools/get.js.map +1 -0
  174. package/build/tools/index.d.ts +4 -0
  175. package/build/tools/index.d.ts.map +1 -0
  176. package/build/tools/index.js +47 -0
  177. package/build/tools/index.js.map +1 -0
  178. package/build/tools/list.d.ts +4 -0
  179. package/build/tools/list.d.ts.map +1 -0
  180. package/build/tools/list.js +95 -0
  181. package/build/tools/list.js.map +1 -0
  182. package/build/tools/memory-graph.d.ts +4 -0
  183. package/build/tools/memory-graph.d.ts.map +1 -0
  184. package/build/tools/memory-graph.js +84 -0
  185. package/build/tools/memory-graph.js.map +1 -0
  186. package/build/tools/migration.d.ts +5 -0
  187. package/build/tools/migration.d.ts.map +1 -0
  188. package/build/tools/migration.js +158 -0
  189. package/build/tools/migration.js.map +1 -0
  190. package/build/tools/move-memory.d.ts +4 -0
  191. package/build/tools/move-memory.d.ts.map +1 -0
  192. package/build/tools/move-memory.js +170 -0
  193. package/build/tools/move-memory.js.map +1 -0
  194. package/build/tools/policy.d.ts +5 -0
  195. package/build/tools/policy.d.ts.map +1 -0
  196. package/build/tools/policy.js +195 -0
  197. package/build/tools/policy.js.map +1 -0
  198. package/build/tools/project-memory-summary.d.ts +4 -0
  199. package/build/tools/project-memory-summary.d.ts.map +1 -0
  200. package/build/tools/project-memory-summary.js +477 -0
  201. package/build/tools/project-memory-summary.js.map +1 -0
  202. package/build/tools/recall-helpers.d.ts +40 -0
  203. package/build/tools/recall-helpers.d.ts.map +1 -0
  204. package/build/tools/recall-helpers.js +217 -0
  205. package/build/tools/recall-helpers.js.map +1 -0
  206. package/build/tools/recall.d.ts +4 -0
  207. package/build/tools/recall.d.ts.map +1 -0
  208. package/build/tools/recall.js +413 -0
  209. package/build/tools/recall.js.map +1 -0
  210. package/build/tools/recent-memories.d.ts +4 -0
  211. package/build/tools/recent-memories.d.ts.map +1 -0
  212. package/build/tools/recent-memories.js +79 -0
  213. package/build/tools/recent-memories.js.map +1 -0
  214. package/build/tools/relate.d.ts +4 -0
  215. package/build/tools/relate.d.ts.map +1 -0
  216. package/build/tools/relate.js +180 -0
  217. package/build/tools/relate.js.map +1 -0
  218. package/build/tools/remember.d.ts +4 -0
  219. package/build/tools/remember.d.ts.map +1 -0
  220. package/build/tools/remember.js +219 -0
  221. package/build/tools/remember.js.map +1 -0
  222. package/build/tools/set-project-identity.d.ts +4 -0
  223. package/build/tools/set-project-identity.d.ts.map +1 -0
  224. package/build/tools/set-project-identity.js +113 -0
  225. package/build/tools/set-project-identity.js.map +1 -0
  226. package/build/tools/sync.d.ts +4 -0
  227. package/build/tools/sync.d.ts.map +1 -0
  228. package/build/tools/sync.js +127 -0
  229. package/build/tools/sync.js.map +1 -0
  230. package/build/tools/unrelate.d.ts +4 -0
  231. package/build/tools/unrelate.d.ts.map +1 -0
  232. package/build/tools/unrelate.js +179 -0
  233. package/build/tools/unrelate.js.map +1 -0
  234. package/build/tools/update.d.ts +4 -0
  235. package/build/tools/update.d.ts.map +1 -0
  236. package/build/tools/update.js +364 -0
  237. package/build/tools/update.js.map +1 -0
  238. package/build/tools/where-is-memory.d.ts +4 -0
  239. package/build/tools/where-is-memory.d.ts.map +1 -0
  240. package/build/tools/where-is-memory.js +61 -0
  241. package/build/tools/where-is-memory.js.map +1 -0
  242. package/build/validation.d.ts +24 -0
  243. package/build/validation.d.ts.map +1 -0
  244. package/build/validation.js +62 -0
  245. package/build/validation.js.map +1 -0
  246. package/build/vault.d.ts.map +1 -1
  247. package/build/vault.js +38 -47
  248. package/build/vault.js.map +1 -1
  249. package/package.json +5 -2
  250. package/skills/mnemonic-rpi-workflow/SKILL.md +48 -17
@@ -0,0 +1,815 @@
1
+ import { aggregateMergeRisk, buildConsolidateNoteEvidence, buildGroupWarnings, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "../consolidate.js";
2
+ import { cosineSimilarity, embed, embedModel } from "../embeddings.js";
3
+ import { classifyTheme, titleCaseTheme } from "../project-introspection.js";
4
+ import { getErrorMessage, attempt } from "../error-utils.js";
5
+ import { makeId, slugify } from "../helpers/index.js";
6
+ import { memoryId, isoDateString } from "../brands.js";
7
+ import { formatCommitBody, shouldBlockProtectedBranchCommit as shouldBlockProtectedBranchCommitFromModule, wouldRelationshipCleanupTouchProjectVault as wouldRelationshipCleanupTouchProjectVaultFromModule } from "../helpers/git-commit.js";
8
+ import { buildPersistenceStatus, buildMutationRetryContract, formatPersistenceSummary, pushAfterMutation as pushAfterMutationFromModule } from "../helpers/persistence.js";
9
+ import { storageLabel, addVaultChange, removeRelationshipsToNoteIds as removeRelationshipsToNoteIdsFromModule } from "../helpers/vault.js";
10
+ import { toProjectRef } from "../helpers/project.js";
11
+ import { embedTextForNote as embedTextForNoteFromModule } from "../helpers/embed.js";
12
+ import { UnknownConsolidationModeError } from "../domain-errors.js";
13
+ // Re-export helpers that close over ctx for convenience
14
+ async function shouldBlockProtectedBranchCommit(ctx, options) {
15
+ return shouldBlockProtectedBranchCommitFromModule({ ctx, ...options });
16
+ }
17
+ async function wouldRelationshipCleanupTouchProjectVault(ctx, noteIds) {
18
+ return wouldRelationshipCleanupTouchProjectVaultFromModule(ctx, noteIds);
19
+ }
20
+ async function pushAfterMutation(ctx, vault) {
21
+ return pushAfterMutationFromModule(ctx, vault);
22
+ }
23
+ async function removeRelationshipsToNoteIds(ctx, noteIds) {
24
+ return removeRelationshipsToNoteIdsFromModule(ctx, noteIds);
25
+ }
26
+ async function embedTextForNote(storage, note) {
27
+ return embedTextForNoteFromModule(storage, note);
28
+ }
29
+ // ── Consolidate helper functions ────────────────────────────────────────────
30
+ export async function detectDuplicates(entries, threshold, project, evidence) {
31
+ const lines = [];
32
+ lines.push(`Duplicate detection for ${project?.name ?? "global"} (similarity > ${threshold}):`);
33
+ lines.push("");
34
+ const checked = new Set();
35
+ let foundCount = 0;
36
+ const duplicates = [];
37
+ const duplicatePairs = [];
38
+ const embeddings = await loadEmbeddingsByNoteId(entries);
39
+ const allNotes = entries.map((entry) => entry.note);
40
+ for (let i = 0; i < entries.length; i++) {
41
+ const entryA = entries[i];
42
+ if (checked.has(entryA.note.id))
43
+ continue;
44
+ const embeddingA = embeddings.get(entryA.note.id);
45
+ if (!embeddingA)
46
+ continue;
47
+ for (let j = i + 1; j < entries.length; j++) {
48
+ const entryB = entries[j];
49
+ if (checked.has(entryB.note.id))
50
+ continue;
51
+ const embeddingB = embeddings.get(entryB.note.id);
52
+ if (!embeddingB)
53
+ continue;
54
+ const similarity = cosineSimilarity(embeddingA, embeddingB);
55
+ if (similarity >= threshold) {
56
+ const noteAEvidence = buildConsolidateNoteEvidence(entryA.note, allNotes, entryA.note);
57
+ const noteBEvidence = buildConsolidateNoteEvidence(entryB.note, allNotes, entryA.note);
58
+ const groupWarnings = buildGroupWarnings([entryA.note, entryB.note], entryA.note);
59
+ const pairRisk = aggregateMergeRisk([noteAEvidence.mergeRisk, noteBEvidence.mergeRisk]);
60
+ foundCount++;
61
+ lines.push(`${foundCount}. ${entryA.note.title} (${entryA.note.id})`);
62
+ lines.push(` └── ${entryB.note.title} (${entryB.note.id})`);
63
+ lines.push(` Similarity: ${similarity.toFixed(3)}`);
64
+ if (evidence) {
65
+ lines.push(` A: ${noteAEvidence.lifecycle}, ${noteAEvidence.role ?? "untyped"} | ${Math.round(noteAEvidence.ageDays)}d old | rel: ${noteAEvidence.relatedCount} | supersedes: ${noteAEvidence.supersededCount ?? 0} | risk: ${noteAEvidence.mergeRisk}`);
66
+ lines.push(` B: ${noteBEvidence.lifecycle}, ${noteBEvidence.role ?? "untyped"} | ${Math.round(noteBEvidence.ageDays)}d old | rel: ${noteBEvidence.relatedCount} | supersedes: ${noteBEvidence.supersededCount ?? 0} | risk: ${noteBEvidence.mergeRisk}`);
67
+ if (groupWarnings.length > 0) {
68
+ lines.push(` Warnings: ${groupWarnings.join("; ")}`);
69
+ }
70
+ lines.push(` Merge risk: ${pairRisk}`);
71
+ }
72
+ lines.push("");
73
+ checked.add(entryA.note.id);
74
+ checked.add(entryB.note.id);
75
+ duplicates.push({
76
+ noteA: { id: entryA.note.id, title: entryA.note.title },
77
+ noteB: { id: entryB.note.id, title: entryB.note.title },
78
+ similarity,
79
+ });
80
+ if (evidence) {
81
+ duplicatePairs.push({
82
+ similarity,
83
+ noteA: noteAEvidence,
84
+ noteB: noteBEvidence,
85
+ warnings: groupWarnings.length > 0 ? groupWarnings : undefined,
86
+ mergeRisk: pairRisk,
87
+ });
88
+ }
89
+ }
90
+ }
91
+ }
92
+ if (foundCount === 0) {
93
+ lines.push("No duplicates found above the similarity threshold.");
94
+ }
95
+ else {
96
+ lines.push(`Found ${foundCount} potential duplicate pair(s).`);
97
+ lines.push("Use 'suggest-merges' strategy for actionable recommendations.");
98
+ }
99
+ const structuredContent = {
100
+ action: "consolidated",
101
+ strategy: "detect-duplicates",
102
+ project: toProjectRef(project),
103
+ notesProcessed: entries.length,
104
+ notesModified: 0,
105
+ duplicatePairs: evidence ? duplicatePairs : undefined,
106
+ };
107
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
108
+ }
109
+ export function findClusters(entries, project) {
110
+ const lines = [];
111
+ lines.push(`Cluster analysis for ${project?.name ?? "global"}:`);
112
+ lines.push("");
113
+ // Group by theme
114
+ const themed = new Map();
115
+ for (const entry of entries) {
116
+ const theme = classifyTheme(entry.note);
117
+ const bucket = themed.get(theme) ?? [];
118
+ bucket.push(entry);
119
+ themed.set(theme, bucket);
120
+ }
121
+ // Find relationship clusters
122
+ const idToEntry = new Map(entries.map((e) => [e.note.id, e]));
123
+ const visited = new Set();
124
+ const clusters = [];
125
+ for (const entry of entries) {
126
+ if (visited.has(entry.note.id))
127
+ continue;
128
+ const cluster = [];
129
+ const queue = [entry];
130
+ while (queue.length > 0) {
131
+ const current = queue.shift();
132
+ if (visited.has(current.note.id))
133
+ continue;
134
+ visited.add(current.note.id);
135
+ cluster.push(current);
136
+ // Add related notes to queue
137
+ for (const rel of current.note.relatedTo ?? []) {
138
+ const related = idToEntry.get(rel.id);
139
+ if (related && !visited.has(rel.id)) {
140
+ queue.push(related);
141
+ }
142
+ }
143
+ }
144
+ if (cluster.length > 1) {
145
+ clusters.push(cluster);
146
+ }
147
+ }
148
+ // Output theme groups
149
+ const themeGroups = [];
150
+ lines.push("By Theme:");
151
+ for (const [theme, bucket] of themed) {
152
+ if (bucket.length > 1) {
153
+ lines.push(` ${titleCaseTheme(theme)} (${bucket.length} notes)`);
154
+ const examples = bucket.slice(0, 3).map((entry) => entry.note.title);
155
+ for (const entry of bucket.slice(0, 3)) {
156
+ lines.push(` - ${entry.note.title}`);
157
+ }
158
+ if (bucket.length > 3) {
159
+ lines.push(` ... and ${bucket.length - 3} more`);
160
+ }
161
+ themeGroups.push({ name: theme, count: bucket.length, examples });
162
+ }
163
+ }
164
+ // Output relationship clusters
165
+ const relationshipClusters = [];
166
+ if (clusters.length > 0) {
167
+ lines.push("");
168
+ lines.push("Connected Clusters (via relationships):");
169
+ for (let i = 0; i < clusters.length; i++) {
170
+ const cluster = clusters[i];
171
+ lines.push(` Cluster ${i + 1} (${cluster.length} notes):`);
172
+ const hub = cluster.reduce((max, e) => (e.note.relatedTo?.length ?? 0) > (max.note.relatedTo?.length ?? 0) ? e : max);
173
+ lines.push(` Hub: ${hub.note.title}`);
174
+ const clusterNotes = [];
175
+ for (const entry of cluster) {
176
+ if (entry.note.id !== hub.note.id) {
177
+ lines.push(` - ${entry.note.title}`);
178
+ clusterNotes.push({ id: entry.note.id, title: entry.note.title });
179
+ }
180
+ }
181
+ relationshipClusters.push({
182
+ hub: { id: hub.note.id, title: hub.note.title },
183
+ notes: clusterNotes,
184
+ });
185
+ }
186
+ }
187
+ const structuredContent = {
188
+ action: "consolidated",
189
+ strategy: "find-clusters",
190
+ project: toProjectRef(project),
191
+ notesProcessed: entries.length,
192
+ notesModified: 0,
193
+ themeGroups,
194
+ relationshipClusters,
195
+ };
196
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
197
+ }
198
+ export async function suggestMerges(entries, threshold, defaultConsolidationMode, project, explicitMode, evidence = false) {
199
+ const lines = [];
200
+ const modeLabel = explicitMode ?? `${defaultConsolidationMode} (project/default; all-temporary merges auto-delete)`;
201
+ lines.push(`Merge suggestions for ${project?.name ?? "global"} (mode: ${modeLabel}):`);
202
+ lines.push("");
203
+ const checked = new Set();
204
+ let suggestionCount = 0;
205
+ const suggestions = [];
206
+ const mergeSuggestions = [];
207
+ const embeddings = await loadEmbeddingsByNoteId(entries);
208
+ const allNotes = entries.map((entry) => entry.note);
209
+ for (let i = 0; i < entries.length; i++) {
210
+ const entryA = entries[i];
211
+ if (checked.has(entryA.note.id))
212
+ continue;
213
+ const embeddingA = embeddings.get(entryA.note.id);
214
+ if (!embeddingA)
215
+ continue;
216
+ const similar = [];
217
+ for (let j = i + 1; j < entries.length; j++) {
218
+ const entryB = entries[j];
219
+ if (checked.has(entryB.note.id))
220
+ continue;
221
+ const embeddingB = embeddings.get(entryB.note.id);
222
+ if (!embeddingB)
223
+ continue;
224
+ const similarity = cosineSimilarity(embeddingA, embeddingB);
225
+ if (similarity >= threshold) {
226
+ similar.push({ entry: entryB, similarity });
227
+ }
228
+ }
229
+ if (similar.length > 0) {
230
+ suggestionCount++;
231
+ similar.sort((a, b) => b.similarity - a.similarity);
232
+ const sources = [entryA, ...similar.map((s) => s.entry)];
233
+ const effectiveMode = resolveEffectiveConsolidationMode(sources.map((source) => source.note), defaultConsolidationMode, explicitMode);
234
+ lines.push(`${suggestionCount}. MERGE ${sources.length} NOTES`);
235
+ lines.push(` Into: "${entryA.note.title} (consolidated)"`);
236
+ lines.push(" Sources:");
237
+ for (const src of sources) {
238
+ const simStr = src.note.id === entryA.note.id ? "" : ` (${similar.find((s) => s.entry.note.id === src.note.id)?.similarity.toFixed(3)})`;
239
+ lines.push(` - ${src.note.title} (${src.note.id})${simStr}`);
240
+ }
241
+ const modeDescription = (() => {
242
+ switch (effectiveMode) {
243
+ case "supersedes":
244
+ return "preserves history";
245
+ case "delete":
246
+ return "removes sources";
247
+ default: {
248
+ const _exhaustive = effectiveMode;
249
+ throw new UnknownConsolidationModeError(_exhaustive);
250
+ }
251
+ }
252
+ })();
253
+ const noteEvidence = sources.map((source) => buildConsolidateNoteEvidence(source.note, allNotes, entryA.note));
254
+ const mergeWarnings = buildGroupWarnings(sources.map((source) => source.note), entryA.note);
255
+ const mergeRisk = aggregateMergeRisk(noteEvidence.map((e) => e.mergeRisk));
256
+ lines.push(` Mode: ${effectiveMode} (${modeDescription})`);
257
+ if (evidence) {
258
+ for (const note of noteEvidence) {
259
+ lines.push(` Evidence: ${note.title} | ${note.lifecycle}, ${note.role ?? "untyped"} | ${Math.round(note.ageDays)}d | rel:${note.relatedCount} | risk:${note.mergeRisk}`);
260
+ }
261
+ if (mergeWarnings.length > 0) {
262
+ lines.push(` Warnings: ${mergeWarnings.join("; ")}`);
263
+ }
264
+ lines.push(` Merge risk: ${mergeRisk}`);
265
+ }
266
+ lines.push(" To execute:");
267
+ lines.push(` consolidate({ strategy: "execute-merge", mergePlan: {`);
268
+ lines.push(` sourceIds: [${sources.map((s) => `"${s.note.id}"`).join(", ")}],`);
269
+ lines.push(` targetTitle: "${entryA.note.title} (consolidated)"`);
270
+ lines.push(` }})`);
271
+ lines.push("");
272
+ suggestions.push({
273
+ targetTitle: `${entryA.note.title} (consolidated)`,
274
+ sourceIds: sources.map((s) => s.note.id),
275
+ similarities: similar.map((s) => ({ id: s.entry.note.id, similarity: s.similarity })),
276
+ });
277
+ if (evidence) {
278
+ mergeSuggestions.push({
279
+ targetTitle: `${entryA.note.title} (consolidated)`,
280
+ sourceIds: sources.map((source) => source.note.id),
281
+ mode: effectiveMode,
282
+ notes: noteEvidence,
283
+ warnings: mergeWarnings.length > 0 ? mergeWarnings : undefined,
284
+ mergeRisk,
285
+ });
286
+ }
287
+ checked.add(entryA.note.id);
288
+ for (const s of similar)
289
+ checked.add(s.entry.note.id);
290
+ }
291
+ }
292
+ if (suggestionCount === 0) {
293
+ lines.push("No merge suggestions found. Try lowering the threshold or manual review.");
294
+ }
295
+ else {
296
+ lines.push(`Generated ${suggestionCount} merge suggestion(s). Review carefully before executing.`);
297
+ }
298
+ const structuredContent = {
299
+ action: "consolidated",
300
+ strategy: "suggest-merges",
301
+ project: toProjectRef(project),
302
+ notesProcessed: entries.length,
303
+ notesModified: 0,
304
+ mergeSuggestions: evidence ? mergeSuggestions : undefined,
305
+ };
306
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
307
+ }
308
+ async function loadEmbeddingsByNoteId(entries) {
309
+ const embeddings = new Map();
310
+ await Promise.all(entries.map(async (entry) => {
311
+ const record = await entry.vault.storage.readEmbedding(entry.note.id);
312
+ if (record) {
313
+ embeddings.set(entry.note.id, record.embedding);
314
+ }
315
+ }));
316
+ return embeddings;
317
+ }
318
+ export async function executeMerge(ctx, entries, mergePlan, defaultConsolidationMode, project, cwd, explicitMode, policy, allowProtectedBranch = false, evidence = true) {
319
+ const { vaultManager } = ctx;
320
+ const sourceIds = normalizeMergePlanSourceIds(mergePlan.sourceIds);
321
+ const targetTitle = mergePlan.targetTitle.trim();
322
+ const { content: customContent, description, summary, tags } = mergePlan;
323
+ if (sourceIds.length < 2) {
324
+ const structuredContent = {
325
+ action: "consolidated",
326
+ strategy: "execute-merge",
327
+ project: toProjectRef(project),
328
+ notesProcessed: entries.length,
329
+ notesModified: 0,
330
+ warnings: ["execute-merge requires at least two distinct sourceIds."],
331
+ };
332
+ return { content: [{ type: "text", text: "execute-merge requires at least two distinct sourceIds." }], structuredContent };
333
+ }
334
+ if (!targetTitle) {
335
+ const structuredContent = {
336
+ action: "consolidated",
337
+ strategy: "execute-merge",
338
+ project: toProjectRef(project),
339
+ notesProcessed: entries.length,
340
+ notesModified: 0,
341
+ warnings: ["execute-merge requires a non-empty targetTitle."],
342
+ };
343
+ return { content: [{ type: "text", text: "execute-merge requires a non-empty targetTitle." }], structuredContent };
344
+ }
345
+ // Find all source entries
346
+ const sourceEntries = [];
347
+ for (const id of sourceIds) {
348
+ const entry = entries.find((e) => e.note.id === id);
349
+ if (!entry) {
350
+ const structuredContent = {
351
+ action: "consolidated",
352
+ strategy: "execute-merge",
353
+ project: toProjectRef(project),
354
+ notesProcessed: entries.length,
355
+ notesModified: 0,
356
+ warnings: [`Source note '${id}' not found.`],
357
+ };
358
+ return { content: [{ type: "text", text: `Source note '${id}' not found.` }], structuredContent };
359
+ }
360
+ sourceEntries.push(entry);
361
+ }
362
+ const consolidationMode = resolveEffectiveConsolidationMode(sourceEntries.map((entry) => entry.note), defaultConsolidationMode, explicitMode);
363
+ const existingTargetEntry = findExistingExecuteMergeTarget(entries, sourceEntries, targetTitle);
364
+ const projectVault = cwd ? await vaultManager.getProjectVaultIfExists(cwd) : null;
365
+ const targetVault = existingTargetEntry?.vault ?? projectVault ?? vaultManager.main;
366
+ let touchesProjectVault = targetVault.isProject || sourceEntries.some((entry) => entry.vault.isProject);
367
+ if (!touchesProjectVault && consolidationMode === "delete") {
368
+ touchesProjectVault = await wouldRelationshipCleanupTouchProjectVault(ctx, sourceIds);
369
+ }
370
+ if (touchesProjectVault) {
371
+ const projectLabel = project
372
+ ? `${project.name} (${project.id})`
373
+ : "this context";
374
+ const protectedBranchCheck = await shouldBlockProtectedBranchCommit(ctx, {
375
+ cwd,
376
+ writeScope: "project",
377
+ automaticCommit: true,
378
+ projectLabel,
379
+ policy,
380
+ allowProtectedBranch,
381
+ toolName: "consolidate",
382
+ });
383
+ if (protectedBranchCheck.blocked) {
384
+ const message = protectedBranchCheck.message ?? "Protected branch policy blocked this commit.";
385
+ const structuredContent = {
386
+ action: "consolidated",
387
+ strategy: "execute-merge",
388
+ project: toProjectRef(project),
389
+ notesProcessed: entries.length,
390
+ notesModified: 0,
391
+ warnings: [message],
392
+ };
393
+ return { content: [{ type: "text", text: message }], structuredContent };
394
+ }
395
+ }
396
+ const now = isoDateString(new Date().toISOString());
397
+ // Build consolidated content
398
+ const sections = [];
399
+ if (customContent) {
400
+ if (description) {
401
+ sections.push(description);
402
+ sections.push("");
403
+ }
404
+ sections.push(customContent);
405
+ }
406
+ else {
407
+ if (description) {
408
+ sections.push(description);
409
+ sections.push("");
410
+ }
411
+ sections.push("## Consolidated from:");
412
+ for (const entry of sourceEntries) {
413
+ sections.push(`### ${entry.note.title}`);
414
+ sections.push(`*Source: \`${entry.note.id}\`*`);
415
+ sections.push("");
416
+ sections.push(entry.note.content);
417
+ sections.push("");
418
+ }
419
+ }
420
+ // Combine tags (deduplicated)
421
+ const combinedTags = tags ?? Array.from(new Set(sourceEntries.flatMap((e) => e.note.tags)));
422
+ // Collect all unique relationships from sources (excluding relationships among sources)
423
+ const sourceIdsSet = new Set(sourceIds);
424
+ const relationshipSources = existingTargetEntry
425
+ ? [...sourceEntries.map((entry) => entry.note), existingTargetEntry.note]
426
+ : sourceEntries.map((entry) => entry.note);
427
+ const allRelationships = mergeRelationshipsFromNotes(relationshipSources, sourceIdsSet);
428
+ // Create or update the consolidated note
429
+ const targetId = existingTargetEntry?.note.id ?? makeId(targetTitle);
430
+ const consolidatedNote = {
431
+ id: targetId,
432
+ title: targetTitle,
433
+ content: sections.join("\n").trim(),
434
+ tags: combinedTags,
435
+ lifecycle: "permanent",
436
+ project: project?.id,
437
+ projectName: project?.name,
438
+ relatedTo: allRelationships,
439
+ createdAt: existingTargetEntry?.note.createdAt ?? now,
440
+ updatedAt: now,
441
+ memoryVersion: 1,
442
+ };
443
+ // Write consolidated note
444
+ await targetVault.storage.writeNote(consolidatedNote);
445
+ let embeddingStatus = { status: "written" };
446
+ const embedResult = await attempt("consolidate:embed", async () => {
447
+ const text = await embedTextForNote(targetVault.storage, consolidatedNote);
448
+ const vector = await embed(text);
449
+ await targetVault.storage.writeEmbedding({
450
+ id: targetId,
451
+ model: embedModel,
452
+ embedding: vector,
453
+ updatedAt: now,
454
+ });
455
+ });
456
+ if (!embedResult.ok) {
457
+ embeddingStatus = { status: "skipped", reason: getErrorMessage(embedResult.error) };
458
+ console.error(`[embedding] Failed for consolidated note '${targetId}': ${embedResult.error}`);
459
+ }
460
+ const vaultChanges = new Map();
461
+ // Handle sources based on consolidation mode
462
+ switch (consolidationMode) {
463
+ case "delete": {
464
+ // Delete all sources
465
+ for (const entry of sourceEntries) {
466
+ await entry.vault.storage.deleteNote(entry.note.id);
467
+ addVaultChange(vaultChanges, entry.vault, vaultManager.noteRelPath(entry.vault, entry.note.id));
468
+ }
469
+ const cleanupChanges = await removeRelationshipsToNoteIds(ctx, sourceIds);
470
+ for (const [vault, files] of cleanupChanges) {
471
+ for (const file of files) {
472
+ addVaultChange(vaultChanges, vault, file);
473
+ }
474
+ }
475
+ break;
476
+ }
477
+ case "supersedes": {
478
+ // Mark sources with supersedes relationship
479
+ for (const entry of sourceEntries) {
480
+ const updatedRels = [...(entry.note.relatedTo ?? [])];
481
+ if (!updatedRels.some((r) => r.id === targetId)) {
482
+ updatedRels.push({ id: targetId, type: "supersedes" });
483
+ }
484
+ await entry.vault.storage.writeNote({
485
+ ...entry.note,
486
+ relatedTo: updatedRels,
487
+ updatedAt: now,
488
+ });
489
+ addVaultChange(vaultChanges, entry.vault, vaultManager.noteRelPath(entry.vault, entry.note.id));
490
+ }
491
+ break;
492
+ }
493
+ default: {
494
+ const _exhaustive = consolidationMode;
495
+ throw new UnknownConsolidationModeError(_exhaustive);
496
+ }
497
+ }
498
+ // Add consolidated note to changes
499
+ addVaultChange(vaultChanges, targetVault, vaultManager.noteRelPath(targetVault, targetId));
500
+ // Commit changes per vault
501
+ let targetCommitStatus = { status: "skipped", reason: "no-changes" };
502
+ let targetPushStatus = { status: "skipped", reason: "no-remote" };
503
+ let targetCommitBody;
504
+ let targetCommitMessage;
505
+ let targetCommitFiles;
506
+ for (const [vault, files] of vaultChanges) {
507
+ const isTargetVault = vault === targetVault;
508
+ // Determine action and summary based on mode
509
+ let action;
510
+ let sourceSummary;
511
+ switch (consolidationMode) {
512
+ case "delete":
513
+ action = "consolidate(delete)";
514
+ sourceSummary = "Deleted as part of consolidation";
515
+ break;
516
+ case "supersedes":
517
+ action = "consolidate(supersedes)";
518
+ sourceSummary = "Marked as superseded by consolidation";
519
+ break;
520
+ default: {
521
+ const _exhaustive = consolidationMode;
522
+ throw new UnknownConsolidationModeError(_exhaustive);
523
+ }
524
+ }
525
+ const defaultSummary = `Consolidated ${sourceIds.length} notes into new note`;
526
+ const commitSummary = isTargetVault ? (summary ?? defaultSummary) : sourceSummary;
527
+ const commitBody = isTargetVault
528
+ ? formatCommitBody({
529
+ summary: commitSummary,
530
+ noteId: targetId,
531
+ noteTitle: targetTitle,
532
+ projectName: project?.name,
533
+ mode: consolidationMode,
534
+ noteIds: sourceIds,
535
+ description: `Sources: ${sourceIds.join(", ")}`,
536
+ })
537
+ : formatCommitBody({
538
+ summary: commitSummary,
539
+ noteIds: files.map((f) => f.replace(/\.mnemonic\/notes\/(.+)\.md$/, "$1").replace(/notes\/(.+)\.md$/, "$1")),
540
+ });
541
+ const commitMessage = `${action}: ${targetTitle}`;
542
+ const commitStatus = await vault.git.commitWithStatus(commitMessage, files, commitBody);
543
+ const pushStatus = commitStatus.status === "committed"
544
+ ? await pushAfterMutation(ctx, vault)
545
+ : { status: "skipped", reason: "commit-failed" };
546
+ if (isTargetVault) {
547
+ targetCommitStatus = commitStatus;
548
+ targetPushStatus = pushStatus;
549
+ targetCommitBody = commitBody;
550
+ targetCommitMessage = commitMessage;
551
+ targetCommitFiles = [...files];
552
+ }
553
+ }
554
+ const retry = targetCommitMessage && targetCommitFiles
555
+ ? buildMutationRetryContract({
556
+ commit: targetCommitStatus,
557
+ commitMessage: targetCommitMessage,
558
+ commitBody: targetCommitBody,
559
+ files: targetCommitFiles,
560
+ cwd,
561
+ vault: targetVault,
562
+ mutationApplied: true,
563
+ })
564
+ : undefined;
565
+ const persistence = buildPersistenceStatus({
566
+ storage: targetVault.storage,
567
+ id: targetId,
568
+ embedding: embeddingStatus,
569
+ commit: targetCommitStatus,
570
+ push: targetPushStatus,
571
+ commitMessage: targetCommitMessage,
572
+ commitBody: targetCommitBody,
573
+ retry,
574
+ });
575
+ const lines = [];
576
+ lines.push(`Consolidated ${sourceIds.length} notes into '${targetId}'`);
577
+ lines.push(`Mode: ${consolidationMode}`);
578
+ lines.push(`Stored in: ${storageLabel(targetVault)}`);
579
+ if (existingTargetEntry) {
580
+ lines.push("Idempotency: reused existing target note.");
581
+ }
582
+ lines.push(formatPersistenceSummary(persistence));
583
+ switch (consolidationMode) {
584
+ case "supersedes":
585
+ lines.push("Sources preserved with 'supersedes' relationship.");
586
+ lines.push("Use 'prune-superseded' later to clean up if desired.");
587
+ break;
588
+ case "delete":
589
+ lines.push("Source notes deleted.");
590
+ break;
591
+ default: {
592
+ const _exhaustive = consolidationMode;
593
+ throw new UnknownConsolidationModeError(_exhaustive);
594
+ }
595
+ }
596
+ let executeMergeEvidence;
597
+ if (evidence) {
598
+ const allNotes = entries.map((entry) => entry.note);
599
+ const noteEvidence = sourceEntries.map((entry) => buildConsolidateNoteEvidence(entry.note, allNotes, sourceEntries[0]?.note));
600
+ const mergeWarnings = buildGroupWarnings(sourceEntries.map((entry) => entry.note), sourceEntries[0]?.note);
601
+ const mergeRisk = aggregateMergeRisk(noteEvidence.map((e) => e.mergeRisk));
602
+ lines.push(" Evidence:");
603
+ for (const note of noteEvidence) {
604
+ lines.push(` ${note.title} | ${note.lifecycle}, ${note.role ?? "untyped"} | ${Math.round(note.ageDays)}d | rel:${note.relatedCount} | risk:${note.mergeRisk}`);
605
+ }
606
+ if (mergeWarnings.length > 0) {
607
+ lines.push(` Warnings: ${mergeWarnings.join("; ")}`);
608
+ }
609
+ lines.push(` Merge risk: ${mergeRisk}`);
610
+ executeMergeEvidence = {
611
+ notes: noteEvidence,
612
+ warnings: mergeWarnings.length > 0 ? mergeWarnings : undefined,
613
+ mergeRisk,
614
+ };
615
+ }
616
+ const structuredContent = {
617
+ action: "consolidated",
618
+ strategy: "execute-merge",
619
+ project: toProjectRef(project),
620
+ notesProcessed: entries.length,
621
+ notesModified: vaultChanges.size,
622
+ executeMergeEvidence,
623
+ persistence,
624
+ retry,
625
+ };
626
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
627
+ }
628
+ function findExistingExecuteMergeTarget(entries, sourceEntries, targetTitle) {
629
+ const normalizedTitle = targetTitle.trim();
630
+ const targetSlug = slugify(normalizedTitle);
631
+ const sourceIds = new Set(sourceEntries.map((entry) => entry.note.id));
632
+ let sharedTargetIds;
633
+ for (const entry of sourceEntries) {
634
+ const supersededTargetIds = new Set((entry.note.relatedTo ?? [])
635
+ .filter((rel) => rel.type === "supersedes")
636
+ .map((rel) => rel.id)
637
+ .filter((id) => !sourceIds.has(id)));
638
+ if (supersededTargetIds.size === 0) {
639
+ return undefined;
640
+ }
641
+ sharedTargetIds = sharedTargetIds
642
+ ? new Set([...sharedTargetIds].filter((id) => supersededTargetIds.has(id)))
643
+ : supersededTargetIds;
644
+ if (sharedTargetIds.size === 0) {
645
+ return undefined;
646
+ }
647
+ }
648
+ const candidates = entries
649
+ .filter((entry) => sharedTargetIds?.has(entry.note.id))
650
+ .filter((entry) => entry.note.title.trim() === normalizedTitle)
651
+ .filter((entry) => !targetSlug || entry.note.id === targetSlug || entry.note.id.startsWith(`${targetSlug}-`))
652
+ .sort((left, right) => right.note.updatedAt.localeCompare(left.note.updatedAt));
653
+ return candidates[0];
654
+ }
655
+ export async function pruneSuperseded(ctx, entries, consolidationMode, project, cwd, policy, allowProtectedBranch = false) {
656
+ const { vaultManager } = ctx;
657
+ if (consolidationMode !== "delete") {
658
+ const structuredContent = {
659
+ action: "consolidated",
660
+ strategy: "prune-superseded",
661
+ project: toProjectRef(project),
662
+ notesProcessed: entries.length,
663
+ notesModified: 0,
664
+ warnings: [`prune-superseded requires consolidationMode="delete". Current mode: ${consolidationMode}.`],
665
+ };
666
+ return {
667
+ content: [{
668
+ type: "text",
669
+ text: `prune-superseded requires consolidationMode="delete". Current mode: ${consolidationMode}.\nSet mode explicitly or update project policy.`,
670
+ }],
671
+ structuredContent,
672
+ };
673
+ }
674
+ const lines = [];
675
+ lines.push(`Pruning superseded notes for ${project?.name ?? "global"}:`);
676
+ lines.push("");
677
+ // Find all notes that have a supersedes relationship pointing to them
678
+ const supersededIds = new Set();
679
+ const supersededBy = new Map();
680
+ for (const entry of entries) {
681
+ for (const rel of entry.note.relatedTo ?? []) {
682
+ if (rel.type === "supersedes") {
683
+ supersededIds.add(entry.note.id);
684
+ supersededBy.set(entry.note.id, rel.id);
685
+ }
686
+ }
687
+ }
688
+ if (supersededIds.size === 0) {
689
+ lines.push("No superseded notes found.");
690
+ const structuredContent = {
691
+ action: "consolidated",
692
+ strategy: "prune-superseded",
693
+ project: toProjectRef(project),
694
+ notesProcessed: entries.length,
695
+ notesModified: 0,
696
+ };
697
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
698
+ }
699
+ const supersededList = Array.from(supersededIds);
700
+ let touchesProjectVault = supersededList.some((id) => entries.find((e) => e.note.id === id)?.vault.isProject);
701
+ if (!touchesProjectVault) {
702
+ touchesProjectVault = await wouldRelationshipCleanupTouchProjectVault(ctx, supersededList);
703
+ }
704
+ if (touchesProjectVault) {
705
+ const projectLabel = project
706
+ ? `${project.name} (${project.id})`
707
+ : "this context";
708
+ const protectedBranchCheck = await shouldBlockProtectedBranchCommit(ctx, {
709
+ cwd,
710
+ writeScope: "project",
711
+ automaticCommit: true,
712
+ projectLabel,
713
+ policy,
714
+ allowProtectedBranch,
715
+ toolName: "consolidate",
716
+ });
717
+ if (protectedBranchCheck.blocked) {
718
+ const message = protectedBranchCheck.message ?? "Protected branch policy blocked this commit.";
719
+ const structuredContent = {
720
+ action: "consolidated",
721
+ strategy: "prune-superseded",
722
+ project: toProjectRef(project),
723
+ notesProcessed: entries.length,
724
+ notesModified: 0,
725
+ warnings: [message],
726
+ };
727
+ return { content: [{ type: "text", text: message }], structuredContent };
728
+ }
729
+ }
730
+ lines.push(`Found ${supersededIds.size} superseded note(s) to prune:`);
731
+ const vaultChanges = new Map();
732
+ for (const id of supersededIds) {
733
+ const entry = entries.find((e) => e.note.id === id);
734
+ if (!entry)
735
+ continue;
736
+ const targetId = supersededBy.get(id);
737
+ lines.push(` - ${entry.note.title} (${id}) -> superseded by ${targetId}`);
738
+ await entry.vault.storage.deleteNote(memoryId(id));
739
+ addVaultChange(vaultChanges, entry.vault, vaultManager.noteRelPath(entry.vault, id));
740
+ }
741
+ const cleanupChanges = await removeRelationshipsToNoteIds(ctx, Array.from(supersededIds));
742
+ for (const [vault, files] of cleanupChanges) {
743
+ for (const file of files) {
744
+ addVaultChange(vaultChanges, vault, file);
745
+ }
746
+ }
747
+ // Commit changes per vault
748
+ let retry;
749
+ for (const [vault, files] of vaultChanges) {
750
+ const prunedIds = files.map((f) => f.replace(/\.mnemonic\/notes\/(.+)\.md$/, "$1").replace(/notes\/(.+)\.md$/, "$1"));
751
+ const commitBody = formatCommitBody({
752
+ noteIds: prunedIds,
753
+ description: `Pruned ${prunedIds.length} superseded note(s)\nNotes: ${prunedIds.join(", ")}`,
754
+ });
755
+ const commitMessage = `prune: removed ${files.length} superseded note(s)`;
756
+ const commitStatus = await vault.git.commitWithStatus(commitMessage, files, commitBody);
757
+ if (!retry) {
758
+ retry = buildMutationRetryContract({
759
+ commit: commitStatus,
760
+ commitMessage,
761
+ commitBody,
762
+ files,
763
+ cwd,
764
+ vault,
765
+ mutationApplied: true,
766
+ });
767
+ }
768
+ if (commitStatus.status === "committed") {
769
+ await pushAfterMutation(ctx, vault);
770
+ }
771
+ }
772
+ lines.push("");
773
+ lines.push(`Pruned ${supersededIds.size} note(s).`);
774
+ const structuredContent = {
775
+ action: "consolidated",
776
+ strategy: "prune-superseded",
777
+ project: toProjectRef(project),
778
+ notesProcessed: entries.length,
779
+ notesModified: vaultChanges.size,
780
+ retry,
781
+ };
782
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
783
+ }
784
+ export async function dryRunAll(entries, threshold, defaultConsolidationMode, project, explicitMode, evidence = false) {
785
+ const lines = [];
786
+ lines.push(`Consolidation analysis for ${project?.name ?? "global"}:`);
787
+ const modeLabel = explicitMode ?? `${defaultConsolidationMode} (project/default; all-temporary merges auto-delete)`;
788
+ lines.push(`Mode: ${modeLabel} | Threshold: ${threshold}`);
789
+ lines.push("");
790
+ // Run all analysis strategies
791
+ const dupes = await detectDuplicates(entries, threshold, project, evidence);
792
+ lines.push("=== DUPLICATE DETECTION ===");
793
+ lines.push(dupes.content[0]?.text ?? "No output");
794
+ lines.push("");
795
+ const clusters = findClusters(entries, project);
796
+ lines.push("=== CLUSTER ANALYSIS ===");
797
+ lines.push(clusters.content[0]?.text ?? "No output");
798
+ lines.push("");
799
+ const merges = await suggestMerges(entries, threshold, defaultConsolidationMode, project, explicitMode, evidence);
800
+ lines.push("=== MERGE SUGGESTIONS ===");
801
+ lines.push(merges.content[0]?.text ?? "No output");
802
+ const structuredContent = {
803
+ action: "consolidated",
804
+ strategy: "dry-run",
805
+ project: toProjectRef(project),
806
+ notesProcessed: entries.length,
807
+ notesModified: 0,
808
+ duplicatePairs: dupes.structuredContent.duplicatePairs,
809
+ mergeSuggestions: merges.structuredContent.mergeSuggestions,
810
+ themeGroups: clusters.structuredContent.themeGroups,
811
+ relationshipClusters: clusters.structuredContent.relationshipClusters,
812
+ };
813
+ return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
814
+ }
815
+ //# sourceMappingURL=consolidate-helpers.js.map