rag-memory-epf-mcp 6.4.3 → 6.4.4

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/README.md CHANGED
@@ -154,6 +154,24 @@ storeDocument(id, content, metadata)
154
154
 
155
155
  ## Changelog
156
156
 
157
+ ### v6.4.4 (2026-11-07)
158
+
159
+ - **Fixed — the similarity numbers are real cosine similarities.** The vec0 tables use the default L2 metric and the
160
+ embeddings are unit-normalized, so cosine similarity = 1 − d²/2. The engine used 1 − d/2 (L2 read as a cosine
161
+ distance): `searchNodes` `similarity` and the default `hybridSearch` `vector_similarity` were too low above cos 0.5
162
+ (cos 0.8 showed ≈0.684) and too high below it (orthogonal vectors showed ≈0.293 instead of 0). Ranking of the default
163
+ search is unchanged (rank fusion uses ranks only).
164
+ - **Unchanged on purpose — the legacy paths keep the 6.3.2 score.** `hybridSearch` with `useGraph: true` or with
165
+ summaries on, and the graph-seed threshold (`> 0.4`, about cosine 0.28), stay byte-identical to 6.3.2 for evaluation
166
+ comparability; that score is named `legacyL2AsCosineScore` and is not a cosine.
167
+ - **Fixed — chunk vectors without metadata are cleaned up.** Reconciliation now deletes searchable chunk vectors that
168
+ have no `chunk_metadata` row (same rule and batching as entity orphans; off mode only classifies it as deferred).
169
+ `importGraph` no longer creates them: a full replace (`merge: false`) and a same-id document replace delete that
170
+ document's chunk vectors first. Rowids past 2^53 are kept exact.
171
+ - **Added — `getKnowledgeGraphStats().server.coverage.{chunk,entity}.orphan_vectors`**: searchable vectors with no
172
+ metadata row (`null` when the slot map cannot be resolved). `missing` keeps its meaning.
173
+ - Not measured: Windows, Drive-synced DBs under two machines.
174
+
157
175
  ### v6.4.3 (2026-11-02)
158
176
 
159
177
  - **Fixed — re-embedding no longer leaks vector slots.** sqlite-vec puts a new row only into a free slot of the latest
package/dist/index.d.ts CHANGED
@@ -489,7 +489,7 @@ export declare class RAGKnowledgeGraphManager {
489
489
  }>;
490
490
  /**
491
491
  * Diagnostic seam for the graph re-ranker (evaluation change graph-role-evaluation, R2).
492
- * Returns the seed entities (query-vector matched, similarity > 0.4, top-10 per variant) and the
492
+ * Returns the seed entities (query-vector matched, legacy score > 0.4 = cosine ≈0.28, top-10 per variant) and the
493
493
  * 1-hop connected entities exactly as hybridSearch(useGraph:true) computes them, plus edge detail
494
494
  * that hybridSearch itself does not use (edge id, type, direction, confidence). It never generates
495
495
  * candidates and never changes ranking; hybridSearch consumes only the name sets.
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ import { migrations } from './src/migrations/migrations.js';
34
34
  import { EmbeddingGate, GateNotReadyError, GateDisabledError, TerminalConfigError } from './src/embeddingGate.js';
35
35
  import { resolveModelCacheDir, preflightCacheDir, artifactKey, ModelDownloadLock, handleLoaderFailure } from './src/modelCache.js';
36
36
  import { BackfillCoordinator } from './src/backfillCoordinator.js';
37
+ import { l2ToCosineSimilarity, legacyL2AsCosineScore } from './src/vecSimilarity.js';
37
38
  import { writeVector, deleteVector, registerVecSlotFunctions, InvalidSlotError, slotSummary, rebuildVecTable, rebuildReason, DEFAULT_REBUILD_POLICY } from './src/vecSlots.js';
38
39
  import { calendarDate, resolveCalendarTimeZone, stampDatePrefix, stripDatePrefix } from './src/observations/date-prefix.js';
39
40
  import os from 'node:os';
@@ -1894,7 +1895,7 @@ export class RAGKnowledgeGraphManager {
1894
1895
  name: result.name,
1895
1896
  entityType: result.entityType,
1896
1897
  observations: JSON.parse(result.observations),
1897
- similarity: Math.max(0, 1 - result.distance / 2) // Convert cosine distance (0-2) to similarity (1-0)
1898
+ similarity: l2ToCosineSimilarity(result.distance) // vec0 L2 distance of unit vectors -> cosine similarity
1898
1899
  }));
1899
1900
  // hybrid-partial (spec §4): entities without vectors must not vanish from
1900
1901
  // search while backfill catches up — merge lexical FTS hits for the gap.
@@ -3665,12 +3666,18 @@ export class RAGKnowledgeGraphManager {
3665
3666
  this.db.exec(`DELETE FROM entity_observations`);
3666
3667
  this.db.exec(`DELETE FROM observation_roots`);
3667
3668
  this.db.exec(`DELETE FROM entities`);
3669
+ // documents → chunk_metadata is a CASCADE, but vec0 has no FK: delete the document chunk vectors first,
3670
+ // or they stay searchable-less orphans (IN-42 · same order as storeDocument's replace path).
3671
+ // rowids stay BigInt end to end (2_ r1 R1: a Number cast past 2^53 deletes the wrong vector)
3672
+ for (const c of this.db.prepare(`SELECT rowid FROM chunk_metadata WHERE document_id IS NOT NULL`).safeIntegers(true).all()) {
3673
+ deleteVector(this.db, 'chunks', c.rowid);
3674
+ }
3668
3675
  this.db.exec(`DELETE FROM documents`);
3669
3676
  // entities 를 지워도 파생 데이터는 따라오지 않는다: chunk_metadata 에는
3670
3677
  // entities 로 가는 FK 가 없고 entity_embedding_metadata.entity_id 는 UNIQUE 일
3671
3678
  // 뿐이다. 그래서 replace-import 뒤에 **사라진 entity 의 벡터와 KG chunk 가
3672
- // 검색에 남았다**(advisor beta 발견 2). document chunk 는 documents 의
3673
- // CASCADE 로 이미 정리되므로 여기서는 entity·relationship chunk 만 지운다.
3679
+ // 검색에 남았다**(advisor beta 발견 2). document chunk 메타는 documents 의
3680
+ // CASCADE 로 정리되고 그 벡터는 위에서 먼저 지웠다(IN-42) — 여기서는 entity·relationship chunk 만 지운다.
3674
3681
  const orphanChunks = this.db.prepare(`SELECT rowid FROM chunk_metadata WHERE chunk_type IN ('entity','relationship')`)
3675
3682
  .all();
3676
3683
  for (const c of orphanChunks) {
@@ -3732,6 +3739,10 @@ export class RAGKnowledgeGraphManager {
3732
3739
  // doc.id is a string here (checked above, before any write).
3733
3740
  for (const r of collectEvidenceRowidsCitingDocument(this.db, doc.id))
3734
3741
  citedEvidence.add(r);
3742
+ // REPLACE deletes an existing row → its chunk_metadata goes by CASCADE, the vec0 vectors would not (IN-42).
3743
+ for (const c of this.db.prepare(`SELECT rowid FROM chunk_metadata WHERE document_id = ?`).safeIntegers(true).all(doc.id)) {
3744
+ deleteVector(this.db, 'chunks', c.rowid);
3745
+ }
3735
3746
  const result = stmt.run(doc.id, doc.content, JSON.stringify(doc.metadata || {}), doc.created_at || new Date().toISOString());
3736
3747
  if (result.changes > 0) {
3737
3748
  imported.documents++;
@@ -3901,7 +3912,7 @@ export class RAGKnowledgeGraphManager {
3901
3912
  }
3902
3913
  /**
3903
3914
  * Diagnostic seam for the graph re-ranker (evaluation change graph-role-evaluation, R2).
3904
- * Returns the seed entities (query-vector matched, similarity > 0.4, top-10 per variant) and the
3915
+ * Returns the seed entities (query-vector matched, legacy score > 0.4 = cosine ≈0.28, top-10 per variant) and the
3905
3916
  * 1-hop connected entities exactly as hybridSearch(useGraph:true) computes them, plus edge detail
3906
3917
  * that hybridSearch itself does not use (edge id, type, direction, confidence). It never generates
3907
3918
  * candidates and never changes ranking; hybridSearch consumes only the name sets.
@@ -3953,7 +3964,7 @@ export class RAGKnowledgeGraphManager {
3953
3964
  WHERE r.source_entity = ? OR r.target_entity = ?
3954
3965
  ORDER BY r.id`);
3955
3966
  for (const entity of similar) {
3956
- const similarity = Math.max(0, 1 - entity.distance / 2);
3967
+ const similarity = legacyL2AsCosineScore(entity.distance); // legacy seed score, pinned (4ac3155) — not a cosine
3957
3968
  if (similarity > 0.4) {
3958
3969
  seeds.push({ entity_id: entity.entity_id, name: entity.name, similarity });
3959
3970
  for (const row of edgeStmt.all(entity.entity_id, entity.entity_id, entity.entity_id, entity.entity_id, entity.entity_id)) {
@@ -4300,7 +4311,8 @@ export class RAGKnowledgeGraphManager {
4300
4311
  ({ summary, keyHighlight, relevanceScore } = await this.generateContentSummary(result.text, primaryQueryEmbedding, chunkEntities, result.chunk_type === 'relationship' ? 1 : 2 // Shorter summary for relationships
4301
4312
  ));
4302
4313
  }
4303
- const vectorSimilarity = Math.max(0, 1 - result.distance / 2);
4314
+ // default (rank fusion) = real cosine · legacy paths (useGraph:true or summaries on) keep the pinned 6.3.2 score
4315
+ const vectorSimilarity = rankFusion ? l2ToCosineSimilarity(result.distance) : legacyL2AsCosineScore(result.distance);
4304
4316
  const ftsBoost = ftsBoostMap.get(result.chunk_id) || 0; // = 1/(60 + FTS rank), 0 if not an FTS hit
4305
4317
  const vectorRank = vectorRankOf.get(result.chunk_id);
4306
4318
  const finalScore = rankFusion
@@ -4566,11 +4578,11 @@ export class RAGKnowledgeGraphManager {
4566
4578
  coverage: cov ? {
4567
4579
  // vec-slot-reuse D1c: an unresolvable slot map is reported as error with missing = null, not as a count.
4568
4580
  chunk: cov.chunk.error
4569
- ? { total: cov.chunk.total, embedded: null, verified: null, legacy_assumed: null, missing: null, error: cov.chunk.error }
4570
- : { total: cov.chunk.total, embedded: cov.chunk.embedded, verified: cov.chunk.verified, legacy_assumed: cov.chunk.legacy_assumed, missing: cov.chunk.total - cov.chunk.embedded },
4581
+ ? { total: cov.chunk.total, embedded: null, verified: null, legacy_assumed: null, missing: null, orphan_vectors: null, error: cov.chunk.error }
4582
+ : { total: cov.chunk.total, embedded: cov.chunk.embedded, verified: cov.chunk.verified, legacy_assumed: cov.chunk.legacy_assumed, missing: cov.chunk.total - cov.chunk.embedded, orphan_vectors: cov.chunk.orphan_vectors ?? null },
4571
4583
  entity: cov.entity.error
4572
- ? { total: cov.entity.total, embedded: null, verified: null, legacy_assumed: null, missing: null, error: cov.entity.error }
4573
- : { total: cov.entity.total, embedded: cov.entity.embedded, verified: cov.entity.verified, legacy_assumed: cov.entity.legacy_assumed, missing: cov.entity.total - cov.entity.embedded },
4584
+ ? { total: cov.entity.total, embedded: null, verified: null, legacy_assumed: null, missing: null, orphan_vectors: null, error: cov.entity.error }
4585
+ : { total: cov.entity.total, embedded: cov.entity.embedded, verified: cov.entity.verified, legacy_assumed: cov.entity.legacy_assumed, missing: cov.entity.total - cov.entity.embedded, orphan_vectors: cov.entity.orphan_vectors ?? null },
4574
4586
  } : null,
4575
4587
  // vec-slot-reuse: per vec0 table = slots, searchable, validity-0 rows, reclaimable chunks, why a rebuild
4576
4588
  // is (not) due, and the last attempt of this process. Read-only: computing it writes nothing.
@@ -6,6 +6,7 @@ export interface CoverageSnapshot {
6
6
  embedded: number;
7
7
  verified: number;
8
8
  legacy_assumed: number;
9
+ orphan_vectors?: number;
9
10
  error?: string;
10
11
  };
11
12
  entity: {
@@ -13,6 +14,7 @@ export interface CoverageSnapshot {
13
14
  embedded: number;
14
15
  verified: number;
15
16
  legacy_assumed: number;
17
+ orphan_vectors?: number;
16
18
  error?: string;
17
19
  };
18
20
  }
@@ -92,6 +92,8 @@ export class BackfillCoordinator {
92
92
  // vec-slot-reuse r3 M2: searchable orphans only (validity-0 rows belong to the rebuild); vectorless
93
93
  // metadata counts only while still stamped (it is normalized to missing, not deleted, to keep its rowid).
94
94
  + q(`SELECT COUNT(*) c FROM entity_embeddings_rowids m WHERE m.rowid NOT IN (SELECT rowid FROM entity_embedding_metadata) AND ${searchableVectorSql('entity_embeddings', 'm.rowid')}`)
95
+ // IN-42: searchable chunk vectors without chunk_metadata (same rule as entity orphans)
96
+ + q(`SELECT COUNT(*) c FROM chunks_rowids m WHERE m.rowid NOT IN (SELECT rowid FROM chunk_metadata) AND ${searchableVectorSql('chunks', 'm.rowid')}`)
95
97
  + q(`SELECT COUNT(*) c FROM entity_embedding_metadata WHERE provenance_state IS NOT NULL AND rowid NOT IN (SELECT rowid FROM entity_embeddings_rowids)`)
96
98
  + q(`SELECT COUNT(*) c FROM entity_embedding_metadata WHERE provenance_state IS NOT NULL AND profile_id IS NOT ?`, profileId)
97
99
  + q(`SELECT COUNT(*) c FROM chunk_metadata m WHERE m.provenance_state IS NOT NULL AND m.profile_id IS NOT ? AND m.rowid IN (SELECT rowid FROM chunks_rowids)`, profileId)
@@ -136,6 +138,22 @@ export class BackfillCoordinator {
136
138
  break;
137
139
  await new Promise(r => setImmediate(r));
138
140
  }
141
+ // 1b. searchable chunk vectors without metadata -> delete (IN-42 · same batching and validity rule as 1)
142
+ for (;;) {
143
+ if (this.shuttingDown)
144
+ throw new Error('shutdown during sanitation');
145
+ const orphans = db.prepare(`SELECT m.rowid FROM chunks_rowids m
146
+ WHERE m.rowid NOT IN (SELECT rowid FROM chunk_metadata) AND ${searchableVectorSql('chunks', 'm.rowid')} LIMIT ${RECON_BATCH}`).safeIntegers(true).all();
147
+ if (orphans.length === 0)
148
+ break;
149
+ batchTx(() => { for (const r of orphans) {
150
+ deleteVector(db, 'chunks', r.rowid);
151
+ touched++;
152
+ } });
153
+ if (orphans.length < RECON_BATCH)
154
+ break;
155
+ await new Promise(r => setImmediate(r));
156
+ }
139
157
  // 2. stamped entity metadata whose vector row is gone -> normalize to missing (was: delete). Keeping the
140
158
  // row keeps its rowid, so the backfill re-inserts at the same rowid (r3 M2 case C: restart after repair).
141
159
  await batchedRun(`UPDATE entity_embedding_metadata SET input_hash = NULL, profile_id = NULL, provenance_state = NULL
@@ -178,8 +196,10 @@ export class BackfillCoordinator {
178
196
  });
179
197
  await new Promise(r => setImmediate(r));
180
198
  }
181
- if (touched > 0)
182
- console.error(`🧾 provenance sanitation: ${touched} split-state/old-profile rows invalidated`);
199
+ if (touched > 0) {
200
+ this.snapshot = null; // cached coverage (incl. orphan_vectors) is stale — also on the legacy=0 early return (2_ r1 R2)
201
+ console.error(`🧾 provenance sanitation: ${touched} split-state/old-profile/orphan rows invalidated`);
202
+ }
183
203
  return touched;
184
204
  }
185
205
  // Single-flight, idempotent. off mode defers (spec §9); fresh DBs go straight
@@ -612,6 +632,18 @@ export class BackfillCoordinator {
612
632
  const entQ = byState(`SELECT m.provenance_state s, COUNT(*) c FROM entity_embedding_metadata m
613
633
  WHERE ${searchableVectorSql('entity_embeddings', 'm.rowid')} GROUP BY m.provenance_state`);
614
634
  const entBy = entQ.rows;
635
+ const orphanQ = (t, meta) => {
636
+ try {
637
+ return db.prepare(`SELECT COUNT(*) c FROM ${t}_rowids m WHERE m.rowid NOT IN (SELECT rowid FROM ${meta}) AND ${searchableVectorSql(t, 'm.rowid')}`).get().c;
638
+ }
639
+ catch (e) {
640
+ if (!(e instanceof VecSlotError) && !String(e.message).includes('vec0 slot'))
641
+ throw e;
642
+ return undefined;
643
+ }
644
+ };
645
+ const chunkOrphans = orphanQ('chunks', 'chunk_metadata');
646
+ const entOrphans = orphanQ('entity_embeddings', 'entity_embedding_metadata');
615
647
  const pick = (rows, s) => rows.find(r => r.s === s)?.c ?? 0;
616
648
  // Searchable = verified + legacy_assumed. NULL rows (pre-reconciliation or
617
649
  // deferred in off mode) are NOT counted as embedded (6R note 4).
@@ -620,9 +652,9 @@ export class BackfillCoordinator {
620
652
  const entVerified = pick(entBy, 'verified');
621
653
  const entLegacy = pick(entBy, 'legacy_assumed');
622
654
  this.snapshot = {
623
- chunk: { total: chunkTotal, embedded: chunkVerified + chunkLegacy, verified: chunkVerified, legacy_assumed: chunkLegacy,
655
+ chunk: { total: chunkTotal, embedded: chunkVerified + chunkLegacy, verified: chunkVerified, legacy_assumed: chunkLegacy, orphan_vectors: chunkOrphans,
624
656
  ...(chunkQ.error ? { error: chunkQ.error } : {}) },
625
- entity: { total: entTotal, embedded: entVerified + entLegacy, verified: entVerified, legacy_assumed: entLegacy,
657
+ entity: { total: entTotal, embedded: entVerified + entLegacy, verified: entVerified, legacy_assumed: entLegacy, orphan_vectors: entOrphans,
626
658
  ...(entQ.error ? { error: entQ.error } : {}) },
627
659
  };
628
660
  return this.snapshot;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * vec0 tables (chunks · entity_embeddings) are created without `distance_metric`, so sqlite-vec returns the
3
+ * default L2 distance. Embeddings are unit-normalized (`normalize: true`), so for unit vectors
4
+ * ‖a − b‖² = 2 − 2·cos → cosine similarity = 1 − d²/2.
5
+ * The earlier conversion 1 − d/2 treated the L2 value as a cosine distance: right − old = (d − d²)/2, so it
6
+ * under-reported for d < 1 (cos > 0.5: cos 0.8 showed ≈0.684) and over-reported for d > 1 (orthogonal showed
7
+ * ≈0.293 instead of 0 · cos 0.3 showed ≈0.408 and passed the 0.4 graph-seed threshold). Clamped to [0, 1].
8
+ * Guarded by test/vec-similarity.test.mjs (also asserts the metric is still L2).
9
+ */
10
+ export declare function l2ToCosineSimilarity(distance: number): number;
11
+ /**
12
+ * The 6.3.2 score, kept ONLY for the legacy paths that are pinned byte-for-byte (engine commit 4ac3155 · test
13
+ * graph-context-explain): hybridSearch with useGraph:true or summaries on, and the graph-seed `> 0.4` threshold of
14
+ * explainGraphContext. It is the historical 1 − d/2 conversion of an L2 distance and is NOT a cosine similarity
15
+ * (its 0.4 threshold equals a real cosine of about 0.28). Default search and searchNodes use l2ToCosineSimilarity.
16
+ */
17
+ export declare function legacyL2AsCosineScore(distance: number): number;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * vec0 tables (chunks · entity_embeddings) are created without `distance_metric`, so sqlite-vec returns the
3
+ * default L2 distance. Embeddings are unit-normalized (`normalize: true`), so for unit vectors
4
+ * ‖a − b‖² = 2 − 2·cos → cosine similarity = 1 − d²/2.
5
+ * The earlier conversion 1 − d/2 treated the L2 value as a cosine distance: right − old = (d − d²)/2, so it
6
+ * under-reported for d < 1 (cos > 0.5: cos 0.8 showed ≈0.684) and over-reported for d > 1 (orthogonal showed
7
+ * ≈0.293 instead of 0 · cos 0.3 showed ≈0.408 and passed the 0.4 graph-seed threshold). Clamped to [0, 1].
8
+ * Guarded by test/vec-similarity.test.mjs (also asserts the metric is still L2).
9
+ */
10
+ export function l2ToCosineSimilarity(distance) {
11
+ if (!Number.isFinite(distance))
12
+ return 0;
13
+ return Math.min(1, Math.max(0, 1 - (distance * distance) / 2));
14
+ }
15
+ /**
16
+ * The 6.3.2 score, kept ONLY for the legacy paths that are pinned byte-for-byte (engine commit 4ac3155 · test
17
+ * graph-context-explain): hybridSearch with useGraph:true or summaries on, and the graph-seed `> 0.4` threshold of
18
+ * explainGraphContext. It is the historical 1 − d/2 conversion of an L2 distance and is NOT a cosine similarity
19
+ * (its 0.4 threshold equals a real cosine of about 0.28). Default search and searchNodes use l2ToCosineSimilarity.
20
+ */
21
+ export function legacyL2AsCosineScore(distance) {
22
+ return Math.max(0, 1 - distance / 2);
23
+ }
package/docs/UPDATING.md CHANGED
@@ -108,6 +108,14 @@ path and holder pid (e.g. `.download-<key>.lock`). Verify the holder process
108
108
  is genuinely gone or hung (`ps -p <pid>`), then remove the lock file manually;
109
109
  the next start becomes a clean download owner.
110
110
 
111
+ ## v6.4.4 (schema v14, unchanged): real cosine similarity in default search, chunk orphan vectors cleaned
112
+
113
+ **What changes for an existing database.** Nothing is migrated and nothing is re-embedded. On the first reconciliation
114
+ after start, searchable chunk vectors that have no metadata row are deleted (the count shows as
115
+ `coverage.chunk.orphan_vectors` before, 0 after). Similarity numbers shown by `searchNodes` and the default
116
+ `hybridSearch` change to real cosine values; their order does not. `useGraph: true` and summaries-on results stay as in
117
+ 6.4.3. An engine that is already running keeps the old behaviour until it restarts on 6.4.4.
118
+
111
119
  ## v6.4.3 (schema v14, unchanged): vector slots rewritten in place, idle rebuild, searchable-only coverage
112
120
 
113
121
  **What changes for an existing database.** Nothing is migrated and nothing is re-embedded. Re-embeds stop leaving empty
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rag-memory-epf-mcp",
3
- "version": "6.4.3",
3
+ "version": "6.4.4",
4
4
  "engines": {
5
5
  "node": ">=24"
6
6
  },
@@ -45,7 +45,7 @@
45
45
  "prepare": "npm run build",
46
46
  "watch": "tsc --watch",
47
47
  "verify:invariants": "node test/chunk-invariants.test.mjs",
48
- "verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/search-graph-default.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node --no-concurrent-sparkplug test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/alias-link-gate.test.mjs && node test/entity-name-boundary.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs && node test/search-fusion-rrf.test.mjs && node test/query-input.test.mjs && node test/document-return-contracts.test.mjs && node test/observation-date-prefix.test.mjs && node test/delete-entities-cascade.test.mjs && node test/delete-entities-kg-hygiene.test.mjs && node test/backup-publish-portable.test.mjs && node test/graph-context-explain.test.mjs && node test/eval-graph-role-libs.test.mjs && node test/eval-graph-role-t5b.test.mjs && node --test test/eval-graph-role-t8-fix.test.mjs && node --test test/eval-graph-role-t7-upstream.test.mjs && node --test test/eval-graph-role-t11-decision.test.mjs && node --test test/eval-graph-role-prereq-fix.test.mjs && node test/db-path.test.mjs && node test/mmap-option.test.mjs && node test/wal-exit.test.mjs && node test/wal-sidecar-guard.test.mjs && node test/kg-evidence-core.test.mjs && node test/kg-delete-relation.test.mjs && node test/kg-confidence-copy.test.mjs && node test/kg-embed-cas.test.mjs && node test/kg-relation-evidence.test.mjs && node test/kg-detail-context.test.mjs && node test/kg-evidence-invalidation.test.mjs && node test/kg-evidence-cas.test.mjs && node test/kg-defaults.test.mjs && node test/chunker-oversize-cost.test.mjs && node test/chunker-structure-parity.test.mjs && node test/search-limit-depth.test.mjs && node test/kg-import-id-contract.test.mjs && node test/kg-evidence-guards.test.mjs && node test/model-session-options.test.mjs && node test/write-seq.test.mjs && node test/vec-slot-reuse.test.mjs && node test/vec-rebuild.test.mjs && node test/vec-slot-invalid.test.mjs",
48
+ "verify:engine": "node test/engine-smoke.test.mjs && node test/launch-smoke.test.mjs && node test/sync-atomicity.test.mjs && node test/dedup.test.mjs && node test/search-degradation.test.mjs && node test/entity-embed-cap.test.mjs && node test/migration12.test.mjs && node test/model-cache.test.mjs && node test/embedding-gate.test.mjs && node test/lazy-boot.test.mjs && node test/reconciliation.test.mjs && node test/backfill.test.mjs && node test/fts-query.test.mjs && node test/search-contracts.test.mjs && node test/tool-contracts.test.mjs && node test/bounded-exit.test.mjs && node test/observation-schema.test.mjs && node test/search-graph-default.test.mjs && node test/observation-migration.test.mjs && node test/observation-lifecycle.test.mjs && node test/observation-contracts.test.mjs && node test/observation-search.test.mjs && node test/observation-cascade.test.mjs && node test/observation-realdata.test.mjs && node --no-concurrent-sparkplug test/chunker-c.test.mjs && node test/migration14.test.mjs && node test/chunk-params-validation.test.mjs && node test/vector-reuse.test.mjs && node test/entity-range-linking.test.mjs && node test/alias-link-gate.test.mjs && node test/entity-name-boundary.test.mjs && node test/stats-chunking.test.mjs && node test/migration14-realdata.test.mjs && node test/migration14-realdata-sync.test.mjs && node test/search-summaries-off.test.mjs && node test/search-fusion-rrf.test.mjs && node test/query-input.test.mjs && node test/document-return-contracts.test.mjs && node test/observation-date-prefix.test.mjs && node test/delete-entities-cascade.test.mjs && node test/delete-entities-kg-hygiene.test.mjs && node test/backup-publish-portable.test.mjs && node test/graph-context-explain.test.mjs && node test/eval-graph-role-libs.test.mjs && node test/eval-graph-role-t5b.test.mjs && node --test test/eval-graph-role-t8-fix.test.mjs && node --test test/eval-graph-role-t7-upstream.test.mjs && node --test test/eval-graph-role-t11-decision.test.mjs && node --test test/eval-graph-role-prereq-fix.test.mjs && node test/db-path.test.mjs && node test/mmap-option.test.mjs && node test/wal-exit.test.mjs && node test/wal-sidecar-guard.test.mjs && node test/kg-evidence-core.test.mjs && node test/kg-delete-relation.test.mjs && node test/kg-confidence-copy.test.mjs && node test/kg-embed-cas.test.mjs && node test/kg-relation-evidence.test.mjs && node test/kg-detail-context.test.mjs && node test/kg-evidence-invalidation.test.mjs && node test/kg-evidence-cas.test.mjs && node test/kg-defaults.test.mjs && node test/chunker-oversize-cost.test.mjs && node test/chunker-structure-parity.test.mjs && node test/search-limit-depth.test.mjs && node test/kg-import-id-contract.test.mjs && node test/kg-evidence-guards.test.mjs && node test/model-session-options.test.mjs && node test/write-seq.test.mjs && node test/vec-slot-reuse.test.mjs && node test/vec-rebuild.test.mjs && node test/vec-slot-invalid.test.mjs && node test/vec-similarity.test.mjs && node test/chunk-orphan-vectors.test.mjs",
49
49
  "test": "npm run build && npm run verify:invariants && npm run verify:engine",
50
50
  "prepublishOnly": "npm run build && npm run verify:invariants && npm run verify:engine"
51
51
  },