@danielmarbach/mnemonic-mcp 0.27.1 → 0.28.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.
- package/CHANGELOG.md +32 -0
- package/build/auto-relate.d.ts.map +1 -1
- package/build/auto-relate.js +11 -5
- package/build/auto-relate.js.map +1 -1
- package/build/brands.d.ts +38 -0
- package/build/brands.d.ts.map +1 -0
- package/build/brands.js +51 -0
- package/build/brands.js.map +1 -0
- package/build/cache.d.ts.map +1 -1
- package/build/cache.js +2 -4
- package/build/cache.js.map +1 -1
- package/build/cli/import-claude-memory.d.ts +4 -0
- package/build/cli/import-claude-memory.d.ts.map +1 -0
- package/build/cli/import-claude-memory.js +144 -0
- package/build/cli/import-claude-memory.js.map +1 -0
- package/build/cli/migrate.d.ts +2 -0
- package/build/cli/migrate.d.ts.map +1 -0
- package/build/cli/migrate.js +104 -0
- package/build/cli/migrate.js.map +1 -0
- package/build/config.d.ts +2 -0
- package/build/config.d.ts.map +1 -1
- package/build/config.js +41 -14
- package/build/config.js.map +1 -1
- package/build/consolidate.d.ts +2 -1
- package/build/consolidate.d.ts.map +1 -1
- package/build/consolidate.js +3 -6
- package/build/consolidate.js.map +1 -1
- package/build/context.d.ts +5 -0
- package/build/context.d.ts.map +1 -0
- package/build/context.js +41 -0
- package/build/context.js.map +1 -0
- package/build/date-utils.d.ts +3 -0
- package/build/date-utils.d.ts.map +1 -0
- package/build/date-utils.js +10 -0
- package/build/date-utils.js.map +1 -0
- package/build/embeddings.d.ts +2 -1
- package/build/embeddings.d.ts.map +1 -1
- package/build/embeddings.js +24 -2
- package/build/embeddings.js.map +1 -1
- package/build/error-utils.d.ts +3 -0
- package/build/error-utils.d.ts.map +1 -0
- package/build/error-utils.js +9 -0
- package/build/error-utils.js.map +1 -0
- package/build/git-constants.d.ts +2 -0
- package/build/git-constants.d.ts.map +1 -0
- package/build/git-constants.js +2 -0
- package/build/git-constants.js.map +1 -0
- package/build/git.d.ts +27 -18
- package/build/git.d.ts.map +1 -1
- package/build/git.js +39 -22
- package/build/git.js.map +1 -1
- package/build/helpers/embed.d.ts +13 -0
- package/build/helpers/embed.d.ts.map +1 -0
- package/build/helpers/embed.js +70 -0
- package/build/helpers/embed.js.map +1 -0
- package/build/helpers/git-commit.d.ts +43 -0
- package/build/helpers/git-commit.d.ts.map +1 -0
- package/build/helpers/git-commit.js +129 -0
- package/build/helpers/git-commit.js.map +1 -0
- package/build/helpers/index.d.ts +19 -0
- package/build/helpers/index.d.ts.map +1 -0
- package/build/helpers/index.js +83 -0
- package/build/helpers/index.js.map +1 -0
- package/build/helpers/persistence.d.ts +34 -0
- package/build/helpers/persistence.d.ts.map +1 -0
- package/build/helpers/persistence.js +176 -0
- package/build/helpers/persistence.js.map +1 -0
- package/build/helpers/project.d.ts +21 -0
- package/build/helpers/project.d.ts.map +1 -0
- package/build/helpers/project.js +75 -0
- package/build/helpers/project.js.map +1 -0
- package/build/helpers/vault.d.ts +50 -0
- package/build/helpers/vault.d.ts.map +1 -0
- package/build/helpers/vault.js +196 -0
- package/build/helpers/vault.js.map +1 -0
- package/build/index.js +12 -5425
- package/build/index.js.map +1 -1
- package/build/lexical.d.ts +1 -1
- package/build/lexical.d.ts.map +1 -1
- package/build/lexical.js +2 -3
- package/build/lexical.js.map +1 -1
- package/build/markdown-ast.d.ts.map +1 -1
- package/build/markdown-ast.js +4 -2
- package/build/markdown-ast.js.map +1 -1
- package/build/migration.d.ts.map +1 -1
- package/build/migration.js +6 -5
- package/build/migration.js.map +1 -1
- package/build/project-introspection.d.ts +4 -4
- package/build/project-introspection.d.ts.map +1 -1
- package/build/project-introspection.js +68 -26
- package/build/project-introspection.js.map +1 -1
- package/build/project-memory-policy.d.ts.map +1 -1
- package/build/project-memory-policy.js +38 -3
- package/build/project-memory-policy.js.map +1 -1
- package/build/project.d.ts +2 -1
- package/build/project.d.ts.map +1 -1
- package/build/project.js +5 -4
- package/build/project.js.map +1 -1
- package/build/projections.d.ts.map +1 -1
- package/build/projections.js +2 -1
- package/build/projections.js.map +1 -1
- package/build/prompts.d.ts +3 -0
- package/build/prompts.d.ts.map +1 -0
- package/build/prompts.js +138 -0
- package/build/prompts.js.map +1 -0
- package/build/provenance.d.ts +1 -1
- package/build/provenance.d.ts.map +1 -1
- package/build/provenance.js +8 -11
- package/build/provenance.js.map +1 -1
- package/build/recall.d.ts +2 -1
- package/build/recall.d.ts.map +1 -1
- package/build/recall.js +29 -14
- package/build/recall.js.map +1 -1
- package/build/relationships.d.ts +2 -2
- package/build/relationships.d.ts.map +1 -1
- package/build/relationships.js +34 -38
- package/build/relationships.js.map +1 -1
- package/build/semantic-patch.d.ts.map +1 -1
- package/build/semantic-patch.js +10 -4
- package/build/semantic-patch.js.map +1 -1
- package/build/server-context.d.ts +18 -0
- package/build/server-context.d.ts.map +1 -0
- package/build/server-context.js +2 -0
- package/build/server-context.js.map +1 -0
- package/build/startup.d.ts +5 -0
- package/build/startup.d.ts.map +1 -0
- package/build/startup.js +37 -0
- package/build/startup.js.map +1 -0
- package/build/storage.d.ts +17 -15
- package/build/storage.d.ts.map +1 -1
- package/build/storage.js +32 -40
- package/build/storage.js.map +1 -1
- package/build/structured-content.d.ts +94 -74
- package/build/structured-content.d.ts.map +1 -1
- package/build/structured-content.js +30 -17
- package/build/structured-content.js.map +1 -1
- package/build/temporal-interpretation.d.ts +2 -1
- package/build/temporal-interpretation.d.ts.map +1 -1
- package/build/temporal-interpretation.js +13 -7
- package/build/temporal-interpretation.js.map +1 -1
- package/build/tools/consolidate-helpers.d.ts +55 -0
- package/build/tools/consolidate-helpers.d.ts.map +1 -0
- package/build/tools/consolidate-helpers.js +815 -0
- package/build/tools/consolidate-helpers.js.map +1 -0
- package/build/tools/consolidate.d.ts +4 -0
- package/build/tools/consolidate.d.ts.map +1 -0
- package/build/tools/consolidate.js +127 -0
- package/build/tools/consolidate.js.map +1 -0
- package/build/tools/detect-project.d.ts +6 -0
- package/build/tools/detect-project.d.ts.map +1 -0
- package/build/tools/detect-project.js +79 -0
- package/build/tools/detect-project.js.map +1 -0
- package/build/tools/discover-tags.d.ts +4 -0
- package/build/tools/discover-tags.d.ts.map +1 -0
- package/build/tools/discover-tags.js +236 -0
- package/build/tools/discover-tags.js.map +1 -0
- package/build/tools/forget.d.ts +4 -0
- package/build/tools/forget.d.ts.map +1 -0
- package/build/tools/forget.js +123 -0
- package/build/tools/forget.js.map +1 -0
- package/build/tools/get-project-identity.d.ts +4 -0
- package/build/tools/get-project-identity.d.ts.map +1 -0
- package/build/tools/get-project-identity.js +59 -0
- package/build/tools/get-project-identity.js.map +1 -0
- package/build/tools/get.d.ts +4 -0
- package/build/tools/get.d.ts.map +1 -0
- package/build/tools/get.js +115 -0
- package/build/tools/get.js.map +1 -0
- package/build/tools/index.d.ts +4 -0
- package/build/tools/index.d.ts.map +1 -0
- package/build/tools/index.js +47 -0
- package/build/tools/index.js.map +1 -0
- package/build/tools/list.d.ts +4 -0
- package/build/tools/list.d.ts.map +1 -0
- package/build/tools/list.js +95 -0
- package/build/tools/list.js.map +1 -0
- package/build/tools/memory-graph.d.ts +4 -0
- package/build/tools/memory-graph.d.ts.map +1 -0
- package/build/tools/memory-graph.js +84 -0
- package/build/tools/memory-graph.js.map +1 -0
- package/build/tools/migration.d.ts +5 -0
- package/build/tools/migration.d.ts.map +1 -0
- package/build/tools/migration.js +157 -0
- package/build/tools/migration.js.map +1 -0
- package/build/tools/move-memory.d.ts +4 -0
- package/build/tools/move-memory.d.ts.map +1 -0
- package/build/tools/move-memory.js +170 -0
- package/build/tools/move-memory.js.map +1 -0
- package/build/tools/policy.d.ts +5 -0
- package/build/tools/policy.d.ts.map +1 -0
- package/build/tools/policy.js +195 -0
- package/build/tools/policy.js.map +1 -0
- package/build/tools/project-memory-summary.d.ts +4 -0
- package/build/tools/project-memory-summary.d.ts.map +1 -0
- package/build/tools/project-memory-summary.js +477 -0
- package/build/tools/project-memory-summary.js.map +1 -0
- package/build/tools/recall-helpers.d.ts +28 -0
- package/build/tools/recall-helpers.d.ts.map +1 -0
- package/build/tools/recall-helpers.js +137 -0
- package/build/tools/recall-helpers.js.map +1 -0
- package/build/tools/recall.d.ts +4 -0
- package/build/tools/recall.d.ts.map +1 -0
- package/build/tools/recall.js +343 -0
- package/build/tools/recall.js.map +1 -0
- package/build/tools/recent-memories.d.ts +4 -0
- package/build/tools/recent-memories.d.ts.map +1 -0
- package/build/tools/recent-memories.js +79 -0
- package/build/tools/recent-memories.js.map +1 -0
- package/build/tools/relate.d.ts +4 -0
- package/build/tools/relate.d.ts.map +1 -0
- package/build/tools/relate.js +180 -0
- package/build/tools/relate.js.map +1 -0
- package/build/tools/remember.d.ts +4 -0
- package/build/tools/remember.d.ts.map +1 -0
- package/build/tools/remember.js +219 -0
- package/build/tools/remember.js.map +1 -0
- package/build/tools/set-project-identity.d.ts +4 -0
- package/build/tools/set-project-identity.d.ts.map +1 -0
- package/build/tools/set-project-identity.js +113 -0
- package/build/tools/set-project-identity.js.map +1 -0
- package/build/tools/sync.d.ts +4 -0
- package/build/tools/sync.d.ts.map +1 -0
- package/build/tools/sync.js +127 -0
- package/build/tools/sync.js.map +1 -0
- package/build/tools/unrelate.d.ts +4 -0
- package/build/tools/unrelate.d.ts.map +1 -0
- package/build/tools/unrelate.js +179 -0
- package/build/tools/unrelate.js.map +1 -0
- package/build/tools/update.d.ts +4 -0
- package/build/tools/update.d.ts.map +1 -0
- package/build/tools/update.js +365 -0
- package/build/tools/update.js.map +1 -0
- package/build/tools/where-is-memory.d.ts +4 -0
- package/build/tools/where-is-memory.d.ts.map +1 -0
- package/build/tools/where-is-memory.js +61 -0
- package/build/tools/where-is-memory.js.map +1 -0
- package/build/validation.d.ts +24 -0
- package/build/validation.d.ts.map +1 -0
- package/build/validation.js +62 -0
- package/build/validation.js.map +1 -0
- package/build/vault.d.ts.map +1 -1
- package/build/vault.js +11 -5
- package/build/vault.js.map +1 -1
- package/package.json +2 -1
|
@@ -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 } 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
|
+
// Re-export helpers that close over ctx for convenience
|
|
13
|
+
async function shouldBlockProtectedBranchCommit(ctx, options) {
|
|
14
|
+
return shouldBlockProtectedBranchCommitFromModule({ ctx, ...options });
|
|
15
|
+
}
|
|
16
|
+
async function wouldRelationshipCleanupTouchProjectVault(ctx, noteIds) {
|
|
17
|
+
return wouldRelationshipCleanupTouchProjectVaultFromModule(ctx, noteIds);
|
|
18
|
+
}
|
|
19
|
+
async function pushAfterMutation(ctx, vault) {
|
|
20
|
+
return pushAfterMutationFromModule(ctx, vault);
|
|
21
|
+
}
|
|
22
|
+
async function removeRelationshipsToNoteIds(ctx, noteIds) {
|
|
23
|
+
return removeRelationshipsToNoteIdsFromModule(ctx, noteIds);
|
|
24
|
+
}
|
|
25
|
+
async function embedTextForNote(storage, note) {
|
|
26
|
+
return embedTextForNoteFromModule(storage, note);
|
|
27
|
+
}
|
|
28
|
+
// ── Consolidate helper functions ────────────────────────────────────────────
|
|
29
|
+
export async function detectDuplicates(entries, threshold, project, evidence) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
lines.push(`Duplicate detection for ${project?.name ?? "global"} (similarity > ${threshold}):`);
|
|
32
|
+
lines.push("");
|
|
33
|
+
const checked = new Set();
|
|
34
|
+
let foundCount = 0;
|
|
35
|
+
const duplicates = [];
|
|
36
|
+
const duplicatePairs = [];
|
|
37
|
+
const embeddings = await loadEmbeddingsByNoteId(entries);
|
|
38
|
+
const allNotes = entries.map((entry) => entry.note);
|
|
39
|
+
for (let i = 0; i < entries.length; i++) {
|
|
40
|
+
const entryA = entries[i];
|
|
41
|
+
if (checked.has(entryA.note.id))
|
|
42
|
+
continue;
|
|
43
|
+
const embeddingA = embeddings.get(entryA.note.id);
|
|
44
|
+
if (!embeddingA)
|
|
45
|
+
continue;
|
|
46
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
47
|
+
const entryB = entries[j];
|
|
48
|
+
if (checked.has(entryB.note.id))
|
|
49
|
+
continue;
|
|
50
|
+
const embeddingB = embeddings.get(entryB.note.id);
|
|
51
|
+
if (!embeddingB)
|
|
52
|
+
continue;
|
|
53
|
+
const similarity = cosineSimilarity(embeddingA, embeddingB);
|
|
54
|
+
if (similarity >= threshold) {
|
|
55
|
+
const noteAEvidence = buildConsolidateNoteEvidence(entryA.note, allNotes, entryA.note);
|
|
56
|
+
const noteBEvidence = buildConsolidateNoteEvidence(entryB.note, allNotes, entryA.note);
|
|
57
|
+
const groupWarnings = buildGroupWarnings([entryA.note, entryB.note], entryA.note);
|
|
58
|
+
const pairRisk = aggregateMergeRisk([noteAEvidence.mergeRisk, noteBEvidence.mergeRisk]);
|
|
59
|
+
foundCount++;
|
|
60
|
+
lines.push(`${foundCount}. ${entryA.note.title} (${entryA.note.id})`);
|
|
61
|
+
lines.push(` └── ${entryB.note.title} (${entryB.note.id})`);
|
|
62
|
+
lines.push(` Similarity: ${similarity.toFixed(3)}`);
|
|
63
|
+
if (evidence) {
|
|
64
|
+
lines.push(` A: ${noteAEvidence.lifecycle}, ${noteAEvidence.role ?? "untyped"} | ${Math.round(noteAEvidence.ageDays)}d old | rel: ${noteAEvidence.relatedCount} | supersedes: ${noteAEvidence.supersededCount ?? 0} | risk: ${noteAEvidence.mergeRisk}`);
|
|
65
|
+
lines.push(` B: ${noteBEvidence.lifecycle}, ${noteBEvidence.role ?? "untyped"} | ${Math.round(noteBEvidence.ageDays)}d old | rel: ${noteBEvidence.relatedCount} | supersedes: ${noteBEvidence.supersededCount ?? 0} | risk: ${noteBEvidence.mergeRisk}`);
|
|
66
|
+
if (groupWarnings.length > 0) {
|
|
67
|
+
lines.push(` Warnings: ${groupWarnings.join("; ")}`);
|
|
68
|
+
}
|
|
69
|
+
lines.push(` Merge risk: ${pairRisk}`);
|
|
70
|
+
}
|
|
71
|
+
lines.push("");
|
|
72
|
+
checked.add(entryA.note.id);
|
|
73
|
+
checked.add(entryB.note.id);
|
|
74
|
+
duplicates.push({
|
|
75
|
+
noteA: { id: entryA.note.id, title: entryA.note.title },
|
|
76
|
+
noteB: { id: entryB.note.id, title: entryB.note.title },
|
|
77
|
+
similarity,
|
|
78
|
+
});
|
|
79
|
+
if (evidence) {
|
|
80
|
+
duplicatePairs.push({
|
|
81
|
+
similarity,
|
|
82
|
+
noteA: noteAEvidence,
|
|
83
|
+
noteB: noteBEvidence,
|
|
84
|
+
warnings: groupWarnings.length > 0 ? groupWarnings : undefined,
|
|
85
|
+
mergeRisk: pairRisk,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (foundCount === 0) {
|
|
92
|
+
lines.push("No duplicates found above the similarity threshold.");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
lines.push(`Found ${foundCount} potential duplicate pair(s).`);
|
|
96
|
+
lines.push("Use 'suggest-merges' strategy for actionable recommendations.");
|
|
97
|
+
}
|
|
98
|
+
const structuredContent = {
|
|
99
|
+
action: "consolidated",
|
|
100
|
+
strategy: "detect-duplicates",
|
|
101
|
+
project: toProjectRef(project),
|
|
102
|
+
notesProcessed: entries.length,
|
|
103
|
+
notesModified: 0,
|
|
104
|
+
duplicatePairs: evidence ? duplicatePairs : undefined,
|
|
105
|
+
};
|
|
106
|
+
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
|
|
107
|
+
}
|
|
108
|
+
export function findClusters(entries, project) {
|
|
109
|
+
const lines = [];
|
|
110
|
+
lines.push(`Cluster analysis for ${project?.name ?? "global"}:`);
|
|
111
|
+
lines.push("");
|
|
112
|
+
// Group by theme
|
|
113
|
+
const themed = new Map();
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const theme = classifyTheme(entry.note);
|
|
116
|
+
const bucket = themed.get(theme) ?? [];
|
|
117
|
+
bucket.push(entry);
|
|
118
|
+
themed.set(theme, bucket);
|
|
119
|
+
}
|
|
120
|
+
// Find relationship clusters
|
|
121
|
+
const idToEntry = new Map(entries.map((e) => [e.note.id, e]));
|
|
122
|
+
const visited = new Set();
|
|
123
|
+
const clusters = [];
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (visited.has(entry.note.id))
|
|
126
|
+
continue;
|
|
127
|
+
const cluster = [];
|
|
128
|
+
const queue = [entry];
|
|
129
|
+
while (queue.length > 0) {
|
|
130
|
+
const current = queue.shift();
|
|
131
|
+
if (visited.has(current.note.id))
|
|
132
|
+
continue;
|
|
133
|
+
visited.add(current.note.id);
|
|
134
|
+
cluster.push(current);
|
|
135
|
+
// Add related notes to queue
|
|
136
|
+
for (const rel of current.note.relatedTo ?? []) {
|
|
137
|
+
const related = idToEntry.get(rel.id);
|
|
138
|
+
if (related && !visited.has(rel.id)) {
|
|
139
|
+
queue.push(related);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (cluster.length > 1) {
|
|
144
|
+
clusters.push(cluster);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Output theme groups
|
|
148
|
+
const themeGroups = [];
|
|
149
|
+
lines.push("By Theme:");
|
|
150
|
+
for (const [theme, bucket] of themed) {
|
|
151
|
+
if (bucket.length > 1) {
|
|
152
|
+
lines.push(` ${titleCaseTheme(theme)} (${bucket.length} notes)`);
|
|
153
|
+
const examples = bucket.slice(0, 3).map((entry) => entry.note.title);
|
|
154
|
+
for (const entry of bucket.slice(0, 3)) {
|
|
155
|
+
lines.push(` - ${entry.note.title}`);
|
|
156
|
+
}
|
|
157
|
+
if (bucket.length > 3) {
|
|
158
|
+
lines.push(` ... and ${bucket.length - 3} more`);
|
|
159
|
+
}
|
|
160
|
+
themeGroups.push({ name: theme, count: bucket.length, examples });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Output relationship clusters
|
|
164
|
+
const relationshipClusters = [];
|
|
165
|
+
if (clusters.length > 0) {
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push("Connected Clusters (via relationships):");
|
|
168
|
+
for (let i = 0; i < clusters.length; i++) {
|
|
169
|
+
const cluster = clusters[i];
|
|
170
|
+
lines.push(` Cluster ${i + 1} (${cluster.length} notes):`);
|
|
171
|
+
const hub = cluster.reduce((max, e) => (e.note.relatedTo?.length ?? 0) > (max.note.relatedTo?.length ?? 0) ? e : max);
|
|
172
|
+
lines.push(` Hub: ${hub.note.title}`);
|
|
173
|
+
const clusterNotes = [];
|
|
174
|
+
for (const entry of cluster) {
|
|
175
|
+
if (entry.note.id !== hub.note.id) {
|
|
176
|
+
lines.push(` - ${entry.note.title}`);
|
|
177
|
+
clusterNotes.push({ id: entry.note.id, title: entry.note.title });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
relationshipClusters.push({
|
|
181
|
+
hub: { id: hub.note.id, title: hub.note.title },
|
|
182
|
+
notes: clusterNotes,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const structuredContent = {
|
|
187
|
+
action: "consolidated",
|
|
188
|
+
strategy: "find-clusters",
|
|
189
|
+
project: toProjectRef(project),
|
|
190
|
+
notesProcessed: entries.length,
|
|
191
|
+
notesModified: 0,
|
|
192
|
+
themeGroups,
|
|
193
|
+
relationshipClusters,
|
|
194
|
+
};
|
|
195
|
+
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
|
|
196
|
+
}
|
|
197
|
+
export async function suggestMerges(entries, threshold, defaultConsolidationMode, project, explicitMode, evidence = false) {
|
|
198
|
+
const lines = [];
|
|
199
|
+
const modeLabel = explicitMode ?? `${defaultConsolidationMode} (project/default; all-temporary merges auto-delete)`;
|
|
200
|
+
lines.push(`Merge suggestions for ${project?.name ?? "global"} (mode: ${modeLabel}):`);
|
|
201
|
+
lines.push("");
|
|
202
|
+
const checked = new Set();
|
|
203
|
+
let suggestionCount = 0;
|
|
204
|
+
const suggestions = [];
|
|
205
|
+
const mergeSuggestions = [];
|
|
206
|
+
const embeddings = await loadEmbeddingsByNoteId(entries);
|
|
207
|
+
const allNotes = entries.map((entry) => entry.note);
|
|
208
|
+
for (let i = 0; i < entries.length; i++) {
|
|
209
|
+
const entryA = entries[i];
|
|
210
|
+
if (checked.has(entryA.note.id))
|
|
211
|
+
continue;
|
|
212
|
+
const embeddingA = embeddings.get(entryA.note.id);
|
|
213
|
+
if (!embeddingA)
|
|
214
|
+
continue;
|
|
215
|
+
const similar = [];
|
|
216
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
217
|
+
const entryB = entries[j];
|
|
218
|
+
if (checked.has(entryB.note.id))
|
|
219
|
+
continue;
|
|
220
|
+
const embeddingB = embeddings.get(entryB.note.id);
|
|
221
|
+
if (!embeddingB)
|
|
222
|
+
continue;
|
|
223
|
+
const similarity = cosineSimilarity(embeddingA, embeddingB);
|
|
224
|
+
if (similarity >= threshold) {
|
|
225
|
+
similar.push({ entry: entryB, similarity });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (similar.length > 0) {
|
|
229
|
+
suggestionCount++;
|
|
230
|
+
similar.sort((a, b) => b.similarity - a.similarity);
|
|
231
|
+
const sources = [entryA, ...similar.map((s) => s.entry)];
|
|
232
|
+
const effectiveMode = resolveEffectiveConsolidationMode(sources.map((source) => source.note), defaultConsolidationMode, explicitMode);
|
|
233
|
+
lines.push(`${suggestionCount}. MERGE ${sources.length} NOTES`);
|
|
234
|
+
lines.push(` Into: "${entryA.note.title} (consolidated)"`);
|
|
235
|
+
lines.push(" Sources:");
|
|
236
|
+
for (const src of sources) {
|
|
237
|
+
const simStr = src.note.id === entryA.note.id ? "" : ` (${similar.find((s) => s.entry.note.id === src.note.id)?.similarity.toFixed(3)})`;
|
|
238
|
+
lines.push(` - ${src.note.title} (${src.note.id})${simStr}`);
|
|
239
|
+
}
|
|
240
|
+
const modeDescription = (() => {
|
|
241
|
+
switch (effectiveMode) {
|
|
242
|
+
case "supersedes":
|
|
243
|
+
return "preserves history";
|
|
244
|
+
case "delete":
|
|
245
|
+
return "removes sources";
|
|
246
|
+
default: {
|
|
247
|
+
const _exhaustive = effectiveMode;
|
|
248
|
+
return _exhaustive;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
const noteEvidence = sources.map((source) => buildConsolidateNoteEvidence(source.note, allNotes, entryA.note));
|
|
253
|
+
const mergeWarnings = buildGroupWarnings(sources.map((source) => source.note), entryA.note);
|
|
254
|
+
const mergeRisk = aggregateMergeRisk(noteEvidence.map((e) => e.mergeRisk));
|
|
255
|
+
lines.push(` Mode: ${effectiveMode} (${modeDescription})`);
|
|
256
|
+
if (evidence) {
|
|
257
|
+
for (const note of noteEvidence) {
|
|
258
|
+
lines.push(` Evidence: ${note.title} | ${note.lifecycle}, ${note.role ?? "untyped"} | ${Math.round(note.ageDays)}d | rel:${note.relatedCount} | risk:${note.mergeRisk}`);
|
|
259
|
+
}
|
|
260
|
+
if (mergeWarnings.length > 0) {
|
|
261
|
+
lines.push(` Warnings: ${mergeWarnings.join("; ")}`);
|
|
262
|
+
}
|
|
263
|
+
lines.push(` Merge risk: ${mergeRisk}`);
|
|
264
|
+
}
|
|
265
|
+
lines.push(" To execute:");
|
|
266
|
+
lines.push(` consolidate({ strategy: "execute-merge", mergePlan: {`);
|
|
267
|
+
lines.push(` sourceIds: [${sources.map((s) => `"${s.note.id}"`).join(", ")}],`);
|
|
268
|
+
lines.push(` targetTitle: "${entryA.note.title} (consolidated)"`);
|
|
269
|
+
lines.push(` }})`);
|
|
270
|
+
lines.push("");
|
|
271
|
+
suggestions.push({
|
|
272
|
+
targetTitle: `${entryA.note.title} (consolidated)`,
|
|
273
|
+
sourceIds: sources.map((s) => s.note.id),
|
|
274
|
+
similarities: similar.map((s) => ({ id: s.entry.note.id, similarity: s.similarity })),
|
|
275
|
+
});
|
|
276
|
+
if (evidence) {
|
|
277
|
+
mergeSuggestions.push({
|
|
278
|
+
targetTitle: `${entryA.note.title} (consolidated)`,
|
|
279
|
+
sourceIds: sources.map((source) => source.note.id),
|
|
280
|
+
mode: effectiveMode,
|
|
281
|
+
notes: noteEvidence,
|
|
282
|
+
warnings: mergeWarnings.length > 0 ? mergeWarnings : undefined,
|
|
283
|
+
mergeRisk,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
checked.add(entryA.note.id);
|
|
287
|
+
for (const s of similar)
|
|
288
|
+
checked.add(s.entry.note.id);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (suggestionCount === 0) {
|
|
292
|
+
lines.push("No merge suggestions found. Try lowering the threshold or manual review.");
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
lines.push(`Generated ${suggestionCount} merge suggestion(s). Review carefully before executing.`);
|
|
296
|
+
}
|
|
297
|
+
const structuredContent = {
|
|
298
|
+
action: "consolidated",
|
|
299
|
+
strategy: "suggest-merges",
|
|
300
|
+
project: toProjectRef(project),
|
|
301
|
+
notesProcessed: entries.length,
|
|
302
|
+
notesModified: 0,
|
|
303
|
+
mergeSuggestions: evidence ? mergeSuggestions : undefined,
|
|
304
|
+
};
|
|
305
|
+
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
|
|
306
|
+
}
|
|
307
|
+
async function loadEmbeddingsByNoteId(entries) {
|
|
308
|
+
const embeddings = new Map();
|
|
309
|
+
await Promise.all(entries.map(async (entry) => {
|
|
310
|
+
const record = await entry.vault.storage.readEmbedding(entry.note.id);
|
|
311
|
+
if (record) {
|
|
312
|
+
embeddings.set(entry.note.id, record.embedding);
|
|
313
|
+
}
|
|
314
|
+
}));
|
|
315
|
+
return embeddings;
|
|
316
|
+
}
|
|
317
|
+
export async function executeMerge(ctx, entries, mergePlan, defaultConsolidationMode, project, cwd, explicitMode, policy, allowProtectedBranch = false, evidence = true) {
|
|
318
|
+
const { vaultManager } = ctx;
|
|
319
|
+
const sourceIds = normalizeMergePlanSourceIds(mergePlan.sourceIds);
|
|
320
|
+
const targetTitle = mergePlan.targetTitle.trim();
|
|
321
|
+
const { content: customContent, description, summary, tags } = mergePlan;
|
|
322
|
+
if (sourceIds.length < 2) {
|
|
323
|
+
const structuredContent = {
|
|
324
|
+
action: "consolidated",
|
|
325
|
+
strategy: "execute-merge",
|
|
326
|
+
project: toProjectRef(project),
|
|
327
|
+
notesProcessed: entries.length,
|
|
328
|
+
notesModified: 0,
|
|
329
|
+
warnings: ["execute-merge requires at least two distinct sourceIds."],
|
|
330
|
+
};
|
|
331
|
+
return { content: [{ type: "text", text: "execute-merge requires at least two distinct sourceIds." }], structuredContent };
|
|
332
|
+
}
|
|
333
|
+
if (!targetTitle) {
|
|
334
|
+
const structuredContent = {
|
|
335
|
+
action: "consolidated",
|
|
336
|
+
strategy: "execute-merge",
|
|
337
|
+
project: toProjectRef(project),
|
|
338
|
+
notesProcessed: entries.length,
|
|
339
|
+
notesModified: 0,
|
|
340
|
+
warnings: ["execute-merge requires a non-empty targetTitle."],
|
|
341
|
+
};
|
|
342
|
+
return { content: [{ type: "text", text: "execute-merge requires a non-empty targetTitle." }], structuredContent };
|
|
343
|
+
}
|
|
344
|
+
// Find all source entries
|
|
345
|
+
const sourceEntries = [];
|
|
346
|
+
for (const id of sourceIds) {
|
|
347
|
+
const entry = entries.find((e) => e.note.id === id);
|
|
348
|
+
if (!entry) {
|
|
349
|
+
const structuredContent = {
|
|
350
|
+
action: "consolidated",
|
|
351
|
+
strategy: "execute-merge",
|
|
352
|
+
project: toProjectRef(project),
|
|
353
|
+
notesProcessed: entries.length,
|
|
354
|
+
notesModified: 0,
|
|
355
|
+
warnings: [`Source note '${id}' not found.`],
|
|
356
|
+
};
|
|
357
|
+
return { content: [{ type: "text", text: `Source note '${id}' not found.` }], structuredContent };
|
|
358
|
+
}
|
|
359
|
+
sourceEntries.push(entry);
|
|
360
|
+
}
|
|
361
|
+
const consolidationMode = resolveEffectiveConsolidationMode(sourceEntries.map((entry) => entry.note), defaultConsolidationMode, explicitMode);
|
|
362
|
+
const existingTargetEntry = findExistingExecuteMergeTarget(entries, sourceEntries, targetTitle);
|
|
363
|
+
const projectVault = cwd ? await vaultManager.getProjectVaultIfExists(cwd) : null;
|
|
364
|
+
const targetVault = existingTargetEntry?.vault ?? projectVault ?? vaultManager.main;
|
|
365
|
+
let touchesProjectVault = targetVault.isProject || sourceEntries.some((entry) => entry.vault.isProject);
|
|
366
|
+
if (!touchesProjectVault && consolidationMode === "delete") {
|
|
367
|
+
touchesProjectVault = await wouldRelationshipCleanupTouchProjectVault(ctx, sourceIds);
|
|
368
|
+
}
|
|
369
|
+
if (touchesProjectVault) {
|
|
370
|
+
const projectLabel = project
|
|
371
|
+
? `${project.name} (${project.id})`
|
|
372
|
+
: "this context";
|
|
373
|
+
const protectedBranchCheck = await shouldBlockProtectedBranchCommit(ctx, {
|
|
374
|
+
cwd,
|
|
375
|
+
writeScope: "project",
|
|
376
|
+
automaticCommit: true,
|
|
377
|
+
projectLabel,
|
|
378
|
+
policy,
|
|
379
|
+
allowProtectedBranch,
|
|
380
|
+
toolName: "consolidate",
|
|
381
|
+
});
|
|
382
|
+
if (protectedBranchCheck.blocked) {
|
|
383
|
+
const message = protectedBranchCheck.message ?? "Protected branch policy blocked this commit.";
|
|
384
|
+
const structuredContent = {
|
|
385
|
+
action: "consolidated",
|
|
386
|
+
strategy: "execute-merge",
|
|
387
|
+
project: toProjectRef(project),
|
|
388
|
+
notesProcessed: entries.length,
|
|
389
|
+
notesModified: 0,
|
|
390
|
+
warnings: [message],
|
|
391
|
+
};
|
|
392
|
+
return { content: [{ type: "text", text: message }], structuredContent };
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
const now = isoDateString(new Date().toISOString());
|
|
396
|
+
// Build consolidated content
|
|
397
|
+
const sections = [];
|
|
398
|
+
if (customContent) {
|
|
399
|
+
if (description) {
|
|
400
|
+
sections.push(description);
|
|
401
|
+
sections.push("");
|
|
402
|
+
}
|
|
403
|
+
sections.push(customContent);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
if (description) {
|
|
407
|
+
sections.push(description);
|
|
408
|
+
sections.push("");
|
|
409
|
+
}
|
|
410
|
+
sections.push("## Consolidated from:");
|
|
411
|
+
for (const entry of sourceEntries) {
|
|
412
|
+
sections.push(`### ${entry.note.title}`);
|
|
413
|
+
sections.push(`*Source: \`${entry.note.id}\`*`);
|
|
414
|
+
sections.push("");
|
|
415
|
+
sections.push(entry.note.content);
|
|
416
|
+
sections.push("");
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// Combine tags (deduplicated)
|
|
420
|
+
const combinedTags = tags ?? Array.from(new Set(sourceEntries.flatMap((e) => e.note.tags)));
|
|
421
|
+
// Collect all unique relationships from sources (excluding relationships among sources)
|
|
422
|
+
const sourceIdsSet = new Set(sourceIds);
|
|
423
|
+
const relationshipSources = existingTargetEntry
|
|
424
|
+
? [...sourceEntries.map((entry) => entry.note), existingTargetEntry.note]
|
|
425
|
+
: sourceEntries.map((entry) => entry.note);
|
|
426
|
+
const allRelationships = mergeRelationshipsFromNotes(relationshipSources, sourceIdsSet);
|
|
427
|
+
// Create or update the consolidated note
|
|
428
|
+
const targetId = existingTargetEntry?.note.id ?? makeId(targetTitle);
|
|
429
|
+
const consolidatedNote = {
|
|
430
|
+
id: targetId,
|
|
431
|
+
title: targetTitle,
|
|
432
|
+
content: sections.join("\n").trim(),
|
|
433
|
+
tags: combinedTags,
|
|
434
|
+
lifecycle: "permanent",
|
|
435
|
+
project: project?.id,
|
|
436
|
+
projectName: project?.name,
|
|
437
|
+
relatedTo: allRelationships,
|
|
438
|
+
createdAt: existingTargetEntry?.note.createdAt ?? now,
|
|
439
|
+
updatedAt: now,
|
|
440
|
+
memoryVersion: 1,
|
|
441
|
+
};
|
|
442
|
+
// Write consolidated note
|
|
443
|
+
await targetVault.storage.writeNote(consolidatedNote);
|
|
444
|
+
let embeddingStatus = { status: "written" };
|
|
445
|
+
// Generate embedding for consolidated note
|
|
446
|
+
try {
|
|
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
|
+
catch (err) {
|
|
457
|
+
embeddingStatus = { status: "skipped", reason: getErrorMessage(err) };
|
|
458
|
+
console.error(`[embedding] Failed for consolidated note '${targetId}': ${err}`);
|
|
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 Error(`Unknown consolidation mode: ${_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 Error(`Unknown consolidation mode: ${_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 Error(`Unknown consolidation mode: ${_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
|