Commit graph

907 commits

Author SHA1 Message Date
48bea9e20f chore(release): 8.0.1 2026-07-02 15:16:24 -07:00
e44620e91d docs: flagship README for the 8.0 GA; GA version is 8.0.1
Rewrites the README around the feature-showcase structure: write->index->query
table, runnable quick start (subtype-correct on the 8.0 strict default, real
VerbTypes), feature tour with per-pillar code, the scale-up path to
@soulcraft/cor, and measured-only performance claims with benchmark citations.

GA retargets to 8.0.1: npm permanently retired the 8.0.0 version number after
a January development-cycle publish/unpublish, so the first stable 8.x release
is 8.0.1. RELEASES.md documents this; the phantom 8.0.0 CHANGELOG entry is
removed (the release run regenerates it as 8.0.1).
2026-07-02 15:11:41 -07:00
bf4a333f9b docs: rename the native provider to @soulcraft/cor across public docs and JSDoc
The 3.0 native engine ships as @soulcraft/cor (brainy 8.x <-> cor 3.x are a
version-matched pair); the old @soulcraft/cortex package stays on the 2.x/7.x
line. Public docs and .d.ts-visible comments now name the correct package.
Historical 2.x contract references (e.g. the 2.3.1 read-side fallback) keep
the old name deliberately.
2026-07-02 15:11:41 -07:00
a3c2717ddb chore(release): 8.0.0 2026-07-02 14:47:27 -07:00
4584d0b8b8 docs: RELEASES.md 8.0.0 GA entry (RC notes become history) 2026-07-02 14:41:24 -07:00
55d57f89f7 feat: promote the 8.0 u64-id line to main for the 8.0.0 GA
Brings the u64-id core to main as a merge of feat/8.0-u64-ids. main's tree now
matches the gated 8.0 line exactly; the 7.31.8-7.33.5 hotfix history is retained
as the merge's first parent (every one of those fixes is already in the 8.0 line).
2026-07-02 14:36:49 -07:00
a30ed72dc3 fix(8.0): byte-copy _id_mapper/* in the pre-upgrade backup (cor's write-new nuance)
cor confirmed the migration write-new / tmp+rename invariant the hard-link backup
relies on — with one exception: the native shared mmap `_id_mapper/*` is mutated
IN PLACE (msync + a rebuild truncate+re-inject), the same category as the tx-log.
A hard-linked snapshot of it would be corrupted by a live in-place mutation
reaching through the shared inode, so the backup would not be a faithful
pre-upgrade copy.

Add a SNAPSHOT_BYTE_COPY_DIRS set (`_id_mapper`) so snapshotToDirectory byte-copies
every file under it (the directory analogue of SNAPSHOT_BYTE_COPY_PATHS); the id
map is bounded, so the copy cost is small. Everything else (SSTables / .dkann /
manifests / objects, all tmp+rename) still hard-links. Test: `_id_mapper/*` gets
an independent inode + faithful content; a regular file stays hard-linked.
2026-07-02 13:35:43 -07:00
29b9d5f805 chore(release): 7.33.5 2026-07-02 10:29:01 -07:00
9dc4c5e62b fix: metadata cold-read guard — no more silent [] on cold find({where}) (7.33.5)
Companion to 7.33.4's graph cold-read guard, now for the metadata field index. A
downstream deployment reported find({where}) returning a silent [] on a
freshly-opened brain (a native metadata provider that reports data but hasn't
loaded its field postings), blanking filtered pages after every restart.

verifyMetadataLive() — one-shot per brain on the first filtered find(): probe a
known persisted field value; if served, live (one O(1) probe on a warm brain). If
not, rebuild from canonical + re-probe; if still unserved, throw the new exported
MetadataIndexNotReadyError rather than let a silent [] pass. Inconclusive cases
(empty store, no plain field, shared-store foreign entity) resolve to live, no
false rebuild. Also exports BrainyError + GraphIndexNotReadyError (were internal).

The JS index cold-loads correctly; this guards the native path (durable cure is
cortex-side, same shape as the graph 2.7.8 cure). 4 unit tests. tsc 0, guard 4/4,
full unit 1538 pass (+1 pre-existing flaky VFS perf test, green in isolation).
2026-07-02 10:28:04 -07:00
79e8709c35 fix(8.0): metadata cold-read guard — no more silent [] on cold find({where})
A downstream deployment reported find({where}) returning a silent [] on a
freshly-opened brain (a native metadata provider that reports data but hasn't
loaded its field postings), blanking filtered pages after every restart. Brainy
had a cold-read guard for GRAPH reads (verifyGraphAdjacencyLive → self-heal or a
loud GraphIndexNotReadyError) but no equivalent for metadata `where`.

Add verifyMetadataLive() — the field-index counterpart, one-shot per brain on the
first filtered find(): probe a known persisted entity's plain field value; if the
index serves it, live (the only cost on a warm brain — one O(1) probe). If not,
the postings didn't load: rebuild from canonical + re-probe; if still unserved,
throw the new exported MetadataIndexNotReadyError rather than let a silent [] pass.
Inconclusive cases (empty store, no plain field, shared-store foreign entity,
migrating provider) resolve to live — never a false rebuild.

The 8.0 open-core JS index already cold-loads correctly (verified: cold where 3/3,
cold related 2/2) — this guards the NATIVE path, where the durable cold-load cure
is cortex-side (same shape as the graph 2.7.8 cure). 4 unit tests (warm no-rebuild,
self-heal, loud-fail, no-filter-no-probe). Gates: typecheck 0, build 0, test:unit
1757/1757.
2026-07-02 10:13:07 -07:00
ab53fa0893 docs(8.0): add module JSDoc to typeValidation.ts (the one file missing a module block) 2026-07-01 15:37:25 -07:00
1aad1f67de feat(8.0): auto pre-upgrade backup — hard-link snapshot before the 7.x→8.0 migration
David's ask: back up the brain before the one-time upgrade, drop it after. On
open, when the on-disk format is stale (a 7.x→8.0 rebuild will run) and the store
holds data, brainy hard-link-snapshots the brain dir to a sibling
`<brainDir>.migration-backup` BEFORE any provider rebuilds — near-zero cost/space
(shared inodes; the store is immutable-by-rename), instant even at scale. Removed
automatically once the upgrade verifies + stamps the marker; retained on failure
for rollback. Default-on; opt out with `migrationBackup: false`.

- Reuses the existing snapshotToDirectory() hard-link farm via new optional
  adapter methods createMigrationBackup()/removeMigrationBackup() (filesystem
  only — feature-detected; memory + non-fs are a graceful no-op).
- Taken before the native provider inits, so it captures true pre-migration
  state; a prior failed upgrade's backup is reused, not overwritten.
- Best-effort: a backup failure never blocks the upgrade (the migration is itself
  safe — reads canonical, reconstructable, self-heals). Catastrophe-insurance
  against a migration bug, not a data-loss guard or an off-device backup.

4 lifecycle tests (adapter hard-link / reuse / remove-without-touching-live /
empty→null; stale-epoch reopen create+remove; opt-out; memory no-op). Gates:
typecheck 0, build 0, test:unit 1753/1753, layout-migration suite 5/5.
2026-07-01 15:04:01 -07:00
ed178e2ce9 fix(ci): commit the prebuilt wasm pkg + build before test:bun (green CI on fresh clone)
The Phase-0 CI was red on all three runtimes: `src/embeddings/wasm/pkg/` (the
wasm-pack output the dynamic `import('./pkg/candle_embeddings.js')` resolves) was
gitignored + untracked, so tsc failed to resolve it on a fresh clone (TS2307), and
the Bun job ran test:bun without building dist first.

Fixes:
- Track the 3.1 MB prebuilt pkg (candle_embeddings.js/.d.ts + _bg.wasm/.d.ts). It
  ships in the npm tarball anyway; versioning it lets consumers + CI build without
  a Rust/wasm-pack toolchain and makes the shipped artifact reproducible (closes
  the "publish ships whatever the maintainer last built" supply-chain gap). The
  top-level .gitignore intended this (`!*.wasm` etc.) but excluded the whole dir,
  so the re-includes were dead, and a nested wasm-pack `.gitignore *` also masked
  it — force-added past both.
- CI Bun job: `npm run build` before `test:bun` (it imports the built dist/).

Verified against a tracked-files-only tree (fresh-clone sim): typecheck 0, build 0,
test:bun 8/8.
2026-07-01 14:40:11 -07:00
3f4947fc92 chore(release): 8.0.0-rc.9 2026-07-01 12:46:49 -07:00
3a33987136 docs(8.0): RELEASES.md — rc.9 (migration LOCK + 6x cosine + ES2023/Node22 floor) 2026-07-01 11:52:52 -07:00
cf74c25d90 chore(8.0): ES2023 target + drop DOM lib + downlevelIteration (config truth-up)
Make the compiler config honestly describe a Node 22 / Bun 1.1+ / Deno library:
target ES2020 -> ES2023, lib [DOM, ESNext, DOM.Asynciterable] -> [ES2023], drop
the now-inert downlevelIteration. 8.0 dropped the browser path, so the DOM lib
was misleading (it implied web APIs the package no longer supports). No
ES2021-2023 syntax is used anywhere, so .js emit is effectively unchanged — this
is hygiene, not a perf/behavior change.

The one real consequence of dropping DOM: the Cloudflare-Workers edge probe read
globalThis.caches from the DOM lib — declared `caches?: unknown` locally on the
runtime-globals intersection alongside Deno/Bun/HTMLRewriter.

.d.ts is shape-stable (TS-5.x-parseable preserved — no cor coordination). Gates:
typecheck 0, cli-tsconfig 0, typecheck-tsconfig (the .d.ts contract surface) 0,
build 0, test:unit 1753/1753, test:bun 8/8.
2026-07-01 10:58:04 -07:00
b5bc73fb17 perf(8.0): allocation-free distance loops (6x cosine) — evidence-revised Fork X
Rewrite the four open-core distance functions (cosine / euclidean / manhattan /
dot-product) from object-accumulating `reduce` to single-pass allocation-free
indexed loops. cosine's per-element `{dotProduct,normA,normB}` object was the
hot-path GC lever.

MEASURED (tests/benchmarks/distance-microbench.mjs, dim=384, N=20000, median of
41): cosine 44.3ms -> 7.4ms (~6x), euclidean 9.2ms -> 6.6ms (~1.4x); the built
cosineDistance drops ~44ms -> ~9ms. Numerically identical (same ops, same order)
so recall is unchanged; full suite green (1753/1753).

Also drop the unfounded perf JSDoc ("faster than GPU", "Node.js 23.11+") and the
`new Function(distanceFn.toString())` eval in calculateDistancesBatch — with the
functions now tight loops, the batch is a thin JIT-inlined map (no worker, no
stringify/reconstruct).

Evidence-revised scope: the Float32Array resident-storage half of the original
Fork X is DROPPED. The same microbench shows Float32Array is ~1.7x SLOWER for
this compute (V8 widens f32 -> f64 on every element read), so it would regress
the hot path for a RAM win the open-core JS path does not need — billion-scale
vector RAM is the native provider's SIMD/mmap/quantized domain. The resident
representation stays number[]; no type-chain or cache changes.
2026-07-01 10:55:04 -07:00
67bbf69a5c feat(8.0): #18 coordinated migration LOCK — block-and-queue the 7.x→8.0 auto-upgrade
Replaces rc.8's no-freeze deference with an automatic, observable, coordinated
migration LOCK (David's reversal: "unknown/halfway states are more dangerous
than blocking"). While a native provider runs its one-time 7.x→8.0
rebuild-from-canonical (`isMigrating()===true`), brainy blocks/queues data-plane
reads AND writes so no operation ever touches a half-built index — closing the
three seam-map gaps (lost mid-rebuild writes, degraded reads, read-time rebuild).

Mechanism (one choke point):
- awaitMigrationLock() at ensureInitialized() covers all ~40 data-plane methods;
  a non-migrating brain pays one boolean check. Polls isMigrating() at 250ms;
  after migrationWaitTimeoutMs (default 30s) throws a retryable, exported
  MigrationInProgressError. The timeout bounds the CALLER'S WAIT, not the
  migration — the rebuild is unbounded and never interrupted.
- init() awaits the lock before VFS bootstrap + before serving ("not ready until
  upgraded"); a rebuild past the budget surfaces MigrationInProgressError at init
  (raise the budget, or run the offline migrator).

Observability (readiness-probe correct — never gated):
- getIndexStatus() gains `migrating` + `migration` (MigrationProgress); a probe
  maps migrating→HTTP 503+Retry-After, not 500. health()/checkHealth() lock-exempt
  (health() reports a `warn` migration check). stampBrainFormat()/close() are
  ungated so cor can clear the lock — no deadlock.
- Optional provider migrationStatus() is relayed verbatim for a live %.

verifyGraphAdjacencyLive() honors the lock (no self-rebuild mid-migration); the
stamp is still withheld while any provider migrates (cor stamps after verify).

Adversarially verified: fixed a critical init/VFS-bootstrap deadlock, a mixed-
provider busy-spin (dropped the event-driven signal → pure poll), a not-yet-
initialized getIndexStatus crash, and once-per-window log/clock resets. 10 lock
tests (incl. the init-during-migration regression). Gates: typecheck 0, build 0,
test:unit 1753/1753.
2026-07-01 10:26:27 -07:00
ca9129a924 chore(8.0): modernize toolchain + position Bun as a runtime
Consumer-invisible modernization pass — no public API or runtime-behavior
change; dist for the override-only files is byte-identical.

Toolchain:
- CI: GitHub Actions matrix — Node 22/24 (test:unit) + Bun latest (test:bun).
- engines: node ">=22" (was "22.x"), bun ">=1.1.0".
- tsconfig: isolatedModules + noImplicitOverride; add the 33 `override`
  modifiers the flag requires across storage/integrations/vfs/transaction.
- deps: @types/node ^22; add prettier; drop dead standard-version,
  @rollup/plugin-* and the redundant embedded eslintConfig (flat
  eslint.config.js is the active config — verified identical lint output).

paramValidation: replace the top-level `await import('node:os'/'node:fs')`
with static ESM imports. The top-level-await form poisoned the module graph;
static imports also drop the browser/edge fallback branches no supported
runtime reaches (8.0 is Node/Bun/Deno-only).

Bun positioning: recommend Bun as a runtime (`bun add` / `bun run`), which is
green (test:bun 8/8). Drop single-binary `bun build --compile` as a target —
native addons cannot embed into it, and Bun 1.3.10 has a `--compile` codegen
regression around top-level await. Rename the Bun test to bun-runtime-test.ts
and correct docs that overclaimed single-binary support.

Gates: typecheck 0, build 0, test:unit 1743/1743, test:bun 8/8.
2026-07-01 09:16:46 -07:00
ae55d54cb5 chore(release): 8.0.0-rc.8 2026-06-30 13:41:17 -07:00
5af48a925c docs(8.0): RELEASES.md — rc.8 (no-freeze online whole-brain auto-upgrade) 2026-06-30 13:38:34 -07:00
b6b919890c feat(8.0): no-freeze auto-upgrade hooks — isMigrating() deference + stampBrainFormat() + brain-format export
Three hooks so a native provider can run a non-blocking, online, background
build-new→verify→swap index migration on a large-brain upgrade while brainy stays
out of the way — no minutes-long blocking rebuild-on-open/first-query.

- isMigrating?(): boolean — OPTIONAL on MetadataIndexProvider / GraphIndexProvider /
  VectorIndexProvider (mirrors isReady?()/init?(), feature-detected). While a
  provider reports migrating, brainy SKIPS its rebuild for that index — per-index,
  so a non-migrating sibling still rebuilds; skipped even under epoch-drift or
  size()===0 — in both rebuildIndexesIfNeeded and the lazy first-query force path.
  The provider serves correct reads from canonical until it verifies-and-swaps.
- brain.stampBrainFormat() — public; the provider calls it once its background swap
  verifies, so brainy authors dataFormat (the provider never does). brainy withholds
  its own marker stamp while any provider is migrating, so the shared epoch is never
  advanced ahead of a deferred index.
- ./brain-format export — the marker module's EXPECTED_INDEX_EPOCH / CURRENT_DATA_FORMAT
  are importable so a native provider shares the constant (no duplicate = no
  lockstep-drift). 8-case test; unit 1743 green.
2026-06-30 13:37:23 -07:00
bd6faf7499 chore(release): 8.0.0-rc.7 2026-06-30 10:34:21 -07:00
1ddc786c22 docs(8.0): RELEASES.md — rc.7 (cold-graph self-heal + billion-scale RAM + version handshake) 2026-06-30 10:31:36 -07:00
a859d6ecf8 perf(8.0): bound per-id generation history chains (O(W+L) resident, was O(N))
The MVCC time-travel layer kept nounChains/verbChains as unbounded
Map<id, number[]> — one resident chain per id ever touched across retained
history (O(N) RAM, defeating billion-scale time travel). Replace with a hot-tail
window + bounded cold LRU + a mutex-free bulk resolver, keeping resolveAt exactly
correct.

The most-recent W generations stay resident as full chains (the common recent-pin
read is O(log), zero scan); deeper pins reconstruct one id's chain on demand into
an L-bounded LRU. Reconstruction is LOCK-LIGHT — it holds no commit mutex, so a
historical read never stalls writers; correctness holds because a live pin's
answer is always > minPinnedGeneration, which compaction can never reclaim, and a
concurrently-reclaimed gen below that is skipped. materializeAtGeneration routes
through a new mutex-free resolveManyAt (one forward pass, O(R + |ids|)) — without
it a deep-pin materialize both regresses to O(N*R) and deadlocks on snapshotWith's
mutex. The cold cache is invalidated on re-touch to stay coherent. Resident RAM is
O(W*d + L), independent of N. 11-case test (oracle-vs-bruteforce, held-Db across
eviction + concurrent compaction, no-deadlock, write-path I/O-free); unit 1735 +
integration 613 (db-mvcc/db-temporal/db-asof green).
2026-06-30 10:30:17 -07:00
8f4787bb10 feat(8.0): eager graphIndex.init() before the isReady() rebuild gate
A native graph provider cold-loads its source->target adjacency lazily, so
isReady() would report false at the rebuild gate on a cold open and force a
spurious rebuild (failing the §7.1 rebuild()==0 acceptance). Add an OPTIONAL
GraphIndexProvider.init() (mirrors MetadataIndexProvider.init) and call it during
init, AFTER metadataIndex.init() (id-mapper hydrated first, so a native int
adjacency resolves endpoints through it) and BEFORE rebuildIndexesIfNeeded. The JS
graph index omits init() and self-loads on demand, so the JS path is unchanged.
2026-06-30 09:55:19 -07:00
fc7f110479 feat(8.0): version-handshake marker (formatInfo + indexEpoch) for whole-brain auto-upgrade
Add _system/brain-format.json { dataFormat, indexEpoch } + a sync brain.formatInfo()
accessor + a compiled EXPECTED_INDEX_EPOCH, the surface a native provider reads at
init() to drive whole-brain auto-upgrade, and brainy's own derived-index
rebuild-on-format-drift trigger (closing the gap where JS indexes rebuilt only on
size()===0, never on a format-version change).

indexEpoch is shared and lockstep-bumped with the native provider on any coordinated
release whose on-disk derived-index format changes; dataFormat is brainy-owned. On open,
the marker is read in the store-open phase BEFORE provider construction (so formatInfo()
is synchronously available at the provider's init); a drifted or absent epoch rebuilds
the derived indexes from canonical records, and the marker is stamped only AFTER the
rebuild verifies (build-new -> verify -> stamp; a crash before the stamp re-triggers the
idempotent rebuild). brainFormat.ts is the single source of the shared constants.
6-case test; 1724 unit green.
2026-06-30 09:53:10 -07:00
229b0679fc fix(8.0): never serve a silent [] from find({connected}) on a cold-loaded graph
8.0 shipped with no cold-graph guard (the 7.33.4 fix was never ported), so on a
cold open of a large brain where the native adjacency reports membership but its
source->target edges did not load, find({connected})/neighbors()/related() could
silently return [] for persisted edges.

Add the converged isReady() contract: GraphIndexProvider.isReady?(): boolean is
the honest cold-load readiness signal (true ONLY when edges are loaded), so brainy
gates on it instead of the lying membership-size() proxy. verifyGraphAdjacencyLive()
checks it — Strategy 1: false -> hydrate the id-mapper, rebuild from storage, re-check,
throw GraphIndexNotReadyError if still not ready; providers without isReady() fall
back to a global known-edge-sample probe (Strategy 2, not the queried anchor, so a
genuinely edgeless node still returns []). rebuildIndexesIfNeeded gates the graph
rebuild on it too, with the id-mapper hydrated before the adjacency rebuild on the
lazy cold-open path (the CTX-BR-RESTORE-REBUILD ordering, shared with restore()).
executeGraphSearch re-verifies before trusting an empty connected result and
re-collects on a heal. 6-case integration test + 1718 unit green.
2026-06-30 09:30:11 -07:00
2be3d0f88b chore(release): 7.33.4 2026-06-29 16:40:37 -07:00
fd699d0e07 fix: never serve a silent [] from find({connected}) on a cold-loaded graph
On a cold process start of a large brain (above the eager index-rebuild
threshold), a native graph adjacency can report size()>0 — its membership set
reloaded — while the source->target edges did NOT load, so find({connected}),
neighbors() and related() returned [] for edges that are persisted on disk. A
database returning empty for data that exists, based purely on warm/cold state,
is a correctness bug; it hit a production deployment after every deploy.

Refactor the cold-load guard into verifyGraphAdjacencyLive(), which gates its
heal/throw decision on a GLOBAL known-edge sample (a persisted verb's source,
which by definition has an outgoing edge) rather than the queried anchor: a
stale adjacency is rebuilt from storage, an unrecoverable one throws
GraphIndexNotReadyError instead of serving [], and a genuinely edgeless anchor
still returns [] with no spurious rebuild. executeGraphSearch re-verifies before
trusting an empty connected result and re-collects after a heal. Adds a 5-case
integration test (self-heal / loud-throw / edgeless-no-false-positive / healthy
/ re-collect) plus updated unit coverage.
2026-06-29 16:40:02 -07:00
93f61dbc79 perf(8.0): represent the committed-generation ledger as an interval set
committedGens was a number[] with one element per committed generation. Every
single-op write reserves a distinct generation, so an insert-built 1B corpus
held a ~1B-element array resident regardless of provider — the MVCC sibling of
the storage-cache gate.

Replace it with a sorted disjoint interval set (committedRanges). With no
compaction gaps the whole ledger collapses to a single [start,end] pair, so
resident size is O(number-of-gaps) not O(writes). reservedGens becomes a
generator; point-resolution binary-searches the ranges; compaction trims the
oldest prefix with a defensive prefix-invariant guard. Behaviour is identical
across asOf/diff/since/history/transactionLog — unit 1718 + integration 607 +
the focused MVCC suite green.
2026-06-29 16:05:03 -07:00
b6beb7f96a perf(8.0): drop O(N)-resident id-keyed storage caches; source counts from the record
The storage layer held five id-keyed Maps (nounTypeByIdCache + the subtype and
visibility caches) resident for the writer's lifetime — one entry per live
entity, present even when a native provider is registered (it swaps the indexes,
not the storage adapter). At billion scale that is hundreds of GB of writer RAM,
breaking the "nothing O(N)-resident" invariant.

Eliminate all five. Per-type/subtype counts are attributed at the metadata-save
site where the type is already in hand, and the delete path re-derives the prior
values by reading the record before removing it. O(1) writer RAM on both the
native and standalone paths; the latent rebuildTypeCounts staleness edge is gone
by construction. Unit 1718 + integration 607 green.
2026-06-29 16:05:03 -07:00
855298ab79 chore(release): 8.0.0-rc.6 2026-06-29 12:27:14 -07:00
6daa70ed6a docs(8.0): RELEASES.md — rc.6 (perf + native-provider contract + test hygiene)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:11:56 -07:00
8b191224ce feat(8.0): wire the two cor-confirmed metadata-provider contract additions
Both shapes confirmed with cor for the lockstep (cor builds the native side
against these; the JS index is a no-op / unchanged):

- probeConsistency?(): Promise<boolean> — OPTIONAL O(1) cold-open consistency
  sampler on MetadataIndexProvider. brainy calls it ONCE per brain on the first
  read; on `false` it self-heals via detectAndRepairCorruption() — the metadata
  counterpart of the 7.33.2 graph cold-load guard, completing the phantom triad's
  detect-and-repair-on-open. Best-effort: a probe failure never breaks a read
  (the one-shot guard resets so a transient failure retries). The JS index omits
  the method, so it is simply never probed.

- getIdsForFilter(filter, opts?: { limit?; offset? }) — brainy passes a page
  bound on the UNSORTED find({ type, where, limit }) path so a native provider can
  early-stop and return only the [0, limit) prefix, killing the O(N) FFI marshal
  at billion scale (pairs with cor #75). brainy passes offset:0 and ALWAYS
  re-windows the result itself (visibility filter + slice); the JS index ignores
  opts and returns all matches (behaviour unchanged).

New unit tests cover brainy's call behaviour for both (probe→repair on false,
healthy→no-repair, failure-is-best-effort, once-per-brain; opts passed with
offset 0 on the unsorted path). Full gate green: unit 1718, integration 607.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 12:11:00 -07:00
3f9f140c8c test(8.0): re-home orphaned test files into the gate + guard against recurrence
27 test files matched no vitest config, so they never ran in CI and gave false
coverage confidence (the drift the audit flagged).

- Re-homed 10 functional suites into the gate (renamed to *.unit.test.ts /
  *.integration.test.ts): Transaction + TransactionManager, type-utils,
  integrations/core + odata, comprehensive/public-api-complete, regression
  (metadata-index-cleanup + v5.7.0-deadlock), vfs/tree-operations +
  vfs-bulkwrite-race. ~192 previously-dark tests now run and pass.
- Deleted 8 bit-rotted/redundant files that fail against 8.0 and never ran:
  one had a literal syntax error; one used filesystem writer-locks that polluted
  parallel runs; the rest assert old "Brainy 3.0/v3.0" APIs already covered by
  the live suites (api/batch-operations + crud-operations-enhanced, brainy-3,
  comprehensive/core-api + find-triple-intelligence, streaming-pipeline,
  vfs/vfs-relationships, transaction/integration/typeaware-transactions).
- Added a coverage guard (tests/unit/test-suite-coverage-guard.test.ts) that
  FAILS CI if any *.test.ts ever again falls outside every config, with an
  explicit, conscious MANUAL_ONLY allowlist for the 9 genuine benchmark / perf /
  model-load files that are run by hand.

Gate green: unit 1712, integration 607.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:47:18 -07:00
5f974abc8a perf(8.0): negation/absence where-operators via roaring-bitmap difference
The JS metadata index served `ne`, `exists:false`, and `missing:true` by
materializing the ENTIRE id universe as an array of UUID strings, building a Set
of the excluded ids, and filtering — O(N) heap and two full passes on the
open-core path, on the most common shape (`deleted !== true`).

Compute the complement as a roaring-bitmap difference over the int-id universe
instead (`complementIds(excludeInts)` = universe \ exclude), converting only the
result back to UUIDs. The exclude set is small (the matching value), so this
avoids the full-corpus string materialization entirely. Behaviour is unchanged,
including the load-bearing soft-delete semantic that `field !== value` includes
entities with no such field. New focused tests pin `ne`/`exists:false`/
`missing:true`/`exists:true` result sets and that the complement reflects deletes.

Full gate green: unit 1520, integration 607.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:19:18 -07:00
72df5572b7 perf(8.0): HNSW removeItem is O(in-degree) via a reverse-adjacency index
removeItem() scanned the ENTIRE corpus on every delete to find nodes that
referenced the removed id (and to repair edges left asymmetric by pruning), so a
delete was O(N) and a bulk delete O(N²) — on the open-core JS vector path that
serves when no native provider is registered.

Maintain a reverse-adjacency index (`target → level → set of nodes that link to
target`), so removeItem touches only the removed node's actual in-neighbors:
O(in-degree) per delete, O(N·degree) for a bulk delete. The index is lazily
built, maintained incrementally at every forward-edge mutation (add-link and
prune), and invalidated (rebuilt on next use) by the bulk paths (cold-load
restore + clear). New tests assert the three invariants that matter for a
reverse index: no dangling references to deleted nodes, the incrementally-
maintained index exactly equals a fresh rebuild from the live adjacency, and
search still returns the survivors' true nearest neighbours (exact vs brute force).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:06:22 -07:00
9bfba637da chore(release): 8.0.0-rc.5 2026-06-29 10:35:51 -07:00
6c9a43816c docs(8.0): RELEASES.md — rc.5 hardening + the breaking operator removal
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:30:44 -07:00
ddcc0c723d refactor(8.0): remove the 4 deprecated query-operator aliases (clean break)
8.0 keeps the canonical operators (eq/ne/gt/gte/lt/lte) and their clean
long-form aliases (equals/notEquals/greaterThan/greaterThanOrEqual/lessThan/
lessThanOrEqual), and drops the four redundant deprecated spellings:
  is → eq,  isNot → ne,  greaterEqual → gte,  lessEqual → lte

Removed from every evaluator (metadataIndex criteria + range switches,
metadataFilter, the db whereMatcher egress path) and from the
BrainyFieldOperators type, the unsupported-operator error message, the docs
(QUERY_OPERATORS / api README / VFS projection + semantic guides), and the
whereMatcher alias tests. Also migrated Brainy's own internal use — the VFS
TemporalProjection queried with greaterEqual/lessEqual, which would have
silently broken — to gte/lte.

Full gate green: build, unit 1512, integration 607.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:29:20 -07:00
b9369f260b refactor(8.0): remove dead/deprecated code (legacy sweep)
- Deleted getIdsForFilterOld() — a 74-line "DEPRECATED old implementation"
  private method in the metadata index with no callers.
- Deleted getEdgesBySource/ByTarget/ByType from FileSystemStorage — three
  deprecated methods that only `console.warn` + `return []` (stub returns the
  repo forbids); not called anywhere and not required by any interface.
- Removed dead commented-out code fragments (an aspirational find() block in the
  NLP processor; a stale duplicate `const groups` line in the VFS generator).

Build + unit gate green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:09:39 -07:00
a52dba2168 refactor(8.0): API-surface + quality polish from the readiness audit
- find() search-mode: collapsed the two overlapping options to one canonical
  `searchMode: SearchMode` (the redundant `mode` alias was silently ignored on
  the primary find() path while honored on the historical path — a footgun) and
  removed the unwired `explain?` FindParams field (never read by find()).
- Documentation accuracy on the public type surface: entity ids documented as
  UUID v7 (auto) / v5 (natural key), not v4; dropped the stale "Brainy 3.0"
  banners and "backward compatibility" hedging on the fresh-8.0 Result type;
  documented the via/type alias; removed the dead GraphConstraints.bidirectional;
  fixed the no-op `EmbeddedGraphVerb = Omit<GraphVerb,'source'>` to omit the real
  sourceId; corrected the GraphIndexProvider.isInitialized JSDoc; documented
  removeMany's per-chunk generation granularity; JSDoc'd the public VFS surface.
- Honest perf comments in source: removed unmeasured billion-scale figures from
  the LSMTree / graphAdjacencyIndex headers (the JS fallback is in-memory after
  open; native is the scale path).
- Robustness: getVerbMetadata propagates read errors symmetrically with
  getNounMetadata; the find() egress integrity-guard keeps a row when the JS
  matcher doesn't implement an operator the provider already matched on; hardened
  the boundary-no-native CI guard to catch side-effect imports + re-exports.
- Tests: de-theatricalized a relateMany test to assert the real contract; fixed
  a stale Model-B header.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:04:19 -07:00
47e8031124 fix(8.0): close GA-blocking correctness gaps from the readiness audit
- Version-coupling cold-init: getBrainyVersion() returned a stale build-time
  default ('3.14.0') on the FIRST synchronous call — the one loadPlugins() makes
  to build context.version — because the package.json read was async. A native
  provider declaring a realistic `>=8.0.0` range was therefore rejected on cold
  init. version.ts now reads package.json synchronously (8.0 targets Node-like
  runtimes only); added a cold-init coupling regression with a `^8.0.0` plugin.
- No-silent-failures on the highest-fan-in read path: getNouns()/getVerbs()
  converted a storage read failure into a success-shaped empty page, which the
  cold-start rebuild then read as "store empty" and skipped the rebuild — booting
  a permanently-empty index with no signal (the same silent-failure class as the
  phantom bug). Both now re-throw a named BrainyError; the rebuild path re-throws
  read failures (fail loud) and records a queryable degraded state, surfaced via
  checkHealth(), for non-fatal rebuild hiccups.
- LSM SSTable leak: graph compaction wrote a merged SSTable but only dropped the
  old ones from the manifest, orphaning their payloads forever ("In production
  we'd add a cleanup mechanism"). Added StorageAdapter.deleteMetadata() and now
  reclaim each compacted-away SSTable.
- Documented the two headline methods add() and find() (the only undocumented
  public methods on the class).

Full gate green: build, unit 1512, integration 607.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:03:38 -07:00
40d2cd5419 docs(8.0): correct public docs to the real 8.0 API + honest perf claims
The GA-readiness audit found the public docs had drifted from the shipped
surface and presented uncited performance numbers as measured fact.

- quick-start: `FindResult`→`Result`, `VerbType.BuiltOn`→`DependsOn` (the
  canonical getting-started example now compiles).
- noun-verb-taxonomy: rewrote every sample off removed/fictional APIs
  (`augment`/`connectModel`/`getVerbs`/two-arg `add`/`like`/`$gte`) onto the
  real single-object `add`/`find`/`relate`/`related`; replaced the stale
  31-noun/40-verb catalogs with accurate, complete tables (42 nouns, 127 verbs).
- triple-intelligence: `like:`→`query:`, dollar-operators→bare operators, and
  several other fictional keys swept to the real `FindParams`.
- FIND_SYSTEM / PERFORMANCE / index-architecture / BATCHING: replaced
  fabricated, mutually-inconsistent latency tables and uncited speedup
  multipliers with Big-O characterizations, qualitative mechanism descriptions,
  and the one genuinely-measured benchmark (graph O(1) neighbor lookup), per the
  evidence-based-claims rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 10:03:02 -07:00
d1665bb1a9 chore(release): 7.33.3 2026-06-24 16:37:22 -07:00
7b5db0ddf9 fix: re-validate find() results against the predicate (index-integrity guard)
find() trusted the metadata index's returned ids: it loaded each id's entity
and returned it, checking only that the entity existed — never that it actually
matched the query. The indexes are acceleration structures; the loaded entity
is ground truth. A stale or cross-bucket index posting (e.g. an id left in a
field-value bucket by a delete or an `update({ field: undefined })`) therefore
surfaced an entity matching NEITHER the requested type NOR the where filter — a
production report saw a timeslot (NounType.Event) returned for
`find({ type: Person, where: { entityType: 'staff' } })`.

Add a single egress chokepoint after the result IIFE that re-validates every
result with a new `entityMatchesFindParams` predicate (type/subtype/where/
service/excludeVFS, reusing matchesMetadataFilter for the where leg). It covers
every find() branch (metadata, vector, text, proximity, graph) and similar()
(which delegates to find) in one place. A no-op on a healthy index; on a
corrupted one it drops the bad row instead of returning a phantom. Full unit
gate green (1535) confirms the where re-validation is consistent with the index
(no valid rows dropped).

This is the safety net. The durable corruption itself lives in the metadata
index that owns the query (the native provider when present) and is addressed
separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:36:50 -07:00
3d116190f3 fix(8.0): re-validate find() results against the predicate (index-integrity guard)
find() trusted the metadata index's returned ids: it loaded each id's entity
and returned it, checking only that the entity existed — never that it actually
matched the query. The indexes are acceleration structures; the loaded entity
is ground truth. A stale or cross-bucket index posting (e.g. an id left in a
field-value bucket by a delete or an `update({ field: undefined })`) therefore
surfaced an entity matching NEITHER the requested type NOR the where filter — a
production report saw a timeslot (NounType.Event) returned for
`find({ type: Person, where: { entityType: 'staff' } })`.

Add a single egress chokepoint after the result IIFE that re-validates every
result with `entityMatchesFind` (type/subtype/where/service/excludeVFS) — the
live-path mirror of the historical path's per-candidate check in db.ts. It
covers every find() branch (metadata, vector, text, proximity, graph) and
`similar()` (which delegates to find) in one place. A no-op on a healthy index;
on a corrupted one it drops the bad row instead of returning a phantom. Full
unit + integration gate green confirms the where re-validation is consistent
with the index (no valid rows dropped).

This is the brainy-side safety net. The durable corruption itself lives in the
metadata index that owns the query (the native provider when present) and is
addressed separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 16:25:12 -07:00
8d5032bee5 chore(release): 8.0.0-rc.4 2026-06-24 15:54:52 -07:00
e7b50cf520 docs(8.0): drop the DeletedItemsIndex section + pseudo-code from index-architecture
The dead-code sweep removed `src/utils/deletedItemsIndex.ts` (a utility class
the doc itself noted was "not instantiated"). Remove its "Notes on Other
Indexes" section and the two illustrative `this.deletedItemsIndex.*` lines in
the find()/stats() pseudo-code so the architecture doc no longer references a
deleted, never-wired module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:50:49 -07:00