@danielmarbach/mnemonic-mcp 0.26.2 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -12,7 +12,7 @@ import { enrichTemporalHistory } from "./temporal-interpretation.js";
12
12
  import { getOrBuildProjection } from "./projections.js";
13
13
  import { invalidateActiveProjectCache, getOrBuildVaultEmbeddings, getOrBuildVaultNoteList, getRecentSessionNoteAccesses, getRecentSessionAccessNote, getSessionCachedNote, getSessionCachedProjection, getSessionCachedProjectionTokens, recordSessionNoteAccess, setSessionCachedNote, setSessionCachedProjection, setSessionCachedProjectionTokens, } from "./cache.js";
14
14
  import { performance } from "perf_hooks";
15
- import { filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
15
+ import { aggregateMergeRisk, buildConsolidateNoteEvidence, buildGroupWarnings, filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
16
16
  import { suggestAutoRelationships } from "./auto-relate.js";
17
17
  import { computeRecallMetadataBoost, computeHybridScore, selectRecallResults, selectWorkflowResults, applyLexicalReranking, enrichRescueCandidateScores, resolveDiscoveredVaults, applyCanonicalExplanationPromotion, applyGraphSpreadingActivation, assignDenseRanks, detectTemporalQueryHint, computeTemporalRecencyBoost, shouldApplyTemporalFiltering, isWithinTemporalFilterWindow, } from "./recall.js";
18
18
  import { shouldTriggerLexicalRescue, prepareTfIdfCorpusFromTokenizedDocuments, rankDocumentsByTfIdf, LEXICAL_RESCUE_CANDIDATE_LIMIT, LEXICAL_RESCUE_THRESHOLD, LEXICAL_RESCUE_RESULT_LIMIT, tokenize, } from "./lexical.js";
@@ -439,6 +439,34 @@ function formatRelationshipPreview(preview) {
439
439
  : "";
440
440
  return `**related (${preview.totalDirectRelations}):** ${shown}${more}`;
441
441
  }
442
+ function toRecallFreshness(updatedAt) {
443
+ const updated = new Date(updatedAt);
444
+ if (Number.isNaN(updated.getTime())) {
445
+ return "older";
446
+ }
447
+ const ageDays = Math.max(0, (Date.now() - updated.getTime()) / (1000 * 60 * 60 * 24));
448
+ if (ageDays <= 1)
449
+ return "today";
450
+ if (ageDays <= 7)
451
+ return "thisWeek";
452
+ if (ageDays <= 31)
453
+ return "thisMonth";
454
+ return "older";
455
+ }
456
+ function toRecallRankBand(semanticRank) {
457
+ if (semanticRank !== undefined && semanticRank <= 3)
458
+ return "top3";
459
+ if (semanticRank !== undefined && semanticRank <= 10)
460
+ return "top10";
461
+ return "lower";
462
+ }
463
+ function formatRetrievalEvidenceHint(evidence, role) {
464
+ const rolePart = role ?? "untyped";
465
+ const supersedesPart = evidence.superseded
466
+ ? ` | supersedes ${evidence.supersededCount ?? 1} note${(evidence.supersededCount ?? 1) === 1 ? "" : "s"}`
467
+ : "";
468
+ return `${evidence.rankBand} | channels: ${evidence.channels.join(", ")} | ${evidence.projectRelevant ? "project-relevant" : "cross-project"}\n${rolePart}, ${evidence.freshness}${supersedesPart}`;
469
+ }
442
470
  // ── Git commit message helpers ────────────────────────────────────────────────
443
471
  /**
444
472
  * Extract a short human-readable summary from note content.
@@ -1900,7 +1928,8 @@ server.registerTool("recall", {
1900
1928
  "Returns:\n" +
1901
1929
  "- Ranked memory matches with scores, vault label, tags, lifecycle, and updated time\n" +
1902
1930
  "- Bounded 1-hop relationship previews automatically attached to top results\n" +
1903
- "- In temporal mode, optional compact history entries for top matches\n\n" +
1931
+ "- In temporal mode, optional compact history entries for top matches\n" +
1932
+ "- Optional retrieval evidence via `evidence: \"compact\"` for why a result ranked\n\n" +
1904
1933
  "Read-only.\n\n" +
1905
1934
  "Typical next step:\n" +
1906
1935
  "- Use `get`, `update`, `relate`, or `consolidate` based on the results.",
@@ -1917,6 +1946,7 @@ server.registerTool("recall", {
1917
1946
  minSimilarity: z.number().min(0).max(1).optional().default(DEFAULT_MIN_SIMILARITY),
1918
1947
  mode: z.enum(["default", "temporal", "workflow"]).optional().default("default").describe("Use `temporal` for compact git-backed history, or `workflow` for RPIR-oriented chain reconstruction."),
1919
1948
  verbose: z.boolean().optional().default(false).describe("Only meaningful with `mode: \"temporal\"`. Adds richer stats-based history context without returning raw diffs."),
1949
+ evidence: z.enum(["compact"]).optional().describe("Optional retrieval rationale. Omit for default output; use `compact` for bounded rank and lineage signals."),
1920
1950
  tags: z.array(z.string()).optional().describe("Filter results to notes with all of these tags."),
1921
1951
  scope: z
1922
1952
  .enum(["project", "global", "all"])
@@ -1928,7 +1958,7 @@ server.registerTool("recall", {
1928
1958
  lifecycle: z.enum(["temporary", "permanent"]).optional().describe("Filter results by lifecycle. Useful for recovering working-state with `lifecycle: temporary` after `project_memory_summary` orientation."),
1929
1959
  }),
1930
1960
  outputSchema: RecallResultSchema,
1931
- }, async ({ query, cwd, limit, minSimilarity, mode, verbose, tags, scope, lifecycle }) => {
1961
+ }, async ({ query, cwd, limit, minSimilarity, mode, verbose, evidence, tags, scope, lifecycle }) => {
1932
1962
  const t0Recall = performance.now();
1933
1963
  await ensureBranchSynced(cwd);
1934
1964
  const project = await resolveProject(cwd);
@@ -2049,12 +2079,14 @@ server.registerTool("recall", {
2049
2079
  };
2050
2080
  const strongestSemanticScore = scored.reduce((max, candidate) => max === undefined ? candidate.score : Math.max(max, candidate.score), undefined);
2051
2081
  const reranked = applyLexicalReranking(scored, query, getProjectionText);
2082
+ const semanticCandidateIds = new Set(reranked.map((candidate) => candidate.id));
2052
2083
  // Apply graph spreading activation: traverse related notes and boost their scores
2053
2084
  const preSpreadIds = new Set(reranked.map((c) => c.id));
2054
2085
  const getNoteRelationships = (id) => {
2055
2086
  return noteRelationships.get(id);
2056
2087
  };
2057
2088
  const withGraphSpread = applyGraphSpreadingActivation(reranked, getNoteRelationships);
2089
+ const graphDiscoveredIds = new Set(withGraphSpread.filter((candidate) => !semanticCandidateIds.has(candidate.id)).map((candidate) => candidate.id));
2058
2090
  // Resolve correct vault for graph-discovered candidates that inherited their
2059
2091
  // entry point's vault instead of their own.
2060
2092
  await resolveDiscoveredVaults(withGraphSpread, preSpreadIds, async (id) => {
@@ -2074,6 +2106,7 @@ server.registerTool("recall", {
2074
2106
  candidate.semanticRank = rank;
2075
2107
  });
2076
2108
  let promoted = applyCanonicalExplanationPromotion(withGraphSpread);
2109
+ let rescueCandidateIds = new Set();
2077
2110
  // Lexical rescue: when semantic results are weak, scan projections for additional candidates.
2078
2111
  // Skip rescue when the caller set a strict minSimilarity above the default,
2079
2112
  // because rescue candidates lack genuine semantic backing.
@@ -2081,6 +2114,7 @@ server.registerTool("recall", {
2081
2114
  if (rescueAllowed && shouldTriggerLexicalRescue(strongestSemanticScore, scored.length)) {
2082
2115
  const rescueCandidates = await collectLexicalRescueCandidates(vaults, query, temporalQueryHint, project ?? undefined, scope, tags, lifecycle, promoted);
2083
2116
  promoted.push(...rescueCandidates);
2117
+ rescueCandidateIds = new Set(rescueCandidates.map((candidate) => candidate.id));
2084
2118
  enrichRescueCandidateScores(promoted, query, getProjectionText);
2085
2119
  promoted = applyCanonicalExplanationPromotion(promoted);
2086
2120
  }
@@ -2099,7 +2133,7 @@ server.registerTool("recall", {
2099
2133
  // Determine how many top results get relationship expansion
2100
2134
  // Top 1 by default, top 3 if result count is small
2101
2135
  const recallRelationshipLimit = top.length <= 3 ? 3 : 1;
2102
- for (const [index, { id, score, vault, boosted }] of top.entries()) {
2136
+ for (const [index, { id, score, vault, boosted, semanticRank, lexicalRank, canonicalExplanationScore, metadata, isCurrentProject }] of top.entries()) {
2103
2137
  const note = await readCachedNote(vault, id);
2104
2138
  if (note) {
2105
2139
  const centrality = note.relatedTo?.length ?? 0;
@@ -2136,8 +2170,30 @@ server.registerTool("recall", {
2136
2170
  const provenanceLine = provenance || confidence
2137
2171
  ? `\n**confidence:** ${confidence ?? "medium"}${provenance?.recentlyChanged ? " | **recently changed**" : ""}`
2138
2172
  : "";
2173
+ const supersededRelations = (note.relatedTo ?? []).filter((rel) => rel.type === "supersedes");
2174
+ const retrievalEvidence = evidence === "compact"
2175
+ ? {
2176
+ channels: [
2177
+ semanticRank !== undefined ? "semantic" : undefined,
2178
+ lexicalRank !== undefined ? "lexical" : undefined,
2179
+ graphDiscoveredIds.has(id) ? "graph" : undefined,
2180
+ rescueCandidateIds.has(id) ? "rescue" : undefined,
2181
+ canonicalExplanationScore !== undefined && canonicalExplanationScore > 0 ? "canonical" : undefined,
2182
+ temporalQueryHint ? "temporal-boost" : undefined,
2183
+ ].filter((value) => value !== undefined),
2184
+ rankBand: toRecallRankBand(semanticRank),
2185
+ projectRelevant: isCurrentProject,
2186
+ freshness: toRecallFreshness(note.updatedAt),
2187
+ superseded: supersededRelations.length > 0,
2188
+ supersededBy: supersededRelations.length > 0 ? supersededRelations[0]?.id : undefined,
2189
+ supersededCount: supersededRelations.length > 0 ? supersededRelations.length : undefined,
2190
+ }
2191
+ : undefined;
2192
+ const evidenceLine = retrievalEvidence
2193
+ ? `\n${formatRetrievalEvidenceHint(retrievalEvidence, metadata?.role)}`
2194
+ : "";
2139
2195
  // Suppress raw related IDs when enriched preview is shown to avoid duplication
2140
- sections.push(`${formatNote(note, score, relationships === undefined)}${provenanceLine}${formattedHistory}${formattedRelationships}`);
2196
+ sections.push(`${formatNote(note, score, relationships === undefined)}${provenanceLine}${evidenceLine}${formattedHistory}${formattedRelationships}`);
2141
2197
  structuredResults.push({
2142
2198
  id,
2143
2199
  title: note.title,
@@ -2154,6 +2210,7 @@ server.registerTool("recall", {
2154
2210
  history,
2155
2211
  historySummary,
2156
2212
  relationships,
2213
+ retrievalEvidence,
2157
2214
  });
2158
2215
  if (project) {
2159
2216
  setSessionCachedNote(project.id, vault.storage.vaultPath, note);
@@ -4333,7 +4390,8 @@ server.registerTool("consolidate", {
4333
4390
  "- The canonical memory, source ids, resulting relationships, and persistence status\n\n" +
4334
4391
  "Side effects: creates or updates the canonical note, modifies or removes source notes according to mode, git commits, and may push.\n\n" +
4335
4392
  "Typical next step:\n" +
4336
- "- Use `get` to inspect the canonical note and `recall` to confirm duplication is reduced.",
4393
+ "- Use `get` to inspect the canonical note and `recall` to confirm duplication is reduced.\n" +
4394
+ "- Evidence defaults on for consolidate analysis strategies and execute-merge (lifecycle, risk, warnings).",
4337
4395
  annotations: {
4338
4396
  readOnlyHint: false,
4339
4397
  destructiveHint: true,
@@ -4353,7 +4411,8 @@ server.registerTool("consolidate", {
4353
4411
  ])
4354
4412
  .describe("What to do: 'dry-run' = full analysis without changes, 'detect-duplicates' = find similar pairs, " +
4355
4413
  "'find-clusters' = group by theme and relationships, 'suggest-merges' = actionable merge recommendations, " +
4356
- "'execute-merge' = perform a merge (requires mergePlan), 'prune-superseded' = delete notes marked as superseded"),
4414
+ "'execute-merge' = perform a merge (requires mergePlan), 'prune-superseded' = delete notes marked as superseded. " +
4415
+ "Use `evidence: true` on analysis strategies for trust/risk signals."),
4357
4416
  mode: z
4358
4417
  .enum(CONSOLIDATION_MODES)
4359
4418
  .optional()
@@ -4365,6 +4424,10 @@ server.registerTool("consolidate", {
4365
4424
  .optional()
4366
4425
  .default(0.85)
4367
4426
  .describe("Cosine similarity threshold for duplicate detection (0.85 default)"),
4427
+ evidence: z
4428
+ .boolean()
4429
+ .optional()
4430
+ .describe("Confidence signals for analysis strategies and execute-merge (lifecycle, risk, warnings). Default true for safety."),
4368
4431
  mergePlan: z
4369
4432
  .object({
4370
4433
  sourceIds: z.array(z.string()).min(2).describe("Ids of notes to merge into one consolidated note"),
@@ -4383,7 +4446,7 @@ server.registerTool("consolidate", {
4383
4446
  "When true, consolidate can commit on a protected branch without changing project policy."),
4384
4447
  }),
4385
4448
  outputSchema: ConsolidateResultSchema,
4386
- }, async ({ cwd, strategy, mode, threshold, mergePlan, allowProtectedBranch = false }) => {
4449
+ }, async ({ cwd, strategy, mode, threshold, evidence = true, mergePlan, allowProtectedBranch = false }) => {
4387
4450
  await ensureBranchSynced(cwd);
4388
4451
  const project = await resolveProject(cwd);
4389
4452
  if (!project && cwd) {
@@ -4403,16 +4466,16 @@ server.registerTool("consolidate", {
4403
4466
  const defaultConsolidationMode = resolveConsolidationMode(policy);
4404
4467
  switch (strategy) {
4405
4468
  case "detect-duplicates":
4406
- return detectDuplicates(projectNotes, threshold, project);
4469
+ return detectDuplicates(projectNotes, threshold, project, evidence);
4407
4470
  case "find-clusters":
4408
4471
  return findClusters(projectNotes, project);
4409
4472
  case "suggest-merges":
4410
- return suggestMerges(projectNotes, threshold, defaultConsolidationMode, project, mode);
4473
+ return suggestMerges(projectNotes, threshold, defaultConsolidationMode, project, mode, evidence);
4411
4474
  case "execute-merge": {
4412
4475
  if (!mergePlan) {
4413
4476
  return { content: [{ type: "text", text: "execute-merge strategy requires a mergePlan with sourceIds and targetTitle." }], isError: true };
4414
4477
  }
4415
- const mergeResult = await executeMerge(entries, mergePlan, defaultConsolidationMode, project, cwd, mode, policy, allowProtectedBranch);
4478
+ const mergeResult = await executeMerge(entries, mergePlan, defaultConsolidationMode, project, cwd, mode, policy, allowProtectedBranch, evidence);
4416
4479
  invalidateActiveProjectCache();
4417
4480
  return mergeResult;
4418
4481
  }
@@ -4422,20 +4485,22 @@ server.registerTool("consolidate", {
4422
4485
  return pruneResult;
4423
4486
  }
4424
4487
  case "dry-run":
4425
- return dryRunAll(projectNotes, threshold, defaultConsolidationMode, project, mode);
4488
+ return dryRunAll(projectNotes, threshold, defaultConsolidationMode, project, mode, evidence);
4426
4489
  default:
4427
4490
  return { content: [{ type: "text", text: `Unknown strategy: ${strategy}` }], isError: true };
4428
4491
  }
4429
4492
  });
4430
4493
  // Consolidate helper functions
4431
- async function detectDuplicates(entries, threshold, project) {
4494
+ async function detectDuplicates(entries, threshold, project, evidence) {
4432
4495
  const lines = [];
4433
4496
  lines.push(`Duplicate detection for ${project?.name ?? "global"} (similarity > ${threshold}):`);
4434
4497
  lines.push("");
4435
4498
  const checked = new Set();
4436
4499
  let foundCount = 0;
4437
4500
  const duplicates = [];
4501
+ const duplicatePairs = [];
4438
4502
  const embeddings = await loadEmbeddingsByNoteId(entries);
4503
+ const allNotes = entries.map((entry) => entry.note);
4439
4504
  for (let i = 0; i < entries.length; i++) {
4440
4505
  const entryA = entries[i];
4441
4506
  if (checked.has(entryA.note.id))
@@ -4452,10 +4517,22 @@ async function detectDuplicates(entries, threshold, project) {
4452
4517
  continue;
4453
4518
  const similarity = cosineSimilarity(embeddingA, embeddingB);
4454
4519
  if (similarity >= threshold) {
4520
+ const noteAEvidence = buildConsolidateNoteEvidence(entryA.note, allNotes, entryA.note);
4521
+ const noteBEvidence = buildConsolidateNoteEvidence(entryB.note, allNotes, entryA.note);
4522
+ const groupWarnings = buildGroupWarnings([entryA.note, entryB.note], entryA.note);
4523
+ const pairRisk = aggregateMergeRisk([noteAEvidence.mergeRisk, noteBEvidence.mergeRisk]);
4455
4524
  foundCount++;
4456
4525
  lines.push(`${foundCount}. ${entryA.note.title} (${entryA.note.id})`);
4457
4526
  lines.push(` └── ${entryB.note.title} (${entryB.note.id})`);
4458
4527
  lines.push(` Similarity: ${similarity.toFixed(3)}`);
4528
+ if (evidence) {
4529
+ lines.push(` A: ${noteAEvidence.lifecycle}, ${noteAEvidence.role ?? "untyped"} | ${Math.round(noteAEvidence.ageDays)}d old | rel: ${noteAEvidence.relatedCount} | supersedes: ${noteAEvidence.supersededCount ?? 0} | risk: ${noteAEvidence.mergeRisk}`);
4530
+ lines.push(` B: ${noteBEvidence.lifecycle}, ${noteBEvidence.role ?? "untyped"} | ${Math.round(noteBEvidence.ageDays)}d old | rel: ${noteBEvidence.relatedCount} | supersedes: ${noteBEvidence.supersededCount ?? 0} | risk: ${noteBEvidence.mergeRisk}`);
4531
+ if (groupWarnings.length > 0) {
4532
+ lines.push(` Warnings: ${groupWarnings.join("; ")}`);
4533
+ }
4534
+ lines.push(` Merge risk: ${pairRisk}`);
4535
+ }
4459
4536
  lines.push("");
4460
4537
  checked.add(entryA.note.id);
4461
4538
  checked.add(entryB.note.id);
@@ -4464,6 +4541,15 @@ async function detectDuplicates(entries, threshold, project) {
4464
4541
  noteB: { id: entryB.note.id, title: entryB.note.title },
4465
4542
  similarity,
4466
4543
  });
4544
+ if (evidence) {
4545
+ duplicatePairs.push({
4546
+ similarity,
4547
+ noteA: noteAEvidence,
4548
+ noteB: noteBEvidence,
4549
+ warnings: groupWarnings.length > 0 ? groupWarnings : undefined,
4550
+ mergeRisk: pairRisk,
4551
+ });
4552
+ }
4467
4553
  }
4468
4554
  }
4469
4555
  }
@@ -4480,6 +4566,7 @@ async function detectDuplicates(entries, threshold, project) {
4480
4566
  project: toProjectRef(project),
4481
4567
  notesProcessed: entries.length,
4482
4568
  notesModified: 0,
4569
+ duplicatePairs: evidence ? duplicatePairs : undefined,
4483
4570
  };
4484
4571
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
4485
4572
  }
@@ -4572,7 +4659,7 @@ function findClusters(entries, project) {
4572
4659
  };
4573
4660
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
4574
4661
  }
4575
- async function suggestMerges(entries, threshold, defaultConsolidationMode, project, explicitMode) {
4662
+ async function suggestMerges(entries, threshold, defaultConsolidationMode, project, explicitMode, evidence = false) {
4576
4663
  const lines = [];
4577
4664
  const modeLabel = explicitMode ?? `${defaultConsolidationMode} (project/default; all-temporary merges auto-delete)`;
4578
4665
  lines.push(`Merge suggestions for ${project?.name ?? "global"} (mode: ${modeLabel}):`);
@@ -4580,7 +4667,9 @@ async function suggestMerges(entries, threshold, defaultConsolidationMode, proje
4580
4667
  const checked = new Set();
4581
4668
  let suggestionCount = 0;
4582
4669
  const suggestions = [];
4670
+ const mergeSuggestions = [];
4583
4671
  const embeddings = await loadEmbeddingsByNoteId(entries);
4672
+ const allNotes = entries.map((entry) => entry.note);
4584
4673
  for (let i = 0; i < entries.length; i++) {
4585
4674
  const entryA = entries[i];
4586
4675
  if (checked.has(entryA.note.id))
@@ -4625,7 +4714,19 @@ async function suggestMerges(entries, threshold, defaultConsolidationMode, proje
4625
4714
  }
4626
4715
  }
4627
4716
  })();
4717
+ const noteEvidence = sources.map((source) => buildConsolidateNoteEvidence(source.note, allNotes, entryA.note));
4718
+ const mergeWarnings = buildGroupWarnings(sources.map((source) => source.note), entryA.note);
4719
+ const mergeRisk = aggregateMergeRisk(noteEvidence.map((e) => e.mergeRisk));
4628
4720
  lines.push(` Mode: ${effectiveMode} (${modeDescription})`);
4721
+ if (evidence) {
4722
+ for (const note of noteEvidence) {
4723
+ lines.push(` Evidence: ${note.title} | ${note.lifecycle}, ${note.role ?? "untyped"} | ${Math.round(note.ageDays)}d | rel:${note.relatedCount} | risk:${note.mergeRisk}`);
4724
+ }
4725
+ if (mergeWarnings.length > 0) {
4726
+ lines.push(` Warnings: ${mergeWarnings.join("; ")}`);
4727
+ }
4728
+ lines.push(` Merge risk: ${mergeRisk}`);
4729
+ }
4629
4730
  lines.push(" To execute:");
4630
4731
  lines.push(` consolidate({ strategy: "execute-merge", mergePlan: {`);
4631
4732
  lines.push(` sourceIds: [${sources.map((s) => `"${s.note.id}"`).join(", ")}],`);
@@ -4637,6 +4738,16 @@ async function suggestMerges(entries, threshold, defaultConsolidationMode, proje
4637
4738
  sourceIds: sources.map((s) => s.note.id),
4638
4739
  similarities: similar.map((s) => ({ id: s.entry.note.id, similarity: s.similarity })),
4639
4740
  });
4741
+ if (evidence) {
4742
+ mergeSuggestions.push({
4743
+ targetTitle: `${entryA.note.title} (consolidated)`,
4744
+ sourceIds: sources.map((source) => source.note.id),
4745
+ mode: effectiveMode,
4746
+ notes: noteEvidence,
4747
+ warnings: mergeWarnings.length > 0 ? mergeWarnings : undefined,
4748
+ mergeRisk,
4749
+ });
4750
+ }
4640
4751
  checked.add(entryA.note.id);
4641
4752
  for (const s of similar)
4642
4753
  checked.add(s.entry.note.id);
@@ -4654,6 +4765,7 @@ async function suggestMerges(entries, threshold, defaultConsolidationMode, proje
4654
4765
  project: toProjectRef(project),
4655
4766
  notesProcessed: entries.length,
4656
4767
  notesModified: 0,
4768
+ mergeSuggestions: evidence ? mergeSuggestions : undefined,
4657
4769
  };
4658
4770
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
4659
4771
  }
@@ -4667,7 +4779,7 @@ async function loadEmbeddingsByNoteId(entries) {
4667
4779
  }));
4668
4780
  return embeddings;
4669
4781
  }
4670
- async function executeMerge(entries, mergePlan, defaultConsolidationMode, project, cwd, explicitMode, policy, allowProtectedBranch = false) {
4782
+ async function executeMerge(entries, mergePlan, defaultConsolidationMode, project, cwd, explicitMode, policy, allowProtectedBranch = false, evidence = true) {
4671
4783
  const sourceIds = normalizeMergePlanSourceIds(mergePlan.sourceIds);
4672
4784
  const targetTitle = mergePlan.targetTitle.trim();
4673
4785
  const { content: customContent, description, summary, tags } = mergePlan;
@@ -4945,12 +5057,33 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
4945
5057
  throw new Error(`Unknown consolidation mode: ${_exhaustive}`);
4946
5058
  }
4947
5059
  }
5060
+ let executeMergeEvidence;
5061
+ if (evidence) {
5062
+ const allNotes = entries.map((entry) => entry.note);
5063
+ const noteEvidence = sourceEntries.map((entry) => buildConsolidateNoteEvidence(entry.note, allNotes, sourceEntries[0]?.note));
5064
+ const mergeWarnings = buildGroupWarnings(sourceEntries.map((entry) => entry.note), sourceEntries[0]?.note);
5065
+ const mergeRisk = aggregateMergeRisk(noteEvidence.map((e) => e.mergeRisk));
5066
+ lines.push(" Evidence:");
5067
+ for (const note of noteEvidence) {
5068
+ lines.push(` ${note.title} | ${note.lifecycle}, ${note.role ?? "untyped"} | ${Math.round(note.ageDays)}d | rel:${note.relatedCount} | risk:${note.mergeRisk}`);
5069
+ }
5070
+ if (mergeWarnings.length > 0) {
5071
+ lines.push(` Warnings: ${mergeWarnings.join("; ")}`);
5072
+ }
5073
+ lines.push(` Merge risk: ${mergeRisk}`);
5074
+ executeMergeEvidence = {
5075
+ notes: noteEvidence,
5076
+ warnings: mergeWarnings.length > 0 ? mergeWarnings : undefined,
5077
+ mergeRisk,
5078
+ };
5079
+ }
4948
5080
  const structuredContent = {
4949
5081
  action: "consolidated",
4950
5082
  strategy: "execute-merge",
4951
5083
  project: toProjectRef(project),
4952
5084
  notesProcessed: entries.length,
4953
5085
  notesModified: vaultChanges.size,
5086
+ executeMergeEvidence,
4954
5087
  persistence,
4955
5088
  retry,
4956
5089
  };
@@ -5111,14 +5244,14 @@ async function pruneSuperseded(entries, consolidationMode, project, cwd, policy,
5111
5244
  };
5112
5245
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
5113
5246
  }
5114
- async function dryRunAll(entries, threshold, defaultConsolidationMode, project, explicitMode) {
5247
+ async function dryRunAll(entries, threshold, defaultConsolidationMode, project, explicitMode, evidence = false) {
5115
5248
  const lines = [];
5116
5249
  lines.push(`Consolidation analysis for ${project?.name ?? "global"}:`);
5117
5250
  const modeLabel = explicitMode ?? `${defaultConsolidationMode} (project/default; all-temporary merges auto-delete)`;
5118
5251
  lines.push(`Mode: ${modeLabel} | Threshold: ${threshold}`);
5119
5252
  lines.push("");
5120
5253
  // Run all analysis strategies
5121
- const dupes = await detectDuplicates(entries, threshold, project);
5254
+ const dupes = await detectDuplicates(entries, threshold, project, evidence);
5122
5255
  lines.push("=== DUPLICATE DETECTION ===");
5123
5256
  lines.push(dupes.content[0]?.text ?? "No output");
5124
5257
  lines.push("");
@@ -5126,7 +5259,7 @@ async function dryRunAll(entries, threshold, defaultConsolidationMode, project,
5126
5259
  lines.push("=== CLUSTER ANALYSIS ===");
5127
5260
  lines.push(clusters.content[0]?.text ?? "No output");
5128
5261
  lines.push("");
5129
- const merges = await suggestMerges(entries, threshold, defaultConsolidationMode, project, explicitMode);
5262
+ const merges = await suggestMerges(entries, threshold, defaultConsolidationMode, project, explicitMode, evidence);
5130
5263
  lines.push("=== MERGE SUGGESTIONS ===");
5131
5264
  lines.push(merges.content[0]?.text ?? "No output");
5132
5265
  const structuredContent = {
@@ -5135,6 +5268,10 @@ async function dryRunAll(entries, threshold, defaultConsolidationMode, project,
5135
5268
  project: toProjectRef(project),
5136
5269
  notesProcessed: entries.length,
5137
5270
  notesModified: 0,
5271
+ duplicatePairs: dupes.structuredContent.duplicatePairs,
5272
+ mergeSuggestions: merges.structuredContent.mergeSuggestions,
5273
+ themeGroups: clusters.structuredContent.themeGroups,
5274
+ relationshipClusters: clusters.structuredContent.relationshipClusters,
5138
5275
  };
5139
5276
  return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
5140
5277
  }
@@ -5177,6 +5314,7 @@ server.registerPrompt("mnemonic-workflow-hint", {
5177
5314
  "- When unsure, prefer `recall` over `remember`.\n" +
5178
5315
  "- For repo-related tasks, pass `cwd` so mnemonic can route project memories correctly.\n\n" +
5179
5316
  "Workflow: `recall`/`list` -> `get` -> `update` or `remember` -> `relate`/`consolidate`/`move_memory`. Use `discover_tags` only when tag choice is ambiguous.\n\n" +
5317
+ "When a merge/prune decision is uncertain, use optional evidence enrichment: `recall` with `evidence: \"compact\"` and `consolidate` analysis strategies with `evidence: true`. Evidence improves confidence but is not required.\n\n" +
5180
5318
  "Roles are optional prioritization hints, not schema. Lifecycle still governs durability. When `lifecycle` is omitted, `remember` applies soft defaults based on role: `research`, `plan`, and `review` default to `temporary`; `decision`, `summary`, and `reference` default to `permanent`. Explicit `lifecycle` always overrides the role-based default. Inferred roles are internal hints only. Prioritization is language-independent by default.\n\n" +
5181
5319
  "### Working-state continuity\n\n" +
5182
5320
  "Preserve in-progress work as temporary notes when continuation value is high. Recovery happens after project orientation.\n\n" +
@@ -5217,6 +5355,8 @@ server.registerPrompt("mnemonic-workflow-hint", {
5217
5355
  "- Existing bug note found by `recall` -> inspect with `get` -> refine with `update`.\n" +
5218
5356
  "- No matching note found by `recall` -> optional `discover_tags` with note context -> create with `remember`.\n" +
5219
5357
  "- Two notes overlap heavily -> inspect -> clean up with `consolidate`.\n" +
5358
+ "- Unsure why a recall hit ranked high -> rerun `recall` with `evidence: \"compact\"`.\n" +
5359
+ "- Unsure whether to merge/prune -> run `consolidate` analysis with `evidence: true` before `execute-merge` or `prune-superseded`.\n" +
5220
5360
  "- Resume work: `project_memory_summary` -> `recall` (lifecycle: temporary) -> continue from temporary notes.\n\n" +
5221
5361
  "### semanticPatch format\n\n" +
5222
5362
  "When using `update` with `semanticPatch`:\n" +