@danielmarbach/mnemonic-mcp 0.13.1 → 0.14.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 +15 -0
- package/README.md +6 -2
- package/build/git.d.ts +30 -0
- package/build/git.d.ts.map +1 -1
- package/build/git.js +99 -0
- package/build/git.js.map +1 -1
- package/build/index.js +295 -35
- package/build/index.js.map +1 -1
- package/build/project-introspection.d.ts +7 -0
- package/build/project-introspection.d.ts.map +1 -1
- package/build/project-introspection.js +46 -0
- package/build/project-introspection.js.map +1 -1
- package/build/provenance.d.ts +18 -0
- package/build/provenance.d.ts.map +1 -0
- package/build/provenance.js +82 -0
- package/build/provenance.js.map +1 -0
- package/build/structured-content.d.ts +245 -12
- package/build/structured-content.d.ts.map +1 -1
- package/build/structured-content.js +80 -11
- package/build/structured-content.js.map +1 -1
- package/package.json +1 -1
package/build/index.js
CHANGED
|
@@ -7,12 +7,13 @@ import path from "path";
|
|
|
7
7
|
import { promises as fs } from "fs";
|
|
8
8
|
import { NOTE_LIFECYCLES } from "./storage.js";
|
|
9
9
|
import { embed, cosineSimilarity, embedModel } from "./embeddings.js";
|
|
10
|
+
import { buildTemporalHistoryEntry, computeConfidence, getNoteProvenance } from "./provenance.js";
|
|
10
11
|
import { filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
|
|
11
12
|
import { selectRecallResults } from "./recall.js";
|
|
12
13
|
import { cleanMarkdown } from "./markdown.js";
|
|
13
14
|
import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
|
|
14
15
|
import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES, WRITE_SCOPES, isProtectedBranch, resolveProtectedBranchBehavior, resolveProtectedBranchPatterns, resolveConsolidationMode, resolveWriteScope, } from "./project-memory-policy.js";
|
|
15
|
-
import { classifyTheme, summarizePreview, titleCaseTheme } from "./project-introspection.js";
|
|
16
|
+
import { classifyTheme, summarizePreview, titleCaseTheme, withinThemeScore, anchorScore, buildThemeCache, computeConnectionDiversity, } from "./project-introspection.js";
|
|
16
17
|
import { detectProject, getCurrentGitBranch, resolveProjectIdentity } from "./project.js";
|
|
17
18
|
import { VaultManager } from "./vault.js";
|
|
18
19
|
import { checkBranchChange } from "./branch-tracker.js";
|
|
@@ -276,6 +277,8 @@ const VAULT_PATH = process.env["VAULT_PATH"]
|
|
|
276
277
|
: defaultVaultPath();
|
|
277
278
|
const DEFAULT_RECALL_LIMIT = 5;
|
|
278
279
|
const DEFAULT_MIN_SIMILARITY = 0.3;
|
|
280
|
+
const TEMPORAL_HISTORY_NOTE_LIMIT = 5;
|
|
281
|
+
const TEMPORAL_HISTORY_COMMIT_LIMIT = 5;
|
|
279
282
|
async function readPackageVersion() {
|
|
280
283
|
const packageJsonPath = path.resolve(import.meta.dirname, "../package.json");
|
|
281
284
|
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
@@ -389,6 +392,17 @@ function formatNote(note, score) {
|
|
|
389
392
|
`**tags:** ${note.tags.join(", ") || "none"} | **${describeLifecycle(note.lifecycle)}** | **updated:** ${note.updatedAt}${relStr}\n\n` +
|
|
390
393
|
note.content);
|
|
391
394
|
}
|
|
395
|
+
function formatTemporalHistory(history) {
|
|
396
|
+
if (history.length === 0) {
|
|
397
|
+
return "**history:** no git history found";
|
|
398
|
+
}
|
|
399
|
+
const lines = ["**history:**"];
|
|
400
|
+
for (const entry of history) {
|
|
401
|
+
const summary = entry.summary ? ` — ${entry.summary}` : "";
|
|
402
|
+
lines.push(`- \`${entry.commitHash.slice(0, 7)}\` ${entry.timestamp} — ${entry.message}${summary}`);
|
|
403
|
+
}
|
|
404
|
+
return lines.join("\n");
|
|
405
|
+
}
|
|
392
406
|
// ── Git commit message helpers ────────────────────────────────────────────────
|
|
393
407
|
/**
|
|
394
408
|
* Extract a short human-readable summary from note content.
|
|
@@ -1594,15 +1608,18 @@ server.registerTool("get_project_memory_policy", {
|
|
|
1594
1608
|
server.registerTool("recall", {
|
|
1595
1609
|
title: "Recall",
|
|
1596
1610
|
description: "Semantic search over stored memories using embeddings.\n\n" +
|
|
1611
|
+
"Supports opt-in temporal mode (`mode: \"temporal\"`) to enrich top semantic matches with compact git-backed history.\n\n" +
|
|
1597
1612
|
"Use this when:\n" +
|
|
1598
1613
|
"- You know the topic but not the exact memory id\n" +
|
|
1599
1614
|
"- You are starting a session and want relevant prior context\n" +
|
|
1600
|
-
"- You want to check whether a memory already exists before creating another one\n
|
|
1615
|
+
"- You want to check whether a memory already exists before creating another one\n" +
|
|
1616
|
+
"- You explicitly want to inspect how a note evolved over time\n\n" +
|
|
1601
1617
|
"Do not use this when:\n" +
|
|
1602
1618
|
"- You already know the exact id; use `get`\n" +
|
|
1603
1619
|
"- You just want to browse by tags or scope; use `list`\n\n" +
|
|
1604
1620
|
"Returns:\n" +
|
|
1605
|
-
"- Ranked memory matches with scores, vault label, tags, lifecycle, and updated time\n
|
|
1621
|
+
"- Ranked memory matches with scores, vault label, tags, lifecycle, and updated time\n" +
|
|
1622
|
+
"- In temporal mode, optional compact history entries for top matches\n\n" +
|
|
1606
1623
|
"Read-only.\n\n" +
|
|
1607
1624
|
"Typical next step:\n" +
|
|
1608
1625
|
"- Use `get`, `update`, `relate`, or `consolidate` based on the results.",
|
|
@@ -1616,6 +1633,8 @@ server.registerTool("recall", {
|
|
|
1616
1633
|
cwd: projectParam,
|
|
1617
1634
|
limit: z.number().int().min(1).max(20).optional().default(DEFAULT_RECALL_LIMIT),
|
|
1618
1635
|
minSimilarity: z.number().min(0).max(1).optional().default(DEFAULT_MIN_SIMILARITY),
|
|
1636
|
+
mode: z.enum(["default", "temporal"]).optional().default("default").describe("Temporal history is opt-in. Use `temporal` to enrich top semantic matches with compact git-backed history."),
|
|
1637
|
+
verbose: z.boolean().optional().default(false).describe("Only meaningful with `mode: \"temporal\"`. Adds richer stats-based history context without returning raw diffs."),
|
|
1619
1638
|
tags: z.array(z.string()).optional().describe("Filter results to notes with all of these tags."),
|
|
1620
1639
|
scope: z
|
|
1621
1640
|
.enum(["project", "global", "all"])
|
|
@@ -1626,7 +1645,7 @@ server.registerTool("recall", {
|
|
|
1626
1645
|
"'all' = both, with project notes boosted (default)"),
|
|
1627
1646
|
}),
|
|
1628
1647
|
outputSchema: RecallResultSchema,
|
|
1629
|
-
}, async ({ query, cwd, limit, minSimilarity, tags, scope }) => {
|
|
1648
|
+
}, async ({ query, cwd, limit, minSimilarity, mode, verbose, tags, scope }) => {
|
|
1630
1649
|
await ensureBranchSynced(cwd);
|
|
1631
1650
|
const project = await resolveProject(cwd);
|
|
1632
1651
|
const queryVec = await embed(query);
|
|
@@ -1687,10 +1706,27 @@ server.registerTool("recall", {
|
|
|
1687
1706
|
: `Recall results (global):`;
|
|
1688
1707
|
const sections = [];
|
|
1689
1708
|
const structuredResults = [];
|
|
1690
|
-
for (const { id, score, vault, boosted } of top) {
|
|
1709
|
+
for (const [index, { id, score, vault, boosted }] of top.entries()) {
|
|
1691
1710
|
const note = await readCachedNote(vault, id);
|
|
1692
1711
|
if (note) {
|
|
1693
|
-
|
|
1712
|
+
const centrality = note.relatedTo?.length ?? 0;
|
|
1713
|
+
const filePath = `${vault.notesRelDir}/${id}.md`;
|
|
1714
|
+
const provenance = await getNoteProvenance(vault.git, filePath);
|
|
1715
|
+
const confidence = computeConfidence(note.lifecycle, note.updatedAt, centrality);
|
|
1716
|
+
let history;
|
|
1717
|
+
if (mode === "temporal") {
|
|
1718
|
+
if (index < TEMPORAL_HISTORY_NOTE_LIMIT) {
|
|
1719
|
+
const commits = await vault.git.getFileHistory(filePath, TEMPORAL_HISTORY_COMMIT_LIMIT);
|
|
1720
|
+
history = await Promise.all(commits.map(async (commit) => {
|
|
1721
|
+
const stats = await vault.git.getCommitStats(filePath, commit.hash);
|
|
1722
|
+
return buildTemporalHistoryEntry(commit, stats, verbose);
|
|
1723
|
+
}));
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
const formattedHistory = mode === "temporal" && history !== undefined
|
|
1727
|
+
? `\n\n${formatTemporalHistory(history)}`
|
|
1728
|
+
: "";
|
|
1729
|
+
sections.push(`${formatNote(note, score)}${formattedHistory}`);
|
|
1694
1730
|
structuredResults.push({
|
|
1695
1731
|
id,
|
|
1696
1732
|
title: note.title,
|
|
@@ -1702,6 +1738,9 @@ server.registerTool("recall", {
|
|
|
1702
1738
|
tags: note.tags,
|
|
1703
1739
|
lifecycle: note.lifecycle,
|
|
1704
1740
|
updatedAt: note.updatedAt,
|
|
1741
|
+
provenance,
|
|
1742
|
+
confidence,
|
|
1743
|
+
history,
|
|
1705
1744
|
});
|
|
1706
1745
|
}
|
|
1707
1746
|
}
|
|
@@ -2461,77 +2500,298 @@ server.registerTool("project_memory_summary", {
|
|
|
2461
2500
|
cwd: z.string().describe("Absolute project working directory. Pass this whenever the task is project-related so routing, search boosting, policy, and vault selection work correctly."),
|
|
2462
2501
|
maxPerTheme: z.number().int().min(1).max(5).optional().default(3),
|
|
2463
2502
|
recentLimit: z.number().int().min(1).max(10).optional().default(5),
|
|
2503
|
+
anchorLimit: z.number().int().min(1).max(10).optional().default(5),
|
|
2504
|
+
includeRelatedGlobal: z.boolean().optional().default(false),
|
|
2505
|
+
relatedGlobalLimit: z.number().int().min(1).max(5).optional().default(3),
|
|
2464
2506
|
}),
|
|
2465
2507
|
outputSchema: ProjectSummaryResultSchema,
|
|
2466
|
-
}, async ({ cwd, maxPerTheme, recentLimit }) => {
|
|
2508
|
+
}, async ({ cwd, maxPerTheme, recentLimit, anchorLimit, includeRelatedGlobal, relatedGlobalLimit }) => {
|
|
2467
2509
|
await ensureBranchSynced(cwd);
|
|
2468
2510
|
const { project, entries } = await collectVisibleNotes(cwd, "all");
|
|
2469
2511
|
if (!project) {
|
|
2470
2512
|
return { content: [{ type: "text", text: `Could not detect a project for: ${cwd}` }], isError: true };
|
|
2471
2513
|
}
|
|
2472
|
-
|
|
2473
|
-
|
|
2514
|
+
// Separate project-scoped notes (for themes/anchors) from global notes
|
|
2515
|
+
const projectEntries = entries.filter(e => e.note.project === project.id || e.vault.isProject);
|
|
2516
|
+
// Empty-project case: no project-scoped notes exist
|
|
2517
|
+
if (projectEntries.length === 0) {
|
|
2518
|
+
const structuredContent = {
|
|
2519
|
+
action: "project_summary_shown",
|
|
2520
|
+
project: { id: project.id, name: project.name },
|
|
2521
|
+
notes: { total: 0, projectVault: 0, mainVault: 0, privateProject: 0 },
|
|
2522
|
+
themes: {},
|
|
2523
|
+
recent: [],
|
|
2524
|
+
anchors: [],
|
|
2525
|
+
orientation: {
|
|
2526
|
+
primaryEntry: { id: "", title: "No notes", rationale: "Empty project vault" },
|
|
2527
|
+
suggestedNext: [],
|
|
2528
|
+
},
|
|
2529
|
+
};
|
|
2474
2530
|
return { content: [{ type: "text", text: `No memories found for project ${project.name}.` }], structuredContent };
|
|
2475
2531
|
}
|
|
2476
2532
|
const policyLine = await formatProjectPolicyLine(project.id);
|
|
2533
|
+
// Build theme cache for connection diversity scoring (project-scoped only)
|
|
2534
|
+
const themeCache = buildThemeCache(projectEntries.map(e => e.note));
|
|
2535
|
+
// Categorize by theme (project-scoped only)
|
|
2477
2536
|
const themed = new Map();
|
|
2478
|
-
for (const entry of
|
|
2537
|
+
for (const entry of projectEntries) {
|
|
2479
2538
|
const theme = classifyTheme(entry.note);
|
|
2480
2539
|
const bucket = themed.get(theme) ?? [];
|
|
2481
2540
|
bucket.push(entry);
|
|
2482
2541
|
themed.set(theme, bucket);
|
|
2483
2542
|
}
|
|
2543
|
+
// Theme order for display
|
|
2484
2544
|
const themeOrder = ["overview", "decisions", "tooling", "bugs", "architecture", "quality", "other"];
|
|
2485
|
-
|
|
2486
|
-
const
|
|
2545
|
+
// Calculate notes distribution (project-scoped only)
|
|
2546
|
+
const projectVaultCount = projectEntries.filter(e => e.vault.isProject).length;
|
|
2547
|
+
const mainVaultProjectEntries = projectEntries.filter(e => !e.vault.isProject);
|
|
2548
|
+
const mainVaultCount = mainVaultProjectEntries.length;
|
|
2549
|
+
const totalProjectNotes = projectEntries.length;
|
|
2550
|
+
// Build output sections
|
|
2487
2551
|
const sections = [];
|
|
2488
2552
|
sections.push(`Project summary: **${project.name}**`);
|
|
2489
2553
|
sections.push(`- id: \`${project.id}\``);
|
|
2490
2554
|
sections.push(`- ${policyLine.replace(/^Policy:\s*/, "policy: ")}`);
|
|
2491
|
-
sections.push(`- memories: ${
|
|
2492
|
-
const mainVaultProjectEntries = entries.filter((entry) => !entry.vault.isProject && entry.note.project === project.id);
|
|
2555
|
+
sections.push(`- memories: ${totalProjectNotes} (project-vault: ${projectVaultCount}, main-vault: ${mainVaultCount})`);
|
|
2493
2556
|
if (mainVaultProjectEntries.length > 0) {
|
|
2494
2557
|
sections.push(`- private project memories: ${mainVaultProjectEntries.length}`);
|
|
2495
2558
|
}
|
|
2496
|
-
const themes =
|
|
2559
|
+
const themes = {};
|
|
2497
2560
|
for (const theme of themeOrder) {
|
|
2498
2561
|
const bucket = themed.get(theme);
|
|
2499
|
-
if (!bucket || bucket.length === 0)
|
|
2562
|
+
if (!bucket || bucket.length === 0)
|
|
2500
2563
|
continue;
|
|
2501
|
-
|
|
2502
|
-
const
|
|
2564
|
+
// Sort by within-theme score
|
|
2565
|
+
const sorted = [...bucket].sort((a, b) => withinThemeScore(b.note) - withinThemeScore(a.note));
|
|
2566
|
+
const top = sorted.slice(0, maxPerTheme);
|
|
2503
2567
|
sections.push(`\n${titleCaseTheme(theme)}:`);
|
|
2504
|
-
sections.push(...top.map(
|
|
2505
|
-
themes
|
|
2506
|
-
name: theme,
|
|
2568
|
+
sections.push(...top.map(e => `- ${e.note.title} (\`${e.note.id}\`)`));
|
|
2569
|
+
themes[theme] = {
|
|
2507
2570
|
count: bucket.length,
|
|
2508
|
-
examples: top.map(
|
|
2509
|
-
|
|
2571
|
+
examples: top.map(e => ({
|
|
2572
|
+
id: e.note.id,
|
|
2573
|
+
title: e.note.title,
|
|
2574
|
+
updatedAt: e.note.updatedAt,
|
|
2575
|
+
})),
|
|
2576
|
+
};
|
|
2510
2577
|
}
|
|
2511
|
-
|
|
2578
|
+
// Recent notes (project-scoped only)
|
|
2579
|
+
const recent = [...projectEntries]
|
|
2512
2580
|
.sort((a, b) => b.note.updatedAt.localeCompare(a.note.updatedAt))
|
|
2513
2581
|
.slice(0, recentLimit);
|
|
2514
|
-
sections.push(`\nRecent:`);
|
|
2515
|
-
sections.push(...recent.map(
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2582
|
+
sections.push(`\nRecent activity (start here):`);
|
|
2583
|
+
sections.push(...recent.map(e => `- ${e.note.updatedAt} — ${e.note.title}`));
|
|
2584
|
+
// Anchor notes with diversity constraint (project-scoped only)
|
|
2585
|
+
// Separate tagged anchors (can have no relationships) from scored anchors (need relationships)
|
|
2586
|
+
const taggedAnchorEntries = projectEntries.filter(e => e.note.lifecycle === "permanent" &&
|
|
2587
|
+
e.note.tags.some(t => t.toLowerCase() === "anchor" || t.toLowerCase() === "alwaysload"));
|
|
2588
|
+
const scoredAnchorCandidates = projectEntries
|
|
2589
|
+
.filter(e => e.note.lifecycle === "permanent" && (e.note.relatedTo?.length ?? 0) > 0)
|
|
2590
|
+
.map(e => ({
|
|
2591
|
+
entry: e,
|
|
2592
|
+
score: anchorScore(e.note, themeCache),
|
|
2593
|
+
theme: classifyTheme(e.note),
|
|
2594
|
+
}))
|
|
2595
|
+
.filter(x => x.score > -Infinity)
|
|
2596
|
+
.sort((a, b) => b.score - a.score);
|
|
2597
|
+
// Score tagged anchors too, so they can compete for primaryEntry
|
|
2598
|
+
const scoredTaggedAnchors = taggedAnchorEntries
|
|
2599
|
+
.map(e => ({
|
|
2600
|
+
entry: e,
|
|
2601
|
+
score: anchorScore(e.note, themeCache),
|
|
2602
|
+
theme: classifyTheme(e.note),
|
|
2603
|
+
}))
|
|
2604
|
+
.sort((a, b) => b.score - a.score);
|
|
2605
|
+
// Enforce max 2 per theme for scored anchors
|
|
2606
|
+
const anchorThemeCounts = new Map();
|
|
2607
|
+
const anchors = [];
|
|
2608
|
+
const anchorIds = new Set();
|
|
2609
|
+
// Add tagged anchors first (capped at 10 total across all themes), scored by anchorScore
|
|
2610
|
+
for (const candidate of scoredTaggedAnchors.slice(0, 10)) {
|
|
2611
|
+
if (anchors.length >= 10)
|
|
2612
|
+
break;
|
|
2613
|
+
anchors.push({
|
|
2614
|
+
id: candidate.entry.note.id,
|
|
2615
|
+
title: candidate.entry.note.title,
|
|
2616
|
+
centrality: candidate.entry.note.relatedTo?.length ?? 0,
|
|
2617
|
+
connectionDiversity: computeConnectionDiversity(candidate.entry.note, themeCache),
|
|
2618
|
+
updatedAt: candidate.entry.note.updatedAt,
|
|
2619
|
+
});
|
|
2620
|
+
anchorIds.add(candidate.entry.note.id);
|
|
2621
|
+
}
|
|
2622
|
+
// Add scored anchors with theme diversity constraint
|
|
2623
|
+
for (const candidate of scoredAnchorCandidates) {
|
|
2624
|
+
if (anchors.length >= 10)
|
|
2625
|
+
break;
|
|
2626
|
+
if (anchorIds.has(candidate.entry.note.id))
|
|
2627
|
+
continue;
|
|
2628
|
+
const theme = candidate.theme;
|
|
2629
|
+
const themeCount = anchorThemeCounts.get(theme) ?? 0;
|
|
2630
|
+
if (themeCount >= 2)
|
|
2631
|
+
continue;
|
|
2632
|
+
anchors.push({
|
|
2633
|
+
id: candidate.entry.note.id,
|
|
2634
|
+
title: candidate.entry.note.title,
|
|
2635
|
+
centrality: candidate.entry.note.relatedTo?.length ?? 0,
|
|
2636
|
+
connectionDiversity: computeConnectionDiversity(candidate.entry.note, themeCache),
|
|
2637
|
+
updatedAt: candidate.entry.note.updatedAt,
|
|
2638
|
+
});
|
|
2639
|
+
anchorIds.add(candidate.entry.note.id);
|
|
2640
|
+
anchorThemeCounts.set(theme, themeCount + 1);
|
|
2519
2641
|
}
|
|
2642
|
+
if (anchors.length > 0) {
|
|
2643
|
+
sections.push(`\nAnchors:`);
|
|
2644
|
+
sections.push(...anchors.slice(0, 5).map(a => `- ${a.title} (\`${a.id}\`) — centrality: ${a.centrality}, diversity: ${a.connectionDiversity}`));
|
|
2645
|
+
}
|
|
2646
|
+
// Compute orientation after anchors are computed (for text output)
|
|
2647
|
+
let relatedGlobal;
|
|
2648
|
+
if (includeRelatedGlobal) {
|
|
2649
|
+
const anchorEmbeddings = await Promise.all(anchors.slice(0, 5).map(async (a) => {
|
|
2650
|
+
for (const vault of vaultManager.allKnownVaults()) {
|
|
2651
|
+
const emb = await vault.storage.readEmbedding(a.id);
|
|
2652
|
+
if (emb)
|
|
2653
|
+
return { id: a.id, embedding: emb.embedding };
|
|
2654
|
+
}
|
|
2655
|
+
return null;
|
|
2656
|
+
}));
|
|
2657
|
+
const validAnchors = anchorEmbeddings.filter((e) => e !== null);
|
|
2658
|
+
if (validAnchors.length > 0) {
|
|
2659
|
+
// Get global notes (not project-scoped)
|
|
2660
|
+
const globalEntries = entries.filter(e => !e.note.project);
|
|
2661
|
+
const globalCandidates = [];
|
|
2662
|
+
for (const entry of globalEntries) {
|
|
2663
|
+
const emb = await entry.vault.storage.readEmbedding(entry.note.id);
|
|
2664
|
+
if (!emb)
|
|
2665
|
+
continue;
|
|
2666
|
+
// Find max similarity to any anchor
|
|
2667
|
+
let maxSim = 0;
|
|
2668
|
+
for (const anchor of validAnchors) {
|
|
2669
|
+
const sim = cosineSimilarity(anchor.embedding, emb.embedding);
|
|
2670
|
+
if (sim > maxSim)
|
|
2671
|
+
maxSim = sim;
|
|
2672
|
+
}
|
|
2673
|
+
if (maxSim > 0.4) {
|
|
2674
|
+
globalCandidates.push({
|
|
2675
|
+
id: entry.note.id,
|
|
2676
|
+
title: entry.note.title,
|
|
2677
|
+
similarity: maxSim,
|
|
2678
|
+
preview: summarizePreview(entry.note.content, 100),
|
|
2679
|
+
});
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
globalCandidates.sort((a, b) => b.similarity - a.similarity);
|
|
2683
|
+
if (globalCandidates.length > 0) {
|
|
2684
|
+
relatedGlobal = {
|
|
2685
|
+
notes: globalCandidates.slice(0, relatedGlobalLimit),
|
|
2686
|
+
computedAt: new Date().toISOString(),
|
|
2687
|
+
};
|
|
2688
|
+
sections.push(`\nRelated Global:`);
|
|
2689
|
+
sections.push(...relatedGlobal.notes.map(n => `- ${n.title} (\`${n.id}\`) — similarity: ${n.similarity.toFixed(2)}`));
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
// Compute orientation layer for actionable guidance
|
|
2694
|
+
const primaryAnchor = anchors[0];
|
|
2695
|
+
// Build noteId -> vault lookup for provenance enrichment
|
|
2696
|
+
const noteVaultMap = new Map();
|
|
2697
|
+
for (const entry of projectEntries) {
|
|
2698
|
+
noteVaultMap.set(entry.note.id, entry.vault);
|
|
2699
|
+
}
|
|
2700
|
+
// Helper to enrich an anchor with provenance and confidence
|
|
2701
|
+
const enrichOrientationNote = async (anchor) => {
|
|
2702
|
+
const vault = noteVaultMap.get(anchor.id);
|
|
2703
|
+
if (!vault)
|
|
2704
|
+
return {};
|
|
2705
|
+
const filePath = `${vault.notesRelDir}/${anchor.id}.md`;
|
|
2706
|
+
const provenance = await getNoteProvenance(vault.git, filePath);
|
|
2707
|
+
const confidence = computeConfidence("permanent", anchor.updatedAt, anchor.centrality);
|
|
2708
|
+
return { provenance, confidence };
|
|
2709
|
+
};
|
|
2710
|
+
const primaryEnriched = primaryAnchor ? await enrichOrientationNote(primaryAnchor) : {};
|
|
2711
|
+
const suggestedEnriched = await Promise.all(anchors.slice(1, 4).map(enrichOrientationNote));
|
|
2712
|
+
// Enrich fallback primaryEntry when no anchors exist
|
|
2713
|
+
let fallbackEnriched = {};
|
|
2714
|
+
if (!primaryAnchor && recent[0]) {
|
|
2715
|
+
const fallbackNote = recent[0].note;
|
|
2716
|
+
const vault = noteVaultMap.get(fallbackNote.id);
|
|
2717
|
+
if (vault) {
|
|
2718
|
+
const filePath = `${vault.notesRelDir}/${fallbackNote.id}.md`;
|
|
2719
|
+
const provenance = await getNoteProvenance(vault.git, filePath);
|
|
2720
|
+
const confidence = computeConfidence(fallbackNote.lifecycle, fallbackNote.updatedAt, 0);
|
|
2721
|
+
fallbackEnriched = { provenance, confidence };
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
const orientation = {
|
|
2725
|
+
primaryEntry: primaryAnchor
|
|
2726
|
+
? {
|
|
2727
|
+
id: primaryAnchor.id,
|
|
2728
|
+
title: primaryAnchor.title,
|
|
2729
|
+
rationale: `Centrality ${primaryAnchor.centrality}, connects ${primaryAnchor.connectionDiversity} themes`,
|
|
2730
|
+
...primaryEnriched,
|
|
2731
|
+
}
|
|
2732
|
+
: {
|
|
2733
|
+
id: recent[0]?.note.id ?? projectEntries[0]?.note.id ?? "",
|
|
2734
|
+
title: recent[0]?.note.title ?? projectEntries[0]?.note.title ?? "No notes",
|
|
2735
|
+
rationale: recent[0]
|
|
2736
|
+
? "Most recent note — no high-centrality anchors found"
|
|
2737
|
+
: "Only note available",
|
|
2738
|
+
...fallbackEnriched,
|
|
2739
|
+
},
|
|
2740
|
+
suggestedNext: anchors.slice(1, 4).map((a, i) => ({
|
|
2741
|
+
id: a.id,
|
|
2742
|
+
title: a.title,
|
|
2743
|
+
rationale: `Centrality ${a.centrality}, connects ${a.connectionDiversity} themes`,
|
|
2744
|
+
...suggestedEnriched[i],
|
|
2745
|
+
})),
|
|
2746
|
+
};
|
|
2747
|
+
// Warning for taxonomy dilution
|
|
2748
|
+
const otherBucket = themed.get("other");
|
|
2749
|
+
const otherCount = otherBucket?.length ?? 0;
|
|
2750
|
+
const otherRatio = projectEntries.length > 0 ? otherCount / projectEntries.length : 0;
|
|
2751
|
+
if (otherRatio > 0.3) {
|
|
2752
|
+
orientation.warnings = [
|
|
2753
|
+
`${Math.round(otherRatio * 100)}% of notes in "other" bucket — consider improving thematic classification`,
|
|
2754
|
+
];
|
|
2755
|
+
}
|
|
2756
|
+
// Orientation text output
|
|
2757
|
+
sections.push(`\nOrientation:`);
|
|
2758
|
+
sections.push(`Start with: ${orientation.primaryEntry.title} (\`${orientation.primaryEntry.id}\`)`);
|
|
2759
|
+
sections.push(` Rationale: ${orientation.primaryEntry.rationale}`);
|
|
2760
|
+
if (orientation.primaryEntry.confidence) {
|
|
2761
|
+
sections.push(` Confidence: ${orientation.primaryEntry.confidence}`);
|
|
2762
|
+
}
|
|
2763
|
+
if (orientation.suggestedNext.length > 0) {
|
|
2764
|
+
sections.push(`Then check:`);
|
|
2765
|
+
for (const next of orientation.suggestedNext) {
|
|
2766
|
+
sections.push(` - ${next.title} (\`${next.id}\`) — ${next.rationale}${next.confidence ? ` [${next.confidence}]` : ""}`);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
if (orientation.warnings && orientation.warnings.length > 0) {
|
|
2770
|
+
sections.push(`Warnings:`);
|
|
2771
|
+
for (const w of orientation.warnings) {
|
|
2772
|
+
sections.push(` - ${w}`);
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
// Related global notes (optional, anchor-based similarity)
|
|
2520
2776
|
const structuredContent = {
|
|
2521
2777
|
action: "project_summary_shown",
|
|
2522
2778
|
project: { id: project.id, name: project.name },
|
|
2523
2779
|
notes: {
|
|
2524
|
-
total:
|
|
2780
|
+
total: totalProjectNotes,
|
|
2525
2781
|
projectVault: projectVaultCount,
|
|
2526
2782
|
mainVault: mainVaultCount,
|
|
2527
2783
|
privateProject: mainVaultProjectEntries.length,
|
|
2528
2784
|
},
|
|
2529
|
-
themes
|
|
2530
|
-
recent: recent.map(
|
|
2531
|
-
id:
|
|
2532
|
-
title:
|
|
2533
|
-
updatedAt:
|
|
2785
|
+
themes,
|
|
2786
|
+
recent: recent.map(e => ({
|
|
2787
|
+
id: e.note.id,
|
|
2788
|
+
title: e.note.title,
|
|
2789
|
+
updatedAt: e.note.updatedAt,
|
|
2790
|
+
theme: classifyTheme(e.note),
|
|
2534
2791
|
})),
|
|
2792
|
+
anchors,
|
|
2793
|
+
orientation,
|
|
2794
|
+
relatedGlobal,
|
|
2535
2795
|
};
|
|
2536
2796
|
return { content: [{ type: "text", text: sections.join("\n") }], structuredContent };
|
|
2537
2797
|
});
|