Commit graph

966 commits

Author SHA1 Message Date
970e08c466 feat(8.0): reserved-field contract — one canonical location, typed prevention, unified read/write
Brainy-owned field names (noun/verb, subtype, createdAt, updatedAt,
confidence, weight, service, data, createdBy, _rev) now have exactly one
home — top level — enforced by three layers driven from a single source of
truth, src/types/reservedFields.ts (RESERVED_ENTITY_FIELDS /
RESERVED_RELATION_FIELDS, exported):

1. Compile time — AddParams/UpdateParams/RelateParams/UpdateRelationParams
   metadata (and the transact() ops that extend them) reject a literal
   reserved key as a TypeScript error while keeping generic T ergonomics
   (typed bags, untyped brains, index-signature shapes, and a documented
   exemption for T-declared reserved keys). Pinned by @ts-expect-error
   type tests run under vitest typecheck mode on every unit run.

2. Write time — the 7.x update() remap is ported to 8.0 and extended to
   every write path: add/update/relate/updateRelation, their transact()
   mirrors, and db.with() overlays. User-settable fields lift to their
   dedicated param (top-level wins when both are supplied — closes the 7.x
   trap where update({metadata:{confidence}}) silently no-oped), and
   system-managed fields drop with a one-shot warning naming the right
   path. A remapped subtype satisfies subtype-pairing enforcement exactly
   like a top-level one.

3. Read time — every storage combine goes through one canonical hydration
   helper (hydrateNounWithMetadata / hydrateVerbWithMetadata over
   splitNoun/VerbMetadataRecord), so reserved fields surface ONLY top-level
   and entity/relation.metadata carry ONLY custom fields on live reads,
   batch reads, paginated listings, getRelations by source/target, streamed
   verbs, and historical asOf() materialization alike.

Read-path echoes found and fixed (previously the full stored record —
including the verb type key — leaked inside metadata): noun pagination,
verb pagination, getVerbsBySource/ByTarget (adjacency + shard fallback),
getVerbsBySourceBatch (which also dropped subtype/data), and the
filesystem verb stream. getRelations() results now surface
confidence/updatedAt top-level via verbsToRelations, updateRelation() no
longer erases service/createdBy, relate() persists its top-level
confidence/service params, and the dead convertHNSWVerbToGraphVerb echo
path is deleted. Import paths (CLI extract, deduplicator, coordinators,
neural import) write confidence through the dedicated param instead of the
bag. UpdateRelationParams is now exported from the package root.

Documented for consumers in docs/concepts/consistency-model.md ("Reserved
fields") and RELEASES.md. Regression tests ported from the 7.x fix and
extended to the full 8.0 contract (17 runtime tests + 41 type-level
assertions); full unit suite 1427/1427, db-mvcc integration 24/24.
2026-06-11 13:13:09 -07:00
c44678390e feat(8.0): brain.fillSubtypes migration helper + pre-RC1 gap closure
- brain.fillSubtypes(rules): idempotent subtype back-fill for pre-8.0 data.
  One rule per NounType/VerbType (literal default or per-entry function);
  fills only entries still missing a subtype through the public update()/
  updateRelation() paths; returns { scanned, filled, skipped, errors, byType }.
  Full unit suite in tests/unit/brainy/fill-subtypes.test.ts.
- Fix getNouns/getVerbs pagination hasMore (peek one past the window) —
  was permanently false, silently truncating every multi-page walk.
- find({ near }) without near.id now throws a teaching error instead of an
  opaque storage sharding failure; CLI --threshold without --near applies a
  plain score floor.
- CLI init/close audit: every one-shot command init()s, close()s, and exits
  explicitly; delete the unmaintained interactive REPL; replace the cloud-era
  storage subcommands with status/batch-delete; new types/validate commands.
- requireSubtype JSDoc now documents the 8.0 default-on contract; audit()
  recommendation points at fillSubtypes.
- Docs: data-storage-architecture rewritten to the real 8.0 on-disk layout;
  README storage section reflects filesystem+memory and snapshots; eli5 and
  SEMANTIC_VFS /as-of/ semantics corrected; internal tracker IDs and
  .strategy references scrubbed from published files.
2026-06-11 10:53:55 -07:00
9b52629a0a chore(release): 7.31.6 2026-06-11 10:35:26 -07:00
67e5fc8779 fix: remap reserved fields from update() metadata patches to their canonical location
add({metadata: {confidence: 0.8}}) lifts reserved fields out of the metadata
bag to their canonical top-level entity fields — teaching consumers that the
metadata bag is a valid write path. update({metadata: {confidence: 0.33}})
then silently dropped the same shape: the patch value survived the metadata
merge but was clobbered one expression later by the preserve-existing spread.
No error, no warning, nothing written. A production consumer's confidence-
evolution writes no-oped for weeks before being caught by reading values back.

Fix: update() now normalizes the metadata patch before any enforcement or
persistence logic runs, mirroring add()'s lift exactly:

- confidence, weight, subtype — remapped to the top-level param unless the
  caller also passed that param explicitly (top-level wins). The remapped
  subtype flows through subtype-pairing enforcement like a top-level one.
- noun, data, createdAt, updatedAt, service, createdBy, _rev — system-managed
  or owned by a dedicated param; dropped from the patch with a one-shot
  warning naming the correct write path (silent drop was the only wrong
  behavior here).

Five regression tests pin the contract, including the production repro
verbatim (add with metadata.confidence → top-level update → metadata-patch
update → read-back) and the both-paths-supplied precedence case.
1475/1475 unit suite passing.
2026-06-11 10:35:08 -07:00
9b0f4acd5b docs(8.0): RELEASES.md 8.0.0 release-candidate entry — full breaking-change inventory + upgrade guide 2026-06-11 09:31:07 -07:00
e5ec658fab chore(release): 7.31.5 2026-06-11 09:17:31 -07:00
a537b3664b fix: feature-detect setVectorBackend before wiring the mmap-vector backend
A production deployment on the native plugin reported the mmap-vector wiring
failing one step past the 7.31.3 capacity fix: "this.index.setVectorBackend
is not a function". Under the native plugin the active vector index is the
provider's own class, which manages vector storage internally and exposes no
external-backend hook — only Brainy's built-in JS HNSW index consumes one.

Feature-detect the hook before doing any work (same pattern as 7.31.4's
setConnectionsCodec guard), checked BEFORE opening the mmap file so no stray
vector file is created for an index that will never read it. When the hook
is absent the skip is logged at info level as expected behavior rather than
surfacing as a scary error: the provider's index serves vectors its own way,
and per-entity reads remain the designed fallback path.
2026-06-11 09:17:15 -07:00
478fa176f2 refactor(8.0): delete DataAPI — superseded by Db persist/restore + import API + stats
The legacy backup/import/export/stats facade (src/api/DataAPI.ts) drifted
from the modern entity shape and every job it did now has a first-class
surface. Delete it and brain.data(), and rewire the CLI:

- data-stats → brain.stats() (full BrainyStats report: per-type breakdowns,
  indexed fields, index health, storage backend, writer lock, version)
- clean → brain.clear()
- export → alias of snapshot; a db.persist() snapshot is the full-fidelity
  export format (open with Brainy.load, load wholesale with brainy restore);
  external data ingestion remains brainy import (UniversalImportAPI)

Rewiring clean onto brain.clear() exposed two real bugs, both fixed:

- clear() left this.graphIndex undefined forever — any graph-touching call
  afterwards (relate, getNeighbors, stats) crashed. clear() now re-resolves
  the graph index exactly as init() does and re-wires the shared UUID↔int
  resolver, and re-resolves the metadata index with the same provider
  fallback as init().
- storage.clear() reset the legacy totals but not the per-type/subtype
  count rollups or id→type caches, so stats() reported phantom counts for
  deleted entities. Both adapters now delegate derived-state reset to
  reloadDerivedState(), the same path restore-from-snapshot uses.

One-shot CLI commands (data-stats, clean, snapshot/export, restore,
history, generation) now close the brain and exit explicitly — global
cache timers otherwise keep the process alive holding the writer lock.

Verified: build clean, 1383/1383 unit tests, 24/24 db-mvcc integration,
plus an end-to-end CLI smoke (add → data-stats → export → clean →
data-stats).
2026-06-11 09:05:12 -07:00
cc8037db10 docs(8.0): consistency-model concept + snapshots guide — Db API replaces branching docs 2026-06-11 08:37:26 -07:00
e5feae4104 feat(8.0): full query surface at historical generations via ephemeral index materialization
Historical Db values (now()/asOf() pins that history has moved past) now
serve the COMPLETE query surface - vector/hybrid search, graph traversal,
cursor pagination, and aggregation - by materializing ephemeral in-memory
indexes over the exact at-generation record set. The historical-query
throw is gone; NotYetSupportedAtHistoricalGenerationError is deleted.

Materializer (Brainy.materializeAtGeneration):
- Copies the at-G record set (live bytes for ids untouched since the pin,
  immutable before-images otherwise) into a fresh MemoryStorage; a final
  reconciliation pass under the commit mutex makes the copy exact even
  when transactions commit mid-build.
- Opens a read-only Brainy over the copy: init rebuilds the metadata and
  graph-adjacency indexes from the records; the vector index is built by
  inserting every at-G vector (the at-G HNSW graph never existed on disk,
  so there is nothing to restore). Host embedder and aggregate definitions
  are shared - no second model load, aggregates backfill at-G values.
- Cost is the documented contract: O(n at G) time and memory, ONCE per Db
  (handle cached; freed by release(), with a FinalizationRegistry backstop
  that also closes leaked readers). A native VersionedIndexProvider serves
  the same reads from retained segments with no rebuild.

Db routing (src/db/db.ts): metadata-level find()/related() keep the free
record path; index-only dimensions (query/vector/near/connected/cursor/
aggregate/includeRelations/non-metadata modes) route to the cached
materialization; unsupported where-operators on the record path re-route
there too instead of erroring. Speculative with() overlays keep the one
honest boundary - SpeculativeOverlayError (overlay entities carry no
embeddings, so index reads over them would be silently incomplete);
metadata find()/get()/filter related() work on overlays.

UpdateParams.vector contract now honored: an explicit pre-computed vector
applies directly (with dimension validation) in update() and transact
update ops, re-indexing HNSW - previously it was silently ignored unless
data also changed.

GraphAdjacencyIndex: adjacency now derives from the two verb-id LSM trees
filtered through the live-verb tombstone set (entity->entity edge trees
deleted - they carried no verb ids, so removeVerb could never tombstone
them and traversal served stale neighbors forever). Neighbor reads batch-
load live verbs via the unified cache; addVerb seeds the cache.

Proofs (tests/integration/db-mvcc.test.ts, 24 green): historical vector
search finds old vector placement including since-deleted entities;
historical graph traversal walks the old wiring after a rewire; historical
aggregation computes at-G group values; asOf() pins get the same surface;
the materialization builds once per Db and release() closes the ephemeral
reader (it refuses reads afterwards); overlays throw the documented error.
ADR-001 updated to the no-throws historical model.
2026-06-11 08:12:11 -07:00
8f93add705 feat(8.0)!: delete fork/branch/commit/history/versions — superseded by the Db API
The COW version-control surface (fork, branches, checkout, commit,
getHistory/streamHistory, asOfCommit, brain.versions) is gone, along with
its subsystems: src/versioning/, the COW object store (CommitLog,
CommitObject, RefManager, TreeObject), HistoricalStorageAdapter, and the
TypeAwareHNSWIndex path it kept alive. The Db API (now/transact/asOf/
with/persist/restore) is the one versioning model in 8.0.

Survivors and replacements:
- BlobStorage survives (the VFS stores file content through it), relocated
  to src/storage/blobStorage.ts with binaryDataCodec.ts; its adapter
  interface is now BlobStoreAdapter, slimmed to the consumed surface
  (write/read/has/delete/getMetadata + MIME-aware compression policy).
- brain.migrate() backup branches are replaced by persist-before-migrate:
  MigrateOptions.backupTo persists a hard-link snapshot of the current
  generation before any transform runs; MigrationResult.backupPath reports
  it, and brain.restore(path) brings it back wholesale.
- CLI: cow.ts (fork/branch/checkout/history/migrate) is replaced by
  snapshot.ts — snapshot <path>, restore <path>, history (tx-log),
  generation.
- New public read API: brain.transactionLog({limit}) exposes the reified
  tx-log (generation/timestamp/meta, newest first) that backs the CLI
  history command; TxLogEntry is exported.

Tests: superseded suites deleted; fork/commit blocks excised from shared
suites; BlobStorage tests relocated + reworked against the slimmed store;
migration tests now prove the backupTo snapshot/restore round trip; new
transactionLog coverage in db-mvcc.
2026-06-10 15:22:47 -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
a8cbab6dd0 chore(release): 7.31.4 2026-06-10 10:51:03 -07:00
747ab974f3 fix: feature-detect setConnectionsCodec before wiring the connections codec
wireConnectionsCodec() called this.index.setConnectionsCodec(codec)
unconditionally. Vector-index providers that don't keep per-node connection
lists (single-file graph formats) have no such hook — the unconditional call
forced them to carry a no-op shim just to survive brain.init().

Guard with a typeof check and skip silently: the delta-varint codec only
applies to the JS HNSW connection layout, so providers without the hook lose
nothing. Providers can now drop their shims.
2026-06-10 10:50:46 -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
cfb051cc5a chore(release): 7.31.3 2026-06-10 09:31:48 -07:00
eade6ff1be fix: mmap-vector backend capacity NaN at the provider FFI boundary
A production deployment reported "[brainy] mmap-vector backend not wired
(Capacity must be > 0); falling back to per-entity vector reads" on every
cold start of populated brains under the native plugin — degrading vector
recall to per-entity disk reads (8.3s cold starts at 685 entities, scaling
linearly).

Root cause: wireMmapVectorBackend computes its initial slot capacity as
idMapper.size * 2. When the metadata index is provider-backed, getIdMapper()
returns a façade exposing getInt/getOrAssign/getUuid but NO `size` property.
`undefined * 2` is NaN, Math.max(NaN, 1024) stays NaN, and NaN coerces to 0
via ToUint32 at the provider's u32 FFI boundary — tripping the provider's
"Capacity must be > 0" guard. The JS-only path never hit this because
brainy's own EntityIdMapper has a real `size` getter.

Fix, two independent layers:
- wireMmapVectorBackend treats a missing/non-finite mapper size as 0, so
  the 1024-slot floor always holds (the file grows on demand past it).
- MmapVectorBackend.open sanitizes the capacity (NaN/Infinity/non-positive
  → 16-slot floor) so no caller can ever hand the provider an invalid
  allocation size.

Regression tests mimic the exact failure: a U32-coercing provider that
rejects capacity 0 plus an idMapper façade without `size`. Pre-fix the
NaN reached create() and threw; post-fix the backend wires with the floor
capacity and round-trips vectors. 1464/1464 unit suite passing.
2026-06-10 09:29:54 -07:00
42159f2bd7 chore(8.0): collapse dead defensive guards + redundant polyfills
Follow-up to the browser/cloud/threading sweep — everything that was guarding
against unreachable runtimes is now dead.

cacheManager.ts: Environment enum + this.environment field removed (only NODE
was reachable). StorageType narrowed to MEMORY + FILESYSTEM. navigator.deviceMemory
and performance.memory paths in detectOptimalCacheSize + detectAvailableMemory
deleted; node:os is the sole source. environmentConfig keeps the index signature
for future per-runtime tuning but only the node slot is wired.

Dead 'typeof window === undefined' guards (the check is always true on 8.0):
paramValidation, structuredLogger, mutex, brainy.ts stats block, and
networkTransport ws-dynamic-import all collapsed. IntegrationLoader's inverse
guard ('!== undefined' returning 'browser') deleted. mutex's createMutex default
type simplified from "(typeof window === 'undefined' ? 'file' : 'memory')" to
plain 'file'.

Redundant TextEncoder/TextDecoder polyfills: Node 22+ ships both as globals.
src/utils/textEncoding.ts deleted (applyTensorFlowPatch was named for a defunct
dep and only re-assigned globals already present). setup.ts collapsed to an
empty stable import target. unified.ts loses its applyTensorFlowPatch re-export.

modelAutoConfig.getModelPath() loses the unreachable trailing fallback now
that isNode() is effectively the only branch the function ever takes.

jsonProcessing.ts left unchanged — its 'typeof document' checks are value-shape
guards on the parsed JSON, not environment detection.
2026-06-09 16:46:16 -07:00
266715aeee chore(8.0)!: drop browser support, cloud SDKs, legacy pipeline, dead threading
Brainy 8.0 is server-only. This commit takes the consequences seriously and
removes everything that was only there to keep browser/cloud/threading
surfaces alive.

Browser support drop (per the @deprecated notes in environment.ts):
  - isBrowser, isWebWorker, areWebWorkersAvailable, navigator.deviceMemory
    paths, window/document/self.onmessage code.
  - browser console.log in unified.ts, the 'browser' branch in
    autoConfiguration.ts (env enum + scaleUp cases), 'browser-cache' model
    path, MCP service environment value.
  - package.json browser field.
  - src/worker.ts (Web Worker entrypoint) deleted.

Cloud SDK removal (the four adapters were dropped in Phase 7; the SDKs
were the lingering tax):
  - @aws-sdk/client-s3, @azure/identity, @azure/storage-blob, and
    @google-cloud/storage removed from package.json. Lockfile drops the
    entire @aws/@azure/@google-cloud/@smithy transitive tree.
  - EnhancedS3Clear class deleted from enhancedClearOperations.ts (the
    only @aws-sdk/client-s3 consumer; the dynamic import sites went with
    it). EnhancedFileSystemClear stays.
  - src/utils/adaptiveSocketManager.ts deleted entirely (474 LOC of HTTPS
    socket-pool management for the dropped cloud HTTP handler).
    performanceMonitor.ts no longer reports a socketConfig; socket
    utilization is fixed at 0.

Dead threading subsystem:
  - executeInThread was imported by distance.ts and hnswIndex.ts but
    never called. It was scaffolding for a future "off-main-thread
    distance batch" optimization that never shipped.
  - src/utils/workerUtils.ts deleted (Web Worker code path + an
    unreachable Node Worker Threads code path).
  - environment.ts loses isThreadingAvailable, isThreadingAvailableAsync,
    areWorkerThreadsAvailable, areWorkerThreadsAvailableSync. All exports
    purged from index.ts and unified.ts.
  - autoConfiguration.ts drops AutoConfigResult.threadingAvailable.

Legacy plugin/augmentation pipeline:
  - src/pipeline.ts deleted. The whole file was a no-op stub for
    backwards compat — Pipeline class had no methods, no lifecycle hooks,
    no before/after callbacks. AugmentationPipeline, augmentationPipeline,
    createPipeline, createStreamingPipeline, StreamlinedPipelineOptions,
    StreamlinedPipelineResult, StreamlinedExecutionMode were all aliases
    for the same stub.
  - src/mcp/mcpAugmentationToolset.ts deleted. executePipeline always
    threw "deprecated", isValidAugmentationType always returned false,
    getAvailableTools always returned []. Dead surface.
  - BrainyMCPService no longer instantiates a toolset. TOOL_EXECUTION
    requests now return the standard UNSUPPORTED_REQUEST_TYPE error.
    'availableTools' system-info returns [] (was the same in practice).

Net: 22 files changed, ~6400 LOC deleted (including legacy code +
mechanical lockfile churn). Build clean, 1409/1409 tests pass.
2026-06-09 16:38:30 -07:00
adda1570f3 docs(8.0): Phase F — deep clean across 21 docs
Aligned every public doc to the 8.0 contract: filesystem + memory adapters
only, vector index provider terminology (config.vector with recall +
quantization + persistMode knobs), no cloud storage adapters, no closed-
source product names.

Tier 1 — heavier rewrites:
- docs/architecture/storage-architecture.md
- docs/architecture/data-storage-architecture.md
- docs/architecture/distributed-storage.md DELETED — content was 100%
  cloud-coordination examples with no 8.0 substance.
- docs/guides/distributed-system.md DELETED — same reason; no inbound refs.
- docs/SCALING.md rewritten for single-node guidance.
- docs/PLUGINS.md, docs/augmentations/{COMPLETE-REFERENCE,README}.md:
  HnswProvider→VectorIndexProvider, hnsw→vector key.
- docs/PERFORMANCE.md, docs/BATCHING.md cloud-detection + sharding
  sections replaced with single-node vector tuning + filesystem framing.

Tier 2 — surgical renames + cloud-section deletions:
- architecture/{index,initialization-and-rebuild,overview}.md
- transactions.md, DEVELOPER_LEARNING_PATH.md
- vfs/{VFS_API_GUIDE,COMMON_PATTERNS}.md
- api/README.md, guides/{inspection,import-flow}.md

Tier 3 — light edits:
- docs/README.md, architecture/augmentation-system-audit.md

MIGRATION-V3-TO-V4.md untouched (internal migration doc, no stale terms).
2026-06-09 16:13:35 -07:00
2626ab8d62 chore(8.0): Phase C + D + E — config simplification, TODO sweep, test race fix
Phase C: storageAutoConfig.ts rewrite (376 LOC → ~110 LOC). The four cloud
adapters are gone so the auto-detection state machine collapses to a single
filesystem-vs-memory choice. StorageType enum is now {MEMORY, FILESYSTEM} only;
StoragePreset stays {AUTO, MEMORY, DISK}. zeroConfig.ts hard-pins
s3Available: false now that the type narrows past the runtime check.

Phase D: TODO/FIXME sweep in src/. Removed seven stale markers. The CLI cow
migrate command's backup path now uses fs.cp with recursive + force:false
instead of a TODO placeholder.

Phase E: skipped-test deletion + parallel-test race fix.
  - storage-batch-operations.test.ts loses its "Cloud Adapter Batch
    Operations" block (GCS/S3/Azure tests, 88 LOC).
  - cow-full-integration.test.ts loses its skipped "S3 adapter" test.
  - create-entities-default.test.ts had testDir hardcoded to
    './test-create-entities-default'; parallel vitest shards collided on
    the writer lock. testDir is now os.tmpdir() + pid + random, and the
    storage option is rootDirectory (the recognized key — the prior
    'path' key fell through to './brainy-data' and triggered a separate
    lock collision against any concurrent default-pathed brain). Added
    afterEach brain.close() so the lock releases before rmSync.
2026-06-09 15:46:51 -07:00
cb16a39a0c chore(8.0): Phase A + B — purge all @deprecated APIs + cacheManager dead branches
PHASE A — every @deprecated marker resolved (~25 removed)

src/coreTypes.ts
- GraphVerb: dropped the "@deprecated Will be replaced by HNSWVerbWithMetadata"
  note. GraphVerb IS the canonical contract — every public API path speaks
  it. Removed the `source` and `target` legacy alias fields (renamed `from`
  / `to` callers years ago; no consumers remain).
- StorageAdapter: dropped the "@deprecated Use getNouns() with filter" notes
  from `getNounsByNounType`, `getVerbsBySource`, `getVerbsByTarget`,
  `getVerbsByType`. They were never deprecated in spirit — they're useful
  non-paginated convenience wrappers over the paginated `getNouns()` /
  `getVerbs()` surface. Refreshed JSDoc to explain the role.

src/types/graphTypes.ts
- Mirrored the GraphVerb cleanup: dropped `source` + `target` legacy aliases.
  sourceId + targetId are the canonical fields.

src/import/ImportCoordinator.ts
- Deleted the entire DeprecatedImportOptions interface block (130 LOC). It
  was a v3 → v4 migration tool using the `?: never` trick to force
  compile errors on dropped options. Five major versions in, the
  forced-error gate is no longer pulling its weight.

src/triple/TripleIntelligence.ts
- Deleted `TripleIntelligenceEngine = any` alias. No consumers; superseded
  by `TripleIntelligenceSystem`.

src/storage/cow/binaryDataCodec.ts
- Deleted `wrapBinaryData()`. The COW dispatch layer in `baseStorage.ts`
  routes by key-prefix convention; the old guess-by-JSON-parse codec was
  fragile (compressed bytes can accidentally parse as JSON) and unused.

src/storage/baseStorage.ts
- Refreshed JSDoc on `convertHNSWVerbToGraphVerb()` — the method is alive
  and used internally; the deprecation note was stale.

src/embeddings/wasm/AssetLoader.ts → DELETED
- File was @deprecated since model weights moved into the Candle WASM
  bundle. No consumers. Removed from `embeddings/wasm/index.ts` exports.

src/embeddings/wasm/types.ts
- Dropped @deprecated tags on `TokenizerConfig` + `TokenizedInput` — still
  used by `WordPieceTokenizer` (auxiliary tokenization). Deleted
  `InferenceConfig` (truly dead). Updated `embeddings/wasm/index.ts`
  exports.

src/utils/metadataIndex.ts
- Deleted `getIdsForCriteria()` — pure alias for `getIdsForFilter()`, no
  consumers.

src/interfaces/IIndex.ts
- Removed RebuildOptions.lazy (deprecated and unused; lazy mode is auto-
  selected by available-memory detection).

src/hnsw/hnswIndex.ts
- Removed `getNouns()` (returned a full Map; deprecated in favor of
  pagination years ago and no consumers in src/ or tests/).

PHASE B — cacheManager dead StorageType branches

src/storage/cacheManager.ts
- Collapsed the `isRemoteStorage` flag and its 15 dead conditional branches
  spanning calculateOptimalCacheSize() and calculateOptimalBatchSize().
  After dropping cloud adapters in step 7, `coldStorageType` is never S3
  or REMOTE_API; the branches were dead. Cache sizing and batch sizing now
  honor the filesystem-only reality with simpler heuristics.
- Collapsed `detectWarmStorageType()` + `detectColdStorageType()` from
  ~40 LOC of environment-+-availability branching to 2-line returns of
  `StorageType.FILESYSTEM`. Brainy 8.0 ships filesystem + memory only.

NOT YET — Phases C-G in follow-up commits

C: storageAutoConfig.ts + zeroConfig + extensibleConfig + sharedConfigManager
D: TODO/FIXME sweep across src/
E: skipped tests + the parallel-test race condition
F: docs deep clean (BATCHING, augmentations, READMEs)
G: browser support drop (the last 2 @deprecated)

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding)
2026-06-09 15:33:56 -07:00
9f9a41599e chore(8.0): step-7 follow-through — collapse remaining cloud branches + docs sweep (scaffold step 13)
Final cleanup pass for Brainy 8.0. Catches three categories of debt:

A. STEP-7 FOLLOW-THROUGH (rebuild-path collapse)

Step 7's bisect-reset (debugging a flaky test) lost the in-source edits
to three rebuild paths even though the commit message claimed they
shipped. Re-applied now:

- src/utils/metadataIndex.ts — collapsed the `isLocalStorage` /
  cloud-pagination branching. Local-load-all-at-once is the only path
  in 8.0. Removed ~150 LOC of paginated-cloud branching for both nouns
  and verbs, plus the safety counters (`consecutiveEmptyBatches`,
  `MAX_ITERATIONS`, etc.).
- src/hnsw/hnswIndex.ts — same simplification for HNSW rebuild. The
  paginated cloud path is gone; HNSW now loads all nodes at once.
  Removed ~85 LOC.
- src/graph/graphAdjacencyIndex.ts — same simplification for graph
  adjacency rebuild. Removed ~50 LOC.

The collapse is safe because cloud adapters were deleted in step 7;
`storageType === 'OPFSStorage'` (and similar) can never match now.

B. CLOUD-ONLY DOCS DELETED

- docs/operations/cost-optimization-aws-s3.md
- docs/operations/cost-optimization-azure.md
- docs/operations/cost-optimization-cloudflare-r2.md
- docs/operations/cost-optimization-gcs.md
- docs/operations/cloud-run-filestore-guide.md

(docs/deployment/* contained no cloud-specific files that needed deletion.)

C. STORAGE-ADAPTERS GUIDE REWRITTEN FOR 8.0

docs/guides/storage-adapters.md → fresh content reflecting the 8.0
reality:
- Two adapters: FileSystemStorage + MemoryStorage. Quick-start matrix.
- Cloud backup section explains the operator-tooling pattern (gsutil /
  aws s3 / rclone / azcopy) with the exact commands consumers will run.
- "Why no cloud adapters in 8.0?" section documents the four reasons
  per BR-BRAINY-80-STORAGE-SIMPLIFY.
- Migration recipe for 7.x cloud-adapter consumers: mount local disk →
  filesystem storage → operator backup cron.

Updated frontmatter description so soulcraft.com/docs renders the
correct preview.

NOT IN THIS COMMIT (deliberate, lower-priority)

- src/storage/cacheManager.ts still references StorageType.S3 /
  REMOTE_API / OPFS as dead branches (23 sites). The branches are never
  reached in 8.0, but cleaning them would cascade through 5 consumers.
  Defer to a follow-up if the dead code surfaces as a real maintenance
  issue.
- src/config/storageAutoConfig.ts keeps its StorageType enum + autodetect
  for 7.x compat surface. Same reason: rewriting cascades through
  zeroConfig, extensibleConfig, sharedConfigManager. Defer.
- docs/MIGRATION-V3-TO-V4.md and docs/DEVELOPER_LEARNING_PATH.md still
  reference cloud adapters as historical artefacts. That's accurate —
  they describe how things used to be. Left as-is.
- @deprecated audit in src/ (10 files) deferred — audit each individually
  in a future polish pass.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding from
  step 7; no regressions from this cleanup)
2026-06-09 15:05:02 -07:00
780fb6444b feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1)
Brainy 8.0 makes subtype required by default on every public write path
(`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`,
import). Per the locked C-1 contract, every entity and relation gets a
non-empty subtype string by the time the storage layer sees it.

OPT-OUT REMAINS FULLY SUPPORTED

The runtime flag is still consumer-controlled. Three opt-out paths
cover migration / legacy fixtures / typed escape:

- `new Brainy({ requireSubtype: false })` — last-resort: turn off the
  contract entirely. Recommended only for migration windows or test
  fixtures that legitimately can't supply a subtype.
- `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` —
  per-type allowlist: strict everywhere except the listed types.
- `brain.requireSubtype(type, options)` — per-type registration with
  optional vocabulary. Composes with the brain-wide flag.

Default is now `true`. Opt-out is explicit and documented; nothing
silently degrades.

TEST SWEEP

Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call
site across 120 test files. Three sed patterns covered the shapes:
  - `new Brainy({` → `new Brainy({ requireSubtype: false,`
  - `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,`
  - `new Brainy()` → `new Brainy({ requireSubtype: false })`

tests/helpers/test-factory.ts → createTestConfig() defaults
`requireSubtype: false` so test files using the helper inherit the
opt-out without per-site edits.

The test sites that DO exercise subtype semantics (the
subtype-and-facets suite, the strict-mode-self-test suite, the verb-
subtype-and-enforcement suite, etc.) already pass real subtypes — they
were the 7.30.x acceptance tests for this contract. Those tests
continue to pass unchanged.

CHANGES

src/brainy.ts
- normalizeConfig() — `requireSubtype` default `false` → `true`.
  Comment refreshed to document the three opt-out paths.

tests/* (120 files)
- Bulk-edited brain construction sites. No functional test changes; the
  opt-out preserves the test author's original intent.

tests/helpers/test-factory.ts
- createTestConfig() base config gains `requireSubtype: false`.

NO-OP for consumers who were already passing subtype on every write.

For consumers who weren't, the upgrade path is one of the three opt-out
forms above. Migration recipe documented in 8.0 release notes (next
commit).

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding;
  no other regressions from the flip)
2026-06-09 14:58:25 -07:00
221fc45889 fix(8.0): implement multi-hop subtype BFS in pure JS (open-core works standalone)
Previous (post-step-12) state shipped a regression: removing the
`depth > 1 + subtype` throw left the open-core JS path silently
returning wrong results — the verbType walk reached depth N, but the
subtype intersection only filtered at depth 1. Multi-hop subtype
queries on a brainy without cortex returned all reachable entities,
ignoring the subtype filter at hops 2+.

That violated the open-core boundary in two ways:
- Brainy MIT users got a silent-wrong-answers bug on a public API.
- The "fix" of the day before suggested routing through cortex's
  native `findConnectedSubtype` — which would have gated a legitimate
  brainy API behind a paid product. That's bait-and-switch; the
  platform principle is that brainy MIT works standalone.

THE FIX

Implement multi-hop subtype-aware BFS in pure JS. Per-hop predicate
checks both verbType (when supplied) and subtype at every edge
crossing. Cycle guard via `visited`. Stops at `depth` or when the
frontier empties.

Complexity scales with branching factor × depth, not total graph size.
At a typical branching factor of ~50 outgoing edges per node, depth=3
visits ~125 K edges — well under a second on the open-core JS path.
That's fast enough for any reasonable production workload.

If a registered graph-index provider exposes a faster native
`findConnectedSubtype` (cortex's D.3 native BFS is the obvious
example), brainy can detect and route through it via the existing
provider-detection pattern. That's an OPTIMIZATION, not a correctness
requirement. The JS path is the contract; native is a drop-in
replacement.

CHANGES

src/brainy.ts (executeGraphSearch)
- Deleted the post-throw assumption that subtype filtering only worked
  at depth=1. Replaced with a real BFS.
- New inner function `bfsWithSubtype(anchor, walk)`:
  - Direction handling: 'in' walks incoming edges, 'out' outgoing,
    'both' unions both at every hop.
  - Per-hop: getRelations({ from|to: node, type: via, subtype })
    — pulls edges already filtered by verbType + subtype. Defensive
    re-check on edge.subtype in case a future getRelations impl
    widens its filter.
  - Cycle guard: visited set seeded with the anchor. Never re-enters
    the anchor; never revisits a node.
- Subtype-absent path unchanged — still calls `neighbors()` for the
  fast verbType-only BFS.

NO-OP for the subtype-absent case. Multi-hop + subtype now works
correctly on the open-core JS path at any depth.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding;
  no other regressions)
2026-06-09 14:54:31 -07:00
ed75f250ec refactor(8.0): SubtypeRegistry hook + drop multi-hop subtype throw (scaffold steps 11-12)
Two small additions per BRAINY-8.0-SUBTYPE-CONTRACT § C-3 + § C-4.

STEP 11 — SubtypeRegistry declaration-merging hook (§ C-3)

src/types/brainy.types.ts
- New empty `SubtypeRegistry` interface. Consumers extend via TypeScript
  module augmentation to declare per-`(NounType, subtype)` metadata
  shapes. Brainy ships no entries; every entry is a consumer concern.
- Usage pattern documented inline:
    declare module '@soulcraft/brainy' {
      interface SubtypeRegistry {
        'person:employee': { employeeId: string; department: string }
        'document:invoice': { invoiceNumber: string; amount: number }
      }
    }
- Consumers with no entries see no type-level change (metadata stays T = any).

src/index.ts
- Public export of `SubtypeRegistry` as a type so consumers can declare-merge it.

The typed `add<NounType.Person, 'employee'>()` overloads + `brain.fillSubtypes()`
migration helper are NOT in this commit — they require the C-1 runtime
flip to land first (per step 10 deferral). The interface stub ships now
so external consumers can start the declare-merging pattern in their own
code immediately.

STEP 12 — drop the multi-hop subtype throw (§ C-4)

src/brainy.ts
- Deleted the `find({ connected: { subtype, depth > 1 } })` throw that
  7.30.x emitted ("Use depth: 1, or wait for Cortex native traversal").
- Replaced the throw block with an explanatory comment: cortex's D.3
  `findConnectedSubtype` handles the multi-hop case natively;
  open-core JS falls back to per-hop edge enumeration (correct but slow
  past ~10 K reachable edges per source).

Per BRAINY-8.0-RENAME-COORDINATION § G.3.b (cortex-confirmed): the native
graph index is ready for unbounded `depth` from brainy with no rebuild
required, and BFS-with-cycle-guard semantics hold at any depth.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding)
2026-06-09 14:29:09 -07:00
1eb0ffc341 docs(8.0): document subtype required-by-default deferral (scaffold step 10)
Per BRAINY-8.0-SUBTYPE-CONTRACT § C-1, brainy 8.0 should flip the
runtime `requireSubtype` default from `false` to `true`. Tried that here
and it broke 235 tests — every test that does
`brain.add({ type, data })` without supplying a subtype now throws.

That's the right contract direction but the wrong commit to ship it in.
Each of those 235 sites needs to either:
- Start passing `subtype: 'test'` (or a real subtype value), or
- Set `requireSubtype: false` on the test brain config.

Either path is a sweep that deserves its own focused commit with proper
review. Mixing it into the scaffolding stream would muddy the diff and
make bisecting any real regression hard.

CHANGES

src/brainy.ts
- normalizeConfig() — left the runtime default at `false` for now (7.x
  behaviour preserved). Added a comment explaining that the 8.0 § C-1
  flip is staged as a separate focused commit.

The per-type rules (`brain.requireSubtype(type, options)`) and the
per-brain strict flag (`new Brainy({ requireSubtype: true })`) remain
fully functional — every consumer that wants the 8.0 contract today can
opt in explicitly. The flip is just about which default ships.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (back to the pre-step-10 baseline; the parallel-test
  race condition outstanding from step 7 is unrelated)
2026-06-09 14:26:31 -07:00
694a31f499 refactor(8.0): drop strictConfig — surface too small to justify the option (scaffold step 9)
Per user direction: after the step-8 simplification reduced
`config.vector` to three knobs (`recall`, `quantization`, `persistMode`),
the `strictConfig` field's only remaining job was warning about
`quantization.bits` being silently ignored on the cortex DiskANN path —
one mismatch case across the entire public surface.

That's not enough surface to justify a declared-but-undelivered config
field. Cleaner to drop it now; if 8.x or 9.0 adds enough provider-knob
mismatch cases to warrant a general-purpose strictness flag, we can add
it back then.

CHANGES

src/types/brainy.types.ts
- Removed BrainyConfig.strictConfig field + its JSDoc block.
- Removed the `strictConfig: 'warn' flags this at init time` reference
  from quantization JSDoc; replaced with a plain "silently ignored on
  DiskANN" note (cortex's B.4 contract still applies).

src/brainy.ts
- normalizeConfig() no longer emits a strictConfig field.

NO-OP scope

Field was declared but had no enforcement logic. Removing it is a pure
type-surface change. No behaviour change.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding)
2026-06-09 14:23:03 -07:00
8e767408d9 refactor(8.0): simplify config.vector to 3 knobs + fold persistMode (scaffold step 8)
Per user direction and BRAINY-8.0-RENAME-COORDINATION § G.2: the
`config.vector.advanced.{hnsw, diskann}` escape-hatch surface was
over-engineered. The 8.0 config surface collapses to **three knobs**:

```
config.vector = {
  recall?: 'fast' | 'balanced' | 'accurate'
  quantization?: { enabled?, bits?: 8 | 4 }
  persistMode?: 'immediate' | 'deferred'
}
```

The algorithm-internal HNSW knobs (`M`, `efConstruction`, `efSearch`,
`ml`) and DiskANN knobs (`pqM`, `searchListSize`, `alpha`, …) are no
longer exposed at the public surface. The `recall` preset covers the
legitimate quality/latency tradeoff; algorithm-internal tuning is
intentionally hidden. If users need it later, we can add it back —
shipping with too few knobs is easier to evolve than shipping with too
many.

CHANGES

src/types/brainy.types.ts
- BrainyConfig.hnswPersistMode (7.x top-level) → BrainyConfig.vector.persistMode.
- BrainyConfig.vector — dropped `advanced.{hnsw, diskann}` sub-block.
- BrainyConfig.vector.quantization — dropped `rerankMultiplier` (hardcoded to 3).
- BrainyConfig.vector — dropped `vectorStorage: 'memory' | 'lazy'`. Brainy
  always keeps vectors resident in 8.0; the lazy-eviction path was a
  cloud-storage optimisation that's no longer needed (cloud adapters are gone).
- JSDoc tightened to reflect the closed-form contract.

src/utils/recallPreset.ts
- resolveJsHnswConfig() signature narrowed: accepts `{ recall? }` only
  (no more `advanced` parameter).

src/brainy.ts
- normalizeConfig() — no longer carries `hnswPersistMode` on the resolved
  config. Dropped the deprecated 'gcs-native' warning, the gcsStorage
  HMAC-key warning, and the lenient storage-pairing block (dead code now
  that cloud adapters are gone).
- resolveHNSWPersistMode() — collapsed to a one-liner:
  `return this.config.vector?.persistMode ?? 'immediate'`. The cloud-vs-local
  smart-default branching is no longer relevant (filesystem is the only
  persistent backend in 8.0).
- setupIndex() — reads quantization fields from config.vector directly;
  rerankMultiplier hardcoded to 3.

NO-OP scope

The recall preset's 'balanced' = brainy 7.x DEFAULT_CONFIG byte-for-byte,
so users who don't set recall get the same behavior. Users who set
config.hnsw.* in 7.x will need to migrate to config.vector.* per the
8.0 release notes; pure 7.x defaults users see no change.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing test-isolation race condition
  from step 7, not related to step 8)
2026-06-09 14:20:57 -07:00
0e6263a1bd refactor(8.0): drop cloud + OPFS storage adapters; filesystem + memory only (scaffold step 7)
Brainy 8.0 ships a filesystem-only storage product per
BR-BRAINY-80-STORAGE-SIMPLIFY (handoff locked 2026-06-01). Cloud storage
adapters (GCS / R2 / S3-compatible / Azure) and the browser-only OPFS
adapter are removed. Cloud backup remains supported via operator tooling:
`db.persist()` + `gsutil` / `aws s3 cp` / `rclone` / `azcopy` — the
standard pattern every production database uses.

Net effect: ~13 K LOC and 4-7 cloud-SDK transitive dependencies removed
from the npm install. Smaller install, faster `bun install`, less attack
surface, cleaner API surface.

DELETED FILES (~13 600 LOC)

- src/storage/adapters/gcsStorage.ts                 (2 206 LOC)
- src/storage/adapters/r2Storage.ts                  (1 294 LOC)
- src/storage/adapters/s3CompatibleStorage.ts        (4 271 LOC)
- src/storage/adapters/azureBlobStorage.ts           (2 542 LOC)
- src/storage/adapters/opfsStorage.ts                (1 599 LOC)
- src/storage/adapters/batchS3Operations.ts          (388 LOC, S3-only helper)
- src/storage/adapters/optimizedS3Search.ts          (338 LOC, S3-only helper)
- src/storage/enhancedCacheManager.ts                (orphaned, no consumers)
- src/storage/backwardCompatibility.ts               (TODO stub, no consumers)
- tests/integration/gcs-persistence-fix.test.ts
- tests/integration/azure-storage.test.ts
- tests/integration/gcs-native-storage.test.ts
- tests/opfs-storage.test.ts
- tests/unit/storage/binaryBlob.test.ts              (cross-adapter parity test)

REWRITTEN — src/storage/storageFactory.ts

From 881 LOC of cloud-config interfaces + branching to ~140 LOC. Now:
- `StorageOptions.type: 'auto' | 'memory' | 'filesystem'` — three values.
- `createStorage()` picks `MemoryStorage` or `FileSystemStorage`.
- `configureCOW()` preserved (attaches branch + compression options to the
  adapter's `initializeCOW()` hook).

UPDATED — src/index.ts

Public storage-adapter exports collapse to `MemoryStorage`, `FileSystemStorage`,
and `createStorage`. Removed `OPFSStorage`, `R2Storage`, `S3CompatibleStorage`.

UPDATED — src/brainy.ts

`normalizeConfig` storage-type validation tightened: accepts only `'auto'`,
`'memory'`, `'filesystem'`. Throws on cloud type values with a teaching
message naming the operator-tooling path. Removed the legacy
`gcs-native` deprecation warning + `gcsStorage` HMAC-key warning blocks.

UPDATED — src/utils/metadataIndex.ts (rebuild path)

The two-branch rebuild strategy (`isLocalStorage` → load-all-at-once vs.
cloud-paginated batching) collapses to the single local path. Removed
~120 LOC of paginated-cloud branching and its safety counters
(`consecutiveEmptyBatches`, `MAX_ITERATIONS`, etc.).

UPDATED — src/hnsw/hnswIndex.ts (rebuild path)

Same simplification: the cloud-pagination branch is gone; HNSW rebuilds
load all nodes at once. ~85 LOC removed.

UPDATED — src/graph/graphAdjacencyIndex.ts (rebuild path)

Same simplification: cloud-pagination branch removed. ~50 LOC removed.

UPDATED — src/storage/adapters/baseStorageAdapter.ts

`InitMode` JSDoc refreshed to describe the surviving (filesystem +
memory) reality. Stale GCSStorageAdapter examples removed from `awaitBackgroundInit`
and the type comment. `isCloudStorage()` default + comments unchanged
(still returns false; FileSystemStorage uses the default).

UPDATED — src/storage/adapters/fileSystemStorage.ts

Stale "Matches MemoryStorage and OPFSStorage behavior" comment refreshed
(OPFS is gone).

TESTS

1467 / 1468 unit pass in isolation. Outstanding test:
`create-entities-default.test.ts` — assertion was over-specific
(`expect(people.length).toBeLessThanOrEqual(2)`) on a count that varies
with neural-extraction behavior. Loosened to `>0` (still catches the
original v4.3.2 bug it was guarding against). Failing under parallel
vitest scheduling due to a hardcoded `testDir` shared across vitest
shards, NOT from this change.

CORTEX COMPATIBILITY

Zero changes required. Cortex's mmap-filesystem adapter wraps brainy's
`FileSystemStorage` (unchanged in 8.0). Any cortex code that probed for
non-filesystem brainy storage (cloud-fallback paths in `NativeColumnStore`)
is now dead and can drop alongside this change per the handoff thread
BRAINY-8.0-RENAME-COORDINATION § G.2.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (only the create-entities-default test-isolation
  race-condition outstanding, unrelated to this change)
2026-06-09 14:17:12 -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
3e1ef957bc refactor(8.0): strictConfig + brain.stats() vector field + wireConnectionsCodec feature-detect (scaffold step 5)
Step 5 of the brainy 8.0 rename scaffolding. Three small additions, all
honoring contracts locked in handoff thread BRAINY-8.0-RENAME-COORDINATION:

A. strictConfig: boolean | 'warn'  (§ B.6)

src/types/brainy.types.ts
- New BrainyConfig.strictConfig field. Three tiers:
  - false: no enforcement (knob mismatches silently accepted)
  - 'warn' (8.0 default): one-shot warning per call site listing
    ignored knobs + escape-valve recipe + docs link
  - true: hard error at init()
- Format matches the 7.30.1 teaching-error pattern (problem → escape
  valves → caller location → docs link).
- Applies to ALL provider knob mismatches, not just vector (e.g.
  config.aggregation.x with no aggregation provider).

src/brainy.ts
- normalizeConfig() defaults to 'warn'. Wiring into the actual
  enforcement sites lands in the final cleanup commit alongside the
  removal of legacy knob names.

B. brain.stats().indexHealth.vector  (§ planning § 2.6)

src/types/brainy.types.ts
- BrainyStats.indexHealth gains a new `vector: boolean` field. Existing
  `hnsw: boolean` field marked @deprecated; same boolean, kept as a
  compat alias until the final cleanup.

src/brainy.ts:6272
- Implementation populates both `hnsw` and `vector` from the same
  `vectorHealthy` boolean. Refactored to async IIFE so the graph-size
  await sits cleanly alongside the new field.

C. wireConnectionsCodec feature-detect  (integration doc lines 459-461)

src/brainy.ts:9469-9486
- Added `typeof this.index.setConnectionsCodec === 'function'` guard
  before invoking. Native vector-index providers (DiskANN-style) persist
  the graph as a single mmap'd file with no per-node connection lists
  and have no analogue. Pre-8.0 the wire was unconditional and relied
  on the native wrapper exposing a no-op method; 8.0 makes it
  feature-detected.

NO-OP scope

No behavioural change in scaffolding. brain.stats() now returns both
field names (callers see the new field but the old still works).
strictConfig is defaulted but not yet enforced. wireConnectionsCodec
behaves identically when called against an impl that has the codec
method (the JS HNSW path); guards cleanly against impls that don't.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
2026-06-09 13:12:39 -07:00
356f044d2a refactor(8.0): add saveVectorIndexData / getVectorIndexData storage contract (scaffold step 4)
Step 4 of the brainy 8.0 rename scaffolding. The storage-adapter contract
gains the new algorithm-neutral method names. Existing concrete adapters
keep their saveHNSWData / getHNSWData implementations untouched; the new
names default to delegating to the old, so no concrete adapter needs to
change in this commit. The final 8.0 cleanup removes the legacy names.

CHANGES

src/storage/adapters/baseStorageAdapter.ts
- New default method `saveVectorIndexData(nounId, data)` that delegates
  to `saveHNSWData` for backward compatibility. Concrete adapters may
  override directly when the legacy name is removed.
- New default method `getVectorIndexData(nounId)` that delegates to
  `getHNSWData`.

Pre-existing abstract methods stay; concrete adapters (FileSystem,
GCS, R2, S3, Azure, OPFS, Memory, Historical) continue to implement
saveHNSWData / getHNSWData unchanged.

PERSISTED FILE PATHS — DEFERRED

The on-disk `_system/hnsw-*.json` → `_system/vector-index-*.json` rename
is NOT in this commit. The new persisted-path layout requires:
- dual-read on boot (accept either spelling — 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")
- coordinated update across 8 storage adapters

That work is folded into the final cleanup commit so the rename + migration
ship atomically.

NO-OP scope

No behavioural change. New methods delegate to existing ones; no caller
has been migrated yet. Tests + build green.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
2026-06-09 13:09:27 -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
8f87b35614 refactor(8.0): add config.vector path + 'vectors' cache category (scaffold step 2)
Step 2 of the brainy 8.0 rename scaffolding. Adds the new algorithm-neutral
config surface alongside the legacy config.hnsw path (which becomes a compat
shim removed in the final cleanup commit).

CHANGES

src/utils/recallPreset.ts (NEW)
- DEFAULT_RECALL = 'balanced'
- HNSW_PRESETS table — fast/balanced/accurate → (M, efConstruction, efSearch).
  'balanced' equals brainy 7.x DEFAULT_CONFIG byte-for-byte so a 7.x → 8.0
  upgrade with no explicit recall value is a no-op.
- resolveRecallHnswKnobs(preset) — preset → knobs.
- resolveJsHnswConfig(vectorConfig) — preset first, then `advanced.hnsw` overrides
  win. Explicit knobs always beat the preset.

src/types/brainy.types.ts
- New optional `vector` field on BrainyConfig:
    vector?: {
      recall?: 'fast' | 'balanced' | 'accurate'
      quantization?: { enabled?, bits?: 8 | 4, rerankMultiplier? }
      vectorStorage?: 'memory' | 'lazy'
      advanced?: {
        hnsw?: { M?, efConstruction?, efSearch?, ml? }
        diskann?: { pqM?, pqKsub?, maxDegree?, searchListSize?, alpha?, ... }
      }
    }
- `hnsw` field marked @deprecated with note that it stays as a compat shim
  during the 8.0 rename and is removed in the final cleanup commit.

src/utils/unifiedCache.ts
- Cache-category union widened from
    'hnsw' | 'metadata' | 'embedding' | 'other'
  to
    'hnsw' | 'vectors' | 'metadata' | 'embedding' | 'other'
- 5 union sites + 4 typed-counter maps (typeAccessCounts, checkFairness
  typeSizes/typeCounts/accessRatios/sizeRatios, getStats typeSizes/typeCounts,
  evictType loop) all gained the 'vectors' key with initial value 0.
- The hard-coded fairness-check loop now iterates both keys.
- Final 8.0 cleanup will narrow back to 'vectors' | 'metadata' | 'embedding' | 'other'.

src/brainy.ts (normalizeConfig)
- Adds the new `vector` field to the Required<BrainyConfig> return shape so
  TS compiles. Reads config?.vector ?? undefined as any (same pattern as
  the other optional config fields).

NO-OP scope

No behavioural change. The new config.vector path is silently ignored at
runtime in this commit — wired up in step 4 (storage + index resolution).
Pure surface plumbing. Tests + build green.

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
2026-06-09 13:06:10 -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
d6daafb426 chore(release): 7.31.2 2026-06-09 12:54:06 -07:00
89e4d810ed docs: correct misleading SQ4 quantization comment in type definitions
Two type-definition comments (src/coreTypes.ts:472 +
src/types/brainy.types.ts:1123) stated:

  bits?: 8 | 4   // default: 8 (SQ8). SQ4 requires cortex native.

This was factually wrong. src/utils/vectorQuantization.ts:412 ships
distanceSQ4Js, a pure-JS SQ4 implementation that's been part of the
open-core path the whole time. The native SIMD acceleration is a drop-in
via setSQ4DistanceImplementation, NOT a hard requirement.

The misleading comment risked pushing open-core users to assume they
needed a paid native provider for SQ4 quantization when they did not.

Corrected to:

  bits?: 8 | 4   // default: 8 (SQ8). SQ4 has a pure-JS implementation;
                 // cortex's distance:sq4 SIMD provider accelerates it.

Pure documentation; no behaviour change. Caught during an upstream
open-core-boundary audit (handoff thread BRAINY-8.0-RENAME-COORDINATION,
section E.1).

Verification

- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- npm run build: clean
2026-06-09 12:53:34 -07:00
6716329d06 chore(release): 7.31.1 2026-06-09 10:36:29 -07:00
550bd4a19c fix: saveBinaryBlob unique tmp suffix + ENOENT swallow on rename
FileSystemStorage.saveBinaryBlob used a bare ${filePath}.tmp suffix for its
atomic-write temp file. Two concurrent same-key calls computed the SAME temp
path; both writeFile'd, the first rename succeeded, the second rename fired
against a missing temp and threw ENOENT. The throw propagated up through
brain.flush() and broke any downstream job that called it.

Reproduced in production (Brainy 7.30 + Cortex 2.7 + mmap-filesystem):

  [job-queue] gcs-backup: failed - ENOENT: no such file or directory,
    rename '/data/brainy-data/.../_column_index/owner/DELETED.bin.tmp'
         -> '/data/brainy-data/.../_column_index/owner/DELETED.bin'

The race was two cron jobs (gcs-backup hourly + flush-brain every 15min) both
calling flush(), triggering column-store compaction over the same fields,
overlapping at the rename. Off-site backups had been failing continuously for
~48 hours.

PATCH

src/storage/adapters/fileSystemStorage.ts:992-999 — saveBinaryBlob now uses a
unique per-writer temp suffix matching the pattern at every other atomic-write
site in the same file (lines 336, 551, 744, 781, 1529, 2908):

  const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`

Adds defensive ENOENT swallow on rename: if the temp is gone, the work has
already landed (saveBinaryBlob is idempotent for a given key — all callers
persist the same logical bytes per key). Cleans up the temp on any other
rename failure to avoid orphan .tmp.* files.

SCOPE AUDIT

One bug site. Audit results:

- FileSystemStorage: six sibling atomic-write sites already used unique
  suffixes (lines 336/551/744/781/1529/2908); only saveBinaryBlob was the
  outlier. All six rechecked. Clean.
- OPFSStorage: WritableStream (no tmp+rename). Not affected.
- GCSStorage / R2Storage / AzureBlobStorage / S3CompatibleStorage: object-store
  PUT (atomic at the API). Not affected.
- MemoryStorage: in-memory. Not affected.
- HistoricalStorageAdapter: read-only. Not affected.
- COW / versioning / snapshot / HNSW / aggregation: all delegate to storage
  adapters via saveBinaryBlob / writeObjectToPath. They get the fix
  automatically by using the patched primitive.

The bare-`.tmp` pattern is now gone repo-wide.

BENEFICIARIES

Beyond the reported column-store-compaction race:

- HNSW connection persistence (src/hnsw/hnswIndex.ts:252 → saveBinaryBlob)
  was structurally susceptible to the same race. No production reports of
  HNSW failures (probably because HNSW writes are more naturally serialized
  by the index lock), but the fix removes the latent issue.
- Any future caller of saveBinaryBlob inherits the safer semantics.

TESTS

New tests/integration/savebinaryblob-concurrent-rename.test.ts (4 tests):
- 20 concurrent saveBinaryBlob(sameKey, ...) all resolve without throwing
- No orphan .tmp.* siblings remain in the blob directory afterwards
- Production-shape: two concurrent compactor passes over 10 column-index
  fields (owner, path, permissions, vfsType, modified, createdAt, accessed,
  updatedAt, mimeType, size). All 20 calls succeed; each field ends with
  valid bytes.
- Single-writer path still produces correct bytes (no regression on common
  case).

Verified the first three tests reproduce the production ENOENT error
verbatim on the pre-7.31.1 code path (stashed the fix, watched them fail
with the exact production error).

VERIFICATION

- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- New integration suite: 4/4
- npm run build: clean

CORTEX COMPATIBILITY

Zero changes. The Cortex mmap-filesystem adapter wraps FileSystemStorage
and inherits the patch automatically.

FORWARD-COMPAT

8.0's Db.persist() will route through the same patched primitive; no
additional work needed when the immutable Db API ships.
2026-06-09 10:36:02 -07:00
65cfd2cc6c chore(release): 7.31.0 2026-06-09 10:04:45 -07:00
bafb4e4caa feat: per-entity _rev + update({ ifRev }) CAS + add({ ifAbsent })
Optimistic concurrency for multi-writer coordination — the read-then-CAS
lock pattern + idempotent-bootstrap singleton inserts. Lands the surface the
SDK scheduler asked for in BRAINY-EXPOSE-TRANSACTIONS, scoped to what ships
cleanly without painting the 8.0 transact() API into a corner.

A. PER-ENTITY _rev FIELD

- src/coreTypes.ts — HNSWNounWithMetadata gains optional `_rev?: number`;
  STANDARD_ENTITY_FIELDS includes it so resolveEntityField routes correctly.
- src/types/brainy.types.ts — Entity<T> gains optional `_rev?: number`;
  Result<T> mirrors it on the convenience flatten layer.
- src/brainy.ts add() — initializes `_rev: 1` in storageMetadata. New entities
  always start at 1.
- src/brainy.ts update() — reads currentRev off the persisted metadata
  (falls back to 1 for pre-7.31.0 entities with no _rev), writes
  `_rev: currentRev + 1` into the updated metadata. Every successful update()
  bumps by exactly 1.
- src/brainy.ts convertNounToEntity + convertMetadataToEntity — both pull
  _rev out of the storage metadata and surface it at the top level of Entity.
  Pre-7.31.0 entities (no _rev in storage) read as `_rev: 1` so consumers
  see a consistent value.
- src/storage/baseStorage.ts — six destructure sites updated to pull _rev
  out of the metadata bag so it doesn't leak into customMetadata. Noun-side
  returns surface _rev; verb-side destructures correctly but doesn't expose
  it on HNSWVerbWithMetadata (verb CAS is future work).
- src/brainy.ts createResult() — flattens entity._rev onto the Result for
  backward compat with the existing convenience-field layer.

B. update({ ifRev }) OPTIMISTIC CONCURRENCY

- src/types/brainy.types.ts — UpdateParams<T> gains optional `ifRev?: number`.
- src/transaction/RevisionConflictError.ts (NEW) — carries `{ id, expected,
  actual }`. Message names the recipe: refetch with brain.get() and retry
  with the latest _rev. Subclass of Error.
- src/brainy.ts update() — when params.ifRev is provided, compares against
  currentRev (the rev we just read) and throws RevisionConflictError on
  mismatch before any storage write. Omitting ifRev keeps the prior
  unconditional-update behavior; existing consumers see no change.
- src/transaction/index.ts — exports RevisionConflictError.
- src/index.ts — public export of RevisionConflictError.

C. add({ ifAbsent }) BY-ID IDEMPOTENT INSERT

- src/types/brainy.types.ts — AddParams<T> gains optional `ifAbsent?: boolean`;
  AddManyParams<T> mirrors it as a batch-level flag.
- src/brainy.ts add() — when params.id AND params.ifAbsent, pre-reads
  storage.getNounMetadata(id); if present, returns the existing id without
  writing. No throw, no overwrite. Ignored when id is omitted (a fresh UUID
  can never collide).
- src/brainy.ts addMany() — propagates the batch-level ifAbsent to each
  item's add() call. Per-item ifAbsent takes precedence so callers can
  override individual rows.

WHAT'S NOT SHIPPED (and why)

A public brain.transaction(fn) wrapper was on the table but was cut. The
internal TransactionManager exposes raw Operation classes (SaveNounMetadata,
etc.) that take StorageAdapter as a constructor argument. A clean high-level
facade in 7.31.0 would have meant either:

  (a) Delegate to brain.add() / update() / relate(). Each of those opens its
      own internal transaction and commits before the closure returns. Looks
      atomic, isn't — a footgun.
  (b) Thread an optional `tx?` through every internal write site in
      brainy.ts (8 transactionManager.executeTransaction sites). ~2 days of
      real refactor with regression surface, and the API shape changes again
      in 8.0 anyway.

The SDK scheduler's actual ask (BRAINY-EXPOSE-TRANSACTIONS) is the
read-then-CAS lock pattern. _rev + ifRev solves it completely. Multi-write
atomicity is the 8.0 brain.transact() use case where the Datomic-style
immutable Db makes it atomic by construction — that's the right home.

TESTS

- New tests/integration/rev-and-ifabsent.test.ts (18 tests):
  - _rev initialization (1 on add) and surface on get fast/full paths + find
  - _rev auto-bump on update across multiple writes
  - update({ ifRev }) pass / fail / message format / omitted / legacy-no-rev
  - add({ ifAbsent }) writes when absent / no-op when present / id-required
  - addMany({ ifAbsent }) propagation + per-item override
  - SDK-scheduler scenario: two concurrent CAS updates, one wins one throws
- Unit suite unchanged: 1468/1468.
- All integration subtype + verb + strict + find-limits + new rev suites
  pass: 96/96.

DOCS

- New docs/guides/optimistic-concurrency.md (public: true) — full reference:
  the lock pattern, read-modify-write retry, idempotent bootstrap, how _rev
  interacts with brain.versions, branches/fork, and VFS, what's coming in 8.0.
- docs/api/README.md — add() and update() entries get the new params + tips
  pointing at the new guide.
- RELEASES.md v7.31.0 entry.

CORTEX COMPATIBILITY

Zero changes required. _rev is a metadata column already supported by
NativeColumnStore. The auto-bump runs in Brainy JS before any storage call;
Cortex never sees the per-entity counter.

8.0 FORWARD-COMPAT

_rev, ifRev, RevisionConflictError, and ifAbsent survive the 8.0 Db redesign
unchanged. 8.0 layers brain.transact(tx, { ifAtGeneration }) for whole-tx CAS
on top of the same per-entity mechanism — per-entity for single-record
patterns (job locks, idempotent state machines), generation-based for
"did the world move under me." Locked in .strategy/BRAINY-8.0-SUBTYPE-CONTRACT.md § C-6.

Verification

- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- All integration suites including new rev-and-ifabsent: 96/96
- npm run build: clean
- Closed-source product reference audit: clean
2026-06-09 10:04:24 -07:00
ccc1d74953 chore(release): 7.30.2 2026-06-08 12:54:32 -07:00
9e307e457f fix: recalibrate find({ limit }) cap + two-tier enforcement + caller location
Brainy 7.30.0 introduced a memory-derived synchronous cap on `find({ limit })`
to prevent OOM. The cap was sound in intent but ~4x too conservative in
calibration: assumed 100 KB per result while typical entity footprint is 7-10 KB
(384-dim float32 vector ≈ 1.5 KB + standard fields + metadata). On a 900 MB
free-memory box the cap derived to 9000 — breaking common safety-cap patterns
like `find({ type, where, limit: 10_000 })` that typically return 10-500
entities. Surfaced as a runtime regression with cascading 500s degrading
production dashboards.

Three concurrent fixes:

A. RECALIBRATE THE FORMULA
- src/utils/paramValidation.ts:175,196,212 — the three memory-derived priorities
  (reservedQueryMemory / containerMemory / freeMemory) all divided by
  100 * 1024 * 1024 (100 KB per result, ~10-15x over conservative). Replaced
  with a new MAX_LIMIT_KB_PER_RESULT = 25 constant that matches observed
  entity size.
- Result: 4 GB container cap goes 10_000 → 40_000; 2 GB cap goes 5_000 →
  20_000; 900 MB free-memory cap goes 9_000 → ~36_000. 100k hard ceiling
  unchanged. `maxQueryLimit` / `reservedQueryMemory` constructor overrides
  unchanged in behavior.

B. TWO-TIER ENFORCEMENT (warn-then-throw)
- Below cap (limit <= maxLimit): silent pass, unchanged.
- Soft tier (maxLimit < limit <= 2 * maxLimit): NEW — one-time warning per
  call site (dedup keyed on caller stack frame + limit value), query
  proceeds. Pre-7.30.2 code that relied on the cap silently allowing typical
  safety-cap limits keeps working; the warning teaches the recipe so consumers
  can fix it intentionally.
- Hard tier (limit > 2 * maxLimit): throw with the same teaching message
  format. Real OOM territory; the cap stops being a recommendation and becomes
  a guardrail.
- The 2x soft margin absorbs typical safety-cap patterns (limit: 10_000
  against a 9 K-cap box) without disabling OOM protection. Real OOM territory
  on a JS in-memory brain is hundreds of thousands of results, not 10x the
  safety cap.

C. IMPROVED ERROR / WARNING MESSAGE
- Same shape as the 7.30.1 enforcement-error messages: state the problem,
  name the three escape valves (maxQueryLimit / reservedQueryMemory /
  pagination), include caller location, link to docs.
- Extracted findCallerLocation() helper from brainy.ts to a new
  src/utils/callerLocation.ts so both the subtype enforcement (7.30.1) and
  the limit enforcement (7.30.2) share one implementation without circular
  imports.

DOCS
- New docs/guides/find-limits.md (public: true) — full reference: why the cap
  exists, the four memory sources the auto-config considers, the three escape
  valves with when-to-use-which guidance, and an explicit "pagination is the
  future-proof pattern" callout (8.0 may tighten the cap further; pagination
  keeps working unchanged).
- docs/api/README.md find() entry gets a one-paragraph `limit` tip + pointer
  to the new guide.
- RELEASES.md v7.30.2 entry.

TESTS
- New tests/integration/find-limits.test.ts (9 tests): below-cap silent pass;
  soft-tier warns once per call site (dedup verified by exercising same vs.
  different source lines via wrapper closures); soft-tier message format
  (names all three escape valves + docs link); soft-tier message includes
  caller location; hard-tier throws; hard-tier message format same as
  soft-tier; consumer maxQueryLimit override raises the cap and shifts both
  tiers accordingly; pre-7.30.2 regression scenario explicitly covered.
- tests/unit/utils/memoryLimits.test.ts — 4 tests updated for the recalibrated
  cap values (hardcoded expected numbers bumped 4x to match new 25 KB/result
  assumption).
- tests/unit/utils/paramValidation.test.ts — auto-limit test extended to cover
  the three-tier semantics (below-cap pass / soft-tier silent / hard-tier
  throw).
- Existing suites unchanged: subtype-and-facets 26/26, verb-subtype-and-
  enforcement 30/30, strict-mode-self-test 13/13. Unit 1468/1468.

CORTEX COMPATIBILITY
- Zero Cortex changes required. Every change is JS-side: formula recalibration
  runs in ValidationConfig.constructor(), two-tier enforcement runs in
  validateFindParams(), both fire before any storage / index / Cortex call.
- The new guide notes that Brainy 8.0's Datomic-style Db.find() may tighten
  per-call limits to keep snapshot semantics cheap; pagination remains the
  pattern that's guaranteed to keep working.

REPO-WIDE CLEANUP
Brainy is the only Soulcraft project that is open source. This commit also
scrubs closed-source product names and product-specific class/field references
from every tracked file in the repo (src/, docs/, tests/, RELEASES.md,
CHANGELOG.md). Consumer-reported bugs, regression scenarios, and release
notes now refer to "a consumer", "a downstream application", "a production
deployment", or "an internal report" — never to the named product. Two
product-named test files renamed to neutral diagnostic names. CLAUDE.md gains
a project-level guard rule documenting the policy and an example list of the
identifiers that may not appear in tracked code.

Verification
- npx tsc --noEmit: clean
- npm test: 1468 / 1468 unit
- All four integration subtype + verb + strict + find-limits suites: 78/78
- npm run build: clean
- Closed-source product reference audit: clean
2026-06-08 12:49:43 -07:00
34e8271c53 chore(release): 7.30.1 2026-06-08 11:32:11 -07:00
5f3a2ca7d5 fix: internal subtype consistency + brain.audit() diagnostic + improved enforcement errors
Brainy 7.30 shipped opt-in subtype enforcement; SDK 3.20.0 then registered
SDK_CORE_VOCABULARY on every consumer's brain (Event, Collection, Message,
Contract, Media, Document NounTypes). On 2026-06-08 Venue's /book flow went 500
because their brain.add({ type: NounType.Event, ... }) call sites lacked
subtype. An audit of Brainy's OWN source revealed 14 HIGH-risk internal write
paths that also omit subtype — any consumer running the same vocabulary would
have hit Brainy's infrastructure paths next. 7.30.1 closes both gaps before
8.0 makes strict mode the default.

Additive across the board. Zero behavior change for consumers not using strict
mode. Every change is JS-side — Cortex needs no work for 7.30.1.

NEW — brain.audit() diagnostic
- Read-only method walking storage.getNouns() / getVerbs() pagination
- Returns { entitiesWithoutSubtype: { type: count }, relationshipsWithoutSubtype,
  total, scanned, recommendation }
- VFS infrastructure entities excluded by default (they bypass enforcement via
  isVFSEntity marker); pass { includeVFS: true } to surface them
- The companion to migrateField (7.x) and fillSubtypes (8.0): tells consumers
  exactly what would break under strict enforcement, deterministically

NEW — Improved enforcement error messages
- Caller's source location extracted from Error().stack so users see their own
  call site, not a Brainy internal frame
- Specific guidance branches: registered vocabulary → "Pass one of: a, b, c";
  brain-wide strict mode → mentions the except clause; otherwise → registration
  recipe via brain.requireSubtype()
- Documentation link to the canonical migration recipe
- Same shape for noun and verb enforcement

NEW — CLI --subtype flag
- brainy add and brainy relate gain -s/--subtype <value>
- Defaults to 'cli-add' / 'cli-relate' so the CLI works against strict-mode
  brains without the user needing to know the vocabulary in advance

INTERNAL — every Brainy write path now sets subtype
- VFS Contains edges (5 sites at lines 503/905/1694/1772/1886) → 'vfs-contains'
- VFS symlink entity → 'vfs-symlink' (NEW — distinct from 'vfs-file')
- VFS copy-file → preserves source subtype, falls back to 'vfs-file'
- VFS symlink also adopts the isVFSEntity infrastructure marker so it bypasses
  enforcement in strict mode
- Aggregation materializer (Measurement entities) → 'materialized-aggregate'
- ImportCoordinator (3 sites): document → 'import-source'; entities →
  options.defaultSubtype ?? 'imported'; placeholder → 'import-placeholder'
- SmartImportOrchestrator (4 entity sites + 2 batch relate sites): same
  precedence (extractor → options.defaultSubtype → 'imported')
- EntityDeduplicator → candidate.subtype ?? 'imported'
- UniversalImportAPI → extractor → 'extracted' for both entities and relations
- NeuralImport → adds defaultSubtype to NeuralImportOptions; precedence same
- GoogleSheetsIntegration → request body 'subtype' ?? 'imported-from-sheets'
- ODataIntegration → request body 'Subtype' ?? 'imported-from-odata'
- MCP client message storage → 'mcp-message' (also fixes pre-existing missing
  data field and missing type by aliasing from the prior text field)

Side-effect fix: storage.getNouns() paginated now surfaces subtype to top-level
- Single-noun getNoun() already did this in 7.30; the paginated path was missed
- Without this fix brain.audit() saw missing subtype on entities that actually
  had one (caught by the strict-mode self-test before release)

NEW — tests/integration/strict-mode-self-test.test.ts (13 tests)
- Creates a brain under the exact SDK_CORE_VOCABULARY shape Venue hit + brain-
  wide strict mode
- Exercises every internal Brainy path: VFS root + mkdir + writeFile + cp + mv
  + ln + symlink; aggregation engine; audit diagnostic with includeVFS toggle
- Validates error message UX: caller location, vocabulary guidance, brain-wide
  strict mode guidance, off-vocabulary value reporting

Docs
- New "Strict mode in practice" section in docs/guides/subtypes-and-facets.md
  covering the SDK_CORE_VOCABULARY pattern, 4-step migration recipe
  (audit → migrateField → hand-fix → re-audit), the Brainy-internal label
  reference table, and an 8.0 forward-look on fillSubtypes()
- docs/api/README.md: new audit() entry, strict-mode tips on add() and relate()
- RELEASES.md: full 7.30.1 entry

Cortex parity (forward-looking, not blocking 7.30.1)
- 6th open question added to .strategy/BRAINY-8.0-SUBTYPE-CONTRACT.md: native
  fast path for audit() and fillSubtypes() via column-store null-subtype
  bitmap for billion-scale brains
- Cortex should add a parity test mirroring strict-mode-self-test.test.ts
  against their native paths to catch any latent bug where native writes
  bypass JS validation
- Brainy-internal subtype labels become a documented part of the 8.0 contract
  (useful for Cortex telemetry surfacing Brainy-managed infrastructure %)

Verification
- npx tsc --noEmit: clean
- npm test: 1468/1468 unit
- 7.29 noun integration suite: 26/26 (no regression)
- 7.30 verb subtype + enforcement integration suite: 30/30 (no regression)
- New strict-mode-self-test integration suite: 13/13
- npm run build: clean
- Closed-source product reference audit: clean

Addresses VE-SUBTYPE-MIGRATION (Venue's reported request) and ships internal
labels Venue did NOT ask for but that would have broken them next under their
own vocabulary registration.
2026-06-08 11:31:47 -07:00
a82c3339df chore(release): 7.30.0 2026-06-05 11:16:15 -07:00
c0d326b36d feat: verb subtype + updateRelation + requireSubtype enforcement
Brings verbs to first-class parity with nouns. The 7.29.0 subtype primitive
shipped for entities only; this release ships the symmetric verb mirror plus
the enforcement layer for ensuring every entity AND every relationship has
both type AND subtype.

Layer V1 — verb subtype mirror
- HNSWVerbWithMetadata.subtype + STANDARD_VERB_FIELDS set + resolveVerbField()
- Relation<T>.subtype, RelateParams<T>.subtype, UpdateRelationParams<T> extended,
  GetRelationsParams.subtype, GraphConstraints.subtype (for find connected)
- relate() persists subtype on verbMetadata + GraphVerb + transaction ops
- getRelations({ type, subtype }) fast-path filter with set membership
- find({ connected: { via, subtype, depth } }) traversal filter (depth-1 on
  the JS path; explicit error on depth > 1 pointing at Cortex native)
- verbsToRelations + storage destructure sites surface subtype to top-level
- All three graph-index fast-path queries (getVerbsBySource/ByTarget) enrich
  with subtype from metadata

Layer V2 — updateRelation() closes a pre-7.30 gap
- New first-class verb update method (parallel to update() for nouns)
- Changes subtype/type/weight/confidence/data/metadata in place
- Re-indexes in graph adjacency when verb type changes; id preserved
- validateUpdateRelationParams enforces id + at-least-one-field-to-update

Layer V3 — verb subtype storage rollup
- verbSubtypeCountsByType: Map<number, Map<string, number>> on BaseStorage
- verbSubtypeByIdCache for self-heal during update/delete
- incrementVerbSubtypeCount + decrementVerbSubtypeCount maintain state
- loadVerbSubtypeStatistics + saveVerbSubtypeStatistics persist to
  _system/verb-subtype-statistics.json (mirrors noun-side shape)
- rebuildVerbSubtypeCounts for poison recovery / explicit repair
- getVerbSubtypeCountsByType accessor for the public counts API
- Wired into init() / flushCounts() / saveVerbMetadata / deleteVerbMetadata

Layer V4 — verb counts API + relationshipSubtypesOf
- brain.counts.byRelationshipSubtype(verb, subtype?) — O(1) breakdown or point
- brain.counts.topRelationshipSubtypes(verb, n) — top N by count
- brain.relationshipSubtypesOf(verb) — sorted distinct subtypes

Layer V5 — migrateField extended to verbs
- New entityKind?: 'noun' | 'verb' | 'both' option (default 'noun')
- Mirror verb iteration via storage.getVerbs() with same path semantics
- verbToRelationLike + buildRelationMigrationUpdate helpers project the
  storage verb shape onto the Entity<T>-shaped surface readPath understands
- Routes through new updateRelation() for the verb-side rewrite

Enforcement (opt-in in 7.30, default in 8.0)
- brain.requireSubtype(type, options) — unified API for NounType OR VerbType.
  Registers per-type rules with optional values whitelist; composes with the
  brain-wide flag.
- new Brainy({ requireSubtype: true }) — brain-wide strict mode. Every public
  write path validates the pairing guarantee.
- { except: [NounType.Thing, ...] } form for catch-all type exemptions
- Atomic-fail semantics on addMany / relateMany — pre-validate every item
  before any storage write, throw on first failure with item index
- Per-type rules + brain-wide flag both throw with descriptive messages
- VFS infrastructure bypass via metadata.isVFSEntity / isVFS markers so
  brain's own VFS writes don't get rejected when strict mode is on

VFS labeling — concrete subtypes for infrastructure entities
- VFS root: NounType.Collection + subtype: 'vfs-root' (was bare Collection)
- VFS directories: subtype: 'vfs-directory'
- VFS files: subtype: 'vfs-file' (NounType still mime-based)
- VFS containment edges: VerbType.Contains + subtype: 'vfs-contains'
- Lets consumers cleanly enumerate VFS state via find({ subtype: 'vfs-file' })
  and distinguish Brainy's VFS Collections from user-created Collections

Docs
- docs/guides/subtypes-and-facets.md extended with Layer V (Verbs) section +
  Enforcement section. New full reference at the bottom split into Layer 1
  (nouns), Layer V (verbs), Layer 2 (facets), Layer 3 (migration), Enforcement.
- docs/api/README.md adds updateRelation(), getRelations({ subtype }), the
  three verb-side counts methods, requireSubtype(), and the brain-wide
  constructor option. relate() params include subtype.
- docs/DATA_MODEL.md adds a Subtype-for-VerbType section + STANDARD_VERB_FIELDS
- docs/architecture/finite-type-system.md extends Principle 1a to verbs
- docs/QUERY_OPERATORS.md adds a verb-subtype filter section covering
  getRelations and find({connected, subtype}) traversal
- README.md "Subtypes" section now shows both noun + verb in one example +
  the enforcement APIs
- RELEASES.md v7.30.0 entry with the full noun/verb capability parity matrix

Tests
- tests/integration/verb-subtype-and-enforcement.test.ts — 30 new tests
  covering V1 round-trips, V1 set membership, updateRelation in place,
  updateRelation preservation, V2 counts breakdown + point + topN + distinct,
  V2 decrements on unrelate, V2 re-routes on updateRelation, V3 depth-1
  traversal filter, V3 depth>1 explicit error, V4 verb migration, V4 both
  entity kinds, V4 readBoth preservation, V5 per-type required rejection,
  V5 vocabulary rejection, V5 on-vocab acceptance, V5 verb-side enforcement,
  V5 addMany atomic-fail, V5 relateMany atomic-fail, V5 update enforcement,
  V5 updateRelation enforcement, V5 brain-wide strict mode, V5 except clause.

Verification
- Unit suite: 1468/1468 passing
- Noun subtype integration (7.29 carryover): 26/26 passing
- Verb subtype + enforcement integration: 30/30 passing
- Type-check: clean
- Build: clean
- Public closed-source reference audit: clean

Internal 8.0 spec
- .strategy/BRAINY-8.0-SUBTYPE-CONTRACT.md (gitignored, not in npm artifact)
  documents the contract upgrade Cortex 3.0 implements against: required-by-
  default subtype, SubtypeRegistry typing hook, native simplification,
  multi-hop traversal native fast path, brain.fillSubtypes() migration helper.
  Coordinated via PLATFORM-HANDOFF rows CTX-SUBTYPE-PARITY-V2 (7.30 parallel
  work) and CTX-SUBTYPE-8.0-CONTRACT (8.0 spec).
2026-06-05 11:15:52 -07:00