@danielmarbach/mnemonic-mcp 0.26.2 → 0.27.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 +12 -0
- package/README.md +10 -4
- package/build/consolidate.d.ts +21 -0
- package/build/consolidate.d.ts.map +1 -1
- package/build/consolidate.js +124 -0
- package/build/consolidate.js.map +1 -1
- package/build/index.js +158 -18
- package/build/index.js.map +1 -1
- package/build/structured-content.d.ts +218 -0
- package/build/structured-content.d.ts.map +1 -1
- package/build/structured-content.js +77 -0
- package/build/structured-content.js.map +1 -1
- package/package.json +1 -1
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
|
|
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" +
|