@soulcraft/cor 3.0.19 → 3.0.20

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.
@@ -92,6 +92,8 @@ export declare class GraphAdjacencyIndex implements GraphIndexProvider {
92
92
  private lastMembershipStampBody;
93
93
  /** Byte length of the last persisted membership blob (stamp needs it without re-serializing). */
94
94
  private lastMembershipBlobBytes;
95
+ /** Monotonic stamp lineage (unified shape); restored at load, bumped at persist. */
96
+ private membershipStampGeneration;
95
97
  /** How this boot obtained membership: restored (stamp), healed (walk), or fresh (no verbs). */
96
98
  private membershipBootState;
97
99
  private flushTimer?;
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { dirname } from 'node:path';
16
16
  import { createHash } from 'node:crypto';
17
+ import { readCommittedGeneration } from '../utils/generationWatermark.js';
17
18
  import { TypeUtils, VerbType } from '@soulcraft/brainy/types/graphTypes';
18
19
  import { loadNativeModule } from '../native/index.js';
19
20
  import { composeInvariantReport } from '../utils/invariantReport.js';
@@ -58,6 +59,26 @@ const GRAPH_VERB_NS_PROBE_KEY = '_graph_verb_ns/x';
58
59
  */
59
60
  const GRAPH_MEMBERSHIP_BLOB_KEY = 'graph-verb-membership';
60
61
  const GRAPH_MEMBERSHIP_STAMP_KEY = 'graph-membership-stamp';
62
+ /** Normalized view both wire shapes reduce to; null = unrecognized. */
63
+ function normalizeMembershipStamp(raw) {
64
+ const s = raw;
65
+ if (s?.members?.mode === 'rollup' && s.members.invariants) {
66
+ const inv = s.members.invariants;
67
+ const blobBytes = inv.blobBytes;
68
+ if (typeof blobBytes !== 'number')
69
+ return null;
70
+ const trees = {};
71
+ for (const [k, v] of Object.entries(inv)) {
72
+ if (k.startsWith('tree:') && typeof v === 'string')
73
+ trees[k.slice(5)] = v;
74
+ }
75
+ return { blobBytes, trees, generation: typeof s.generation === 'number' ? s.generation : 0 };
76
+ }
77
+ if (s?.v === 1 && typeof s.blobBytes === 'number' && s.trees) {
78
+ return { blobBytes: s.blobBytes, trees: s.trees, generation: 0 };
79
+ }
80
+ return null;
81
+ }
61
82
  /**
62
83
  * Resolve a verb-type string to its canonical `VerbTypeEnum` index,
63
84
  * falling back to {@link VERB_FALLBACK_INDEX} (`VerbType.RelatedTo`)
@@ -110,6 +131,8 @@ export class GraphAdjacencyIndex {
110
131
  lastMembershipStampBody = '';
111
132
  /** Byte length of the last persisted membership blob (stamp needs it without re-serializing). */
112
133
  lastMembershipBlobBytes = -1;
134
+ /** Monotonic stamp lineage (unified shape); restored at load, bumped at persist. */
135
+ membershipStampGeneration = 0;
113
136
  /** How this boot obtained membership: restored (stamp), healed (walk), or fresh (no verbs). */
114
137
  membershipBootState = 'fresh';
115
138
  flushTimer;
@@ -369,23 +392,29 @@ export class GraphAdjacencyIndex {
369
392
  this.lastMembershipBlobBytes = blob.length;
370
393
  this.membershipDirty = false;
371
394
  }
372
- const trees = {};
395
+ const invariants = {
396
+ blobBytes: this.lastMembershipBlobBytes,
397
+ };
373
398
  for (const treeName of this.native.treeNames()) {
374
- trees[treeName] = this.treeFingerprint(treeName);
399
+ invariants[`tree:${treeName}`] = this.treeFingerprint(treeName);
375
400
  }
376
- const body = JSON.stringify({ v: 1, blobBytes: this.lastMembershipBlobBytes, trees });
401
+ const body = JSON.stringify(invariants);
377
402
  if (body === this.lastMembershipStampBody)
378
403
  return;
379
404
  const stamp = {
380
- v: 1,
381
- committedAt: Date.now(),
382
- blobBytes: this.lastMembershipBlobBytes,
383
- trees,
405
+ family: 'graph-membership',
406
+ generation: this.membershipStampGeneration + 1,
407
+ committedAt: new Date().toISOString(),
408
+ members: { mode: 'rollup', invariants },
384
409
  };
410
+ const sourceGeneration = await readCommittedGeneration(this.storage);
411
+ if (sourceGeneration !== undefined)
412
+ stamp.sourceGeneration = sourceGeneration;
385
413
  await this.storage.saveMetadata(GRAPH_MEMBERSHIP_STAMP_KEY, {
386
414
  noun: 'thing',
387
415
  data: stamp,
388
416
  });
417
+ this.membershipStampGeneration = stamp.generation;
389
418
  this.lastMembershipStampBody = body;
390
419
  }
391
420
  catch (error) {
@@ -405,8 +434,8 @@ export class GraphAdjacencyIndex {
405
434
  return false;
406
435
  try {
407
436
  const meta = await this.storage.getMetadata(GRAPH_MEMBERSHIP_STAMP_KEY);
408
- const stamp = meta?.data;
409
- if (!stamp || stamp.v !== 1)
437
+ const stamp = normalizeMembershipStamp(meta?.data);
438
+ if (!stamp)
410
439
  return false;
411
440
  const blob = await this.storage.loadBinaryBlob(GRAPH_MEMBERSHIP_BLOB_KEY);
412
441
  if (!blob || blob.length !== stamp.blobBytes) {
@@ -422,11 +451,11 @@ export class GraphAdjacencyIndex {
422
451
  }
423
452
  this.native.loadVerbMembership(blob);
424
453
  this.lastMembershipBlobBytes = blob.length;
425
- this.lastMembershipStampBody = JSON.stringify({
426
- v: 1,
427
- blobBytes: stamp.blobBytes,
428
- trees: stamp.trees,
429
- });
454
+ this.membershipStampGeneration = stamp.generation;
455
+ const invariants = { blobBytes: stamp.blobBytes };
456
+ for (const [name, fp] of Object.entries(stamp.trees))
457
+ invariants[`tree:${name}`] = fp;
458
+ this.lastMembershipStampBody = JSON.stringify(invariants);
430
459
  this.membershipBootState = 'restored';
431
460
  prodLog.info(`GraphAdjacencyIndex: verb membership restored from stamp (${this.native.verbIdCount()} live verbs, no canonical walk)`);
432
461
  return true;
@@ -458,6 +458,15 @@ export declare class NativeDiskAnnWrapper implements VectorIndexProvider {
458
458
  getPersistMode(): 'immediate' | 'deferred';
459
459
  /** Absolute path of a segment file (lives next to the base indexPath). */
460
460
  private segPath;
461
+ /**
462
+ * The committed-transaction watermark (M3 PD-2 `sourceGeneration`): read
463
+ * from brainy's generation manifest (`_system/manifest.json`, the same
464
+ * object the generation store treats as committed truth). Read-only and
465
+ * ADVISORY — hosts without the manifest (or a faulted read) stamp no
466
+ * sourceGeneration and verifiers skip the comparison. Never constructs a
467
+ * FactLog (its open() is writer-side reconciliation).
468
+ */
469
+ private readCommittedGeneration;
461
470
  /**
462
471
  * Move poisoned L0 segments out of the live set into
463
472
  * `<indexDir>/quarantine/` — preserved for forensics, NEVER deleted
@@ -78,6 +78,7 @@ import { prodLog } from '@soulcraft/brainy/internals';
78
78
  import { mkdirSync, existsSync, rmSync, renameSync } from 'node:fs';
79
79
  import { dirname, join, basename } from 'node:path';
80
80
  import { autoModeForHeader, selectModeFromResourceManager, } from './AdaptiveDiskAnnModeSelector.js';
81
+ import { readCommittedGeneration } from '../utils/generationWatermark.js';
81
82
  /**
82
83
  * Node-count threshold above which `useMmapAdjacency: 'auto'`
83
84
  * resolves to file-backed. Below this, the in-RAM build adjacency
@@ -1412,6 +1413,7 @@ export class NativeDiskAnnWrapper {
1412
1413
  outputPath: this.config.indexPath,
1413
1414
  cfg,
1414
1415
  slotIds: slotIdsBuf,
1416
+ sourceGeneration: await this.readCommittedGeneration(),
1415
1417
  });
1416
1418
  // Atomic swap. The native engine now owns both off-heap slotmaps
1417
1419
  // (written inside rebuildFromExisting), so there is NO resident
@@ -1501,6 +1503,17 @@ export class NativeDiskAnnWrapper {
1501
1503
  segPath(file) {
1502
1504
  return `${dirname(this.config.indexPath)}/${file}`;
1503
1505
  }
1506
+ /**
1507
+ * The committed-transaction watermark (M3 PD-2 `sourceGeneration`): read
1508
+ * from brainy's generation manifest (`_system/manifest.json`, the same
1509
+ * object the generation store treats as committed truth). Read-only and
1510
+ * ADVISORY — hosts without the manifest (or a faulted read) stamp no
1511
+ * sourceGeneration and verifiers skip the comparison. Never constructs a
1512
+ * FactLog (its open() is writer-side reconciliation).
1513
+ */
1514
+ async readCommittedGeneration() {
1515
+ return readCommittedGeneration(this.storage);
1516
+ }
1504
1517
  /**
1505
1518
  * Move poisoned L0 segments out of the live set into
1506
1519
  * `<indexDir>/quarantine/` — preserved for forensics, NEVER deleted
@@ -1746,6 +1759,7 @@ export class NativeDiskAnnWrapper {
1746
1759
  outputPath: this.segPath(file),
1747
1760
  cfg,
1748
1761
  slotIds: Buffer.from(slotIdsArr.buffer, slotIdsArr.byteOffset, slotIdsArr.byteLength),
1762
+ sourceGeneration: await this.readCommittedGeneration(),
1749
1763
  });
1750
1764
  // File (+ sidecars) durably on disk → register → manifest (crash-safe order).
1751
1765
  this.l0Segments.push({ id, file, nodeCount: builtDelta.size, native: newNative });
@@ -1858,10 +1872,17 @@ export class NativeDiskAnnWrapper {
1858
1872
  violations: [`stamp ${stampPath} has no members map — cannot prove family coherence`],
1859
1873
  };
1860
1874
  }
1875
+ // Member sizes, both wire modes (M3 unified stamp, 2026-07-15): the
1876
+ // unified shape carries {mode:'enumerated', files:[{path,bytes}]}; the
1877
+ // legacy cor shape is a flat basename→bytes map. One verifier, one page.
1878
+ const rawMembers = stamp.members;
1879
+ const memberSizes = Array.isArray(rawMembers.files)
1880
+ ? Object.fromEntries(rawMembers.files.map((f) => [f.path, f.bytes]))
1881
+ : stamp.members;
1861
1882
  const dir = dirname(indexPath);
1862
1883
  const violations = [];
1863
1884
  const faults = [];
1864
- for (const [name, expected] of Object.entries(stamp.members)) {
1885
+ for (const [name, expected] of Object.entries(memberSizes)) {
1865
1886
  try {
1866
1887
  const actual = statSync(join(dir, name)).size;
1867
1888
  if (actual !== expected) {
@@ -858,6 +858,24 @@ export declare class MetadataIndexManager implements MetadataIndexProvider {
858
858
  */
859
859
  private flushChain;
860
860
  flush(): Promise<void>;
861
+ /**
862
+ * Write the metadata index's family stamp (shared shape/functions from
863
+ * brainy internals): rollup invariant = the posted-entity total the
864
+ * detector already computes; `sourceGeneration` = the committed
865
+ * watermark these postings cover. Advisory end to end — a host without
866
+ * the stamp surface or the watermark simply doesn't stamp, and opens
867
+ * keep their walk-based healing.
868
+ */
869
+ private persistMetadataStamp;
870
+ /**
871
+ * M3 PD-4: incremental catch-up from the generation fact log. Applies
872
+ * exactly the ops in `[fromGeneration, head]` — after-image upserts via
873
+ * the normal add path (idempotent: eviction removes stale buckets),
874
+ * tombstones via the id-only removal path (removal law). Returns false
875
+ * (and logs) on ANY break — no capability, scan gap, apply fault — so
876
+ * the caller falls back to the walk; a half-replay never claims success.
877
+ */
878
+ private replayFactGap;
861
879
  private flushOnce;
862
880
  /**
863
881
  * Persist all state and release the column store's resources. Brainy's
@@ -18,6 +18,7 @@
18
18
  import { existsSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
19
19
  import { dirname, join } from 'node:path';
20
20
  import { prodLog, getGlobalCache, FieldTypeInference, resolveEntityField } from '@soulcraft/brainy/internals';
21
+ import { readCommittedGeneration } from './generationWatermark.js';
21
22
  import { compareCodePoints } from './collation.js';
22
23
  /**
23
24
  * Fields whose values are stored in the sparse index as BUCKETED values
@@ -84,6 +85,33 @@ const LSM_MANIFEST_KEY = '__metadata_lsm_manifest__';
84
85
  * CORTEX-RESTART-STRAND 2026-07-13 — four consecutive boots).
85
86
  */
86
87
  const UNPOSTABLE_STAMP_KEY = '__metadata_verified_unpostable__';
88
+ /**
89
+ * M3 PD-2/PD-4 (Stage 1): the metadata index's family stamp — written via
90
+ * brainy's SHARED stamp functions (one verifier, literally one function).
91
+ * `sourceGeneration` records the committed watermark the postings cover;
92
+ * a coherent-but-BEHIND stamp at open triggers an incremental FACT REPLAY
93
+ * of exactly the gap instead of any walk. Path/family per brainy's
94
+ * FAMILY_STAMPS_PREFIX convention.
95
+ */
96
+ const METADATA_FAMILY = 'cor-metadata-lsm';
97
+ const METADATA_STAMP_PATH = '_system/family-stamps/cor-metadata-lsm.json';
98
+ let familyStampFnsCache;
99
+ async function familyStampFns() {
100
+ if (familyStampFnsCache !== undefined)
101
+ return familyStampFnsCache;
102
+ try {
103
+ const internals = (await import('@soulcraft/brainy/internals'));
104
+ familyStampFnsCache =
105
+ typeof internals.readFamilyStamp === 'function' &&
106
+ typeof internals.writeFamilyStamp === 'function'
107
+ ? internals
108
+ : null;
109
+ }
110
+ catch {
111
+ familyStampFnsCache = null;
112
+ }
113
+ return familyStampFnsCache;
114
+ }
87
115
  /** Above this many ids the stamp stores count-only (the boot-time
88
116
  * re-verify would stop being cheap); detection then carries a bounded
89
117
  * staleness caveat, logged when it engages. */
@@ -1406,6 +1434,29 @@ export class MetadataIndexManager {
1406
1434
  async detectDerivedStrand() {
1407
1435
  if (!this.lsmEngine || !this.lsmMetaDir)
1408
1436
  return;
1437
+ // M3 PD-4 (Stage 1): a coherent-but-BEHIND family stamp means the
1438
+ // postings simply predate the newest commits — replay EXACTLY the gap
1439
+ // from the generation fact log before any strand verdict, so the
1440
+ // checks below see a caught-up index and no walk fires for what was
1441
+ // never damage. Advisory end to end: any break (no 8.5.0 surface, no
1442
+ // watermark, scan fault) falls through and the detector stays
1443
+ // authoritative.
1444
+ try {
1445
+ const fns = await familyStampFns();
1446
+ const head = await readCommittedGeneration(this.storage);
1447
+ if (fns && head !== undefined) {
1448
+ const stamp = (await fns.readFamilyStamp(this.storage, METADATA_STAMP_PATH));
1449
+ const src = stamp?.family === METADATA_FAMILY ? stamp.sourceGeneration : undefined;
1450
+ if (typeof src === 'number' && src < head) {
1451
+ if (await this.replayFactGap(src + 1)) {
1452
+ await this.persistMetadataStamp();
1453
+ }
1454
+ }
1455
+ }
1456
+ }
1457
+ catch {
1458
+ // Detector below stays authoritative.
1459
+ }
1409
1460
  // 0. Replay-stats verdict (3.0.12). The native open now RECREATES a
1410
1461
  // missing live log and recovers what its archives hold, so the
1411
1462
  // filename census below can no longer see the deleted-live-log
@@ -2664,12 +2715,95 @@ export class MetadataIndexManager {
2664
2715
  'flush would multiply heal IO).');
2665
2716
  return;
2666
2717
  }
2667
- const run = this.flushChain.then(() => this.flushOnce());
2718
+ const run = this.flushChain.then(async () => {
2719
+ await this.flushOnce();
2720
+ // M3 Stage 1: the flushed postings durably cover the committed
2721
+ // watermark — stamp it so the next open replays only the gap
2722
+ // (advisory: no-ops without the 8.5.0 stamp surface).
2723
+ await this.persistMetadataStamp();
2724
+ });
2668
2725
  // Keep the chain alive on failure — the NEXT flush must still run; the
2669
2726
  // failure itself propagates to THIS caller below.
2670
2727
  this.flushChain = run.catch(() => { });
2671
2728
  return run;
2672
2729
  }
2730
+ /**
2731
+ * Write the metadata index's family stamp (shared shape/functions from
2732
+ * brainy internals): rollup invariant = the posted-entity total the
2733
+ * detector already computes; `sourceGeneration` = the committed
2734
+ * watermark these postings cover. Advisory end to end — a host without
2735
+ * the stamp surface or the watermark simply doesn't stamp, and opens
2736
+ * keep their walk-based healing.
2737
+ */
2738
+ async persistMetadataStamp() {
2739
+ try {
2740
+ const fns = await familyStampFns();
2741
+ if (!fns)
2742
+ return;
2743
+ const sourceGeneration = await readCommittedGeneration(this.storage);
2744
+ if (sourceGeneration === undefined)
2745
+ return;
2746
+ const { NounType } = await import('@soulcraft/brainy');
2747
+ let posted = 0;
2748
+ for (const t of Object.values(NounType)) {
2749
+ posted += this.native.getEntityCountByType(t);
2750
+ }
2751
+ await fns.writeFamilyStamp(this.storage, METADATA_STAMP_PATH, {
2752
+ family: METADATA_FAMILY,
2753
+ sourceGeneration,
2754
+ members: { mode: 'rollup', invariants: { posted } },
2755
+ });
2756
+ }
2757
+ catch {
2758
+ // Advisory: a failed stamp means the next open heals the old way.
2759
+ }
2760
+ }
2761
+ /**
2762
+ * M3 PD-4: incremental catch-up from the generation fact log. Applies
2763
+ * exactly the ops in `[fromGeneration, head]` — after-image upserts via
2764
+ * the normal add path (idempotent: eviction removes stale buckets),
2765
+ * tombstones via the id-only removal path (removal law). Returns false
2766
+ * (and logs) on ANY break — no capability, scan gap, apply fault — so
2767
+ * the caller falls back to the walk; a half-replay never claims success.
2768
+ */
2769
+ async replayFactGap(fromGeneration) {
2770
+ const scanFn = this.storage.scanFacts;
2771
+ if (typeof scanFn !== 'function')
2772
+ return false;
2773
+ const scan = scanFn.call(this.storage, { fromGeneration, batchSize: 256 });
2774
+ if (!scan)
2775
+ return false;
2776
+ let upserts = 0;
2777
+ let removals = 0;
2778
+ try {
2779
+ for await (const batch of scan.batches()) {
2780
+ for (const fact of batch.facts) {
2781
+ for (const op of fact.ops) {
2782
+ if (op.record === null) {
2783
+ await this.removeFromIndex(op.id);
2784
+ removals++;
2785
+ }
2786
+ else if (op.record.metadata != null) {
2787
+ await this.addToIndex(op.id, op.record.metadata, true, true);
2788
+ upserts++;
2789
+ }
2790
+ // record present with null metadata = vector-only write —
2791
+ // nothing to post here.
2792
+ }
2793
+ }
2794
+ }
2795
+ }
2796
+ catch (error) {
2797
+ opsLine(`fact replay from generation ${fromGeneration} ABORTED (${String(error)}) — ` +
2798
+ `falling back to the walk-based heal path`);
2799
+ return false;
2800
+ }
2801
+ if (upserts + removals > 0) {
2802
+ opsLine(`fact replay: caught up ${upserts} upsert(s) + ${removals} removal(s) ` +
2803
+ `from the generation log — no walk needed`);
2804
+ }
2805
+ return true;
2806
+ }
2673
2807
  async flushOnce() {
2674
2808
  // Always save field registry + msync the entity ID mapper — even with
2675
2809
  // no dirty fields. The field registry is the critical file init() needs
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @module utils/generationWatermark
3
+ * @description The committed-generation watermark (M3 PD-2
4
+ * `sourceGeneration`) — what a projection stamp records as the source
5
+ * truth it reflects. Read CAPABILITY-FIRST: brainy 8.5.0 exposes
6
+ * `storage.committedGeneration()` precisely so providers never parse the
7
+ * store's private manifest format; the direct `_system/manifest.json`
8
+ * read remains as the 8.4.0-host fallback for one release line. Advisory
9
+ * everywhere: `undefined` (no capability, no manifest, faulted read)
10
+ * means the stamp carries no sourceGeneration and verifiers skip the
11
+ * comparison — never a blocked write, never a guessed value.
12
+ */
13
+ /** Best-effort committed watermark; `undefined` when unknowable. */
14
+ export declare function readCommittedGeneration(storage: unknown): Promise<number | undefined>;
15
+ //# sourceMappingURL=generationWatermark.d.ts.map
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @module utils/generationWatermark
3
+ * @description The committed-generation watermark (M3 PD-2
4
+ * `sourceGeneration`) — what a projection stamp records as the source
5
+ * truth it reflects. Read CAPABILITY-FIRST: brainy 8.5.0 exposes
6
+ * `storage.committedGeneration()` precisely so providers never parse the
7
+ * store's private manifest format; the direct `_system/manifest.json`
8
+ * read remains as the 8.4.0-host fallback for one release line. Advisory
9
+ * everywhere: `undefined` (no capability, no manifest, faulted read)
10
+ * means the stamp carries no sourceGeneration and verifiers skip the
11
+ * comparison — never a blocked write, never a guessed value.
12
+ */
13
+ /** Best-effort committed watermark; `undefined` when unknowable. */
14
+ export async function readCommittedGeneration(storage) {
15
+ try {
16
+ const s = storage;
17
+ if (typeof s?.committedGeneration === 'function') {
18
+ const g = s.committedGeneration();
19
+ if (typeof g === 'number' && Number.isFinite(g) && g >= 0)
20
+ return g;
21
+ // Capability present but unwired (bare adapter) — fall through to the
22
+ // manifest read: a stamped brain opened outside a host brain should
23
+ // still stamp truthfully when the manifest exists.
24
+ }
25
+ if (typeof s?.readRawObject !== 'function')
26
+ return undefined;
27
+ const manifest = (await s.readRawObject('_system/manifest.json'));
28
+ const g = manifest?.generation;
29
+ return typeof g === 'number' && Number.isFinite(g) && g >= 0 ? g : undefined;
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ //# sourceMappingURL=generationWatermark.js.map
package/dist/version.d.ts CHANGED
@@ -11,5 +11,5 @@
11
11
  * `check:brainy`-style drift between the two fails the release.
12
12
  */
13
13
  /** The @soulcraft/cor version this build was cut from. */
14
- export declare const COR_VERSION = "3.0.19";
14
+ export declare const COR_VERSION = "3.0.20";
15
15
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -11,5 +11,5 @@
11
11
  * `check:brainy`-style drift between the two fails the release.
12
12
  */
13
13
  /** The @soulcraft/cor version this build was cut from. */
14
- export const COR_VERSION = '3.0.19';
14
+ export const COR_VERSION = '3.0.20';
15
15
  //# sourceMappingURL=version.js.map
Binary file
package/native/index.d.ts CHANGED
@@ -2629,6 +2629,13 @@ export interface DiskAnnRebuildFromExistingParams {
2629
2629
  * the slot-only benchmark paths that don't carry entity ids.
2630
2630
  */
2631
2631
  slotIds?: Buffer
2632
+ /**
2633
+ * M3 PD-2: the source-of-truth log generation this publish reflects
2634
+ * (the committed watermark the JS wrapper read at rebuild time).
2635
+ * Omitted on hosts without a generation manifest — the stamp then
2636
+ * carries no `sourceGeneration` and verifiers skip the comparison.
2637
+ */
2638
+ sourceGeneration?: number
2632
2639
  }
2633
2640
 
2634
2641
  /** Vamana graph build knobs exposed to JS. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/cor",
3
- "version": "3.0.19",
3
+ "version": "3.0.20",
4
4
  "description": "Native Rust acceleration for Brainy — SIMD distance, vector quantization, zero-copy mmap, native embeddings. Free tier for storage, Pro license for compute acceleration.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -82,7 +82,7 @@
82
82
  },
83
83
  "devDependencies": {
84
84
  "@napi-rs/cli": "^3.0.0",
85
- "@soulcraft/brainy": "8.3.2",
85
+ "@soulcraft/brainy": "8.5.0",
86
86
  "@types/node": "^22.0.0",
87
87
  "pg": "^8.21.0",
88
88
  "tsx": "^4.21.0",