@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.
- package/dist/graph/NativeGraphAdjacencyIndex.d.ts +2 -0
- package/dist/graph/NativeGraphAdjacencyIndex.js +43 -14
- package/dist/hnsw/NativeDiskAnnWrapper.d.ts +9 -0
- package/dist/hnsw/NativeDiskAnnWrapper.js +22 -1
- package/dist/utils/NativeMetadataIndex.d.ts +18 -0
- package/dist/utils/NativeMetadataIndex.js +135 -1
- package/dist/utils/generationWatermark.d.ts +15 -0
- package/dist/utils/generationWatermark.js +35 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/native/brainy-native.node +0 -0
- package/native/index.d.ts +7 -0
- package/package.json +2 -2
|
@@ -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
|
|
395
|
+
const invariants = {
|
|
396
|
+
blobBytes: this.lastMembershipBlobBytes,
|
|
397
|
+
};
|
|
373
398
|
for (const treeName of this.native.treeNames()) {
|
|
374
|
-
|
|
399
|
+
invariants[`tree:${treeName}`] = this.treeFingerprint(treeName);
|
|
375
400
|
}
|
|
376
|
-
const body = JSON.stringify(
|
|
401
|
+
const body = JSON.stringify(invariants);
|
|
377
402
|
if (body === this.lastMembershipStampBody)
|
|
378
403
|
return;
|
|
379
404
|
const stamp = {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
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.
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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(
|
|
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(() =>
|
|
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.
|
|
14
|
+
export declare const COR_VERSION = "3.0.20";
|
|
15
15
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
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.
|
|
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.
|
|
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",
|