@danielmarbach/mnemonic-mcp 0.14.0 → 0.15.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 CHANGED
@@ -4,6 +4,19 @@ All notable changes to `mnemonic` will be documented in this file.
4
4
 
5
5
  The format is loosely based on Keep a Changelog and uses semver-style version headings.
6
6
 
7
+ ## [0.15.0] - 2026-03-23
8
+
9
+ ### Added
10
+
11
+ - Projection layer: compact, deterministic derived representations of notes used as embedding input instead of raw title+content. Projections extract title, lifecycle, tags, summary, and headings (h1–h3) into a structured format capped at 1200 characters, improving embedding quality by removing prose noise and markdown formatting.
12
+ - `project_memory_summary` now uses projection summaries for related global note previews when available, falling back to content extraction. Previews are capped at 100 characters for consistent output.
13
+
14
+ ### Changed
15
+
16
+ - All embedding operations (`remember`, `update`, `embedMissingNotes`, `consolidate`) now use `embedTextForNote()` which builds projections on demand and falls back to raw title+content if projection fails. Embeddings are never blocked by projection errors.
17
+ - Projections are stored in `vaultPath/projections/` as JSON files alongside embeddings, gitignored and never synced. Each projection includes `noteId`, `title`, `summary`, `headings`, `tags`, `lifecycle`, `updatedAt` (staleness anchor), `projectionText` (what's embedded), and `generatedAt`.
18
+ - Staleness detection is timestamp-based: a projection is stale when `projection.updatedAt !== note.updatedAt`. No hashing required.
19
+
7
20
  ## [0.14.0] - 2026-03-22
8
21
 
9
22
  ### Added
package/README.md CHANGED
@@ -259,11 +259,13 @@ Two vault types store notes:
259
259
 
260
260
  ```
261
261
  ~/mnemonic-vault/
262
- .gitignore ← auto-created, gitignores embeddings/
262
+ .gitignore ← auto-created, gitignores embeddings/ and projections/
263
263
  notes/
264
264
  setup-notes-a1b2c3.md
265
265
  embeddings/ ← local only, never committed
266
266
  setup-notes-a1b2c3.json
267
+ projections/ ← local only, never committed
268
+ setup-notes-a1b2c3.json
267
269
  ```
268
270
 
269
271
  **Project vault** — project-specific memories committed into the project repo:
@@ -271,11 +273,13 @@ Two vault types store notes:
271
273
  ```
272
274
  <git-root>/
273
275
  .mnemonic/
274
- .gitignore ← auto-created, gitignores embeddings/
276
+ .gitignore ← auto-created, gitignores embeddings/ and projections/
275
277
  notes/
276
278
  auth-bug-fix-d4e5f6.md
277
279
  embeddings/ ← local only, never committed
278
280
  auth-bug-fix-d4e5f6.json
281
+ projections/ ← local only, never committed
282
+ auth-bug-fix-d4e5f6.json
279
283
  ```
280
284
 
281
285
  ### Routing
@@ -339,10 +343,21 @@ We fixed the JWT expiry issue by switching to RS256 and...
339
343
 
340
344
  Content is markdown-linted on `remember`/`update`: fixable issues are auto-corrected before save; non-fixable issues are rejected.
341
345
 
342
- ### Embeddings and migrations
346
+ ### Embeddings and projections
343
347
 
344
348
  Embeddings are generated by Ollama's `/api/embed` with truncation enabled, stored as local JSON alongside notes, and gitignored. `sync` backfills missing embeddings on every run; `sync { force: true }` rebuilds all.
345
349
 
350
+ **Projections** improve embedding quality by extracting structured representations instead of embedding raw markdown. Each note has a projection stored in `projections/<noteId>.json` (also gitignored) containing:
351
+
352
+ - `projectionText`: compact embedding input (max 1200 chars) with title, lifecycle, tags, summary, and h1–h3 headings
353
+ - `summary`: extracted from the first non-heading paragraph, first bullet list, or first 200 chars of body
354
+ - `headings`: up to 8 deduplicated h1–h3 headings (plain text, in order)
355
+ - `updatedAt`: staleness anchor matching the note's updatedAt timestamp
356
+
357
+ Projections are built lazily on first embed and rebuilt when `note.updatedAt !== projection.updatedAt`. No global rebuild needed — staleness is timestamp-based. If projection generation fails, the system falls back to raw `title + content` so embeds never block.
358
+
359
+ ### Migrations
360
+
346
361
  Each vault has its own `config.json` with a `schemaVersion`, so main and project vaults migrate independently:
347
362
 
348
363
  - `list_migrations` reports schema version and pending migrations per vault.
package/build/index.js CHANGED
@@ -8,6 +8,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
10
  import { buildTemporalHistoryEntry, computeConfidence, getNoteProvenance } from "./provenance.js";
11
+ import { getOrBuildProjection } from "./projections.js";
11
12
  import { filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
12
13
  import { selectRecallResults } from "./recall.js";
13
14
  import { cleanMarkdown } from "./markdown.js";
@@ -539,6 +540,20 @@ async function wouldRelationshipCleanupTouchProjectVault(noteIds) {
539
540
  }
540
541
  return false;
541
542
  }
543
+ /**
544
+ * Get the text to use for embedding a note.
545
+ * Uses the projection's projectionText when available and fresh;
546
+ * falls back to the raw title+content if projection fails.
547
+ */
548
+ async function embedTextForNote(storage, note) {
549
+ try {
550
+ const projection = await getOrBuildProjection(storage, note);
551
+ return projection.projectionText;
552
+ }
553
+ catch {
554
+ return `${note.title}\n\n${note.content}`;
555
+ }
556
+ }
542
557
  async function embedMissingNotes(storage, noteIds, force = false) {
543
558
  const notes = noteIds
544
559
  ? (await Promise.all(noteIds.map((id) => storage.readNote(id)))).filter(Boolean)
@@ -560,7 +575,8 @@ async function embedMissingNotes(storage, noteIds, force = false) {
560
575
  }
561
576
  }
562
577
  try {
563
- const vector = await embed(`${note.title}\n\n${note.content}`);
578
+ const text = await embedTextForNote(storage, note);
579
+ const vector = await embed(text);
564
580
  await storage.writeEmbedding({
565
581
  id: note.id,
566
582
  model: embedModel,
@@ -1359,7 +1375,8 @@ server.registerTool("remember", {
1359
1375
  await vault.storage.writeNote(note);
1360
1376
  let embeddingStatus = { status: "written" };
1361
1377
  try {
1362
- const vector = await embed(`${title}\n\n${cleanedContent}`);
1378
+ const text = await embedTextForNote(vault.storage, note);
1379
+ const vector = await embed(text);
1363
1380
  await vault.storage.writeEmbedding({ id, model: embedModel, embedding: vector, updatedAt: now });
1364
1381
  }
1365
1382
  catch (err) {
@@ -1838,7 +1855,8 @@ server.registerTool("update", {
1838
1855
  await vault.storage.writeNote(updated);
1839
1856
  let embeddingStatus = { status: "written" };
1840
1857
  try {
1841
- const vector = await embed(`${updated.title}\n\n${updated.content}`);
1858
+ const text = await embedTextForNote(vault.storage, updated);
1859
+ const vector = await embed(text);
1842
1860
  await vault.storage.writeEmbedding({ id, model: embedModel, embedding: vector, updatedAt: now });
1843
1861
  }
1844
1862
  catch (err) {
@@ -2671,11 +2689,15 @@ server.registerTool("project_memory_summary", {
2671
2689
  maxSim = sim;
2672
2690
  }
2673
2691
  if (maxSim > 0.4) {
2692
+ const projection = await entry.vault.storage.readProjection(entry.note.id);
2693
+ const preview = projection?.summary
2694
+ ? projection.summary.slice(0, 100)
2695
+ : summarizePreview(entry.note.content, 100);
2674
2696
  globalCandidates.push({
2675
2697
  id: entry.note.id,
2676
2698
  title: entry.note.title,
2677
2699
  similarity: maxSim,
2678
- preview: summarizePreview(entry.note.content, 100),
2700
+ preview,
2679
2701
  });
2680
2702
  }
2681
2703
  }
@@ -3855,7 +3877,8 @@ async function executeMerge(entries, mergePlan, defaultConsolidationMode, projec
3855
3877
  let embeddingStatus = { status: "written" };
3856
3878
  // Generate embedding for consolidated note
3857
3879
  try {
3858
- const vector = await embed(`${targetTitle}\n\n${consolidatedNote.content}`);
3880
+ const text = await embedTextForNote(targetVault.storage, consolidatedNote);
3881
+ const vector = await embed(text);
3859
3882
  await targetVault.storage.writeEmbedding({
3860
3883
  id: targetId,
3861
3884
  model: embedModel,