Commit graph

28 commits

Author SHA1 Message Date
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
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
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
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
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
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
8c2b57a488 feat(8.0): brain.graph.subgraph() + native-provider routing
Introduces the brain.graph namespace and its first op, subgraph() — bounded
multi-hop neighborhood extraction returning a hydrated GraphView (nodes with hop
depth + type/subtype, edges as Relations, a truncated flag). The one-call answer
to 'show me everything around this node, N hops out' the graph-viz path needs.

- brain.graph.subgraph(seeds, { depth, direction, type, subtype, includeInternal,
  includeSystem, maxNodes, maxEdges, hydrateNodes }). seeds: id or id[].
- Feature-detect routing: when a GraphAccelerationProvider is registered
  (isGraphAccelerationProvider on the 'graphAcceleration' provider) it routes to
  native traverse() and hydrates the columnar Subgraph (node ints↔ids paired with
  depth position-preserving, edge verb ints → Relations); otherwise a pure-TS BFS
  fallback expands each frontier through the O(degree) related() adjacency.
- Both paths produce the identical GraphView, so CI exercises the shape via the
  fallback; the native path is cross-layer-tested against cor's provider.
- New public types: GraphView, GraphNode, SubgraphOptions, GraphApi (the surface
  grows: export/rank/path/communities follow). isGraphAccelerationProvider guard
  added to plugin.ts.

Test: graph-subgraph.test.ts — depth tracking, direction, type filter, default-
hides-internal, node hydration on/off, maxNodes truncation, multi-seed.
2026-06-21 09:23:33 -07:00
a3d6fdb8b3 feat(8.0): GraphAccelerationProvider contract — the native graph-engine seam
Defines the optional native graph-acceleration provider (cor 3.0) that
brainy feature-detects and routes brain.graph.* / related({node}) /
find({connected}) to, falling back to pure-TS adjacency when absent. This is
the published seam cor wires its native build against (mirrors the existing
VersionedIndexProvider pattern: defined in plugin.ts, exported from index.ts,
implemented externally, feature-detected at runtime).

Contract (locked cross-team in the design thread):
- GraphAccelerationProvider: traverse, edgesForNode, graphCursorOpen/Next/Close,
  pageRank, connectedComponents, shortestPath, neighborhoodSample, topByDegree.
  Every read op is generation-aware (optional trailing generation?: bigint) so
  db.asOf(g).graph.* resolves historically; omitted = now.
- Subgraph: columnar wire format (BigInt64Array node/edge columns, Uint16Array
  edgeTypes, optional Uint8Array nodeDepth, truncated flag) — parallel typed
  arrays, never array-of-objects; brainy maps u64<->UUID lazily for rendered rows.
- OpaqueIdSet: a find() universe forwarded opaquely into traverse/search for the
  zero-crossing query->expand fusion (brainy never inspects it; the provider
  version-tags + validates the envelope).
- VectorIndexProvider.search gains allowedIds?: OpaqueIdSet | ReadonlySet<string>
  (predicate pushdown — recovers filtered recall lost to post-filtering).
- EntityIdMapperProvider gains entityIntsToUuids(BigInt64Array) — the bigint batch
  reverse resolver for Subgraph.nodes; implemented on the JS mapper (TS fallback).

All new public types exported from index.ts. Test: entityIntsToUuids round-trip
+ order-preservation + empty-int sentinel.
2026-06-21 08:12:22 -07:00
1264fec534 feat(8.0): version-coupling guard — a mismatched/failed native plugin fails loud, never silent JS fallback
A wrong or missing accelerator (@soulcraft/cor) used to silently degrade to the
default JS engine — the #1 source of invisible cross-repo drift. Brainy does no
plugin auto-detection (a registered plugin is always explicitly requested via
config.plugins or brain.use()), so any mismatch/failure is now fatal:

- BrainyPlugin gains optional `brainyRange` (semver range of brainy it supports);
  init() throws if the running brainy is outside it. New dep-free, prerelease-safe
  matcher pluginRangeSatisfies() (8.0.0-rc1 satisfies >=8.0.0).
- activateAll(): a plugin that THROWS during activate now re-throws (was swallowed
  to a JS fallback); a graceful decline (activate()→false) stays non-fatal but logs
  a loud warning.
- loadPlugins(): an explicitly-listed package that can't load — or isn't a valid
  plugin — throws (was a silent skip).
- Provider-key cliff: a pre-8.0 plugin that registers a legacy vector key
  ('hnsw'/'diskann') but not the 8.0 'vector' key throws (brainy 8.x reads only
  'vector', so it would otherwise run JS vectors invisibly).

Tests: tests/unit/plugin-version-coupling.test.ts (range matcher incl. rc1; range
mismatch / activate-throw / cliff / missing-package all throw; decline non-fatal).
Updated two plugin.test.ts cases that asserted the old silent-swallow behavior.
Build green; 1464 unit green. cor declares its brainyRange per handoff LV.1 #2.
2026-06-19 10:13:04 -07:00
0951fa1da0 feat(8.0): thread commit generation through the graph-write provider contract
Graph time-travel needs an edge's existence recorded per generation so
db.asOf(g) hops resolve historically correct endpoints. The metadata layer
already threads brainy's commit generation per write; the graph write path did
not, leaving a versioned verb-endpoint store unable to answer "which edges
existed at generation g".

- GraphIndexProvider.addVerb/removeVerb gain a `generation: bigint` parameter
  (the same watermark the storage layer stamps onto the record). A provider with
  a per-generation edge chain stamps the edge at that generation; the JS baseline
  has no such chain and accepts-and-ignores it — graph time-travel is a
  native-provider capability, and the open-core path serves edges as-of-now (the
  one documented graph time-travel limitation).
- The two graph transaction operations resolve the generation via a thunk at
  EXECUTE time: the generation store assigns the batch generation only once the
  commit begins executing, after the operations are planned. The same generation
  is reused for an operation's rollback half.
- All graph-write call sites pass the in-flight generation.

Adds a spy-provider test proving the threading, execute-time resolution, and
forward/rollback generation reuse. The JS index ignores the value, so behaviour
is unchanged: unit 1402/1402, db-mvcc 25/25, bigint-contract relate/unrelate 10/10.
2026-06-16 09:42:35 -07:00
1f7e365a4e chore(8.0): final pre-RC1 sweep — API consistency, named errors, orphans, zero-cast codebase 2026-06-11 14:51:00 -07:00
431cd64406 feat(8.0): generational MVCC storage + Datomic-style Db API (now/transact/asOf/with/persist)
One mechanism replaces the COW and versioning subsystems: immutable
generation-stamped records behind a Datomic-style database value.

Record layer (src/db/generationStore.ts):
- Monotonic generation counter in _system/generation.json; bumped once per
  transact() commit and once per single-operation write (storage hook), so
  brain.generation() is always a meaningful watermark.
- Commit protocol: stage before-images + tx.json delta -> fsync -> execute
  batch via TransactionManager -> atomic tmp+rename of _system/manifest.json
  (the rename IS the commit point) -> append _system/tx-log.jsonl.
- Crash recovery on open rolls uncommitted generations back byte-identically
  and forces an index rebuild; refcounted pins gate compactHistory(), which
  records a horizon (asOf below it throws GenerationCompactedError).

Db API:
- Db: get/find/search/related pinned at a generation, with() speculative
  overlays, since() diffs, persist() hard-link-farm snapshots, timestamp,
  generation, release() + FinalizationRegistry backstop.
- Brainy: now() O(1) pin, transact(ops, {meta, ifAtGeneration}) atomic batch
  (GenerationConflictError CAS), asOf(generation|Date|path), restore(path,
  {confirm}), compactHistory(), generation(), static open() + load().
- get()/metadata find()/related() are fully correct at any reachable pinned
  generation; index-accelerated queries at historical generations throw
  NotYetSupportedAtHistoricalGenerationError - never silently-wrong results.
- VersionedIndexProvider (generation/isGenerationVisible/pin/release) in
  plugin.ts: feature-detected, balanced pin/release in lockstep with Db
  lifecycle, post-commit applier + replay-gap model documented.

Storage primitives (BaseStorage + filesystem/memory adapters): raw-object
read/write/list/remove, fsync barrier, noun/verb raw before-image capture,
tx-log append, snapshotToDirectory (hard-link farm; byte-copy fallback and
append-in-place exceptions), restoreFromDirectory + derived-state reload.

Proof suite (tests/integration/db-mvcc.test.ts + tests/unit/db/): isolation
across 200 mutations, batch atomicity under injected execution failure,
ifAtGeneration CAS, snapshot immunity to source mutation, compaction safety
under pins, with() overlay isolation, generation monotonicity across
reopen, crash consistency through the real recovery path, and versioned
provider pin/release balance. Design record in
docs/ADR-001-generational-mvcc.md.
2026-06-10 14:14:07 -07:00
49e49483d1 fix(8.0): createIndex resolves the canonical 'vector' provider key — drop diskann/hnsw key lookups + legacy migration APIs
createIndex() still resolved the retired 'diskann' and 'hnsw' provider
keys and never looked up 'vector', so an 8.0-era plugin registering its
vector engine under the canonical 'vector' key silently never engaged
and Brainy always fell back to the built-in JS HNSW index. createIndex()
now resolves only getProvider('vector') (same factory call shape as the
old hnsw path) and falls back to setupIndex().

The brainy-side mode/migration machinery is obsolete in the 8.0 provider
world — the registered engine adapts internally (in-memory / hybrid /
on-disk selection is the provider's job, not Brainy's):

- delete migrateToDiskAnn() / migrateToHnsw() and the ADR-002
  index-engine migration block
- delete diskAnnAutoEngageConditionsMet() / instantiateDiskAnn()
- narrow HNSWConfig.type to 'vector' and drop the diskann tuning block
  (coreTypes.ts)
- well-known provider key lists + diagnostics now report 'vector'
  instead of the never-registered 'hnsw' key
- plugin.ts docs: 'vector' is the only vector-index key consulted; the
  pre-8.0 'hnsw'/'diskann' keys are retired

The schema-migration machinery (migrate(), checkMigrations(),
MigrationRunner, autoMigrate) serves data migrations and is untouched.
2026-06-10 11:29:05 -07:00
2427bb7960 feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
  return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
  identity-fingerprint design — verb ids are UUIDs by contract, so the
  provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
  removeVerb(verbId) joins the contract

Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.

JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].

relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.

Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
62f6472fa0 chore(8.0): delete vectorStore:mmap wiring — dead in the 8.0 provider world
The mmap-vector backend was a 2.x-era accelerator for the JS HNSW read path:
a native provider registered under 'vectorStore:mmap' supplied a single-file
mmap'd vector store, and JsHnswVectorIndex tried it before per-entity storage
reads with lazy write-back migration.

In the 8.0 provider world nothing registers that key — the native plugin's
3.0 line replaces the entire vector index under the 'vector' provider key
(vectors live inside its own index file), and the open-core path never had
an mmap provider. The wiring is unreachable; per the no-unwired-code rule
it goes away:

- src/hnsw/mmapVectorBackend.ts deleted (175 LOC)
- wireMmapVectorBackend() + its init call site removed from brainy.ts
- VectorStoreMmapProvider + VectorStoreMmapInstance contracts removed
  from plugin.ts
- JsHnswVectorIndex loses the vectorBackend field, setVectorBackend(),
  and the mmap-first branches in getVectorSafe/preloadVectors — the
  storage + UnifiedCache path is now the single read path
- tests/unit/hnsw/mmap-vector-backend.test.ts deleted

The 7.x line keeps the wiring and got the capacity-NaN bugfix as 7.31.3
(see the platform handoff mmap thread). EntityIdMapperProvider stays — the
connections codec and the id mapper still implement it.

1403/1403 tests, build clean.
2026-06-10 09:40:38 -07:00
b20666e020 refactor(8.0): final cleanup — drop HnswProvider alias + config.hnsw + 'hnsw' surface (scaffold step 6)
Step 6 of the brainy 8.0 rename scaffolding. Removes every legacy 'hnsw'-
flavoured public name introduced as a compat shim in steps 1-5. The
algorithm-neutral surface is now the only surface.

REMOVED — PHASE A (type aliases)

src/plugin.ts
- Removed `export type HnswProvider = VectorIndexProvider` alias.
- Removed `export type DiskAnnProvider = VectorIndexProvider` alias.

src/index.ts
- Removed `export const HNSWIndex = JsHnswVectorIndex` alias.

src/types/brainy.types.ts
- Removed `BrainyStats.indexHealth.hnsw` field (the new `vector` field
  carries the same boolean).

src/brainy.ts
- `stats()` no longer emits the legacy `hnsw` field on `indexHealth`.

REMOVED — PHASE B (storage adapter rename, 8 adapters)

Repo-wide rename: `saveHNSWData` → `saveVectorIndexData` and
`getHNSWData` → `getVectorIndexData` across:

- src/storage/adapters/baseStorageAdapter.ts (abstract declarations)
- src/storage/adapters/fileSystemStorage.ts
- src/storage/adapters/gcsStorage.ts
- src/storage/adapters/r2Storage.ts
- src/storage/adapters/s3CompatibleStorage.ts
- src/storage/adapters/azureBlobStorage.ts
- src/storage/adapters/opfsStorage.ts
- src/storage/adapters/memoryStorage.ts
- src/storage/adapters/historicalStorageAdapter.ts
- src/brainy.ts (call sites)
- src/hnsw/hnswIndex.ts + src/hnsw/typeAwareHNSWIndex.ts (call sites)

The default-delegation wrappers added in scaffold step 4 are removed
(would have been duplicate declarations after the rename).

REMOVED — PHASE C (cache category 'hnsw')

src/utils/unifiedCache.ts
- Cache-category union narrowed from
    'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
  to
    'vectors' | 'metadata' | 'embedding' | 'other'
- typeAccessCounts, typeSizes, typeCounts, accessRatios, sizeRatios,
  and the fairness-check iterator all drop the 'hnsw' key.
- Per planning § 2.5: pre-8.0 'hnsw' cache entries are an in-memory
  category. Cache is rebuildable; entries naturally don't exist after
  a restart, so no migration path is needed.

src/hnsw/hnswIndex.ts
- 3 cache.set() call sites migrated from category 'hnsw' to 'vectors'.
- Cache key prefix `hnsw:vector:` → `vector:`.
- typeCounts/typeSizes/typeAccessCounts accessor renames
  `.hnsw` → `.vectors`.

tests/unit/utils/unifiedCache-eviction.test.ts
- Test fixtures updated: cache.set(..., 'hnsw', ...) → cache.set(..., 'vectors', ...).
- Stats assertion `stats.typeSizes.hnsw` → `stats.typeSizes.vectors`.

REMOVED — PHASE D (config.hnsw)

src/types/brainy.types.ts
- Removed `BrainyConfig.hnsw` field entirely. The 8.0 surface is
  `BrainyConfig.vector.{recall, quantization, vectorStorage, advanced}`.

src/brainy.ts
- normalizeConfig() no longer emits a `hnsw` field on Required<BrainyConfig>.
- setupIndex() rewired:
  - Reads from `this.config.vector` (not `this.config.hnsw`).
  - Calls `resolveJsHnswConfig(this.config.vector)` to translate the
    `recall` preset into M / efConstruction / efSearch knobs (with
    `advanced.hnsw` overrides winning when supplied).
- Imports `resolveJsHnswConfig` from './utils/recallPreset.js'.

NOT IN THIS COMMIT (deliberately)

- Persisted file path migration `_system/hnsw-*.json` →
  `_system/vector-index-*.json`. Requires dual-read logic on boot
  across 8 storage adapters. Per integration doc lines 531-539:
  "Reader accepts either spelling on load; writer emits the new spelling
  only; brains self-migrate on the next persist after upgrade." This is
  a separate body of work and lands in a follow-up commit before 8.0 GA.
- `config.hnswPersistMode` top-level field is unchanged. It's not in
  the rename inventory; a future commit can fold it into
  `config.vector.persistMode` if desired.
- strictConfig enforcement wiring. The field is accepted by
  normalizeConfig() with default 'warn'; the actual warning emission at
  knob-mismatch sites lands when the config-resolution layer is touched
  for the persisted-path migration.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit (one test fixture updated to use the new
  category name; assertion still passes after fixture rename)
- npm run build: clean

The 8.0 PR's user-facing surface is now algorithm-neutral end-to-end:
VectorIndexProvider, config.vector.recall, JsHnswVectorIndex,
saveVectorIndexData, 'vectors' cache category, indexHealth.vector,
strictConfig — all standalone. No legacy 'hnsw' name remains in any
public type or method signature.
2026-06-09 13:28:53 -07:00
f39d420cb4 refactor(8.0): rename HNSWIndex class → JsHnswVectorIndex (scaffold step 3)
Step 3 of the brainy 8.0 rename scaffolding. The class name now matches
its role: the JS HNSW implementation of the VectorIndexProvider contract.
Per planning § 2.3: this is NOT cosmetic — leaving HNSWIndex when the
public contract is VectorIndexProvider creates a confusing read at the
implementation layer.

CHANGES

Repo-wide mechanical rename: HNSWIndex → JsHnswVectorIndex across 52
references in src/ + 5 references in tests/ via sed. Class declaration,
imports, JSDoc, comments, and identifiers all updated.

src/index.ts
- Primary public export: JsHnswVectorIndex (the new name).
- Backwards-compat alias: `export const HNSWIndex = JsHnswVectorIndex`
  with @deprecated note. Removed in the final 8.0 cleanup commit.

NO-OP scope

No behavioural change. The class is exactly the same; only the name
moved. Tests + build green.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
2026-06-09 13:07:56 -07:00
076c26f6fd refactor(8.0): rename HnswProvider → VectorIndexProvider (8.0 scaffold)
Brainy 8.0 collapses the vector-index contract to algorithm-neutral names.
"Vector index" describes the role; "HNSW" was the name of one specific
implementation. The role name is what the public contract should carry;
the algorithm name belongs to the concrete class.

This is step 1 of the brainy 8.0 rename scaffolding tracked in handoff
thread BRAINY-8.0-RENAME-COORDINATION § A.1 (cortex shipped the matching
VectorIndexProvider in commit 0e4d637).

CHANGES

src/plugin.ts
- Primary interface name flipped: HnswProvider → VectorIndexProvider.
  Same byte-for-byte shape (8 methods, no signatures changed).
- HnswProvider kept as a deprecated type alias so call sites compile
  mid-migration. Removed in a later 8.0 commit.
- DiskAnnProvider was already a type alias of HnswProvider; redeclared
  as alias of VectorIndexProvider with a deprecation note explaining
  the 'diskann' provider key folds into 'vector' in 8.0.
- JSDoc updated to describe the implementation-neutral surface:
  Brainy's own JS HNSW + any native acceleration provider both
  satisfy it.

src/hnsw/hnswIndex.ts
- Internal class HNSWIndex now declares `implements VectorIndexProvider`
  instead of `implements HnswProvider`. Import + class comment updated.

NO-OP scope

No behavioural change. Every existing call site continues to work
because HnswProvider remains as a temporary alias. Tests + build green.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- npm run build: clean (verified at parent commit 89e4d81; this commit
  is type-only so doesn't change dist)

NEXT IN SCAFFOLDING

- Add 'vector' provider key registration alongside 'hnsw' (cache + cortex)
- Add config.vector top-level path alongside config.hnsw with recall preset
- Migrate brainy.ts call sites to the new names
- Rename HNSWIndex class → JsHnswVectorIndex (per planning § 2.3)
- Final cleanup commit: remove HnswProvider alias + config.hnsw + 'hnsw' key
2026-06-09 12:58:26 -07:00
f885f813fe feat(plugin): DiskAnnProvider contract + HNSWConfig.type/diskann knobs
Adds the plugin-side surface for the new billion-scale index option:

- plugin.ts: DiskAnnProvider type alias (mirrors HnswProvider's shape
  so cortex's DiskANN wrapper is a drop-in for the 'hnsw' factory).
- coreTypes.ts: HNSWConfig.type ('hnsw' | 'diskann') for explicit
  engine selection, plus HNSWConfig.diskann for the build-time tuning
  knobs (pqM, pqKsub, maxDegree, searchListSize, alpha, mmap
  adjacency).

No algorithm code here — the implementation lives in the cortex
plugin. Brainy without cortex continues to use HNSW exactly as before.
2026-05-28 14:29:12 -07:00
617c156feb feat: graph link compression — delta-varint connections (2.4.0 #3)
Cortex registers a graph:compression provider exposing `encode`/`decode`
for HNSW connection lists (delta-varint, ~4× smaller edges than the
JSON-UUID-array shape brainy has persisted since the beginning). This
wires the consumer side without changing the saveHNSWData/getHNSWData
adapter signatures.

Architecture:

- NEW ConnectionsCodec (src/hnsw/connectionsCodec.ts) — translates between
  UUID-keyed Map<level, Set<UUID>> and a single compact per-node buffer
  via the provider + stable EntityIdMapper. Custom wire format:
  [count: u8] [[level: u8] [len: u32 LE] [bytes...]]*. One blob per node
  regardless of level count, so the storage I/O is identical to the
  legacy path (one saveHNSWData call) plus a single saveBinaryBlob.

- HNSWIndex changes — a `connectionsCodec: ConnectionsCodec | null` field
  with `setConnectionsCodec()` setter. THREE save sites (deferred flush,
  immediate-mode entity persist, immediate-mode neighbor updates) now go
  through a single `persistNodeConnections(nodeId, noun)` helper: when the
  codec is wired AND the storage adapter exposes saveBinaryBlob, it
  encodes the connections, stores them at `_hnsw_conn/<nodeId>`, and
  records `connections: {}` in saveHNSWData as the marker. Otherwise the
  legacy JSON-array path is taken. TWO load sites use a matching
  `restoreNodeConnections` helper that tries loadBinaryBlob first and
  falls back to the legacy hnswData.connections field on miss / decode
  error. Format convergence is lazy: pre-2.4.0 nodes still load via the
  legacy path, then write the compressed form on next dirty save.

- brainy.ts wireConnectionsCodec — runs alongside wireMmapVectorBackend
  during init. Activates when (a) the graph:compression provider is
  registered and (b) the metadata index exposes its idMapper. Unlike the
  mmap-vector backend, this layer engages on EVERY brainy 7.25.0
  adapter — the blob primitive itself is universal; only the codec
  presence gates activation.

- Provider interface GraphCompressionProvider in plugin.ts — encode +
  decode static signatures. Brainy depends on the interface; cortex's
  registered { encode: encodeConnections, decode: decodeConnections }
  satisfies it structurally.

Tests (1437 total, +4 vs prior tip):

- tests/unit/hnsw/connections-codec.test.ts — 4 unit tests with a JSON
  mock provider: single-level round-trip, empty-Map round-trip (one-byte
  buffer), multi-level fan-out round-trip, and silent-drop of UUIDs the
  decoding idMapper no longer knows. The real cross-language byte
  format is exercised when cortex 2.4.0 wires its delta-varint
  encode/decode in.

This completes the brainy half of the 2.4.0 storage foundation: stable
ids (#23), mmap vectors (#24), graph link compression (#25), and
column-store interchange (#26). Coordinated release as brainy 7.26.0 +
cortex 2.4.0 follows.
2026-05-28 10:37:47 -07:00
d4cb26c604 feat: mmap-vector backend wiring — HNSWIndex consumes vectorStore:mmap (2.4.0 #2)
Cortex already registers the vectorStore:mmap provider (its Rust
NativeMmapVectorStore), but brainy has never consumed it — preloadVectors
and getVectorSafe still go straight to storage.getNounVector for every id,
even when an mmap layer is available. This wires the consumer end.

Architecture:

- NEW MmapVectorBackend (src/hnsw/mmapVectorBackend.ts) — bridges brainy's
  UUID-keyed vector reads to a int-slot mmap file via the
  vectorStore:mmap provider. Slots are addressed by the stable int id
  from the post-2.4.0 #1 EntityIdMapper (the foundation this depends on).
  Auto-grows the file (doubling) when a write lands beyond capacity, so
  HNSWIndex never has to think about sizing. The class never touches
  per-entity storage — it owns only the mmap layer.

- HNSWIndex changes — adds a vectorBackend field + a setVectorBackend
  setter. The vector read paths (preloadVectors, getVectorSafe) try the
  mmap layer first; on a storage fallback hit, they LAZILY write back into
  the mmap slot. An upgraded install converges to the zero-copy fast path
  under live traffic — no big-bang migration step. The legacy per-entity
  path is preserved and still used when no backend is set.

- brainy.ts wiring — a new private wireMmapVectorBackend() runs once
  during init, after plugin activation + metadataIndex setup. It activates
  the backend only when (a) the vectorStore:mmap provider is registered,
  (b) the storage adapter resolves a real local path via
  getBinaryBlobPath(), and (c) the metadata index exposes its idMapper.
  Cloud adapters return null on (b) and the backend is silently skipped;
  HNSWIndex's behaviour is then identical to pre-2.4.0.

- Provider interfaces in plugin.ts — VectorStoreMmapProvider and
  VectorStoreMmapInstance document the contract cortex's class fulfils
  (the class IS the provider — static factory methods). Brainy depends on
  the interfaces, not on cortex; the structural match is verified when
  cortex 2.4.0 picks up this brainy release.

Tests (1428 total, +11 vs pre-2.4.0):

- tests/unit/hnsw/mmap-vector-backend.test.ts — 6 unit tests with an
  in-memory mock provider. Covers round-trip, batch reads with interleaved
  misses, slot stability (no re-slotting on overwrite), file growth without
  data loss, idempotent open, and null returns for unwritten slots. The
  real perf integration with cortex's NativeMmapVectorStore is exercised
  when cortex 2.4.0 wires this in.

- tests/unit/utils/entity-id-mapper-stability.test.ts — moved here from
  tests/regression/ (which is NOT in the unit-config include glob, so the
  five #23 tests were not actually being run by npm test). The unit
  config matches tests/unit/**/*.test.ts.

The 2.4.0 #2 follow-up will be the chunked-segment layout for remote
storage adapters (S3 / R2 / GCS) where a single growing file doesn't fit
immutable objects. For 2.4.0 release: local-FS only.
2026-05-28 10:37:47 -07:00
4b6f63ed67 feat: export provider contracts for the plugin surface brainy consumes
Native accelerators (cortex) register providers for metadataIndex, graphIndex,
hnsw, entityIdMapper, cache, columnStore, and aggregation. Until now the exact
method/property surface brainy calls on each was implicit — a provider could
drop a member brainy depends on and only fail at runtime when that path ran.

This defines and exports the provider contracts from the stable
@soulcraft/brainy/plugin entrypoint:
- MetadataIndexProvider, GraphIndexProvider, HnswProvider,
  EntityIdMapperProvider, CacheProvider — each typed as exactly the surface
  brainy calls (optional/feature-detected members like HNSW setPersistMode and
  enableCOW are intentionally excluded).
- Re-exports ColumnStoreProvider and AggregationProvider (and the aggregate
  types) from the same entrypoint so a plugin author can import the whole
  provider surface from one place.

Brainy's own baseline classes now `implements` these contracts
(MetadataIndexManager, GraphAdjacencyIndex, HNSWIndex, EntityIdMapper,
UnifiedCache), so the interfaces can never silently diverge from what brainy
ships — and any provider that declares `implements` gets a compile error the
moment brainy starts requiring a new member.

The embeddings/embedBatch providers stay typed by the existing EmbeddingFunction
(no duplicate interface added). Type-only changes; no runtime behavior change.
2026-05-27 14:45:40 -07:00
f024e56ee7 feat: add aggregation engine with incremental SUM/COUNT/AVG/MIN/MAX, GROUP BY, and time windows
Add a write-time incremental aggregation engine that maintains running
totals on every add/update/delete for O(1) read performance. Integrates
into brain.find({ aggregate }) for a unified query API.

Core features:
- AggregationIndex with defineAggregate()/removeAggregate() API
- Five aggregation operations: SUM, COUNT, AVG, MIN, MAX
- GROUP BY with multiple dimensions including time windows
- Time window bucketing: hour, day, week, month, quarter, year, custom
- Materialization of results as NounType.Measurement entities
- Debounced persistence of definitions and state to storage
- Definition change detection via FNV-1a hashing with auto-rebuild
- Infinite loop prevention for materialized entities
- 'aggregation' plugin provider key for native acceleration
- Lazy initialization (created on first defineAggregate() call)
- 73 tests (unit + integration) covering all functionality

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:57:53 -08:00
932fb9520b fix: set verb.source/target to entity UUID instead of NounType
relate() was setting verb.source = fromEntity.type (a NounType like
"concept") instead of the entity UUID. Cortex's graph index indexes by
verb.source, so lookups by UUID found nothing — causing in-session
reads to return 0 results.

Also fixes:
- PathResolver calling private getIdsFromChunks() → public getIds()
- Plugin auto-detection removed; cortex loads only via explicit config
- GraphVerb types accept number timestamps and sourceId/targetId aliases
- Dead autoDetect() method removed from PluginRegistry
- In-session regression tests added for getRelations after relate()
2026-02-02 09:20:38 -08:00
401e300ff2 feat: harden plugin system wiring and add developer diagnostics
Fix critical wiring bugs that prevented plugin-provided implementations
from being used at runtime. All CRUD operations, fork/checkout/clear,
batch embedding, neural APIs, and VFS path resolution now properly
dispatch through the plugin registry.

Changes:
- Wire graphIndex to storage for getVerbsBySource() fast path
- Replace instanceof checks with duck-typing (indexIsTypeAware flag)
  so plugin HNSW indexes work in add/update/delete/search
- Add createIndex() shared helper for plugin HNSW factory
- Fix fork/checkout/clear to use plugin factories for metadataIndex,
  graphIndex, and HNSW instead of hardcoding JS constructors
- Add three-tier embedBatch priority: embedBatch > embeddings > WASM
- Skip WASM warmup/eagerEmbeddings when plugin provides embeddings
- Fix PathResolver metadataIndex access (was looking on storage)
- Use global UnifiedCache in SemanticPathResolver
- Wire plugin distance function through neural APIs
- Add diagnostics() method and CLI command for provider inspection
- Add requireProviders() for production fail-fast assertions
- Add init-time provider summary log
- Add plugin developer documentation (docs/PLUGINS.md)
- Export DiagnosticsResult type
2026-02-01 13:03:15 -08:00
7f9d2a70a5 feat: update plugin references from @soulcraft/brainy-cortex to @soulcraft/cortex 2026-02-01 08:22:07 -08:00
cc50ac3776 feat: add plugin system for cortex and storage adapters
Simple plugin architecture with two use cases:
1. Native acceleration (@soulcraft/brainy-cortex) — auto-detected
2. Custom storage adapters (e.g., Redis, DynamoDB)

Plugins implement BrainyPlugin interface with activate/deactivate lifecycle.
Auto-detection tries dynamic import of known packages during init().
Manual registration via brain.use(plugin) before init().

Provider keys: metadataIndex, graphIndex, entityIdMapper, cache, hnsw,
roaring, embeddings, distance, msgpack, storage:<name>
2026-01-31 09:22:32 -08:00