nexusmem 0.9.0 → 0.9.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/CHANGELOG.md CHANGED
@@ -11,6 +11,33 @@ built from, matched by publish timestamp: `v0.1.0` → `67a4776`, `v0.1.1` → `
11
11
 
12
12
  No unreleased changes yet.
13
13
 
14
+ ## [0.9.1] — 2026-08-29
15
+
16
+ Three ranking/retrieval correctness fixes, found and validated against a new 28-case retrieval-quality
17
+ eval harness (`npm run eval`, dev-only, not part of the published package).
18
+
19
+ ### Fixed
20
+
21
+ - `nodes_vec` (vector search) over-fetched `k` globally, then filtered by `project_id` after the join —
22
+ a heuristic, not a guarantee. A sparse project sharing `memory.db` with a much larger one could have
23
+ every true nearest neighbour fall outside the over-fetch window and get silently dropped (reproduced:
24
+ 495 rows in one project + 5 in another, global `k=50` surfaced 0 of the 5). Schema V11 gives `nodes_vec`
25
+ a `PARTITION KEY` on `project_id` (`sqlite-vec` 0.1.9+), pushing the equality filter into the k-NN
26
+ search itself so cross-project exactness is now guaranteed, not probable. Migrates existing databases
27
+ automatically on next `sync`/`query`. `--as-of` date filtering still over-fetches — only the
28
+ `project_id` dimension was ever a correctness guarantee.
29
+ - `MAX_PRIOR_OVERTURN` (the cap on how far a recency/signal prior may overturn relevance) raised
30
+ `2 → 2.4`, the highest value that both improves eval MRR (0.924→0.943) and still passes every legacy
31
+ regression test guarding against the original same-day-fix-commits bug this constant exists to bound.
32
+ - A commit's `code_diff` siblings (one node per changed file, all sharing the same `ts`) could crowd a
33
+ packed result and bury that commit's own `git_commit` node — its answer — several ranks down. Now
34
+ capped per commit, matching the existing `conversation_turn`/`doc_section` family cap.
35
+ - `package.json`/`tsconfig.json` diffs were ranking above more relevant results when a changed file's
36
+ path happened to echo its commit's conventional-commit scope (title is weighted 10x body, so the scope
37
+ word counted twice). Down-weighted as mechanical wiring, same reasoning `TEST_PATHS` already applies to
38
+ tests. Only affects nodes ingested from here on — existing manifest/config diffs need `sync --rebuild`
39
+ to get the corrected signal retroactively.
40
+
14
41
  ## [0.9.0] — 2026-08-27
15
42
 
16
43
  Closes the three remaining mechanisms from a recurring external review (Simon Strandgaard, Agent
package/dist/cli/index.js CHANGED
@@ -995,6 +995,24 @@ var V10 = `
995
995
  ALTER TABLE nodes ADD COLUMN trust_state TEXT NOT NULL DEFAULT 'candidate';
996
996
  CREATE INDEX idx_nodes_trust_state ON nodes (project_id, trust_state) WHERE trust_state != 'candidate';
997
997
  `;
998
+ var V11 = `
999
+ CREATE TEMP TABLE nodes_vec_stage AS
1000
+ SELECT v.rowid AS rowid, n.project_id AS project_id, v.embedding AS embedding
1001
+ FROM nodes_vec v
1002
+ JOIN nodes n ON n.rowid = v.rowid;
1003
+
1004
+ DROP TABLE nodes_vec;
1005
+
1006
+ CREATE VIRTUAL TABLE nodes_vec USING vec0 (
1007
+ project_id TEXT PARTITION KEY,
1008
+ embedding float[${EMBEDDING_DIM}]
1009
+ );
1010
+
1011
+ INSERT INTO nodes_vec (rowid, project_id, embedding)
1012
+ SELECT rowid, project_id, embedding FROM nodes_vec_stage;
1013
+
1014
+ DROP TABLE nodes_vec_stage;
1015
+ `;
998
1016
  var MIGRATIONS = [
999
1017
  { version: 1, up: (db) => db.exec(V1) },
1000
1018
  { version: 2, up: (db) => db.exec(V2) },
@@ -1005,7 +1023,8 @@ var MIGRATIONS = [
1005
1023
  { version: 7, up: (db) => db.exec(V7) },
1006
1024
  { version: 8, up: (db) => db.exec(V8) },
1007
1025
  { version: 9, up: (db) => db.exec(V9) },
1008
- { version: 10, up: (db) => db.exec(V10) }
1026
+ { version: 10, up: (db) => db.exec(V10) },
1027
+ { version: 11, up: (db) => db.exec(V11) }
1009
1028
  ];
1010
1029
  var LATEST_SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
1011
1030
  function currentSchemaVersion(db) {
@@ -1467,24 +1486,24 @@ function getEmbedding(db, nodeId) {
1467
1486
  if (!row) return null;
1468
1487
  return new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
1469
1488
  }
1470
- function upsertEmbedding(db, rowid, embedding) {
1471
- db.prepare("INSERT OR REPLACE INTO nodes_vec (rowid, embedding) VALUES (?, ?)").run(BigInt(rowid), embedding);
1489
+ function upsertEmbedding(db, rowid, projectId, embedding) {
1490
+ db.prepare("INSERT OR REPLACE INTO nodes_vec (rowid, project_id, embedding) VALUES (?, ?, ?)").run(BigInt(rowid), projectId, embedding);
1472
1491
  }
1473
1492
  function dropAllEmbeddings(db) {
1474
1493
  return db.prepare("DELETE FROM nodes_vec").run().changes;
1475
1494
  }
1476
1495
  function vectorSearch(db, projectId, embedding, limit = 20, opts = {}) {
1477
- const overfetch = Math.max(limit * 8, 50);
1478
1496
  const asOfEpoch = opts.asOfEpoch ?? null;
1497
+ const k = asOfEpoch === null ? limit : Math.max(limit * 8, 50);
1479
1498
  return db.prepare(
1480
1499
  `SELECT n.id, n.kind, n.ts, n.title, n.body, n.signal, n.provenance, n.trust_state AS trustState, v.distance AS distance
1481
1500
  FROM nodes_vec v
1482
1501
  JOIN nodes n ON n.rowid = v.rowid
1483
- WHERE v.embedding MATCH ? AND k = ? AND n.project_id = ?
1502
+ WHERE v.embedding MATCH ? AND k = ? AND v.project_id = ?
1484
1503
  AND (? IS NULL OR n.created_at <= ?)
1485
1504
  ORDER BY v.distance
1486
1505
  LIMIT ?`
1487
- ).all(embedding, overfetch, projectId, asOfEpoch, asOfEpoch, limit);
1506
+ ).all(embedding, k, projectId, asOfEpoch, asOfEpoch, limit);
1488
1507
  }
1489
1508
 
1490
1509
  // src/store/fts.ts
@@ -1755,8 +1774,8 @@ var MemoryStore = class _MemoryStore {
1755
1774
  countNodesNeedingEmbedding(projectId) {
1756
1775
  return countNodesNeedingEmbedding(this.db, projectId);
1757
1776
  }
1758
- upsertEmbedding(rowid, embedding) {
1759
- upsertEmbedding(this.db, rowid, embedding);
1777
+ upsertEmbedding(rowid, projectId, embedding) {
1778
+ upsertEmbedding(this.db, rowid, projectId, embedding);
1760
1779
  }
1761
1780
  /** The stored vector for one node, or null if it has not been embedded yet. */
1762
1781
  getEmbedding(nodeId) {
@@ -2336,7 +2355,7 @@ var DEFAULT_SUMMARY_CHARS = 320;
2336
2355
  var NODE_OVERHEAD_TOKENS = 8;
2337
2356
  var CONVERSATION_ANSWER_MARKER = "\n\nA: ";
2338
2357
  var MAX_PER_FAMILY = 2;
2339
- var CHUNKED_KINDS = /* @__PURE__ */ new Set(["conversation_turn", "doc_section"]);
2358
+ var CHUNKED_KINDS = /* @__PURE__ */ new Set(["conversation_turn", "doc_section", "code_diff"]);
2340
2359
  var HUNK_BOUNDARY = "\n@@ ";
2341
2360
  var STOPWORDS = /* @__PURE__ */ new Set([
2342
2361
  "the",
@@ -2653,7 +2672,7 @@ var HALF_LIFE_RATIO = {
2653
2672
  };
2654
2673
  var SUPERSEDED_PENALTY = 0.5;
2655
2674
  var REJECTED_TRUST_PENALTY = 0.3;
2656
- var MAX_PRIOR_OVERTURN = 2;
2675
+ var MAX_PRIOR_OVERTURN = 2.4;
2657
2676
  var PRIOR_COUNT = 2;
2658
2677
  var PER_PRIOR_OVERTURN = MAX_PRIOR_OVERTURN ** (1 / PRIOR_COUNT);
2659
2678
  var SIGNAL_EXPONENT = Math.log(PER_PRIOR_OVERTURN) / Math.log(1 / SIGNAL_FLOOR);
@@ -3476,11 +3495,13 @@ function isGeneratedPath(path) {
3476
3495
  return GENERATED_PATHS.some((re) => re.test(path));
3477
3496
  }
3478
3497
  var TEST_PATHS = /(^|\/)(tests?|__tests__|spec|e2e)\/|\.(test|spec)\.[cm]?[jt]sx?$/;
3498
+ var MANIFEST_PATHS = /(^|\/)(package\.json|tsconfig(\..+)?\.json)$/;
3479
3499
  function scoreFileDiff(subject, file) {
3480
3500
  const header = parseConventionalHeader(subject);
3481
3501
  let score = header.type ? TYPE_WEIGHTS[header.type] ?? 0.5 : 0.5;
3482
3502
  if (header.breaking) score += 0.1;
3483
3503
  if (TEST_PATHS.test(file.path)) score -= 0.1;
3504
+ if (MANIFEST_PATHS.test(file.path)) score -= 0.1;
3484
3505
  if (file.status === "added") score += 0.05;
3485
3506
  if (file.status === "deleted") score -= 0.1;
3486
3507
  const churn = file.insertions + file.deletions;
@@ -4939,7 +4960,7 @@ ${node.body}`)
4939
4960
  for (const [index, node] of group.entries()) {
4940
4961
  const vector = vectors[index];
4941
4962
  if (vector && vector.length === provider.dimension) {
4942
- store.upsertEmbedding(node.rowid, vector);
4963
+ store.upsertEmbedding(node.rowid, projectId, vector);
4943
4964
  embedded += 1;
4944
4965
  embeddedHere += 1;
4945
4966
  } else {