Commit graph

983 commits

Author SHA1 Message Date
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
d321cf5f33 fix(8.0): gate native graph analytics on the provider readiness flag
The optional native graph-acceleration provider exposes `isInitialized`,
which is false during its cold-start / rebuild window (engine + cursor not
yet loaded). The accessor cached the resolved provider INSTANCE but never
re-checked readiness, so `brain.graph.*` could dispatch to a not-ready
provider and throw instead of answering.

Resolve and cache the instance once, but check `isInitialized` LIVE on every
call: route to the pure-TS analytics path while the provider is not ready,
and re-engage the native path automatically the moment it flips true. This
gates every native route at once — subgraph, export, rank, communities,
path, and the query→expand fusion. Adds a test asserting the provider is
never called (and the TS fallback returns real answers) while not ready.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:18:50 -07:00
bf0afe8563 refactor(8.0): remove dead, unreachable, and unwired modules
Pre-GA dead-code sweep. A deterministic import-reachability walk from the
package's real entry points (the exports map, the CLI bin, the conversation
surface) found 34 source modules that nothing reachable imports — they
compile and ship as dist output that no consumer or internal path can ever
reach. All were superseded duplicates, abandoned parallel implementations,
or built-but-never-wired features:

  - superseded duplicates of live modules: an older "unified" entry, a
    standalone neural-import variant, a static NLP processor + its matcher,
    a parallel API-types module, a duplicate progress-types module
  - an abandoned import path (orchestrator + entity deduplicator + barrels)
    left behind when ingestion moved to the coordinator
  - unwired feature modules (instance pool, import presets, cached
    embeddings, relationship-confidence scorer) reachable only from tests
  - dead leaf utilities (write buffer, deleted-items index, bounded
    registry, two crypto shims, a cache manager, a structured logger, a
    stale v5 type-migration helper, a browser-only FS type shim) and
    several dead re-export barrels
  - a CLI catalog command wired into no command

Also removed two tests that only exercised deleted modules, repointed two
VFS tests off a deleted barrel onto the implementation module, and deleted
one example that demoed removed features.

Verified: clean build (both tsc passes), unit 1505 + integration 607 green,
and a re-run of the reachability walk reports zero remaining orphans.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:18:40 -07:00
03d654061f build(8.0): clean dist before every build so stale artifacts never ship
The incremental `tsc` build never removed dist output for source files
deleted in earlier refactors, so a cluster of 5-month-old artifacts kept
shipping in the published package — an entire orphaned native module tree
(dist/native/*, the Native* graph/metadata adapters, the mmap adapter)
whose source was removed when those responsibilities moved behind the
provider boundary. Nothing in the live tree imported them; they were dead
weight in every tarball, carrying stale type signatures with no source.

Add a `clean` script (cross-platform `fs.rmSync`) and a `prebuild` hook so
every build — including the `prepare` build that runs on publish — starts
from an empty dist. Verified: a planted stale file is wiped on the next
build, and a clean rebuild drops the entire orphaned tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 15:18:26 -07:00
9593a27338 chore(release): 7.33.2 2026-06-24 10:56:12 -07:00
1694f68419 fix: graph adjacency cold-load consistency guard — no more silent [] on connected
A native graph provider can load its relationship COUNT (manifest) on a cold open
but fail to load the source→target adjacency itself (observed on mmap-filesystem:
the SSTable-segment load is swallowed). The result is find({ connected }),
neighbors(), and related() silently returning [] despite persisted edges — and
staying empty after warm-up. Because it is [] not an error, callers cannot tell
real data is missing.

Brainy now runs a one-time consistency check on the first graph read: if the index
reports relationships exist but a known persisted edge's source resolves to no
neighbors, force a rebuild from storage (which re-scans + repopulates the adjacency);
if even that cannot read the edge, throw the new GraphIndexNotReadyError instead of
serving [] as truth. O(1) probe (one verb + one neighbor lookup), cached per brain,
and a no-op once the adjacency is live — so it self-heals the cold-open case and is
loud when it genuinely can't.

Wired into neighbors() + getRelations() (the graph-read primitives behind
find({ connected })). The underlying cold-open load is a native-provider concern
(addressed upstream); this makes the failure self-healing + loud rather than silent.

Tests: tests/unit/brainy/graph-adjacency-cold-load.test.ts (live → no-op; broken →
rebuild; unfixable → throws; size 0 / no edges → no-op; transient failure → re-checks
without breaking the query). Full unit gate green (1531/1531).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 10:55:12 -07:00
c9e2169415 feat(8.0): #35 part-3 — supply at-gen candidate vectors for the native exact-rerank
Completes brainy's #35 side. When the at-gen vector gate serves a filtered
semantic read, Brainy now ships the historically-correct candidate vectors so the
native provider reranks against them with zero per-vector FFI crossing — the
provider need not duplicate the per-generation retention Brainy already does.

- New `AtGenerationVectors { ids: BigInt64Array; vectors: Float32Array; dim }` on
  the VectorIndexProvider.search options bag (row-major: vectors[i*dim..] == ids[i];
  invariant vectors.length === ids.length*dim; ids = the candidate set; dim must
  match the index dim). The built-in JS index ignores it.
- buildAtGenerationVectors resolves each universe id's vector AS OF the generation
  (the record before-image, or live getNoun when untouched since the pin — not
  readNounRaw, whose canonical path is empty under lazy-vector eviction), interns
  the id via the shared mapper, and packs row-major. Absent/vectorless/wrong-dim
  ids are dropped (not vector-rankable). Bounded by the filtered universe.

Shape + the four clarifications (row-major, ids-are-the-candidate-set, dim-asserted,
filtered-path-only) confirmed with cor in the handoff #35 thread. Inert until cor's
native at-gen rerank advertises isGenerationVisible (honesty guard).

Tested: the mock versioned provider now asserts it receives atGenerationVectors with
the row-major invariant, correct dim, and ids resolving back to the at-gen universe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:50:41 -07:00
6991bbe3d2 chore(release): 8.0.0-rc.3 2026-06-23 16:06:20 -07:00
0e8972c6fa test(8.0): de-flake the VFS path-cache timing assertion
`should cache paths for fast repeated access` compared a single cold vs warm
readFile with Date.now() (ms resolution). Both reads are sub-millisecond, so the
warm read intermittently measured 1ms vs the cold 0ms and the `time2 <= time1`
assertion failed on rounding noise — a non-deterministic gate that blocked a clean
release. Replace it with the average of 100 warm reads via performance.now()
against a generous absolute bound (cached path reads are sub-ms), plus a content
round-trip check. Same intent (cached access is fast), no single-shot ms flake.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 16:02:07 -07:00
1c363e8c4b feat(8.0): asOf at-gen vector defer — provider-served historical semantic search (#35)
A filtered semantic read at a historical generation (db.asOf(g).find({ query, where }))
no longer unconditionally rebuilds an ephemeral JS-HNSW over every at-g vector
(O(n@G), the OOM ceiling the 2026-06-24 scaling audit flagged). When the vector
index is a VersionedIndexProvider that advertises isGenerationVisible(g), Brainy:
  1. resolves the at-g metadata∩graph universe from the record-overlay path (no
     materialization — it's the metadata-only historical find),
  2. routes the vector leg to the provider with { allowedIds, generation }, and
  3. composes the at-g entities ranked by the provider's at-g vector distance.
Without a versioned provider (the JS index, or a native one that refuses the
generation) it falls through to the existing materialization — unchanged.

- Seam (A): VectorIndexProvider.search gains an optional `generation?: bigint`
  ("omitted = now"), the vector mirror of the graph provider's trailing-gen + the
  #46 allowedIds field. JsHnswVectorIndex accepts and ignores it (no per-gen
  segments → refuse/fall-back posture; Brainy never routes a historical read there).
- Gate: Db.find tries the native at-gen vector path before materialize(); host
  exposes canServeVectorAtGeneration + vectorSearchAtGeneration.
- generation is Brainy's u64 commit counter — the same value handed to the graph
  index on writes (graphWriteGeneration), so it maps 1:1 to the native side.

Decided lockstep with the cor team (handoff #35 thread: (A) + vector-only). The
gen-g candidate-vector supply for cor's exact-rerank (part-3) is a separate,
non-blocking seam still being confirmed; the honesty guard keeps this path inactive
(falls through to materialize) until cor's native at-gen rerank is live.

Tested: provider routing + at-gen universe correctness (excludes future-born,
applies the filter) + page window via a mock versioned provider; seam-ignore on the
JS index; 38 db-mvcc/db-temporal historical-read tests green (materialize fallback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:52:09 -07:00
450084b6ce perf(8.0): bound find({ where, orderBy }) sort to the page (CTX-BR-FIND-ORDERBY)
A filtered + ordered find() materialized the FULL sorted match set before slicing
the page — passing k = filteredIds.length to the column store's filteredSortTopK.
A broad filter + orderBy returning 20 rows at billion scale produced hundreds of
millions of sorted ids to discard all but the page (O(matches) heap).

Thread the page bound (offset + limit + the hidden-tier over-fetch) into
getSortedIdsForFilter → filteredSortTopK / sortTopK / the sparse-path slice, so
the sort produces only the page. Ordering and pagination are unchanged.

From cor's 2026-06-24 co-release scaling audit (item 1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:01:49 -07:00
82dde92077 feat(8.0): brain.graph.subgraph(query) query→expand fusion (#61)
The subgraph seed selector now accepts not just entity id(s) but a find() result
or a FindParams query — brain.graph.subgraph({ where: { team: 'platform' } },
{ depth: 1 }) runs the query and expands the neighborhood of every match in one
call. Existing id-seeded calls are unchanged.

The win is the native query→expand path: for a metadata-only query, the matched
universe is forwarded to traverse() as an OpaqueIdSet (a roaring Buffer) with NO
id materialization in TypeScript — the find() result never leaves the engine's
representation (the O(1)-crossing the cor 3.0 contract is built around). The
pure-JS path (or any query carrying vector/text/proximity criteria) materializes
the matched ids via find() and seeds the traversal from them, capped at a bounded
default when the caller pins no limit.

- New public `SubgraphSelector<T>` union (id | id[] | Result[] | FindParams).
- find()'s metadata filter-builder extracted to a shared `buildMetadataFilter`
  (reused by the opaque universe producer); behavior unchanged.
- graphSubgraphNative generalized to accept `bigint[] | OpaqueIdSet` seeds.

Tested: JS query→expand + Result[] selector + empty-match in graph-subgraph, and
the native opaque pass-through (Buffer reaches traverse unmaterialized) vs the
find()-materialized bigint[] seeds via the mock provider in graph-native-routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:30:30 -07:00
dd325f2f94 feat(8.0): vector allowedIds predicate-pushdown into find() (#46)
Filtered semantic search now keeps its recall. A find({ query, where }) that
pairs vector search with a metadata filter restricts the HNSW beam walk to the
matching candidates INSIDE the traversal (walk-all, collect-allowed) rather than
filtering the top-k afterward — so a query whose nearest vectors are all filtered
out still returns the best matches that DO pass the filter, instead of empty.

- JS HNSW search() honors the 8.0 `allowedIds: OpaqueIdSet | ReadonlySet<string>`
  contract param (ANDs with candidateIds; ignores the opaque Buffer form it can't
  decode — only a native provider consumes that).
- MetadataIndexProvider gains an OPTIONAL `getIdSetForFilter(filter): OpaqueIdSet`
  producer — the native (cor) metadata index returns its roaring filter result as
  a serialized buffer; the JS index does not implement it.
- find() forwards the matched universe to the vector walk: the opaque buffer
  (zero id materialization) when the native producer is present, alongside the
  materialized visibility-precise candidateIds for the JS index. Visibility stays
  correct via the existing post-search hard filter.

Tested: JS recall/restriction/AND/opaque-ignore on the index directly, plus
find() forwarding the opaque universe through to the beam walk via a stubbed
native producer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:18:34 -07:00
632d90aac5 feat(8.0): graph analytics — brain.graph.rank / communities / path
Adds three intent-level graph reads to the `brain.graph` namespace, each
native-dispatched to the optional `@soulcraft/cor` 3.0 graph engine when present
and served from pure-TS kernels otherwise (identical public shapes, default
visibility filter respected on both paths):

- `rank(opts?)` → `{ id, score }[]` descending — importance / centrality.
  TS fallback: PageRank power-iteration with dangling-mass redistribution.
- `communities(opts?)` → `{ groups, count }` — connected grouping. TS fallback:
  union-find weakly-connected components, or iterative Tarjan SCC when
  `{ directed: true }`.
- `path(from, to, opts?)` → `{ nodes, relationships, cost } | null` — best route.
  TS fallback: BFS for fewest hops, Dijkstra (min-heap) for least summed edge
  weight (`by: 'weight'`); on-demand frontier expansion so short paths terminate
  early. `direction` / `type` / `maxDepth` filters apply.

These are intent contracts, not algorithm contracts — the question is the
promise, the algorithm is the engine's choice.

Pure kernels live in src/graph/analyticsFallback.ts (PageRank, connected
components, Tarjan SCC, MinHeap) — unit-tested in isolation. The full surface is
tested end-to-end through the TS fallback, and the native dispatch + int↔uuid
hydration paths are covered by a mock provider in graph-native-routing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 12:32:03 -07:00
4d0b64f455 fix(8.0): restore() reloads a native entity-id mapper before graphIndex.rebuild()
A logical snapshot restore could silently lose graph edges on a brain backed by
a native provider: the graph adjacency keys on the shared id-mapper's interned
ints (sourceInt/targetInt), which are derived + never persisted, and restore()
never reloaded the mapper — so a native graph rebuild resolved verb endpoints
against a stale/empty mapper and dropped edges.

restore() now calls entityIdMapper.rebuild() — a new OPTIONAL method on the
EntityIdMapperProvider contract — BEFORE rebuilding the indexes, so a native
mapper reloads its int<->uuid from the restored binary KV first and the graph
resolves endpoints correctly. The JS mapper deliberately has NO rebuild() and
needs none: MetadataIndex.rebuild() re-derives it from the restored entities via
append-only getOrAssign, consistently with the bitmaps it builds — forcing a
reload there would blank a still-referenced mapping and break find() after a
same-instance restore.

Contract: graphIndex.rebuild() resolves sourceId/targetId -> ints through the
shared mapper itself (brainy does not re-feed resolved endpoints); brainy's only
job is to ensure the mapper is reloaded first.

Test: relationships survive a snapshot round-trip (db-mvcc.test.ts). 84
generation/temporal/visibility tests green; tsc clean.
2026-06-23 12:02:17 -07:00
3783e61b30 test(8.0): cover pending-tier range queries + setRetentionBudget adaptive reclaim
Closes the two coverage gaps a temporal-completeness audit surfaced (the code
was already correct, just untested):
- changedBetween / generationsTouching include un-flushed PENDING generations
  and stay identical across the flush — the building blocks of since/diff/history
  see single-op writes with no forced flush.
- setRetentionBudget() drives adaptive auto-compaction on flush() down to the
  byte budget: history is reclaimed, the live record is untouched (the cor #65
  retention-consume wiring, proven end-to-end).

65 generation/temporal tests green; 1528 unit green.
2026-06-23 11:11:43 -07:00
811c7da89e chore(release): 7.33.1 2026-06-22 18:17:09 -07:00
6721c52ad7 fix: getNouns cursor pagination re-scanned the first page forever (permanent CPU loop)
The shard-scan pagination adapter is offset-based and ignored the cursor, so
getNouns({ pagination: { cursor } }) re-returned the first page on every cursor
call. Harmless until 7.32.1 made totalCount the true dataset total — after which
hasMore correctly stays true until a caller has paged through everything. A
caller that paginates by cursor (cursor = page.nextCursor) — notably aggregate
backfill over an already-populated store — then looped forever, re-walking the
entire entity shard tree each iteration and pegging 1-2 CPU cores permanently on
the JS main thread, with zero queries or traffic. (Pre-7.32.1 the same loop
ended after one page, silently backfilling only the first 500 entities — an
incomplete aggregate.)

getNouns() now treats the cursor as an opaque, advancing offset token, so cursor
pagination advances and terminates exactly like offset pagination — and an
aggregate backfill streams the whole corpus exactly once (no longer truncated,
no longer looping). Hardened the backfill loop to pure offset pagination as
defense in depth.

Regression (reproduces the infinite loop, fails fast on any re-scan):
tests/unit/storage/getNouns-cursor-pagination.test.ts
2026-06-22 18:00:01 -07:00
5c3bb2c864 feat(8.0): Model-B per-write generation-stamping + adaptive retention knob
Every write — transact() AND single-op add/update/remove/relate — is now its
own immutable generation (Model-B), so a now() pin always freezes and
asOf/since/diff/history/transactionLog reflect single-ops exactly like
transacts. Closes the Model-A hole where pins did not freeze against single-op
writes.

Generation-stamping:
- GenerationStore.commitSingleOp: a one-operation commitTransaction with
  deferred durability. Wired into add/update/remove/relate/updateRelation/
  unrelate + removeMany (the *Many and VFS paths delegate to these).
- Async group-commit (flushPendingSingleOps): the live write is acknowledged
  immediately; its before-image is buffered in an in-memory pending tier that
  resolveAt/chains/changedBetween/tx-log read like on-disk generations, so the
  synchronous now() freezes with no forced flush. One fsync per window
  (triggers: size / 50ms timer / flush / close / transact / compactHistory).
- Crash recovery is drop-without-restore for group-commit generations (marked
  groupCommit:true): a crash mid-flush discards the partial generation and
  never restores its before-images, which would otherwise revert the
  already-acknowledged live write.
- Init-time infrastructure (the VFS root) is the un-versioned generation-0
  baseline: a fresh brain reports generation()===0 and an empty
  transactionLog(); the first user write is generation 1.
- Historical find()/related() overlay bound is the full reserved watermark
  (generation()), so un-flushed single-op writes are overlaid too.

Retention knob:
- config `history` -> `retention`: 'all' | 'adaptive' |
  { maxGenerations?, maxAge?, maxBytes?, budgetBytes?, autoCompact? }; unset ->
  adaptive (disk/RAM pressure, zero-config). CompactHistoryOptions floors ->
  caps (retainGenerations->maxGenerations, retainMs->maxAge, +maxBytes):
  reclaim oldest-unpinned while ANY cap is exceeded; pins always exempt.
- brain.setRetentionBudget(bytes) drives the adaptive byte budget at runtime
  (a coordinator's fair-share input). Per-generation bytes recorded in each
  delta enable historyBytes() introspection without a storage size API.

Tests: per-write generation resolution, pin freeze vs add/update/remove,
drop-without-restore corruption-trap (fault injector), clean-reopen replay,
maxBytes/maxAge/no-cap reclamation, retention-then-reopen. 107 db/generation/
temporal tests green, tsc clean. Docs (ADR-001, consistency-model, snapshots
guide, api reference, RELEASES) updated to per-write granularity + retention.
2026-06-22 15:19:58 -07:00
afac7f9662 test(8.0): Model-B write-perf + scalability spike harnesses
Dev-only standalone harnesses (run via node --import tsx; not globbed by the
unit/integration vitest configs). model-b-scalability.spike.ts is the evidence
harness referenced by the Model-B build spec — re-validates read-vs-depth,
RAM-vs-depth, reopen, and compaction at scale.
2026-06-22 13:47:46 -07:00
ceed70d7be perf(8.0): per-id history chains for O(log) historical reads + bounded delta cache
Historical reads (asOf/get) scanned the global committedGens list linearly,
making them O(database-age): a read of an unchanged entity at an old pin scaled
~12x for 10x history depth. Add per-id inverted history chains (nounChains/
verbChains) so resolveAt binary-searches the id's own generation chain instead —
O(log) and flat with depth. Chains build lazily under the commit mutex,
maintain incrementally on commit, and invalidate on compaction.

Also bound deltaCache (LRU cap 4096; getDelta re-reads evicted deltas) so a
long-lived high-write process's heap is O(cap) not O(generations) on the
disk-backed path, and binary-search commitTimestampAtOrBefore (O(log)).

Verified: 373 unit tests green; new tests/unit/db/generation-chain.test.ts
covers chain resolution, eviction re-read, and chain rebuild after compaction.
2026-06-22 13:47:33 -07:00
f3e69110f0 refactor(8.0): graph analytics contract — intent names, not algorithm names
The public brain.graph.* surface and the GraphAccelerationProvider seam must name
operations by INTENT, not by the algorithm that happens to implement them — so a
plugin can swap algorithms without the contract lying. The JS fallback computes
communities via connected-components and rank via PageRank, but a native provider
(cor) is free to use Louvain/Leiden for communities and personalized-PageRank /
eigenvector-centrality for rank: same intent, different algorithm, same name.

Renamed the (not-yet-implemented) Phase-C analytics methods + their option/result
types on GraphAccelerationProvider:
- pageRank -> rank            (PageRankOptions -> RankOptions; dropped the
                               pagerank-internal damping/maxIterations/tolerance
                               knobs from the contract — each impl tunes itself)
- connectedComponents -> communities   (ComponentsOptions -> CommunitiesOptions,
                               mode:'weak'|'strong' -> directed?:boolean;
                               GraphComponents -> GraphCommunities, componentIds/
                               Count -> communityIds/Count)
- shortestPath -> path        (ShortestPathOptions -> PathOptions; weight -> by:'hops'|'weight')
- neighborhoodSample -> sample (NeighborhoodSampleOptions -> SampleOptions)
- topByDegree -> mostConnected (TopByDegreeOptions -> MostConnectedOptions)

traverse / edgesForNode / graphCursorOpen/Next/Close + GraphScores/GraphPath/
OpaqueIdSet/Subgraph are UNCHANGED — everything cor has already built is untouched;
this is a pure pre-implementation rename (cor coordinated). JSDoc reworded to state
the algorithm is the provider's choice. index.ts exports + the native-seam test
mock updated. tsc/unit 1517/integration 599 green.
2026-06-22 09:54:01 -07:00
96d9c0b92c docs(8.0): RELEASES — native provider is @soulcraft/cor 3.0 (fix cortex 3.0 self-contradiction)
The 8.0 entry referenced a non-existent '@soulcraft/cortex 3.0' (line 447) while
the graph section already said '@soulcraft/cor 3.0' (line 44) — a self-contradiction.
The native provider was renamed cortex→cor; the 8.0 pairing is @soulcraft/cor 3.0
(the renamed successor to @soulcraft/cortex 2.x). Fixed the 8.0-section refs (the
Cor-compatibility block + the scale-path note); HISTORICAL 2.x refs in the v7.31.2 /
older entries are left intact (renaming them would falsify the changelog).
2026-06-22 09:39:14 -07:00
29410bc568 test(8.0): cover the native graph seam + make provider resolution factory-tolerant
The brain.graph.subgraph/export NATIVE routing (graphSubgraphNative /
graphExportNative / hydrateNativeSubgraph + provider resolution) had ZERO brainy
CI coverage — in production it's exercised only cross-layer against cor's engine,
so a columnar return-shape or hydration-alignment drift would pass brainy CI
silently. This registers a faithful MOCK GraphAccelerationProvider returning a
columnar Subgraph built from the brain's REAL ints, locking the seam:
- subgraph() routes native (traverse called) and hydrates node int->id, the
  nodeDepth column aligned to the node column, node type via batchGet, and edge
  verb-int->Relation via verbIntsToIds + getVerbsBatchCached.
- node<->depth alignment is preserved when a node int does NOT resolve
  (deleted/unknown) — the unresolvable int is dropped, not collapsed (which would
  shift every later depth). This is the exact hydration risk the release audit flagged.
- export() routes to the graph cursor, hydrates chunks, and ALWAYS closes the handle.

Also fixes a latent contract bug found writing the test: graphAccelerationProvider()
only accepted a ready instance, so a provider registered as a (storage)=>provider
FACTORY (the convention graphIndex/metadataIndex/vector use) would fail the duck-test
and the native path would silently never engage. Now resolves instance OR factory,
cached. Covered by the factory-registration test.
2026-06-22 09:34:09 -07:00
89c4b7016c chore(release): 8.0.0-rc.2 2026-06-21 10:45:28 -07:00