2025-09-24 17:31:48 -07:00
/ * *
* Virtual Filesystem Implementation
*
2026-06-11 14:51:00 -07:00
* Virtual filesystem built on Brainy
2025-09-24 17:31:48 -07:00
* Real code , no mocks , actual working implementation
* /
import { Readable , Writable } from 'stream'
perf(vfs): the old-root sweep runs once per store, not once per open
MEASURED on a 14,056-noun / 72,679-verb production-shaped store, measured
solo under an exclusive lock: the vfs-bootstrap phase cost 43,021 ms of a cold open and 52,696 ms of
a WARM REOPEN. What dominates it is a migration sweep — a filtered find() over
the whole store hunting for root directories created before the fixed root id
existed. A store either carries such duplicates or never will, and the sweep
ran on every open, forever, in the foreground.
It is now caused by the store's state instead of by the open count: a durable
marker under _system/ records that the sweep has run, and a store carrying it
never sweeps again. A store without one sweeps in the BACKGROUND — the sweep
only removes duplicate roots, nothing serves from them, and it was already
declared non-critical — narrated at both ends, with whenRootSweepSettled() for
anyone who needs to observe rather than race it. An adapter with no raw-object
door keeps the old behaviour: correctness over cost, never a silent skip.
Pins: tests/integration/vfs-root-sweep-once.test.ts — the sweep runs on the
first open and never on the second or third; a sweep slowed to 4s does not
delay the open.
2026-08-28 11:01:43 -07:00
import { prodLog } from '../utils/logger.js'
2025-09-24 17:31:48 -07:00
import crypto from 'crypto'
import { v4 as uuidv4 } from '../universal/uuid.js'
import { Brainy } from '../brainy.js'
import { Entity , AddParams , RelateParams , FindParams , Relation } from '../types/brainy.types.js'
import { NounType , VerbType } from '../types/graphTypes.js'
fix(vfs): the VFS root never persists a zero-norm vector
A zero-norm vector is lawful inside brainy (cosine distance scores it at
maximum, never a false top hit) but a false attractor for a downstream
engine serving squared-euclidean distance, which cannot tell a real
all-zero vector apart from a legitimate origin point.
- The VFS root now persists with vector [] (the existing "unvectored"
shape) instead of a real all-zero 384-dim placeholder, and is never
routed into the deferred-embed pipeline.
- A one-time migration in the root-init path detects a pre-fix store's
all-zero placeholder root (by norm, not length) and rewrites it to []
through a new sanctioned Brainy method that keeps the canonical
vectored-noun ledger honest and removes the row from the vector index.
- The vector-index write seam (AddToVectorIndexOperation,
ReplaceInVectorIndexOperation, and the generation materializer's direct
insert) now refuses any real all-zero vector before it reaches a
provider, loudly naming the entity, while the canonical write still
lands.
- add()'s dimension-pinning and HNSW-insert gates, and the add-params
validator, now treat any empty vector as carrying no dimension
information, closing a latent trap where an explicit `vector: []`
would have pinned dimensions to 0.
2026-08-27 09:28:44 -07:00
import { isZeroNormVector } from '../utils/distance.js'
2025-09-24 17:31:48 -07:00
import { PathResolver } from './PathResolver.js'
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
import { mimeDetector } from './MimeTypeDetector.js'
2025-09-29 13:51:47 -07:00
import {
SemanticPathResolver ,
ProjectionRegistry ,
ConceptProjection ,
AuthorProjection ,
TemporalProjection ,
RelationshipProjection ,
SimilarityProjection ,
TagProjection
} from './semantic/index.js'
// Knowledge Layer can remain as optional augmentation for now
2025-09-24 17:31:48 -07:00
import {
IVirtualFileSystem ,
VFSConfig ,
VFSEntity ,
VFSMetadata ,
VFSStats ,
VFSDirent ,
VFSTodo ,
VFSError ,
VFSErrorCode ,
WriteOptions ,
ReadOptions ,
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
FileVersion ,
2025-09-24 17:31:48 -07:00
MkdirOptions ,
ReaddirOptions ,
CopyOptions ,
SearchOptions ,
SearchResult ,
SimilarOptions ,
RelatedOptions ,
ReadStreamOptions ,
WriteStreamOptions ,
WatchListener
} from './types.js'
/ * *
* Main Virtual Filesystem Implementation
*
* This is REAL , production - ready code that :
* - Maps filesystem operations to Brainy entities
* - Uses graph relationships for directory structure
* - Provides semantic search and AI features
* - Scales to millions of files
* /
export class VirtualFileSystem implements IVirtualFileSystem {
private brain : Brainy
2025-09-29 13:51:47 -07:00
private pathResolver ! : SemanticPathResolver
private projectionRegistry ! : ProjectionRegistry
2025-09-24 17:31:48 -07:00
private config : Required < Omit < VFSConfig , 'rootEntityId' > > & { rootEntityId? : string }
private rootEntityId? : string
private initialized = false
perf(vfs): the old-root sweep runs once per store, not once per open
MEASURED on a 14,056-noun / 72,679-verb production-shaped store, measured
solo under an exclusive lock: the vfs-bootstrap phase cost 43,021 ms of a cold open and 52,696 ms of
a WARM REOPEN. What dominates it is a migration sweep — a filtered find() over
the whole store hunting for root directories created before the fixed root id
existed. A store either carries such duplicates or never will, and the sweep
ran on every open, forever, in the foreground.
It is now caused by the store's state instead of by the open count: a durable
marker under _system/ records that the sweep has run, and a store carrying it
never sweeps again. A store without one sweeps in the BACKGROUND — the sweep
only removes duplicate roots, nothing serves from them, and it was already
declared non-critical — narrated at both ends, with whenRootSweepSettled() for
anyone who needs to observe rather than race it. An adapter with no raw-object
door keeps the old behaviour: correctness over cost, never a silent skip.
Pins: tests/integration/vfs-root-sweep-once.test.ts — the sweep runs on the
first open and never on the second or third; a sweep slowed to 4s does not
delay the open.
2026-08-28 11:01:43 -07:00
/ * *
* The one - time old - root sweep , in flight . See { @link sweepOldRootsIfNeeded } .
* /
private rootSweep? : Promise < void >
/ * *
* Where the completed old - root sweep is recorded . Engine plumbing under
* ` _system/ ` , like every other marker there — never enumerated as data .
* /
private static readonly ROOT_SWEEP_MARKER_PATH = '_system/vfs-root-sweep.json'
2026-08-28 12:30:30 -07:00
/ * *
* Below this wall , a sweep that removed nothing says nothing — see
* { @link sweepOldRootsIfNeeded } .
* /
private static readonly ROOT_SWEEP_NARRATE_MS = 1 _000
2025-09-25 11:04:36 -07:00
private currentUser : string = 'system' // Track current user for collaboration
2025-09-24 17:31:48 -07:00
2025-09-29 13:51:47 -07:00
// Knowledge Layer features available via augmentation (brain.use('knowledge'))
2025-09-24 17:31:48 -07:00
// Caches for performance
private contentCache : Map < string , { data : Buffer , timestamp : number } >
private statCache : Map < string , { stats : VFSStats , timestamp : number } >
// Watch system
private watchers : Map < string , Set < WatchListener > >
// Background task timer
private backgroundTimer : NodeJS.Timeout | null = null
2025-09-30 12:53:39 -07:00
// Mutex for preventing race conditions in directory creation
private mkdirLocks : Map < string , Promise < void > > = new Map ( )
2026-01-27 15:38:21 -08:00
// Singleton promise for root initialization (prevents duplicate roots)
2025-11-14 11:27:35 -08:00
private rootInitPromise : Promise < string > | null = null
2026-01-27 15:38:21 -08:00
// Fixed VFS root ID (prevents duplicates across instances)
2025-11-14 12:51:25 -08:00
// Uses deterministic UUID format for storage compatibility
private static readonly VFS_ROOT_ID = '00000000-0000-0000-0000-000000000000'
2026-06-29 10:04:19 -07:00
/ * *
* Construct a VFS bound to a Brainy instance .
*
* The VFS stores every file and directory as a Brainy entity and models the
* directory tree with graph relationships . No I / O happens here — call
* { @link init } ( or rely on ` brain.init() ` , which auto - initializes the VFS )
* before using any filesystem operation .
*
* @param brain - The Brainy instance backing this filesystem . When omitted , a
* fresh ` new Brainy() ` is created and owned by this VFS .
* /
2025-09-24 17:31:48 -07:00
constructor ( brain? : Brainy ) {
this . brain = brain || new Brainy ( )
this . contentCache = new Map ( )
this . statCache = new Map ( )
this . watchers = new Map ( )
// Default configuration (will be overridden in init)
this . config = this . getDefaultConfig ( )
}
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
/ * *
2026-01-27 15:38:21 -08:00
* Access to BlobStorage for unified file storage
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
* /
private get blobStorage() {
2026-06-11 14:51:00 -07:00
// Bracket access reaches Brainy's private `storage` field; `blobStorage`
// is a public (optional) member of BaseStorage, set during brain.init().
const storage = this . brain [ 'storage' ]
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
if ( ! storage ? . blobStorage ) {
throw new Error (
'BlobStorage not available. The storage adapter must run ' +
'initializeBlobStorage() before the VFS is used (brain.init() does this).'
)
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
}
return storage . blobStorage
}
2025-09-24 17:31:48 -07:00
/ * *
* Initialize the VFS
* /
async init ( config? : VFSConfig ) : Promise < void > {
if ( this . initialized ) return
// Merge config with defaults
this . config = { . . . this . getDefaultConfig ( ) , . . . config }
2026-01-27 15:38:21 -08:00
// VFS is now auto-initialized during brain.init()
2025-11-02 10:58:52 -08:00
// Brain is guaranteed to be initialized when this is called
// Removed brain.init() check to prevent infinite recursion
2025-09-24 17:31:48 -07:00
// Create or find root entity
this . rootEntityId = await this . initializeRoot ( )
perf(vfs): the old-root sweep runs once per store, not once per open
MEASURED on a 14,056-noun / 72,679-verb production-shaped store, measured
solo under an exclusive lock: the vfs-bootstrap phase cost 43,021 ms of a cold open and 52,696 ms of
a WARM REOPEN. What dominates it is a migration sweep — a filtered find() over
the whole store hunting for root directories created before the fixed root id
existed. A store either carries such duplicates or never will, and the sweep
ran on every open, forever, in the foreground.
It is now caused by the store's state instead of by the open count: a durable
marker under _system/ records that the sweep has run, and a store carrying it
never sweeps again. A store without one sweeps in the BACKGROUND — the sweep
only removes duplicate roots, nothing serves from them, and it was already
declared non-critical — narrated at both ends, with whenRootSweepSettled() for
anyone who needs to observe rather than race it. An adapter with no raw-object
door keeps the old behaviour: correctness over cost, never a silent skip.
Pins: tests/integration/vfs-root-sweep-once.test.ts — the sweep runs on the
first open and never on the second or third; a sweep slowed to 4s does not
delay the open.
2026-08-28 11:01:43 -07:00
// Clean up old UUID-based roots — ONCE PER STORE, BEHIND THE DOORS.
// This is a migration sweep for roots created before the fixed root id
// existed. It ran on EVERY open, forever: a filtered find over the whole
// store hunting for duplicates that a store has either always had or
// never will. MEASURED on a 14,056-noun / 72,679-verb store: the phase it
// dominates cost 43-53 SECONDS of every open, warm reopens included.
// Now: a durable marker records that the sweep has run, and a store
// carrying it never sweeps again; a store without one sweeps in the
// BACKGROUND (the sweep only removes duplicate roots — nothing serves
// from them — and it was always declared non-critical).
this . rootSweep = this . sweepOldRootsIfNeeded ( )
2025-11-14 12:51:25 -08:00
2025-09-29 13:51:47 -07:00
// Initialize projection registry with auto-discovery of built-in projections
this . projectionRegistry = new ProjectionRegistry ( )
this . registerBuiltInProjections ( )
// Initialize semantic path resolver (zero-config, uses brain.config)
this . pathResolver = new SemanticPathResolver (
this . brain ,
this , // Pass VFS instance for resolvePath
this . rootEntityId ,
this . projectionRegistry
)
2025-09-24 17:31:48 -07:00
// Knowledge Layer is now a separate augmentation
// Enable with: brain.use('knowledge')
// Start background tasks
this . startBackgroundTasks ( )
this . initialized = true
}
/ * *
* Create or find the root directory entity
* /
2025-09-29 13:51:47 -07:00
/ * *
* Auto - register built - in projection strategies
* Zero - config : All semantic dimensions work out of the box
* /
private registerBuiltInProjections ( ) : void {
const projections = [
ConceptProjection ,
AuthorProjection ,
TemporalProjection ,
RelationshipProjection ,
SimilarityProjection ,
TagProjection
]
for ( const ProjectionClass of projections ) {
try {
this . projectionRegistry . register ( new ProjectionClass ( ) )
} catch ( err ) {
// Silently skip if already registered (e.g., in tests)
if ( ! ( err instanceof Error && err . message . includes ( 'already registered' ) ) ) {
throw err
}
}
}
}
2025-11-14 11:27:35 -08:00
/ * *
2026-01-27 15:38:21 -08:00
* CRITICAL FIX - Prevent duplicate root creation
2025-11-14 11:27:35 -08:00
* Uses singleton promise pattern to ensure only ONE root initialization
* happens even with concurrent init ( ) calls
* /
2025-09-24 17:31:48 -07:00
private async initializeRoot ( ) : Promise < string > {
2025-11-14 11:27:35 -08:00
// If initialization already in progress, wait for it (automatic mutex)
if ( this . rootInitPromise ) {
return await this . rootInitPromise
}
// Start initialization and cache the promise
this . rootInitPromise = this . doInitializeRoot ( )
try {
const rootId = await this . rootInitPromise
return rootId
} catch ( error ) {
// On error, clear promise so retry is possible
this . rootInitPromise = null
throw error
}
// NOTE: On success, we intentionally keep the promise cached
// This prevents re-initialization and serves as a cache
}
/ * *
2026-01-27 15:38:21 -08:00
* Atomic root initialization with fixed ID
2025-11-14 12:51:25 -08:00
* Uses deterministic ID to prevent duplicates across all VFS instances
*
* ARCHITECTURAL FIX : Instead of query - then - create ( race condition ) ,
* we use a fixed ID so storage - level uniqueness prevents duplicates .
2025-11-14 11:27:35 -08:00
* /
private async doInitializeRoot ( ) : Promise < string > {
2025-11-14 12:51:25 -08:00
const rootId = VirtualFileSystem . VFS_ROOT_ID
fix(vfs): the VFS root never persists a zero-norm vector
A zero-norm vector is lawful inside brainy (cosine distance scores it at
maximum, never a false top hit) but a false attractor for a downstream
engine serving squared-euclidean distance, which cannot tell a real
all-zero vector apart from a legitimate origin point.
- The VFS root now persists with vector [] (the existing "unvectored"
shape) instead of a real all-zero 384-dim placeholder, and is never
routed into the deferred-embed pipeline.
- A one-time migration in the root-init path detects a pre-fix store's
all-zero placeholder root (by norm, not length) and rewrites it to []
through a new sanctioned Brainy method that keeps the canonical
vectored-noun ledger honest and removes the row from the vector index.
- The vector-index write seam (AddToVectorIndexOperation,
ReplaceInVectorIndexOperation, and the generation materializer's direct
insert) now refuses any real all-zero vector before it reaches a
provider, loudly naming the entity, while the canonical write still
lands.
- add()'s dimension-pinning and HNSW-insert gates, and the add-params
validator, now treat any empty vector as carrying no dimension
information, closing a latent trap where an explicit `vector: []`
would have pinned dimensions to 0.
2026-08-27 09:28:44 -07:00
// Try to get existing root by fixed ID (O(1) lookup, not query).
// includeVectors: true — the zero-norm migration below (leg 2) needs to
// inspect the persisted vector to detect the legacy placeholder shape.
2025-11-14 12:51:25 -08:00
try {
fix(vfs): the VFS root never persists a zero-norm vector
A zero-norm vector is lawful inside brainy (cosine distance scores it at
maximum, never a false top hit) but a false attractor for a downstream
engine serving squared-euclidean distance, which cannot tell a real
all-zero vector apart from a legitimate origin point.
- The VFS root now persists with vector [] (the existing "unvectored"
shape) instead of a real all-zero 384-dim placeholder, and is never
routed into the deferred-embed pipeline.
- A one-time migration in the root-init path detects a pre-fix store's
all-zero placeholder root (by norm, not length) and rewrites it to []
through a new sanctioned Brainy method that keeps the canonical
vectored-noun ledger honest and removes the row from the vector index.
- The vector-index write seam (AddToVectorIndexOperation,
ReplaceInVectorIndexOperation, and the generation materializer's direct
insert) now refuses any real all-zero vector before it reaches a
provider, loudly naming the entity, while the canonical write still
lands.
- add()'s dimension-pinning and HNSW-insert gates, and the add-params
validator, now treat any empty vector as carrying no dimension
information, closing a latent trap where an explicit `vector: []`
would have pinned dimensions to 0.
2026-08-27 09:28:44 -07:00
const existingRoot = await this . brain . get ( rootId , { includeVectors : true } )
2025-11-14 12:51:25 -08:00
if ( existingRoot ) {
// Root exists - verify metadata is correct
2026-06-11 14:51:00 -07:00
const metadata = existingRoot . metadata || existingRoot
2025-11-14 12:51:25 -08:00
if ( ! metadata . vfsType || metadata . vfsType !== 'directory' ) {
console . warn ( '⚠️ VFS: Root metadata incomplete, repairing...' )
await this . brain . update ( {
id : rootId ,
feat(8.0): visibility field (public/internal/system) on nouns + verbs
Adds a reserved, top-level `visibility` field (mirrors the subtype rollout):
'public' (default, surfaced) | 'internal' (developer app-internal — hidden from
default find/count/stats, opt-in via includeInternal) | 'system' (Brainy
plumbing, library-set only).
Fixes a real leak: the VFS root entity counted in getNounCount() and appeared in
find() (a fresh brain reported 1 entity). It is now visibility:'system' →
excluded from every user-facing surface. Developers also get a first-class
hidden-unless-asked tier (e.g. learned internals vs user-exposed data).
- Reserved (RESERVED_ENTITY_FIELDS / RESERVED_RELATION_FIELDS) — spoof-proof from metadata.
- Threaded through add/relate/update/transact; surfaced top-level on reads.
- Default exclusion in counts (baseStorage), find()/related() (hard candidate
filter via excludeVisibility — keeps topK/limit correct), and stats;
includeInternal/includeSystem opt-ins.
- VFS root marked 'system'.
Tests: visibility.test.ts 17/17 (fresh-brain getNounCount()===0, internal hidden
+ opt-in, verb symmetry, top-level surfacing, metadata-spoof rejection). Unit
1431 green; count-synchronization integration now passes (off-by-one fixed).
2026-06-16 15:20:26 -07:00
// Re-assert system visibility on repair so a pre-8.0 root (created before the
// tier existed) is moved out of the default-visible counts/find() too.
visibility : 'system' ,
2025-11-14 12:51:25 -08:00
metadata : this.getRootMetadata ( )
} )
}
fix(vfs): the VFS root never persists a zero-norm vector
A zero-norm vector is lawful inside brainy (cosine distance scores it at
maximum, never a false top hit) but a false attractor for a downstream
engine serving squared-euclidean distance, which cannot tell a real
all-zero vector apart from a legitimate origin point.
- The VFS root now persists with vector [] (the existing "unvectored"
shape) instead of a real all-zero 384-dim placeholder, and is never
routed into the deferred-embed pipeline.
- A one-time migration in the root-init path detects a pre-fix store's
all-zero placeholder root (by norm, not length) and rewrites it to []
through a new sanctioned Brainy method that keeps the canonical
vectored-noun ledger honest and removes the row from the vector index.
- The vector-index write seam (AddToVectorIndexOperation,
ReplaceInVectorIndexOperation, and the generation materializer's direct
insert) now refuses any real all-zero vector before it reaches a
provider, loudly naming the entity, while the canonical write still
lands.
- add()'s dimension-pinning and HNSW-insert gates, and the add-params
validator, now treat any empty vector as carrying no dimension
information, closing a latent trap where an explicit `vector: []`
would have pinned dimensions to 0.
2026-08-27 09:28:44 -07:00
// ZERO-NORM ROOT MIGRATION (one-time): a pre-fix store persisted the
// root with a REAL all-zero placeholder vector — lawful inside
// brainy (cosineDistance treats a zero-norm operand as MAXIMUM
// distance, see src/utils/distance.ts) but a "false attractor" for a
// downstream engine serving squared-euclidean distance, which cannot
// tell an all-zero vector apart from a legitimate origin point (a
// production incident silently darkened 150+ rows in a partner
// engine's index this way). THE LAW: a zero-norm vector is not a
// vector — it never crosses an engine boundary. Detect the legacy
// shape via NORM, not length or dimension (any real all-zero vector
// qualifies, not just the historical 384-dim one), and rewrite it to
// the "unvectored" `[]` shape through the sanctioned migration path
// (Brainy.unvectorNounForRootMigration — see its JSDoc), which keeps
// `getCanonicalCounts().vectors.all` honest and removes the row from
// the vector index. Idempotent: a store already on the new shape
// (vector.length === 0) takes the false branch below on every
// subsequent init() — a permanent no-op, not a one-time flag.
const existingVector = existingRoot . vector ? ? [ ]
if ( existingVector . length > 0 && isZeroNormVector ( existingVector ) ) {
const migrated = await this . brain . unvectorNounForRootMigration ( rootId )
if ( migrated ) {
console . log (
'VFS: migrated root vector from the legacy all-zero placeholder to the ' +
'unvectored shape (zero-norm vectors never cross an engine boundary)'
)
}
}
2025-11-14 12:51:25 -08:00
return rootId
2025-10-24 11:12:27 -07:00
}
2025-11-14 12:51:25 -08:00
} catch ( error ) {
// Root doesn't exist yet - proceed to creation
}
2025-10-24 11:12:27 -07:00
2025-11-14 12:51:25 -08:00
// Create root with fixed ID (idempotent - fails gracefully if exists)
try {
console . log ( 'VFS: Creating root directory (fixed ID: 00000000-0000-0000-0000-000000000000)' )
feat(open-path): init never gates on the embedding model; open goes concurrent; slow opens narrate
A production restart storm measured 90,017ms for a single brain init vs
1,117ms quiet (~80x contention multiplier), traced to performInit() eagerly
awaiting the process-global WASM embedding engine before the VFS root even
existed. Every writer's open() queued on the one throttled model compile
(90-140s on throttled CPUs).
- VirtualFileSystem.doInitializeRoot() no longer embeds '/'. The root is
system-tier plumbing nothing ever searches; when the default WASM engine
is active it now gets an explicit all-zero placeholder vector
(cosineDistance returns max distance for a zero vector, so it never ranks
ahead of real content). deferEmbedding was considered and rejected: its
landing path kicks the embed worker synchronously right after commit,
which would still force the cold compile within milliseconds — just off
the awaited path, not avoided. A registered native 'embeddings' provider
(no cold-start cost, possibly a different dimension) still embeds the
root for real, via the new Brainy.usesDefaultWasmEmbedder() seam.
- performInit()'s eager-embedding step now only STARTS the WASM engine warm
in the background instead of awaiting it inline. embed()/embeddingManager
already serialize concurrent callers on one shared init promise, so the
first real embed() converges correctly either way; a failed warm narrates
loudly instead of surfacing as a silent latency spike or an unhandled
rejection. eagerEmbeddings: false still means no warm at all.
- FileSystemStorage.init() batches its ~8 independent bootstrap mkdirs
(each creates its own full subtree via recursive:true, so none depend on
the others existing) into one Promise.all. The restore-completion step
and initializeCounts() stay strictly sequential — they have real order
dependencies on rootDir and systemDir respectively.
- performInit() now times five phases (storage init / generation-store
open+fold / index init+gate / VFS bootstrap / embedding-warm-started) and
logs one warning with the per-phase breakdown when total open exceeds
2000ms; silent otherwise.
2026-08-25 10:09:45 -07:00
// OPEN-PATH FIX: the VFS root is Brainy's own system-tier plumbing — it
// is hidden from find()/getNounCount()/stats() by default and nothing
// ever runs a semantic search against it — so it needs no REAL
// embedding. Historically this add() always called embed('/'), which
// meant every writer's FIRST-EVER open forced the process-global WASM
// engine to cold-compile its model (measured 90-140s on throttled
// CPUs) before the brain could even finish init(). This branch only
fix(vfs): the VFS root never persists a zero-norm vector
A zero-norm vector is lawful inside brainy (cosine distance scores it at
maximum, never a false top hit) but a false attractor for a downstream
engine serving squared-euclidean distance, which cannot tell a real
all-zero vector apart from a legitimate origin point.
- The VFS root now persists with vector [] (the existing "unvectored"
shape) instead of a real all-zero 384-dim placeholder, and is never
routed into the deferred-embed pipeline.
- A one-time migration in the root-init path detects a pre-fix store's
all-zero placeholder root (by norm, not length) and rewrites it to []
through a new sanctioned Brainy method that keeps the canonical
vectored-noun ledger honest and removes the row from the vector index.
- The vector-index write seam (AddToVectorIndexOperation,
ReplaceInVectorIndexOperation, and the generation materializer's direct
insert) now refuses any real all-zero vector before it reaches a
provider, loudly naming the entity, while the canonical write still
lands.
- add()'s dimension-pinning and HNSW-insert gates, and the add-params
validator, now treat any empty vector as carrying no dimension
information, closing a latent trap where an explicit `vector: []`
would have pinned dimensions to 0.
2026-08-27 09:28:44 -07:00
// runs once per store (a previously-opened store already has a root —
// see the migration above for the pre-fix shape — reopening never
// re-adds it), so the fix applies only to brand-new stores.
feat(open-path): init never gates on the embedding model; open goes concurrent; slow opens narrate
A production restart storm measured 90,017ms for a single brain init vs
1,117ms quiet (~80x contention multiplier), traced to performInit() eagerly
awaiting the process-global WASM embedding engine before the VFS root even
existed. Every writer's open() queued on the one throttled model compile
(90-140s on throttled CPUs).
- VirtualFileSystem.doInitializeRoot() no longer embeds '/'. The root is
system-tier plumbing nothing ever searches; when the default WASM engine
is active it now gets an explicit all-zero placeholder vector
(cosineDistance returns max distance for a zero vector, so it never ranks
ahead of real content). deferEmbedding was considered and rejected: its
landing path kicks the embed worker synchronously right after commit,
which would still force the cold compile within milliseconds — just off
the awaited path, not avoided. A registered native 'embeddings' provider
(no cold-start cost, possibly a different dimension) still embeds the
root for real, via the new Brainy.usesDefaultWasmEmbedder() seam.
- performInit()'s eager-embedding step now only STARTS the WASM engine warm
in the background instead of awaiting it inline. embed()/embeddingManager
already serialize concurrent callers on one shared init promise, so the
first real embed() converges correctly either way; a failed warm narrates
loudly instead of surfacing as a silent latency spike or an unhandled
rejection. eagerEmbeddings: false still means no warm at all.
- FileSystemStorage.init() batches its ~8 independent bootstrap mkdirs
(each creates its own full subtree via recursive:true, so none depend on
the others existing) into one Promise.all. The restore-completion step
and initializeCounts() stay strictly sequential — they have real order
dependencies on rootDir and systemDir respectively.
- performInit() now times five phases (storage init / generation-store
open+fold / index init+gate / VFS bootstrap / embedding-warm-started) and
logs one warning with the per-phase breakdown when total open exceeds
2000ms; silent otherwise.
2026-08-25 10:09:45 -07:00
//
fix(vfs): the VFS root never persists a zero-norm vector
A zero-norm vector is lawful inside brainy (cosine distance scores it at
maximum, never a false top hit) but a false attractor for a downstream
engine serving squared-euclidean distance, which cannot tell a real
all-zero vector apart from a legitimate origin point.
- The VFS root now persists with vector [] (the existing "unvectored"
shape) instead of a real all-zero 384-dim placeholder, and is never
routed into the deferred-embed pipeline.
- A one-time migration in the root-init path detects a pre-fix store's
all-zero placeholder root (by norm, not length) and rewrites it to []
through a new sanctioned Brainy method that keeps the canonical
vectored-noun ledger honest and removes the row from the vector index.
- The vector-index write seam (AddToVectorIndexOperation,
ReplaceInVectorIndexOperation, and the generation materializer's direct
insert) now refuses any real all-zero vector before it reaches a
provider, loudly naming the entity, while the canonical write still
lands.
- add()'s dimension-pinning and HNSW-insert gates, and the add-params
validator, now treat any empty vector as carrying no dimension
information, closing a latent trap where an explicit `vector: []`
would have pinned dimensions to 0.
2026-08-27 09:28:44 -07:00
// ZERO-NORM LAW (current shape, superseding the historical all-zero
// placeholder): the root's vector is `[]` — the SAME "unvectored"
// empty-array shape used for a deferred embed's stub and any other
// not-yet-embedded row — never a real all-zero vector. A zero-norm
// vector is lawful inside brainy (`cosineDistance`, see
// src/utils/distance.ts, returns the MAXIMUM distance whenever either
// operand's norm is zero) but is a "false attractor" for a downstream
// engine serving squared-euclidean distance, which cannot tell a real
// all-zero vector apart from a legitimate origin point — it silently
// darkened 150+ rows in a partner engine's index in production. THE
// LAW: a zero-norm vector is not a vector — it never crosses an engine
// boundary. `vector: []` achieves the SAME cold-compile avoidance the
// original placeholder did (`add()`'s dimension-pin and HNSW-insert
// gates both key off `vector.length > 0`, so an empty vector never
// calls embed(), never pins `brain.dimensions`, and never reaches the
// vector index — see brainy.ts add()'s matching comments) while never
// persisting a searchable zero vector for a downstream engine to trip
// over. Deliberately NOT `deferEmbedding: true`: that flag's landing
// path (`kickEmbedWorker()`, called synchronously right after commit —
// see brainy.ts add()/update()) would still force the WASM engine to
// cold-compile within milliseconds of open (just off the awaited path
// instead of never paying it at all) AND would eventually embed the
// root's data for real, which this fix forbids — the root must NEVER
// be embedded, not merely "not yet".
feat(open-path): init never gates on the embedding model; open goes concurrent; slow opens narrate
A production restart storm measured 90,017ms for a single brain init vs
1,117ms quiet (~80x contention multiplier), traced to performInit() eagerly
awaiting the process-global WASM embedding engine before the VFS root even
existed. Every writer's open() queued on the one throttled model compile
(90-140s on throttled CPUs).
- VirtualFileSystem.doInitializeRoot() no longer embeds '/'. The root is
system-tier plumbing nothing ever searches; when the default WASM engine
is active it now gets an explicit all-zero placeholder vector
(cosineDistance returns max distance for a zero vector, so it never ranks
ahead of real content). deferEmbedding was considered and rejected: its
landing path kicks the embed worker synchronously right after commit,
which would still force the cold compile within milliseconds — just off
the awaited path, not avoided. A registered native 'embeddings' provider
(no cold-start cost, possibly a different dimension) still embeds the
root for real, via the new Brainy.usesDefaultWasmEmbedder() seam.
- performInit()'s eager-embedding step now only STARTS the WASM engine warm
in the background instead of awaiting it inline. embed()/embeddingManager
already serialize concurrent callers on one shared init promise, so the
first real embed() converges correctly either way; a failed warm narrates
loudly instead of surfacing as a silent latency spike or an unhandled
rejection. eagerEmbeddings: false still means no warm at all.
- FileSystemStorage.init() batches its ~8 independent bootstrap mkdirs
(each creates its own full subtree via recursive:true, so none depend on
the others existing) into one Promise.all. The restore-completion step
and initializeCounts() stay strictly sequential — they have real order
dependencies on rootDir and systemDir respectively.
- performInit() now times five phases (storage init / generation-store
open+fold / index init+gate / VFS bootstrap / embedding-warm-started) and
logs one warning with the per-phase breakdown when total open exceeds
2000ms; silent otherwise.
2026-08-25 10:09:45 -07:00
//
fix(vfs): the VFS root never persists a zero-norm vector
A zero-norm vector is lawful inside brainy (cosine distance scores it at
maximum, never a false top hit) but a false attractor for a downstream
engine serving squared-euclidean distance, which cannot tell a real
all-zero vector apart from a legitimate origin point.
- The VFS root now persists with vector [] (the existing "unvectored"
shape) instead of a real all-zero 384-dim placeholder, and is never
routed into the deferred-embed pipeline.
- A one-time migration in the root-init path detects a pre-fix store's
all-zero placeholder root (by norm, not length) and rewrites it to []
through a new sanctioned Brainy method that keeps the canonical
vectored-noun ledger honest and removes the row from the vector index.
- The vector-index write seam (AddToVectorIndexOperation,
ReplaceInVectorIndexOperation, and the generation materializer's direct
insert) now refuses any real all-zero vector before it reaches a
provider, loudly naming the entity, while the canonical write still
lands.
- add()'s dimension-pinning and HNSW-insert gates, and the add-params
validator, now treat any empty vector as carrying no dimension
information, closing a latent trap where an explicit `vector: []`
would have pinned dimensions to 0.
2026-08-27 09:28:44 -07:00
// Only the default WASM engine gets this treatment — a plugin-
// registered native 'embeddings' provider has no cold-compile cost and
// may use a different dimension, so it keeps embedding the root for
// real (same as before this fix) rather than leave Brainy's own
// plumbing permanently unvectored on a store where embedding is cheap.
feat(open-path): init never gates on the embedding model; open goes concurrent; slow opens narrate
A production restart storm measured 90,017ms for a single brain init vs
1,117ms quiet (~80x contention multiplier), traced to performInit() eagerly
awaiting the process-global WASM embedding engine before the VFS root even
existed. Every writer's open() queued on the one throttled model compile
(90-140s on throttled CPUs).
- VirtualFileSystem.doInitializeRoot() no longer embeds '/'. The root is
system-tier plumbing nothing ever searches; when the default WASM engine
is active it now gets an explicit all-zero placeholder vector
(cosineDistance returns max distance for a zero vector, so it never ranks
ahead of real content). deferEmbedding was considered and rejected: its
landing path kicks the embed worker synchronously right after commit,
which would still force the cold compile within milliseconds — just off
the awaited path, not avoided. A registered native 'embeddings' provider
(no cold-start cost, possibly a different dimension) still embeds the
root for real, via the new Brainy.usesDefaultWasmEmbedder() seam.
- performInit()'s eager-embedding step now only STARTS the WASM engine warm
in the background instead of awaiting it inline. embed()/embeddingManager
already serialize concurrent callers on one shared init promise, so the
first real embed() converges correctly either way; a failed warm narrates
loudly instead of surfacing as a silent latency spike or an unhandled
rejection. eagerEmbeddings: false still means no warm at all.
- FileSystemStorage.init() batches its ~8 independent bootstrap mkdirs
(each creates its own full subtree via recursive:true, so none depend on
the others existing) into one Promise.all. The restore-completion step
and initializeCounts() stay strictly sequential — they have real order
dependencies on rootDir and systemDir respectively.
- performInit() now times five phases (storage init / generation-store
open+fold / index init+gate / VFS bootstrap / embedding-warm-started) and
logs one warning with the per-phase breakdown when total open exceeds
2000ms; silent otherwise.
2026-08-25 10:09:45 -07:00
const rootVector = this . brain . usesDefaultWasmEmbedder ( )
fix(vfs): the VFS root never persists a zero-norm vector
A zero-norm vector is lawful inside brainy (cosine distance scores it at
maximum, never a false top hit) but a false attractor for a downstream
engine serving squared-euclidean distance, which cannot tell a real
all-zero vector apart from a legitimate origin point.
- The VFS root now persists with vector [] (the existing "unvectored"
shape) instead of a real all-zero 384-dim placeholder, and is never
routed into the deferred-embed pipeline.
- A one-time migration in the root-init path detects a pre-fix store's
all-zero placeholder root (by norm, not length) and rewrites it to []
through a new sanctioned Brainy method that keeps the canonical
vectored-noun ledger honest and removes the row from the vector index.
- The vector-index write seam (AddToVectorIndexOperation,
ReplaceInVectorIndexOperation, and the generation materializer's direct
insert) now refuses any real all-zero vector before it reaches a
provider, loudly naming the entity, while the canonical write still
lands.
- add()'s dimension-pinning and HNSW-insert gates, and the add-params
validator, now treat any empty vector as carrying no dimension
information, closing a latent trap where an explicit `vector: []`
would have pinned dimensions to 0.
2026-08-27 09:28:44 -07:00
? ( [ ] as number [ ] )
feat(open-path): init never gates on the embedding model; open goes concurrent; slow opens narrate
A production restart storm measured 90,017ms for a single brain init vs
1,117ms quiet (~80x contention multiplier), traced to performInit() eagerly
awaiting the process-global WASM embedding engine before the VFS root even
existed. Every writer's open() queued on the one throttled model compile
(90-140s on throttled CPUs).
- VirtualFileSystem.doInitializeRoot() no longer embeds '/'. The root is
system-tier plumbing nothing ever searches; when the default WASM engine
is active it now gets an explicit all-zero placeholder vector
(cosineDistance returns max distance for a zero vector, so it never ranks
ahead of real content). deferEmbedding was considered and rejected: its
landing path kicks the embed worker synchronously right after commit,
which would still force the cold compile within milliseconds — just off
the awaited path, not avoided. A registered native 'embeddings' provider
(no cold-start cost, possibly a different dimension) still embeds the
root for real, via the new Brainy.usesDefaultWasmEmbedder() seam.
- performInit()'s eager-embedding step now only STARTS the WASM engine warm
in the background instead of awaiting it inline. embed()/embeddingManager
already serialize concurrent callers on one shared init promise, so the
first real embed() converges correctly either way; a failed warm narrates
loudly instead of surfacing as a silent latency spike or an unhandled
rejection. eagerEmbeddings: false still means no warm at all.
- FileSystemStorage.init() batches its ~8 independent bootstrap mkdirs
(each creates its own full subtree via recursive:true, so none depend on
the others existing) into one Promise.all. The restore-completion step
and initializeCounts() stay strictly sequential — they have real order
dependencies on rootDir and systemDir respectively.
- performInit() now times five phases (storage init / generation-store
open+fold / index init+gate / VFS bootstrap / embedding-warm-started) and
logs one warning with the per-phase breakdown when total open exceeds
2000ms; silent otherwise.
2026-08-25 10:09:45 -07:00
: undefined
2025-11-14 12:51:25 -08:00
await this . brain . add ( {
id : rootId , // Fixed ID - storage ensures uniqueness
data : '/' ,
type : NounType . Collection ,
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
subtype : 'vfs-root' , // Standard subtype for the VFS root collection (7.30+)
feat(8.0): visibility field (public/internal/system) on nouns + verbs
Adds a reserved, top-level `visibility` field (mirrors the subtype rollout):
'public' (default, surfaced) | 'internal' (developer app-internal — hidden from
default find/count/stats, opt-in via includeInternal) | 'system' (Brainy
plumbing, library-set only).
Fixes a real leak: the VFS root entity counted in getNounCount() and appeared in
find() (a fresh brain reported 1 entity). It is now visibility:'system' →
excluded from every user-facing surface. Developers also get a first-class
hidden-unless-asked tier (e.g. learned internals vs user-exposed data).
- Reserved (RESERVED_ENTITY_FIELDS / RESERVED_RELATION_FIELDS) — spoof-proof from metadata.
- Threaded through add/relate/update/transact; surfaced top-level on reads.
- Default exclusion in counts (baseStorage), find()/related() (hard candidate
filter via excludeVisibility — keeps topK/limit correct), and stats;
includeInternal/includeSystem opt-ins.
- VFS root marked 'system'.
Tests: visibility.test.ts 17/17 (fresh-brain getNounCount()===0, internal hidden
+ opt-in, verb symmetry, top-level surfacing, metadata-spoof rejection). Unit
1431 green; count-synchronization integration now passes (off-by-one fixed).
2026-06-16 15:20:26 -07:00
// visibility 'system' (8.0): the VFS root is Brainy's own plumbing, not user data,
// so it is hidden everywhere by default (getNounCount()/find()/stats()) and surfaces
// only via find({ includeSystem: true }). 'system' is intentionally not part of the
// public AddParams.visibility union ('public' | 'internal') — this is the single
// sanctioned internal setter, hence the cast.
visibility : 'system' as 'public' | 'internal' ,
feat(open-path): init never gates on the embedding model; open goes concurrent; slow opens narrate
A production restart storm measured 90,017ms for a single brain init vs
1,117ms quiet (~80x contention multiplier), traced to performInit() eagerly
awaiting the process-global WASM embedding engine before the VFS root even
existed. Every writer's open() queued on the one throttled model compile
(90-140s on throttled CPUs).
- VirtualFileSystem.doInitializeRoot() no longer embeds '/'. The root is
system-tier plumbing nothing ever searches; when the default WASM engine
is active it now gets an explicit all-zero placeholder vector
(cosineDistance returns max distance for a zero vector, so it never ranks
ahead of real content). deferEmbedding was considered and rejected: its
landing path kicks the embed worker synchronously right after commit,
which would still force the cold compile within milliseconds — just off
the awaited path, not avoided. A registered native 'embeddings' provider
(no cold-start cost, possibly a different dimension) still embeds the
root for real, via the new Brainy.usesDefaultWasmEmbedder() seam.
- performInit()'s eager-embedding step now only STARTS the WASM engine warm
in the background instead of awaiting it inline. embed()/embeddingManager
already serialize concurrent callers on one shared init promise, so the
first real embed() converges correctly either way; a failed warm narrates
loudly instead of surfacing as a silent latency spike or an unhandled
rejection. eagerEmbeddings: false still means no warm at all.
- FileSystemStorage.init() batches its ~8 independent bootstrap mkdirs
(each creates its own full subtree via recursive:true, so none depend on
the others existing) into one Promise.all. The restore-completion step
and initializeCounts() stay strictly sequential — they have real order
dependencies on rootDir and systemDir respectively.
- performInit() now times five phases (storage init / generation-store
open+fold / index init+gate / VFS bootstrap / embedding-warm-started) and
logs one warning with the per-phase breakdown when total open exceeds
2000ms; silent otherwise.
2026-08-25 10:09:45 -07:00
metadata : this.getRootMetadata ( ) ,
. . . ( rootVector ? { vector : rootVector } : { } )
2025-11-14 12:51:25 -08:00
} )
return rootId
} catch ( error : any ) {
// If creation failed due to duplicate ID, another instance created it
// This is normal in concurrent scenarios - just return the fixed ID
const errorMsg = error ? . message ? . toLowerCase ( ) || ''
if ( errorMsg . includes ( 'already exists' ) ||
errorMsg . includes ( 'duplicate' ) ||
errorMsg . includes ( 'eexist' ) ) {
console . log ( 'VFS: Root already created by another instance, using existing' )
return rootId
2025-09-26 15:45:13 -07:00
}
2025-11-14 11:27:35 -08:00
2025-11-14 12:51:25 -08:00
// Unexpected error
throw error
2025-09-24 17:31:48 -07:00
}
2025-11-14 12:51:25 -08:00
}
2025-09-24 17:31:48 -07:00
2025-11-14 12:51:25 -08:00
/ * *
2026-01-27 15:38:21 -08:00
* Get standard root metadata
2025-11-14 12:51:25 -08:00
* Centralized to ensure consistency
* /
private getRootMetadata ( ) : VFSMetadata {
return {
path : '/' ,
name : '' ,
vfsType : 'directory' ,
isVFS : true ,
isVFSEntity : true ,
size : 0 ,
permissions : 0o755 ,
owner : 'root' ,
group : 'root' ,
accessed : Date.now ( ) ,
modified : Date.now ( )
}
2025-09-24 17:31:48 -07:00
}
2025-11-14 11:27:35 -08:00
/ * *
2026-01-27 15:38:21 -08:00
* Cleanup old UUID - based VFS roots
2025-11-14 12:51:25 -08:00
* Called during init to remove duplicate roots created before fixed - ID fix
*
* This is a one - time migration helper that can be removed in future versions .
2025-11-14 11:27:35 -08:00
* /
perf(vfs): the old-root sweep runs once per store, not once per open
MEASURED on a 14,056-noun / 72,679-verb production-shaped store, measured
solo under an exclusive lock: the vfs-bootstrap phase cost 43,021 ms of a cold open and 52,696 ms of
a WARM REOPEN. What dominates it is a migration sweep — a filtered find() over
the whole store hunting for root directories created before the fixed root id
existed. A store either carries such duplicates or never will, and the sweep
ran on every open, forever, in the foreground.
It is now caused by the store's state instead of by the open count: a durable
marker under _system/ records that the sweep has run, and a store carrying it
never sweeps again. A store without one sweeps in the BACKGROUND — the sweep
only removes duplicate roots, nothing serves from them, and it was already
declared non-critical — narrated at both ends, with whenRootSweepSettled() for
anyone who needs to observe rather than race it. An adapter with no raw-object
door keeps the old behaviour: correctness over cost, never a silent skip.
Pins: tests/integration/vfs-root-sweep-once.test.ts — the sweep runs on the
first open and never on the second or third; a sweep slowed to 4s does not
delay the open.
2026-08-28 11:01:43 -07:00
/ * *
* @description Run the old - root sweep at most once per store , in the
* background , and record that it ran . See the call site in { @link init } for
* the measurement that made this necessary .
* @returns A promise that settles when the sweep has finished ( or was
* skipped ) ; nothing in the read path awaits it .
* /
private async sweepOldRootsIfNeeded ( ) : Promise < void > {
const store = this . rawObjectStore ( )
if ( store === null ) {
// A storage adapter with no raw-object door cannot carry the marker.
// Sweep every open, as before — correctness over cost.
await this . cleanupOldRoots ( )
return
}
try {
const marker = await store . readRawObject ( VirtualFileSystem . ROOT_SWEEP_MARKER_PATH )
if ( marker !== null && marker !== undefined ) return
} catch {
// Unreadable marker: sweep, and rewrite it below.
}
2026-08-28 12:30:30 -07:00
// NARRATION HAS A THRESHOLD, like every other line this engine emits on the
// always-visible channel. On a fresh or small store this sweep finds
// nothing and costs a millisecond, and announcing it — twice — on a
// channel a production log level deliberately CANNOT silence would train
// operators to ignore the one channel that exists to be impossible to
// ignore. It speaks when it has something to say: duplicates removed, or a
// wall long enough that somebody watching a slow first open deserves to
// know what is running. Otherwise it does its work and stays quiet.
perf(vfs): the old-root sweep runs once per store, not once per open
MEASURED on a 14,056-noun / 72,679-verb production-shaped store, measured
solo under an exclusive lock: the vfs-bootstrap phase cost 43,021 ms of a cold open and 52,696 ms of
a WARM REOPEN. What dominates it is a migration sweep — a filtered find() over
the whole store hunting for root directories created before the fixed root id
existed. A store either carries such duplicates or never will, and the sweep
ran on every open, forever, in the foreground.
It is now caused by the store's state instead of by the open count: a durable
marker under _system/ records that the sweep has run, and a store carrying it
never sweeps again. A store without one sweeps in the BACKGROUND — the sweep
only removes duplicate roots, nothing serves from them, and it was already
declared non-critical — narrated at both ends, with whenRootSweepSettled() for
anyone who needs to observe rather than race it. An adapter with no raw-object
door keeps the old behaviour: correctness over cost, never a silent skip.
Pins: tests/integration/vfs-root-sweep-once.test.ts — the sweep runs on the
first open and never on the second or third; a sweep slowed to 4s does not
delay the open.
2026-08-28 11:01:43 -07:00
const startedAt = Date . now ( )
2026-08-28 12:30:30 -07:00
const duplicatesRemoved = await this . cleanupOldRoots ( )
const elapsedMs = Date . now ( ) - startedAt
perf(vfs): the old-root sweep runs once per store, not once per open
MEASURED on a 14,056-noun / 72,679-verb production-shaped store, measured
solo under an exclusive lock: the vfs-bootstrap phase cost 43,021 ms of a cold open and 52,696 ms of
a WARM REOPEN. What dominates it is a migration sweep — a filtered find() over
the whole store hunting for root directories created before the fixed root id
existed. A store either carries such duplicates or never will, and the sweep
ran on every open, forever, in the foreground.
It is now caused by the store's state instead of by the open count: a durable
marker under _system/ records that the sweep has run, and a store carrying it
never sweeps again. A store without one sweeps in the BACKGROUND — the sweep
only removes duplicate roots, nothing serves from them, and it was already
declared non-critical — narrated at both ends, with whenRootSweepSettled() for
anyone who needs to observe rather than race it. An adapter with no raw-object
door keeps the old behaviour: correctness over cost, never a silent skip.
Pins: tests/integration/vfs-root-sweep-once.test.ts — the sweep runs on the
first open and never on the second or third; a sweep slowed to 4s does not
delay the open.
2026-08-28 11:01:43 -07:00
try {
await store . writeRawObject ( VirtualFileSystem . ROOT_SWEEP_MARKER_PATH , {
sweptAt : new Date ( ) . toISOString ( ) ,
2026-08-28 12:30:30 -07:00
durationMs : elapsedMs
perf(vfs): the old-root sweep runs once per store, not once per open
MEASURED on a 14,056-noun / 72,679-verb production-shaped store, measured
solo under an exclusive lock: the vfs-bootstrap phase cost 43,021 ms of a cold open and 52,696 ms of
a WARM REOPEN. What dominates it is a migration sweep — a filtered find() over
the whole store hunting for root directories created before the fixed root id
existed. A store either carries such duplicates or never will, and the sweep
ran on every open, forever, in the foreground.
It is now caused by the store's state instead of by the open count: a durable
marker under _system/ records that the sweep has run, and a store carrying it
never sweeps again. A store without one sweeps in the BACKGROUND — the sweep
only removes duplicate roots, nothing serves from them, and it was already
declared non-critical — narrated at both ends, with whenRootSweepSettled() for
anyone who needs to observe rather than race it. An adapter with no raw-object
door keeps the old behaviour: correctness over cost, never a silent skip.
Pins: tests/integration/vfs-root-sweep-once.test.ts — the sweep runs on the
first open and never on the second or third; a sweep slowed to 4s does not
delay the open.
2026-08-28 11:01:43 -07:00
} )
2026-08-28 12:30:30 -07:00
if ( duplicatesRemoved > 0 || elapsedMs >= VirtualFileSystem . ROOT_SWEEP_NARRATE_MS ) {
prodLog . narrate (
` [VFS] one-time old-root sweep complete in ${ elapsedMs } ms ` +
( duplicatesRemoved > 0
? ` , ${ duplicatesRemoved } pre-fixed-id root(s) removed `
: '' ) +
' and recorded — no future open pays for it.'
)
}
perf(vfs): the old-root sweep runs once per store, not once per open
MEASURED on a 14,056-noun / 72,679-verb production-shaped store, measured
solo under an exclusive lock: the vfs-bootstrap phase cost 43,021 ms of a cold open and 52,696 ms of
a WARM REOPEN. What dominates it is a migration sweep — a filtered find() over
the whole store hunting for root directories created before the fixed root id
existed. A store either carries such duplicates or never will, and the sweep
ran on every open, forever, in the foreground.
It is now caused by the store's state instead of by the open count: a durable
marker under _system/ records that the sweep has run, and a store carrying it
never sweeps again. A store without one sweeps in the BACKGROUND — the sweep
only removes duplicate roots, nothing serves from them, and it was already
declared non-critical — narrated at both ends, with whenRootSweepSettled() for
anyone who needs to observe rather than race it. An adapter with no raw-object
door keeps the old behaviour: correctness over cost, never a silent skip.
Pins: tests/integration/vfs-root-sweep-once.test.ts — the sweep runs on the
first open and never on the second or third; a sweep slowed to 4s does not
delay the open.
2026-08-28 11:01:43 -07:00
} catch ( error ) {
// Unrecorded sweep = the next open sweeps again. Conservative, and said
// out loud rather than quietly repeated forever.
prodLog . narrate (
` [VFS] old-root sweep finished in ${ Date . now ( ) - startedAt } ms but could NOT be ` +
` recorded ( ${ ( error as Error ) . message } ) — the next open will sweep again. `
)
}
}
/ * *
* @description Settle once the background old - root sweep has finished .
* Resolves immediately when the store already carried the marker . Exists so
* tests and operators can observe the sweep instead of racing it ; no read
* path waits on it .
* @returns A promise that settles with the sweep .
* /
public async whenRootSweepSettled ( ) : Promise < void > {
await this . rootSweep
}
/ * *
* @description The brain ' s storage adapter , narrowed to the raw - object door
* this migration marker needs . Boundary : ` Brainy.storage ` is private , and
* this is the same reach - in the engine uses elsewhere for exactly this kind
* of engine - internal artifact . Returns null when the adapter has no
* raw - object door .
* /
private rawObjectStore ( ) : {
readRawObject : ( key : string ) = > Promise < unknown >
writeRawObject : ( key : string , value : unknown ) = > Promise < void >
} | null {
const storage = ( this . brain as unknown as { storage? : Record < string , unknown > } ) . storage
if (
storage &&
typeof storage . readRawObject === 'function' &&
typeof storage . writeRawObject === 'function'
) {
return storage as unknown as {
readRawObject : ( key : string ) = > Promise < unknown >
writeRawObject : ( key : string , value : unknown ) = > Promise < void >
}
}
return null
}
2026-08-28 12:30:30 -07:00
private async cleanupOldRoots ( ) : Promise < number > {
let removed = 0
2025-11-14 12:51:25 -08:00
try {
// Find any old VFS roots with UUID-based IDs (not our fixed ID)
const oldRoots = await this . brain . find ( {
type : NounType . Collection ,
where : {
path : '/' ,
vfsType : 'directory'
} ,
limit : 100 ,
excludeVFS : false
2025-11-14 11:27:35 -08:00
} )
2025-11-14 12:51:25 -08:00
// Filter out our fixed-ID root
const duplicates = oldRoots . filter ( r = > r . id !== VirtualFileSystem . VFS_ROOT_ID )
if ( duplicates . length > 0 ) {
2026-01-27 15:38:21 -08:00
console . log ( ` VFS: Found ${ duplicates . length } old UUID-based root(s), cleaning up... ` )
2025-11-14 11:27:35 -08:00
2025-11-14 12:51:25 -08:00
for ( const duplicate of duplicates ) {
try {
2026-06-11 14:51:00 -07:00
await this . brain . remove ( duplicate . id )
2026-08-28 12:30:30 -07:00
removed ++
2025-11-14 12:51:25 -08:00
console . log ( ` VFS: Deleted old root ${ duplicate . id . substring ( 0 , 8 ) } ` )
} catch ( error ) {
console . warn ( ` VFS: Failed to delete old root ${ duplicate . id } : ` , error )
}
}
2025-11-14 11:27:35 -08:00
2025-11-14 12:51:25 -08:00
console . log ( 'VFS: Cleanup complete - all old roots removed' )
2025-11-14 11:27:35 -08:00
}
2025-11-14 12:51:25 -08:00
} catch ( error ) {
// Non-critical error - log and continue
console . warn ( 'VFS: Cleanup of old roots failed (non-critical):' , error )
2025-11-14 11:27:35 -08:00
}
2026-08-28 12:30:30 -07:00
return removed
2025-11-14 11:27:35 -08:00
}
fix: recover VFS content blobs stranded by a 7→8 upgrade, in place on open
The 7.x branch system stored Virtual Filesystem content as blobs in its
copy-on-write area (`_cow/`). 8.0 removed that system and stores content blobs
in the content-addressed store (`_cas/`), but the one-time layout migration only
moves entities — it never adopted the `_cow/` content blobs. A store that used
the VFS was left with every VFS-backed read throwing "Blob metadata not found"
and its pages 500ing, with no first-party recovery and the pre-upgrade backup
already removed on entity-migration "success".
Add an on-open recovery pass (`autoAdoptLegacyVfsBlobsIfNeeded`) that runs right
after the layout migration and adopts every orphaned `_cow/` blob into `_cas/` —
copying both the bytes and the metadata via the raw object primitives,
idempotently and non-destructively (the `_cow/` originals are never deleted). It
is a separate phase, not folded into the migration, so it also heals a store
already upgraded by an earlier 8.0.x that stranded the blobs (whose layout-
migration marker is stamped): it is gated on the presence of `_cow/` and its own
`_system/vfs-blob-adoption.json` marker. Filesystem-only; native-8.0 and non-VFS
stores no-op on a cheap existence check.
- `BaseStorage.adoptLegacyCowBlobs()` — the scan/copy primitive, returning
`{ cowBlobs, adopted, alreadyPresent, incomplete }`; skips (does not half-adopt)
a blob missing its bytes or metadata.
- `brain.vfs.adoptOrphanedBlobs()` — the explicit force path; self-initializes.
- Backup retention: the automatic pre-upgrade backup is now kept if any blob
can't be fully adopted (`incomplete > 0`), instead of being removed on
entity-migration success alone — a blob-parity gate the migration signal
cannot provide.
Adds an end-to-end integration test (storage-level adopt with idempotency and
incomplete handling; self-heal on reopen; the explicit API) and an upgrade guide.
2026-07-07 12:23:36 -07:00
/ * *
* @description Recover VFS content blobs orphaned by a 7 → 8 upgrade . 7 . x stored
* VFS blobs under the copy - on - write area ( ` _cow/ ` ) of the branch / versioning
* system that 8.0 removed ; a brain upgraded before the migration adopted them
* reads a stranded blob and throws "Blob metadata not found" ( every page
* backed by it 500 s ) . This adopts every orphaned ` _cow/ ` blob into the 8.0
* content - addressed store ( ` _cas/ ` ) IN PLACE — no snapshot restore — then
* clears the VFS caches so the recovered content reads live immediately .
*
* Idempotent and non - destructive : blobs already adopted are skipped and the
* ` _cow/ ` originals are never deleted ( a re - run or rollback stays possible ) .
* Safe to run on a healthy or native - 8.0 brain ( returns zeros ) . Delegates to
* { @link BaseStorage . adoptLegacyCowBlobs } .
*
* @returns ` { cowBlobs, adopted, alreadyPresent, incomplete } ` counts .
* @example
* const brain = new Brainy ( { storage : { type : 'filesystem' , path : '/data/brain' } } )
* await brain . init ( )
* const r = await brain . vfs . adoptOrphanedBlobs ( )
* console . log ( ` Adopted ${ r . adopted } stranded VFS blobs; ${ r . alreadyPresent } already present. ` )
* /
async adoptOrphanedBlobs ( ) : Promise < {
cowBlobs : number
adopted : number
alreadyPresent : number
incomplete : number
} > {
// The recovery scan works on raw storage objects, so it only needs the
// brain's storage wired up — not the VFS's own init. brain.init() is
// idempotent (a no-op on an already-open brain) and, on a cold brain,
// sets up storage AND runs the on-open auto-adoption itself; the explicit
// scan below is then the idempotent "force re-scan" equivalent.
await this . brain . init ( )
const storage = this . brain [ 'storage' ] as unknown as
| { adoptLegacyCowBlobs ? : ( ) = > Promise < { cowBlobs : number ; adopted : number ; alreadyPresent : number ; incomplete : number } > }
| undefined
if ( ! storage || typeof storage . adoptLegacyCowBlobs !== 'function' ) {
return { cowBlobs : 0 , adopted : 0 , alreadyPresent : 0 , incomplete : 0 }
}
const result = await storage . adoptLegacyCowBlobs ( )
// Drop any cached content/stat so a re-read hits the freshly adopted blobs.
this . contentCache . clear ( )
this . statCache . clear ( )
return result
}
2025-09-24 17:31:48 -07:00
// ============= File Operations =============
/ * *
* Read a file ' s content
* /
async readFile ( path : string , options? : ReadOptions ) : Promise < Buffer > {
await this . ensureInitialized ( )
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
// Temporal read: the file's exact bytes as of a past generation or
// instant. Materializes the entity from the generation history — content
// blobs referenced by any in-window generation are retention-protected,
// so the bytes are guaranteed present. Bypasses the content cache.
if ( options ? . asOf !== undefined ) {
return this . readFileAt ( path , options . asOf , options )
}
2025-09-24 17:31:48 -07:00
// Check cache first
if ( options ? . cache !== false && this . contentCache . has ( path ) ) {
const cached = this . contentCache . get ( path ) !
if ( Date . now ( ) - cached . timestamp < ( this . config . cache ? . ttl || 300000 ) ) {
return cached . data
}
}
// Resolve path to entity
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Verify it's a file
if ( entity . metadata . vfsType !== 'file' ) {
throw new VFSError ( VFSErrorCode . EISDIR , ` Is a directory: ${ path } ` , path , 'readFile' )
}
2026-01-27 15:38:21 -08:00
// Unified blob storage - ONE path only
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
if ( ! entity . metadata . storage ? . type || entity . metadata . storage . type !== 'blob' ) {
throw new VFSError (
VFSErrorCode . EIO ,
2026-01-27 15:38:21 -08:00
` File has no blob storage: ${ path } . Requires blob storage format. ` ,
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
path ,
'readFile'
)
2025-09-24 17:31:48 -07:00
}
2026-01-27 15:38:21 -08:00
// CRITICAL FIX - Isolate blob errors from VFS tree corruption
2025-11-14 11:27:35 -08:00
// Blob read errors MUST NOT cascade to VFS tree structure
try {
// Read from BlobStorage (handles decompression automatically)
const content = await this . blobStorage . read ( entity . metadata . storage . hash )
2025-09-24 17:31:48 -07:00
2026-01-27 15:38:21 -08:00
// REMOVED updateAccessTime() for performance
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
// Access time updates caused 50-100ms GCS write on EVERY file read
// Modern file systems use 'noatime' for same reason (performance)
// Field 'accessed' still exists in metadata for backward compat but won't update
// await this.updateAccessTime(entityId) // ← REMOVED
2025-09-24 17:31:48 -07:00
2025-11-14 11:27:35 -08:00
// Cache the content
if ( options ? . cache !== false ) {
this . contentCache . set ( path , { data : content , timestamp : Date.now ( ) } )
}
2025-09-24 17:31:48 -07:00
2025-11-14 11:27:35 -08:00
// Apply encoding if requested
if ( options ? . encoding ) {
return Buffer . from ( content . toString ( options . encoding ) )
}
2025-09-24 17:31:48 -07:00
2025-11-14 11:27:35 -08:00
return content
} catch ( blobError ) {
// Blob error isolated - VFS tree structure remains intact
const errorMsg = blobError instanceof Error ? blobError.message : String ( blobError )
console . error ( ` VFS: Cannot read blob for ${ path } : ` , errorMsg )
// Throw VFSError (not blob error) - prevents cascading corruption
throw new VFSError (
VFSErrorCode . EIO ,
` File read failed: ${ errorMsg } ` ,
path ,
'readFile'
)
}
2025-09-24 17:31:48 -07:00
}
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
/ * *
* @description The temporal read behind ` readFile(path, { asOf }) ` : resolve
* the path ' s CURRENT entity , materialize its state at the target
* generation / instant from the Model - B history , and read that version ' s
* content blob ( retention - protected , so present for every in - window
* generation ) . The pinned view is always released so compaction is never
* blocked by a read .
* @param path - The file path ( resolved against the live tree ) .
* @param asOf - A generation number or wall - clock ` Date ` .
* @param options - ` encoding ` applies ; caching does not ( never cached ) .
* @returns The exact bytes the file held at that generation .
* @throws VFSError ENOENT when the file did not exist at that generation ;
* the generation store ' s compacted - generation error when ` asOf ` is past
* the retention window ' s horizon .
* /
private async readFileAt (
path : string ,
asOf : number | Date ,
options? : ReadOptions
) : Promise < Buffer > {
const entityId = await this . pathResolver . resolve ( path )
const db = await this . brain . asOf ( asOf )
try {
const entity = await db . get ( entityId )
if ( ! entity ) {
throw new VFSError (
VFSErrorCode . ENOENT ,
` File did not exist as of ${ String ( asOf ) } : ${ path } ` ,
path ,
'readFile'
)
}
const storage = ( entity . metadata as Record < string , any > | undefined ) ? . storage
if ( storage ? . type !== 'blob' || typeof storage . hash !== 'string' ) {
throw new VFSError (
VFSErrorCode . EIO ,
` File had no blob storage as of ${ String ( asOf ) } : ${ path } ` ,
path ,
'readFile'
)
}
const content = await this . blobStorage . read ( storage . hash )
if ( options ? . encoding ) {
return Buffer . from ( content . toString ( options . encoding ) )
}
return content
} finally {
await db . release ( )
}
}
/ * *
* @description A file ' s version history within the retention window : one
* entry per generation that wrote the file , ascending — the newest entry is
* the current state . Pair with ` readFile(path, { asOf: entry.generation }) `
* to fetch any version ' s exact bytes , and restore by writing those bytes
* back ( a NEW write — history is never rewritten ) . Bounded by the retention
* window : versions whose generations were compacted are not listed .
* @param path - The file path ( resolved against the live tree ) .
* @returns The versions , oldest first .
* @example
* const versions = await brain . vfs . history ( '/pages/home.json' )
* const before = await brain . vfs . readFile ( '/pages/home.json' , {
* asOf : versions [ versions . length - 2 ] . generation
* } )
* /
async history ( path : string ) : Promise < FileVersion [ ] > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
// Boundary note: reaches Brainy's internal generation store (the same
// bracket-access precedent as the blobStorage getter) — the per-id
// generation chain and horizon are not on the public Brainy surface.
const generationStore = ( this . brain as unknown as {
generationStore : {
horizon ( ) : number
generation ( ) : number
generationsTouching (
kind : 'noun' | 'verb' ,
id : string ,
fromGen : number ,
toGen : number
) : Promise < number [ ] >
}
} ) . generationStore
const gens = await generationStore . generationsTouching (
'noun' ,
entityId ,
generationStore . horizon ( ) ,
generationStore . generation ( )
)
const versions : FileVersion [ ] = [ ]
for ( const gen of gens ) {
const db = await this . brain . asOf ( gen )
try {
const entity = await db . get ( entityId )
const meta = entity ? . metadata as Record < string , any > | undefined
const storage = meta ? . storage
if ( storage ? . type === 'blob' && typeof storage . hash === 'string' ) {
versions . push ( {
generation : gen ,
timestamp : db.timestamp ,
hash : storage.hash ,
size : typeof meta ? . size === 'number' ? meta . size : ( storage . size ? ? 0 ) ,
. . . ( typeof meta ? . mimeType === 'string' && { mimeType : meta.mimeType } )
} )
}
} finally {
await db . release ( )
}
}
return versions
}
2025-09-24 17:31:48 -07:00
/ * *
* Write a file
* /
async writeFile ( path : string , data : Buffer | string , options? : WriteOptions ) : Promise < void > {
await this . ensureInitialized ( )
// Convert string to buffer
const buffer = Buffer . isBuffer ( data ) ? data : Buffer.from ( data , options ? . encoding )
// Check size limits
if ( this . config . limits ? . maxFileSize && buffer . length > this . config . limits . maxFileSize ) {
throw new VFSError ( VFSErrorCode . ENOSPC , ` File too large: ${ buffer . length } bytes ` , path , 'writeFile' )
}
// Parse path to get parent and name
const parentPath = this . getParentPath ( path )
const name = this . getBasename ( path )
// Ensure parent directory exists
const parentId = await this . ensureDirectory ( parentPath )
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
// Check if file already exists (and capture its current content hash so
// the overwrite can release the superseded live reference afterwards).
2025-09-24 17:31:48 -07:00
let existingId : string | null = null
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
let previousHash : string | undefined
2025-09-24 17:31:48 -07:00
try {
existingId = await this . pathResolver . resolve ( path , { cache : false } )
// Verify the entity still exists in the brain
const existing = await this . brain . get ( existingId )
if ( ! existing ) {
existingId = null // Entity was deleted but cache wasn't cleared
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
} else if ( existing . metadata ? . storage ? . type === 'blob' ) {
previousHash = existing . metadata . storage . hash as string | undefined
2025-09-24 17:31:48 -07:00
}
} catch ( err ) {
// File doesn't exist, which is fine
existingId = null
}
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
// Detect MIME type BEFORE the blob write so the store's auto-compression
// policy can skip zstd for already-compressed media (JPEG, MP4, ZIP, …).
const mimeType = mimeDetector . detectMimeType ( name , buffer )
2026-01-27 15:38:21 -08:00
// Unified blob storage for ALL files (no size-based branching)
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
// Store in BlobStorage (content-addressable, auto-deduplication)
const blobHash = await this . blobStorage . write ( buffer , { mimeType } )
2025-09-24 17:31:48 -07:00
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
// Get blob metadata (size, compression info)
const blobMetadata = await this . blobStorage . getMetadata ( blobHash )
const storageStrategy : VFSMetadata [ 'storage' ] = {
type : 'blob' ,
hash : blobHash ,
size : buffer.length ,
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
compressed : blobMetadata ? blobMetadata . compression !== 'none' : undefined
2025-09-24 17:31:48 -07:00
}
// Create metadata
const metadata : VFSMetadata = {
path ,
name ,
parent : parentId ,
vfsType : 'file' ,
2026-01-27 15:38:21 -08:00
isVFS : true , // Mark as VFS entity (internal)
isVFSEntity : true , // Explicit flag for developer filtering
2025-09-24 17:31:48 -07:00
size : buffer.length ,
mimeType ,
extension : this.getExtension ( name ) ,
permissions : options?.mode || this . config . permissions ? . defaultFile || 0 o644 ,
owner : 'user' , // In production, get from auth context
group : 'users' ,
accessed : Date.now ( ) ,
modified : Date.now ( ) ,
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
storage : storageStrategy
2026-01-27 15:38:21 -08:00
// No rawData - content is in BlobStorage
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
// Backward compatibility: readFile() checks for rawData for legacy files
2025-09-24 17:31:48 -07:00
}
// Extract additional metadata if enabled
if ( this . config . intelligence ? . autoExtract && options ? . extractMetadata !== false ) {
Object . assign ( metadata , await this . extractMetadata ( buffer , mimeType ) )
}
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
// For embedding: use text content, for storage: use raw data. Computed
// for BOTH branches — an overwrite must refresh the entity's `data` (and
// therefore its embedding), or semantic search and any `data` read keep
// serving the FIRST version's text forever.
const embeddingData = mimeDetector . isTextFile ( mimeType )
? buffer . toString ( 'utf-8' )
: ` File: ${ name } ( ${ mimeType } , ${ buffer . length } bytes) `
2025-09-24 17:31:48 -07:00
if ( existingId ) {
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
// Update existing file — content bytes live in BlobStorage; `data`
// carries the embedding text and MUST track the new content.
2025-09-24 17:31:48 -07:00
await this . brain . update ( {
id : existingId ,
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
data : embeddingData ,
2026-08-05 16:26:43 -07:00
// MT5: the caller's write acks at durability; the re-embed (a neural
// net — it dominated the measured 5.6s p50 per file write) runs on
// the background worker and swaps in atomically. Content is readable
// and metadata-findable immediately; semantic search converges when
// the embed lands (eventual vector index, the documented contract).
deferEmbedding : true ,
2025-09-24 17:31:48 -07:00
metadata
} )
2025-09-26 15:12:04 -07:00
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
// The update committed: drop the superseded live reference. When the
// content is unchanged this cancels the dedup increment the write()
// above just made (net: one live reference per referencing file); when
// it changed, the old bytes stay retention-protected for asOf reads
// via the update's own generation record.
if ( previousHash ) {
await this . blobStorage . release ( previousHash )
}
2025-09-26 15:12:04 -07:00
// Ensure Contains relationship exists (fix for missing relationships)
2026-06-11 14:51:00 -07:00
const existingRelations = await this . brain . related ( {
2025-09-26 15:12:04 -07:00
from : parentId ,
to : existingId ,
type : VerbType . Contains
} )
// Create relationship if it doesn't exist
if ( existingRelations . length === 0 ) {
await this . brain . relate ( {
from : parentId ,
to : existingId ,
2025-10-24 15:59:41 -07:00
type : VerbType . Contains ,
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
subtype : 'vfs-contains' , // Standard subtype for VFS containment edges (7.30+)
2026-01-27 15:38:21 -08:00
metadata : { isVFS : true } // Mark as VFS relationship
2025-09-26 15:12:04 -07:00
} )
}
2025-09-24 17:31:48 -07:00
} else {
// Create new file entity
const entity = await this . brain . add ( {
data : embeddingData , // Always provide string for embeddings
type : this . getFileNounType ( mimeType ) ,
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
subtype : 'vfs-file' , // Standard subtype for VFS file entities (7.30+)
2026-08-05 16:26:43 -07:00
// MT5: ack at durability; embedding backgrounds (see the overwrite
// branch note above).
deferEmbedding : true ,
2025-09-24 17:31:48 -07:00
metadata
} )
2025-09-30 12:53:39 -07:00
// Create parent-child relationship (no need to check for duplicates on new entities)
2025-09-24 17:31:48 -07:00
await this . brain . relate ( {
from : parentId ,
to : entity ,
2025-10-24 15:59:41 -07:00
type : VerbType . Contains ,
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
subtype : 'vfs-contains' , // Standard subtype for VFS containment edges (7.30+)
2026-01-27 15:38:21 -08:00
metadata : { isVFS : true } // Mark as VFS relationship
2025-09-24 17:31:48 -07:00
} )
// Update path resolver cache
await this . pathResolver . createPath ( path , entity )
}
// Invalidate caches
this . invalidateCaches ( path )
// Trigger watchers
this . triggerWatchers ( path , existingId ? 'change' : 'rename' )
// Knowledge Layer hooks will be added by augmentation if enabled
// Knowledge Layer hooks will be added by augmentation if enabled
}
/ * *
* Append to a file
* /
async appendFile ( path : string , data : Buffer | string , options? : WriteOptions ) : Promise < void > {
await this . ensureInitialized ( )
// Read existing content
let existing : Buffer
try {
existing = await this . readFile ( path )
} catch ( err ) {
// File doesn't exist, create it
return this . writeFile ( path , data , options )
}
// Append new data
const newData = Buffer . isBuffer ( data ) ? data : Buffer.from ( data , options ? . encoding )
const combined = Buffer . concat ( [ existing , newData ] )
// Write combined content
await this . writeFile ( path , combined , options )
}
/ * *
* Delete a file
* /
async unlink ( path : string ) : Promise < void > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Verify it's a file
if ( entity . metadata . vfsType !== 'file' ) {
throw new VFSError ( VFSErrorCode . EISDIR , ` Is a directory: ${ path } ` , path , 'unlink' )
}
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
// Delete the entity FIRST, then drop its live blob reference — never
// release a reference while the referencing entity might survive (a
// failed remove must not leave a live file whose bytes compaction could
// later reclaim).
await this . brain . remove ( entityId )
// Drop the file's LIVE reference to its content blob. The bytes are NOT
// deleted — the remove's own generation record references this hash, so
// `readFile(path, { asOf })` keeps serving it inside the retention
// window; history compaction reclaims the bytes when the last referencing
// generation is reclaimed.
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
if ( entity . metadata . storage ? . type === 'blob' ) {
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
await this . blobStorage . release ( entity . metadata . storage . hash )
2025-09-24 17:31:48 -07:00
}
// Invalidate caches
this . pathResolver . invalidatePath ( path )
this . invalidateCaches ( path )
// Trigger watchers
this . triggerWatchers ( path , 'rename' )
// Knowledge Layer hooks will be added by augmentation if enabled
}
2025-09-26 10:17:59 -07:00
// ============= Tree Operations (NEW) =============
/ * *
* Get only direct children of a directory - guaranteed no self - inclusion
* This is the SAFE way to get children for building tree UIs
* /
async getDirectChildren ( path : string ) : Promise < VFSEntity [ ] > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Verify it's a directory
if ( entity . metadata . vfsType !== 'directory' ) {
throw new VFSError ( VFSErrorCode . ENOTDIR , ` Not a directory: ${ path } ` , path , 'getDirectChildren' )
}
// Use the safe getChildren from PathResolver
const children = await this . pathResolver . getChildren ( entityId )
// Double-check no self-inclusion (paranoid safety)
return children . filter ( child = > child . metadata . path !== path )
}
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
/ * *
2026-01-27 15:38:21 -08:00
* Gather descendants using graph traversal + bulk fetch
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
*
* ARCHITECTURE :
* 1 . Traverse graph to collect entity IDs ( in - memory , fast )
* 2 . Batch - fetch all entities in ONE storage call
* 3 . Return flat list of VFSEntity objects
*
* This is the ONLY correct approach :
* - Uses GraphAdjacencyIndex ( in - memory graph ) to traverse relationships
* - Makes ONE storage call to fetch all entities ( not N calls )
* - Respects maxDepth to limit scope ( billion - scale safe )
*
* Performance ( GCS ) :
* - OLD : 111 directories × 50 ms each = 5 , 550 ms
* - NEW : Graph traversal ( 1 ms ) + 1 batch fetch ( 100 ms ) = 101 ms
* - 55 x faster on cloud storage
*
* @param rootId - Root directory entity ID
* @param maxDepth - Maximum depth to traverse
* @returns All descendant entities ( flat list )
* /
private async gatherDescendants ( rootId : string , maxDepth : number ) : Promise < VFSEntity [ ] > {
const entityIds = new Set < string > ( )
const visited = new Set < string > ( [ rootId ] )
let currentLevel = [ rootId ]
let depth = 0
// Phase 1: Traverse graph in-memory to collect all entity IDs
// GraphAdjacencyIndex is in-memory LSM-tree, so this is fast (<10ms for 10k relationships)
while ( currentLevel . length > 0 && depth < maxDepth ) {
const nextLevel : string [ ] = [ ]
// Get all Contains relationships for this level (in-memory query)
for ( const parentId of currentLevel ) {
2026-06-11 14:51:00 -07:00
const relations = await this . brain . related ( {
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
from : parentId ,
type : VerbType . Contains
} )
// Collect child IDs
for ( const rel of relations ) {
if ( ! visited . has ( rel . to ) ) {
visited . add ( rel . to )
entityIds . add ( rel . to )
nextLevel . push ( rel . to ) // Queue for next level
}
}
}
currentLevel = nextLevel
depth ++
}
// Phase 2: Batch-fetch all entities in ONE storage call
// This is the optimization: ONE GCS call instead of 111+ GCS calls
const entityIdArray = Array . from ( entityIds )
if ( entityIdArray . length === 0 ) {
return [ ]
}
const entitiesMap = await this . brain . batchGet ( entityIdArray )
// Convert to VFSEntity array
const entities : VFSEntity [ ] = [ ]
for ( const id of entityIdArray ) {
const entity = entitiesMap . get ( id )
if ( entity && entity . metadata ? . vfsType ) {
entities . push ( entity as VFSEntity )
}
}
return entities
}
2025-09-26 10:17:59 -07:00
/ * *
* Get a properly structured tree for the given path
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
*
2026-01-27 15:38:21 -08:00
* Graph traversal + ONE batch fetch ( 55 x faster on cloud storage )
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
*
* Architecture :
* 1 . Resolve path to entity ID
* 2 . Traverse graph in - memory to collect all descendant IDs
* 3 . Batch - fetch all entities in ONE storage call
* 4 . Build tree structure
*
* Performance :
* - GCS : 5 , 300 ms → ~ 100 ms ( 53 x faster )
* - FileSystem : 200ms → ~ 50 ms ( 4 x faster )
2025-09-26 10:17:59 -07:00
* /
async getTreeStructure ( path : string , options ? : {
maxDepth? : number
includeHidden? : boolean
sort ? : 'name' | 'modified' | 'size'
} ) : Promise < any > {
await this . ensureInitialized ( )
const { VFSTreeUtils } = await import ( './TreeUtils.js' )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
if ( entity . metadata . vfsType !== 'directory' ) {
throw new VFSError ( VFSErrorCode . ENOTDIR , ` Not a directory: ${ path } ` , path , 'getTreeStructure' )
}
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
const maxDepth = options ? . maxDepth ? ? 10
2025-09-26 10:17:59 -07:00
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
// Gather all descendants (graph traversal + ONE batch fetch)
const allEntities = await this . gatherDescendants ( entityId , maxDepth )
2025-09-26 10:17:59 -07:00
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
// Build tree structure
2025-09-26 10:17:59 -07:00
return VFSTreeUtils . buildTree ( allEntities , path , options || { } )
}
/ * *
* Get all descendants of a directory ( flat list )
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
*
2026-01-27 15:38:21 -08:00
* Same optimization as getTreeStructure
2025-09-26 10:17:59 -07:00
* /
async getDescendants ( path : string , options ? : {
includeAncestor? : boolean
type ? : 'file' | 'directory'
} ) : Promise < VFSEntity [ ] > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
if ( entity . metadata . vfsType !== 'directory' ) {
throw new VFSError ( VFSErrorCode . ENOTDIR , ` Not a directory: ${ path } ` , path , 'getDescendants' )
}
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
// Gather all descendants (no depth limit for this API)
const descendants = await this . gatherDescendants ( entityId , Infinity )
2025-09-26 10:17:59 -07:00
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
// Filter by type if specified
const filtered = options ? . type
? descendants . filter ( d = > d . metadata . vfsType === options . type )
: descendants
2025-09-26 10:17:59 -07:00
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
// Include ancestor if requested
if ( options ? . includeAncestor ) {
return [ entity , . . . filtered ]
2025-09-26 10:17:59 -07:00
}
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
return filtered
2025-09-26 10:17:59 -07:00
}
/ * *
* Inspect a path and return structured information
* This is the recommended method for file explorers to use
* /
async inspect ( path : string ) : Promise < {
node : VFSEntity
children : VFSEntity [ ]
parent : VFSEntity | null
stats : VFSStats
} > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
const stats = await this . stat ( path )
let children : VFSEntity [ ] = [ ]
if ( entity . metadata . vfsType === 'directory' ) {
children = await this . getDirectChildren ( path )
}
let parent : VFSEntity | null = null
if ( path !== '/' ) {
const parentPath = path . substring ( 0 , path . lastIndexOf ( '/' ) ) || '/'
const parentId = await this . pathResolver . resolve ( parentPath )
parent = await this . getEntityById ( parentId )
}
return {
node : entity ,
children ,
parent ,
stats
}
}
2025-09-24 17:31:48 -07:00
// ============= Directory Operations =============
/ * *
* Create a directory
* /
async mkdir ( path : string , options? : MkdirOptions ) : Promise < void > {
await this . ensureInitialized ( )
2025-09-30 12:53:39 -07:00
// Use mutex to prevent race conditions when creating the same directory concurrently
// If another call is already creating this directory, wait for it to complete
const existingLock = this . mkdirLocks . get ( path )
if ( existingLock ) {
await existingLock
// After waiting, check if directory now exists
try {
const existing = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( existing )
if ( entity . metadata . vfsType === 'directory' ) {
return // Directory was created by the other call
2025-09-24 17:31:48 -07:00
}
2025-09-30 12:53:39 -07:00
} catch ( err ) {
// Still doesn't exist, proceed to create
2025-09-24 17:31:48 -07:00
}
}
2025-09-30 12:53:39 -07:00
// Create a lock promise for this path
let resolveLock : ( ) = > void
const lockPromise = new Promise < void > ( resolve = > { resolveLock = resolve } )
this . mkdirLocks . set ( path , lockPromise )
2025-09-24 17:31:48 -07:00
2025-09-30 12:53:39 -07:00
try {
// Check if already exists
2025-09-24 17:31:48 -07:00
try {
2025-09-30 12:53:39 -07:00
const existing = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( existing )
if ( entity . metadata . vfsType === 'directory' ) {
if ( ! options ? . recursive ) {
throw new VFSError ( VFSErrorCode . EEXIST , ` Directory exists: ${ path } ` , path , 'mkdir' )
}
return // Already exists and recursive is true
} else {
// Path exists but it's not a directory
throw new VFSError ( VFSErrorCode . EEXIST , ` File exists: ${ path } ` , path , 'mkdir' )
}
2025-09-24 17:31:48 -07:00
} catch ( err ) {
2025-09-30 12:53:39 -07:00
// Only proceed if it's a ENOENT error (path doesn't exist)
if ( err instanceof VFSError && err . code !== VFSErrorCode . ENOENT ) {
throw err // Re-throw non-ENOENT errors
}
// Doesn't exist, proceed to create
2025-09-24 17:31:48 -07:00
}
2025-09-30 12:53:39 -07:00
// Parse path
const parentPath = this . getParentPath ( path )
const name = this . getBasename ( path )
2025-09-24 17:31:48 -07:00
2025-09-30 12:53:39 -07:00
// Ensure parent exists (recursive mkdir if needed)
let parentId : string
if ( parentPath === '/' || parentPath === null ) {
parentId = this . rootEntityId !
} else if ( options ? . recursive ) {
parentId = await this . ensureDirectory ( parentPath )
} else {
try {
parentId = await this . pathResolver . resolve ( parentPath )
} catch ( err ) {
throw new VFSError ( VFSErrorCode . ENOENT , ` Parent directory not found: ${ parentPath } ` , path , 'mkdir' )
}
}
2025-09-24 17:31:48 -07:00
2025-09-30 12:53:39 -07:00
// Create directory entity
const metadata : VFSMetadata = {
path ,
name ,
parent : parentId ,
vfsType : 'directory' ,
2026-01-27 15:38:21 -08:00
isVFS : true , // Mark as VFS entity (internal)
isVFSEntity : true , // Explicit flag for developer filtering
2025-09-30 12:53:39 -07:00
size : 0 ,
permissions : options?.mode || this . config . permissions ? . defaultDirectory || 0 o755 ,
owner : 'user' ,
group : 'users' ,
accessed : Date.now ( ) ,
modified : Date.now ( ) ,
. . . options ? . metadata
}
const entity = await this . brain . add ( {
data : path , // Directory path as string content
type : NounType . Collection ,
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
subtype : 'vfs-directory' , // Standard subtype for VFS directory entities (7.30+)
2026-08-05 16:26:43 -07:00
// MT5: a directory creation on a write path must not wait on the
// embedder either — same ack-at-durability contract as file writes.
deferEmbedding : true ,
2025-09-30 12:53:39 -07:00
metadata
2025-09-24 17:31:48 -07:00
} )
2025-09-30 12:53:39 -07:00
// Create parent-child relationship (no need to check for duplicates on new entities)
if ( parentId !== entity ) { // Don't relate to self (root)
await this . brain . relate ( {
from : parentId ,
to : entity ,
2025-10-24 15:59:41 -07:00
type : VerbType . Contains ,
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
subtype : 'vfs-contains' , // Standard subtype for VFS containment edges (7.30+)
2025-10-28 16:23:58 -07:00
metadata : {
2026-01-27 15:38:21 -08:00
isVFS : true , // Mark as VFS relationship
relationshipType : 'vfs' // Standardized relationship type metadata
2025-10-28 16:23:58 -07:00
}
2025-09-30 12:53:39 -07:00
} )
}
2025-09-24 17:31:48 -07:00
2025-09-30 12:53:39 -07:00
// Update path resolver cache
await this . pathResolver . createPath ( path , entity )
// Trigger watchers
this . triggerWatchers ( path , 'rename' )
} finally {
// Release the lock
resolveLock ! ( )
this . mkdirLocks . delete ( path )
}
2025-09-24 17:31:48 -07:00
}
/ * *
* Remove a directory
2025-12-11 08:37:55 -08:00
*
2026-01-27 15:38:21 -08:00
* Optimized for cloud storage using batch operations
2025-12-11 08:37:55 -08:00
* - Uses gatherDescendants ( ) for efficient graph traversal + batch fetch
2026-06-11 14:51:00 -07:00
* - Uses removeMany ( ) for chunked transactional deletion
2025-12-11 08:37:55 -08:00
* - Parallel blob cleanup with chunking
*
* Performance improvement : 4 - 8 x faster on cloud storage ( GCS , S3 , R2 , Azure )
* - 15 files on GCS : 120s → 15 - 30 s
2025-09-24 17:31:48 -07:00
* /
async rmdir ( path : string , options ? : { recursive? : boolean } ) : Promise < void > {
await this . ensureInitialized ( )
if ( path === '/' ) {
throw new VFSError ( VFSErrorCode . EACCES , 'Cannot remove root directory' , path , 'rmdir' )
}
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Verify it's a directory
if ( entity . metadata . vfsType !== 'directory' ) {
throw new VFSError ( VFSErrorCode . ENOTDIR , ` Not a directory: ${ path } ` , path , 'rmdir' )
}
// Check if empty (unless recursive)
const children = await this . pathResolver . getChildren ( entityId )
if ( children . length > 0 && ! options ? . recursive ) {
throw new VFSError ( VFSErrorCode . ENOTEMPTY , ` Directory not empty: ${ path } ` , path , 'rmdir' )
}
2026-01-27 15:38:21 -08:00
// OPTIMIZED batch deletion for recursive case
2025-12-11 08:37:55 -08:00
if ( options ? . recursive && children . length > 0 ) {
// Phase 1: Gather all descendants in ONE batch fetch
const descendants = await this . gatherDescendants ( entityId , Infinity )
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
// Phase 2: Batch delete all entities (including root directory) FIRST —
// never release a blob reference while its referencing entity might
// survive a failed delete (see unlink's ordering note).
const allIds = [ . . . descendants . map ( d = > d . id ) , entityId ]
await this . brain . removeMany ( { ids : allIds , continueOnError : false } )
// Phase 3: Drop the deleted files' LIVE blob references (chunked).
// Live-reference drops only — the bytes stay for in-window asOf reads
// and are reclaimed by history compaction (see unlink's note).
2025-12-11 08:37:55 -08:00
const blobFiles = descendants . filter ( d = >
d . metadata . vfsType === 'file' && d . metadata . storage ? . type === 'blob'
)
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
const BLOB_CHUNK_SIZE = 20 // Parallel release 20 blob references at a time
2025-12-11 08:37:55 -08:00
for ( let i = 0 ; i < blobFiles . length ; i += BLOB_CHUNK_SIZE ) {
const chunk = blobFiles . slice ( i , i + BLOB_CHUNK_SIZE )
await Promise . all ( chunk . map ( f = >
feat: temporal VFS — file content joins the Model-B immutability model
The temporal model had a hole exactly where files were concerned: every
entity write is an immutable generation with before-images, but VFS content
BYTES lived under an eager refCount GC left over from the pre-8.0 design —
unlink could physically destroy bytes that in-window history still
referenced, and overwrite never released the old hash at all (an unbounded
silent leak whose accidental byproduct was the only thing "preserving"
history). Reading the past could therefore return a stale field, a dangling
hash, or nothing, depending on luck.
Fix: blob reclamation becomes a HISTORY decision instead of a LIVENESS
decision. Each blob's metadata now carries historyRefCount alongside the
live refCount:
- The commit seam counts one history reference per persisted before-image
record carrying a content hash (commitTransaction staging and the
group-commit flush), recorded BEFORE the record-set persists and carried
in the generation delta (blobHashes — always present on new deltas, so
compaction only falls back to reading records for pre-contract
generations). An aborted transaction compensates best-effort.
- unlink/rmdir/overwrite drop ONLY the live reference (BlobStorage.delete →
release; overwrite finally releases the superseded hash — cancelling the
dedup increment on same-content rewrites and closing the leak), and only
AFTER the canonical mutation commits, so a failed delete can never leave a
live file whose bytes compaction might reclaim.
- History compaction is the ONE reclamation point: after deleting a
generation's record-set it releases that set's references and physically
reclaims any hash at zero live AND zero history references. Pins are
exempt automatically. Crash ordering is over-count-only in every path
(record before persist, release after delete), so a crash can leak until
the scrub recounts but can never reclaim bytes a retained generation
needs. scrubBlobHistoryRefCounts() restores exactness; existing stores get
a one-time marker-gated backfill on open, failing into leak-safe mode
(reclamation disabled) rather than guessing.
On top of the protected history, the temporal API the generational model
always implied:
- vfs.readFile(path, { asOf }) — the exact bytes as of a generation or Date,
materialized from the history (pinned view released so compaction is
never blocked by a read).
- vfs.history(path) — FileVersion[] ascending ({ generation, timestamp,
hash, size, mimeType? }), the newest entry being the live state.
- Overwrites now refresh the file entity's data/embedding text — semantic
search and the data field previously served the FIRST version's text
forever (the stale-field defect a consumer's incident recovery depended
on by luck).
Integration suite (temporal-vfs.test.ts): per-version exact reads +
history listing, leak-fix + history protection on overwrite, rm keeps bytes
readable, compaction reclaims past-window bytes and preserves in-window
(including the cross-file dedup case where an old file's history and a
newer file's removal share one hash), data freshness, and scrub exactness.
2026-07-10 16:43:48 -07:00
this . blobStorage . release ( f . metadata . storage ! . hash )
2025-12-11 08:37:55 -08:00
) )
2025-09-24 17:31:48 -07:00
}
2025-12-11 08:37:55 -08:00
} else {
// No children or not recursive - just delete the directory entity
2026-06-11 14:51:00 -07:00
await this . brain . remove ( entityId )
2025-12-11 08:37:55 -08:00
}
2025-09-24 17:31:48 -07:00
2025-12-11 08:37:55 -08:00
// Invalidate caches (recursive invalidation handles all descendants)
2025-09-24 17:31:48 -07:00
this . pathResolver . invalidatePath ( path , true )
2026-01-31 09:09:12 -08:00
this . invalidateCaches ( path , true )
2025-09-24 17:31:48 -07:00
// Trigger watchers
this . triggerWatchers ( path , 'rename' )
}
/ * *
2026-08-25 10:10:01 -07:00
* @description List a directory ' s contents . Non - recursive ( default )
* returns direct children only , named by basename . ` recursive: true `
* lists every descendant at any depth ( files and directories ) , each
* reported as a path RELATIVE TO THE QUERIED DIRECTORY — matching Node ' s
* ` fs.readdir(dir, { recursive: true }) ` convention — e . g . ` 'sub' ` and
* ` 'sub/file.txt' ` for a nested file . With ` withFileTypes: true ` , each
* { @link VFSDirent } ' s ` name ` carries that same value ( relative when
* recursive , basename otherwise ) ; ` path ` is always the absolute VFS path
* either way .
* @param path - The directory to list .
* @param options - ` recursive ` , ` withFileTypes ` , ` filter ` , ` sort ` ,
* ` offset ` / ` limit ` ( pagination applies AFTER filter / sort , over the full
* recursive set when ` recursive: true ` ) .
* @throws { VFSError } ENOTDIR when ` path ` is not a directory .
2025-09-24 17:31:48 -07:00
* /
async readdir ( path : string , options? : ReaddirOptions ) : Promise < string [ ] | VFSDirent [ ] > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Verify it's a directory
if ( entity . metadata . vfsType !== 'directory' ) {
throw new VFSError ( VFSErrorCode . ENOTDIR , ` Not a directory: ${ path } ` , path , 'readdir' )
}
2026-08-25 10:10:01 -07:00
// Direct children, or every descendant at any depth. gatherDescendants()
// is the same graph-traversal + ONE-batch-fetch path getTreeStructure()/
// getDescendants() already use — no per-directory storage round trips.
let children = options ? . recursive
? await this . gatherDescendants ( entityId , Infinity )
: await this . pathResolver . getChildren ( entityId )
2025-09-24 17:31:48 -07:00
// Apply filters
if ( options ? . filter ) {
children = this . filterDirectoryEntries ( children , options . filter )
}
// Sort if requested
if ( options ? . sort ) {
children = this . sortDirectoryEntries ( children , options . sort , options . order )
}
// Apply pagination
if ( options ? . offset ) {
children = children . slice ( options . offset )
}
if ( options ? . limit ) {
children = children . slice ( 0 , options . limit )
}
2026-01-27 15:38:21 -08:00
// REMOVED updateAccessTime() for performance
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
// Directory access time updates caused 50-100ms GCS write on EVERY readdir
// await this.updateAccessTime(entityId) // ← REMOVED
2025-09-24 17:31:48 -07:00
2026-08-25 10:10:01 -07:00
// The queried directory's own canonical (already-normalized) path — the
// base every recursive entry's relative name is computed against. Using
// the resolved entity's OWN path (rather than the raw `path` argument)
// means no separate normalization step is needed here.
const baseDir = entity . metadata . path
const relativeToBase = ( childPath : string ) : string = > {
const prefix = baseDir === '/' ? '/' : ` ${ baseDir } / `
return childPath . startsWith ( prefix ) ? childPath . slice ( prefix . length ) : childPath
}
2025-09-24 17:31:48 -07:00
// Return appropriate format
if ( options ? . withFileTypes ) {
2025-10-28 14:30:31 -07:00
return children . map ( child = > ( {
2026-08-25 10:10:01 -07:00
name : options?.recursive ? relativeToBase ( child . metadata . path ) : child . metadata . name ,
2025-09-24 17:31:48 -07:00
path : child.metadata.path ,
type : child . metadata . vfsType ,
entityId : child.id
} as VFSDirent ) )
}
2026-08-25 10:10:01 -07:00
return children . map ( child = >
options ? . recursive ? relativeToBase ( child . metadata . path ) : child . metadata . name
)
2025-09-24 17:31:48 -07:00
}
// ============= Metadata Operations =============
/ * *
* Get file / directory statistics
* /
async stat ( path : string ) : Promise < VFSStats > {
await this . ensureInitialized ( )
// Check cache
if ( this . statCache . has ( path ) ) {
const cached = this . statCache . get ( path ) !
if ( Date . now ( ) - cached . timestamp < 5000 ) { // 5 second cache
return cached . stats
}
}
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
const stats : VFSStats = {
size : entity.metadata.size ,
mode : entity.metadata.permissions ,
uid : 1000 , // In production, map owner to UID
gid : 1000 , // In production, map group to GID
atime : new Date ( entity . metadata . accessed ) ,
mtime : new Date ( entity . metadata . modified ) ,
ctime : new Date ( entity . updatedAt || entity . createdAt ) ,
birthtime : new Date ( entity . createdAt ) ,
isFile : ( ) = > entity . metadata . vfsType === 'file' ,
isDirectory : ( ) = > entity . metadata . vfsType === 'directory' ,
isSymbolicLink : ( ) = > entity . metadata . vfsType === 'symlink' ,
path ,
entityId : entity.id ,
vector : entity.vector ,
connections : await this . countRelationships ( entityId )
}
// Cache stats
this . statCache . set ( path , { stats , timestamp : Date.now ( ) } )
return stats
}
/ * *
* lstat - same as stat for now ( symlinks not fully implemented )
* /
async lstat ( path : string ) : Promise < VFSStats > {
return this . stat ( path )
}
/ * *
* Check if path exists
* /
async exists ( path : string ) : Promise < boolean > {
await this . ensureInitialized ( )
try {
await this . pathResolver . resolve ( path )
return true
} catch ( err ) {
return false
}
}
// ============= Semantic Operations =============
/ * *
* Search files with natural language
* /
async search ( query : string , options? : SearchOptions ) : Promise < SearchResult [ ] > {
await this . ensureInitialized ( )
// Build find params
const params : FindParams = {
query ,
type : [ NounType . File , NounType . Document , NounType . Media ] ,
limit : options?.limit || 10 ,
offset : options?.offset ,
2025-10-24 12:25:47 -07:00
where : {
2026-01-27 15:38:21 -08:00
vfsType : 'file' // Search VFS files
2025-10-24 12:25:47 -07:00
}
2025-09-24 17:31:48 -07:00
}
// Add path filter if specified
if ( options ? . path ) {
params . where = {
. . . params . where ,
path : { $startsWith : options.path }
}
}
// Add metadata filters
if ( options ? . where ) {
Object . assign ( params . where || { } , options . where )
}
// Execute search using Brainy's Triple Intelligence
const results = await this . brain . find ( params )
// Convert to search results
return results . map ( r = > {
const entity = r . entity as VFSEntity
return {
path : entity.metadata.path ,
entityId : entity.id ,
score : r.score ,
type : entity . metadata . vfsType ,
size : entity.metadata.size ,
modified : new Date ( entity . metadata . modified ) ,
explanation : r.explanation
}
} )
}
/ * *
* Find files similar to a given file
* /
async findSimilar ( path : string , options? : SimilarOptions ) : Promise < SearchResult [ ] > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
// Use Brainy's similarity search
const results = await this . brain . similar ( {
to : entityId ,
limit : options?.limit || 10 ,
threshold : options?.threshold || 0.7 ,
fix: wire up includeVFS parameter to ALL VFS-related APIs (6 critical bugs)
🚨 CRITICAL BUGS FIXED - VFS APIs weren't actually working!
The systematic API audit revealed VFS methods were calling brain.find()
and brain.similar() WITHOUT includeVFS: true, which meant they excluded
VFS entities by default - the exact opposite of what they should do!
**6 Critical Bugs Fixed:**
1. ❌ brain.similar() - Missing includeVFS parameter passthrough
✅ Added includeVFS to SimilarParams, wired to brain.find()
2. ❌ vfs.search() - Brain.find() call missing includeVFS: true
✅ Added includeVFS: true (line 958)
3. ❌ vfs.findSimilar() - Brain.similar() call missing includeVFS: true
✅ Added includeVFS: true (line 1006)
4. ❌ vfs.searchEntities() - Brain.find() call missing includeVFS: true
✅ Added includeVFS: true (line 2321)
5. ❌ VFS semantic projections (TagProjection) - All brain.find() calls missing includeVFS
✅ Fixed 3 calls in TagProjection (toQuery, resolve, list)
6. ❌ VFS semantic projections (AuthorProjection, TemporalProjection) - Missing includeVFS
✅ Fixed 2 calls in AuthorProjection (resolve, list)
✅ Fixed 2 calls in TemporalProjection (resolve, list)
**Impact:**
- VFS search would return 0 results (brain.find() excluded VFS by default)
- VFS similarity would return 0 results
- VFS semantic views (/by-tag, /by-author, /by-date) would be empty
- Users couldn't find ANY VFS files using VFS search APIs
**Root Cause:**
When we added VFS filtering to brain.find() in v4.3.3, we excluded VFS
entities by default. But we forgot to add includeVFS: true to VFS-specific
APIs that NEED to find VFS entities. This is exactly the kind of "created
but not wired up" bug the user warned about.
**Production Quality:**
- ✅ All code actually wired up and used
- ✅ Build passes
- ✅ TypeScript type safety enforced
- ✅ Production scale ready (no mocks, stubs, or workarounds)
- ✅ Works with billions of entities (uses existing O(log n) filtering)
Files modified:
- src/brainy.ts - Added includeVFS passthrough to brain.similar()
- src/types/brainy.types.ts - Added includeVFS to SimilarParams
- src/vfs/VirtualFileSystem.ts - Added includeVFS to 3 search methods
- src/vfs/semantic/projections/*.ts - Added includeVFS to all 3 projections
2025-10-24 12:04:13 -07:00
type : [ NounType . File , NounType . Document , NounType . Media ] ,
2025-10-24 12:25:47 -07:00
where : {
2026-01-27 15:38:21 -08:00
vfsType : 'file' // Find similar VFS files
2025-10-24 12:25:47 -07:00
}
2025-09-24 17:31:48 -07:00
} )
return results . map ( r = > {
const entity = r . entity as VFSEntity
return {
path : entity.metadata.path ,
entityId : entity.id ,
score : r.score ,
type : entity . metadata . vfsType ,
size : entity.metadata.size ,
modified : new Date ( entity . metadata . modified )
}
} )
}
// ============= Helper Methods =============
private async ensureInitialized ( ) : Promise < void > {
if ( ! this . initialized ) {
feat: add confidence/weight to Entity and flatten Result fields for convenient access
Add confidence and weight properties to Entity interface and flatten Result fields to top level for improved developer experience and API consistency.
Breaking Changes: None (all changes are backward compatible)
Phase 2 - Entity Confidence & Weight:
- Add confidence (type classification certainty) and weight (entity importance) to Entity interface
- Add confidence/weight parameters to AddParams and UpdateParams
- Update convertNounToEntity() to extract confidence/weight from storage
- Update add() and update() methods to preserve confidence/weight in metadata
- Enable developers to specify and access entity confidence/weight scores
Phase 3 - Result Field Flattening:
- Flatten commonly-used entity fields (type, metadata, data, confidence, weight) to Result top level
- Add createResult() helper for consistent Result construction
- Update all find() code paths to use createResult()
- Enable direct access: result.metadata instead of result.entity.metadata
- Preserve full entity in result.entity for backward compatibility
VFS Fix (from previous work):
- Fix VFSStructureGenerator to use brain.vfs() cached instance instead of creating separate instance
- Improve VFS error messages with step-by-step guidance
- Update examples to show correct vfs.init() usage
- Add comprehensive VFS import verification tests
Documentation Updates:
- Update API_REFERENCE.md with confidence/weight examples and flattened Result documentation
- Enhance JSDoc for add(), get(), find(), similar() with v4.3.0 examples
- Document Result structure changes and backward compatibility
- Add migration examples showing both old and new access patterns
Tests:
- Add 16 comprehensive tests for Entity confidence/weight exposure
- Add tests for Result field flattening
- Add tests for backward compatibility
- All tests passing (16/16)
API Consistency:
- Entity: direct access to confidence/weight
- Result: flattened fields + nested entity (both work)
- Relation: already had confidence/weight (consistent)
- VFS: inherits from Entity (automatic)
Files Changed:
- src/types/brainy.types.ts - Updated Entity, AddParams, UpdateParams, Result interfaces
- src/brainy.ts - Updated implementation and JSDoc for all affected methods
- tests/integration/entity-confidence-weight.test.ts - 16 comprehensive tests
- docs/API_REFERENCE.md - Updated with v4.3.0 examples
- src/importers/VFSStructureGenerator.ts - VFS fix
- src/vfs/VirtualFileSystem.ts - Improved error messages
- examples/unified-import-example.ts - Added vfs.init() example
- tests/integration/vfs-*-verification.test.ts - VFS verification tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 12:19:50 -07:00
throw new VFSError (
VFSErrorCode . EINVAL ,
'VFS not initialized. Call await vfs.init() before using VFS operations.\n\n' +
'✅ After brain.import():\n' +
' await brain.import(file, { vfsPath: "/imports/data" })\n' +
2025-11-02 10:58:52 -08:00
' const vfs = brain.vfs\n' +
feat: add confidence/weight to Entity and flatten Result fields for convenient access
Add confidence and weight properties to Entity interface and flatten Result fields to top level for improved developer experience and API consistency.
Breaking Changes: None (all changes are backward compatible)
Phase 2 - Entity Confidence & Weight:
- Add confidence (type classification certainty) and weight (entity importance) to Entity interface
- Add confidence/weight parameters to AddParams and UpdateParams
- Update convertNounToEntity() to extract confidence/weight from storage
- Update add() and update() methods to preserve confidence/weight in metadata
- Enable developers to specify and access entity confidence/weight scores
Phase 3 - Result Field Flattening:
- Flatten commonly-used entity fields (type, metadata, data, confidence, weight) to Result top level
- Add createResult() helper for consistent Result construction
- Update all find() code paths to use createResult()
- Enable direct access: result.metadata instead of result.entity.metadata
- Preserve full entity in result.entity for backward compatibility
VFS Fix (from previous work):
- Fix VFSStructureGenerator to use brain.vfs() cached instance instead of creating separate instance
- Improve VFS error messages with step-by-step guidance
- Update examples to show correct vfs.init() usage
- Add comprehensive VFS import verification tests
Documentation Updates:
- Update API_REFERENCE.md with confidence/weight examples and flattened Result documentation
- Enhance JSDoc for add(), get(), find(), similar() with v4.3.0 examples
- Document Result structure changes and backward compatibility
- Add migration examples showing both old and new access patterns
Tests:
- Add 16 comprehensive tests for Entity confidence/weight exposure
- Add tests for Result field flattening
- Add tests for backward compatibility
- All tests passing (16/16)
API Consistency:
- Entity: direct access to confidence/weight
- Result: flattened fields + nested entity (both work)
- Relation: already had confidence/weight (consistent)
- VFS: inherits from Entity (automatic)
Files Changed:
- src/types/brainy.types.ts - Updated Entity, AddParams, UpdateParams, Result interfaces
- src/brainy.ts - Updated implementation and JSDoc for all affected methods
- tests/integration/entity-confidence-weight.test.ts - 16 comprehensive tests
- docs/API_REFERENCE.md - Updated with v4.3.0 examples
- src/importers/VFSStructureGenerator.ts - VFS fix
- src/vfs/VirtualFileSystem.ts - Improved error messages
- examples/unified-import-example.ts - Added vfs.init() example
- tests/integration/vfs-*-verification.test.ts - VFS verification tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 12:19:50 -07:00
' await vfs.init() // ← Required! Safe to call multiple times\n' +
' const files = await vfs.readdir("/imports/data")\n\n' +
'✅ Direct VFS usage:\n' +
2025-11-02 10:58:52 -08:00
' const vfs = brain.vfs\n' +
feat: add confidence/weight to Entity and flatten Result fields for convenient access
Add confidence and weight properties to Entity interface and flatten Result fields to top level for improved developer experience and API consistency.
Breaking Changes: None (all changes are backward compatible)
Phase 2 - Entity Confidence & Weight:
- Add confidence (type classification certainty) and weight (entity importance) to Entity interface
- Add confidence/weight parameters to AddParams and UpdateParams
- Update convertNounToEntity() to extract confidence/weight from storage
- Update add() and update() methods to preserve confidence/weight in metadata
- Enable developers to specify and access entity confidence/weight scores
Phase 3 - Result Field Flattening:
- Flatten commonly-used entity fields (type, metadata, data, confidence, weight) to Result top level
- Add createResult() helper for consistent Result construction
- Update all find() code paths to use createResult()
- Enable direct access: result.metadata instead of result.entity.metadata
- Preserve full entity in result.entity for backward compatibility
VFS Fix (from previous work):
- Fix VFSStructureGenerator to use brain.vfs() cached instance instead of creating separate instance
- Improve VFS error messages with step-by-step guidance
- Update examples to show correct vfs.init() usage
- Add comprehensive VFS import verification tests
Documentation Updates:
- Update API_REFERENCE.md with confidence/weight examples and flattened Result documentation
- Enhance JSDoc for add(), get(), find(), similar() with v4.3.0 examples
- Document Result structure changes and backward compatibility
- Add migration examples showing both old and new access patterns
Tests:
- Add 16 comprehensive tests for Entity confidence/weight exposure
- Add tests for Result field flattening
- Add tests for backward compatibility
- All tests passing (16/16)
API Consistency:
- Entity: direct access to confidence/weight
- Result: flattened fields + nested entity (both work)
- Relation: already had confidence/weight (consistent)
- VFS: inherits from Entity (automatic)
Files Changed:
- src/types/brainy.types.ts - Updated Entity, AddParams, UpdateParams, Result interfaces
- src/brainy.ts - Updated implementation and JSDoc for all affected methods
- tests/integration/entity-confidence-weight.test.ts - 16 comprehensive tests
- docs/API_REFERENCE.md - Updated with v4.3.0 examples
- src/importers/VFSStructureGenerator.ts - VFS fix
- src/vfs/VirtualFileSystem.ts - Improved error messages
- examples/unified-import-example.ts - Added vfs.init() example
- tests/integration/vfs-*-verification.test.ts - VFS verification tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 12:19:50 -07:00
' await vfs.init() // ← Always required before first use\n' +
' await vfs.writeFile("/docs/readme.md", "Hello")\n\n' +
'📖 Docs: https://github.com/soulcraftlabs/brainy/blob/main/docs/vfs/QUICK_START.md' ,
'<unknown>' ,
'VFS'
2025-09-26 14:27:46 -07:00
)
2025-09-24 17:31:48 -07:00
}
}
private async ensureDirectory ( path : string ) : Promise < string > {
if ( ! path || path === '/' ) {
return this . rootEntityId !
}
try {
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
if ( entity . metadata . vfsType !== 'directory' ) {
throw new VFSError ( VFSErrorCode . ENOTDIR , ` Not a directory: ${ path } ` , path )
}
return entityId
} catch ( err ) {
2025-09-26 15:12:04 -07:00
// Only create directory if it doesn't exist (ENOENT error)
if ( err instanceof VFSError && err . code === VFSErrorCode . ENOENT ) {
await this . mkdir ( path , { recursive : true } )
return await this . pathResolver . resolve ( path )
}
// Re-throw other errors (like ENOTDIR)
throw err
2025-09-24 17:31:48 -07:00
}
}
2026-06-29 10:04:19 -07:00
/ * *
* Fetch a Brainy entity by id and normalize it into a { @link VFSEntity } .
*
* Backfills VFS metadata for legacy entities that stored fields at the top
* level ( pre - nested - metadata layout ) and guarantees the root directory always
* carries valid directory metadata .
*
* @param id - The Brainy entity id to fetch .
* @returns The entity with a populated ` metadata.vfsType ` and related VFS fields .
* @throws { VFSError } ENOENT when no entity exists for the given id .
* /
2025-09-24 17:31:48 -07:00
async getEntityById ( id : string ) : Promise < VFSEntity > {
const entity = await this . brain . get ( id )
if ( ! entity ) {
throw new VFSError ( VFSErrorCode . ENOENT , ` Entity not found: ${ id } ` )
}
2025-09-26 15:45:13 -07:00
// Ensure entity has proper VFS metadata structure
// Handle both nested and flat metadata structures for compatibility
if ( ! entity . metadata || ! entity . metadata . vfsType ) {
2026-06-11 14:51:00 -07:00
// Check if metadata is at top level (legacy structure). Legacy entities
// carried VFS fields as extra top-level properties not modeled on Entity.
const anyEntity = entity as Entity & Record < string , unknown >
2025-09-26 15:45:13 -07:00
if ( anyEntity . vfsType || anyEntity . path ) {
entity . metadata = {
path : anyEntity.path || '/' ,
name : anyEntity.name || '' ,
vfsType : anyEntity.vfsType || ( anyEntity . path === '/' ? 'directory' : 'file' ) ,
size : anyEntity.size || 0 ,
permissions : anyEntity.permissions || ( anyEntity . vfsType === 'directory' ? 0o755 : 0o644 ) ,
owner : anyEntity.owner || 'user' ,
group : anyEntity.group || 'users' ,
accessed : anyEntity.accessed || Date . now ( ) ,
modified : anyEntity.modified || Date . now ( ) ,
. . . entity . metadata // Preserve any existing nested metadata
}
} else if ( entity . id === this . rootEntityId ) {
// Special case: ensure root directory always has proper metadata
entity . metadata = {
path : '/' ,
name : '' ,
vfsType : 'directory' ,
size : 0 ,
permissions : 0o755 ,
owner : 'root' ,
group : 'root' ,
accessed : Date.now ( ) ,
modified : Date.now ( ) ,
. . . entity . metadata
}
}
}
2025-09-24 17:31:48 -07:00
return entity as VFSEntity
}
private getParentPath ( path : string ) : string {
const normalized = path . replace ( /\/+/g , '/' ) . replace ( /\/$/ , '' )
const lastSlash = normalized . lastIndexOf ( '/' )
if ( lastSlash <= 0 ) return '/'
return normalized . substring ( 0 , lastSlash )
}
private getBasename ( path : string ) : string {
const normalized = path . replace ( /\/+/g , '/' ) . replace ( /\/$/ , '' )
const lastSlash = normalized . lastIndexOf ( '/' )
return normalized . substring ( lastSlash + 1 )
}
private getExtension ( filename : string ) : string | undefined {
const lastDot = filename . lastIndexOf ( '.' )
if ( lastDot === - 1 || lastDot === 0 ) return undefined
return filename . substring ( lastDot + 1 ) . toLowerCase ( )
}
2026-01-27 15:38:21 -08:00
// MIME detection moved to MimeTypeDetector service
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
// Removed detectMimeType() and isTextFile() - now using mimeDetector singleton
2025-09-24 17:31:48 -07:00
private getFileNounType ( mimeType : string ) : NounType {
if ( mimeType . startsWith ( 'text/' ) || mimeType . includes ( 'json' ) ) {
return NounType . Document
}
if ( mimeType . startsWith ( 'image/' ) || mimeType . startsWith ( 'video/' ) || mimeType . startsWith ( 'audio/' ) ) {
return NounType . Media
}
return NounType . File
}
2026-01-27 15:38:21 -08:00
// Removed compression methods (shouldCompress, compress, decompress)
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
// BlobStorage handles all compression automatically with zstd
2025-09-24 17:31:48 -07:00
private async generateEmbedding ( buffer : Buffer , mimeType : string ) : Promise < number [ ] | undefined > {
try {
// Use text content for text files, description for binary
let content : string
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
if ( mimeDetector . isTextFile ( mimeType ) ) {
2025-09-24 17:31:48 -07:00
// Use first 10KB for embedding
content = buffer . toString ( 'utf8' , 0 , Math . min ( 10240 , buffer . length ) )
} else {
// For binary files, create a description
content = ` Binary file: ${ mimeType } , size: ${ buffer . length } bytes `
}
// Ensure content is actually a string
if ( typeof content !== 'string' ) {
console . debug ( 'Content is not a string:' , typeof content , content )
return undefined
}
// Ensure content is not empty or invalid
if ( ! content || content . length === 0 ) {
console . debug ( 'Content is empty' )
return undefined
}
const vector = await this . brain . embed ( content )
return vector
} catch ( error ) {
console . debug ( 'Failed to generate embedding:' , error )
return undefined
}
}
private async extractMetadata ( buffer : Buffer , mimeType : string ) : Promise < Partial < VFSMetadata > > {
const metadata : Partial < VFSMetadata > = { }
// Extract basic metadata based on content type
feat: add ImageHandler with EXIF extraction and comprehensive MIME detection (v5.2.0)
Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation.
**New Features:**
- ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp
- EXIF extraction: Camera data, GPS, timestamps using exifr library
- Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats
- MimeTypeDetector: Unified MIME type detection with magic byte support
- FormatDetector: Enhanced with image format detection via MIME + magic bytes
**Architecture Fixes:**
- Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154)
- Added parameter spreading for ImportSource objects to enable augmentation access
- Fixed metadata propagation through ImportCoordinator to final results
- Added augmentation data check in ImportCoordinator.extract()
**Integration:**
- ImageHandler registered as built-in handler alongside CSV, Excel, PDF
- Images import as 'media' entities with 'image' subtype
- Full metadata preserved in knowledge graph entities
- Configuration options: enableImage, extractEXIF, imageDefaults
**Test Coverage:**
- 15 integration tests (image-import.test.ts) - 100% passing
- 27 unit tests (image-handler.test.ts) - 100% passing
- Format detection tests for all supported image types
- Error handling and resilience tests
**Breaking Changes:** None - backward compatible
Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-03 14:06:17 -08:00
if ( mimeDetector . isTextFile ( mimeType ) ) {
2025-09-24 17:31:48 -07:00
const text = buffer . toString ( 'utf8' )
metadata . lineCount = text . split ( '\n' ) . length
metadata . wordCount = text . split ( /\s+/ ) . filter ( w = > w ) . length
metadata . charset = 'utf-8'
2025-09-29 13:51:47 -07:00
// Extract concepts using brain.extractConcepts() (neural extraction)
if ( this . config . intelligence ? . autoConcepts ) {
try {
const concepts = await this . brain . extractConcepts ( text , { limit : 20 } )
metadata . conceptNames = concepts // Flattened for O(log n) queries
} catch ( error ) {
// Concept extraction is optional - don't fail if it errors
console . debug ( 'Concept extraction failed:' , error )
}
}
2025-09-24 17:31:48 -07:00
}
// Extract hash for integrity
const crypto = await import ( 'crypto' )
metadata . hash = crypto . createHash ( 'sha256' ) . update ( buffer ) . digest ( 'hex' )
return metadata
}
2026-01-27 15:38:21 -08:00
// REMOVED updateAccessTime() method entirely
perf: eliminate N+1 patterns across all APIs for 10-20x faster cloud storage
Fixed 8 N+1 patterns that caused severe performance degradation on cloud storage (GCS, S3, Azure, R2):
**Core Issues Fixed:**
- find(): 5 code paths loaded entities one-by-one (10x slower)
- batchGet() with vectors: Looped individual get() calls (10x slower)
- executeGraphSearch(): Loaded connected entities individually (20x slower)
- relate() duplicate check: Loaded relationships one-by-one (5x slower)
- deleteMany(): Separate transaction per entity (10x slower)
- VFS tree loading: N+1 getChildren() calls (53x slower)
- VFS file operations: updateAccessTime() write on every read (2-3x slower)
**Solutions Implemented:**
1. Batch entity loading in find() - 5 locations
- Replace individual get() with batchGet()
- GCS: 10 entities = 500ms → 50ms (10x faster)
2. Added storage.getNounBatch(ids) method
- Batch-loads vectors + metadata in parallel
- Eliminates N+1 for includeVectors: true
3. Added storage.getVerbsBatch(ids) method
- Batch-loads relationships with metadata
- Used by relate() duplicate checking
4. Added graphIndex.getVerbsBatchCached(ids)
- Cache-aware batch verb loading
- Checks UnifiedCache before storage
5. Optimized deleteMany() with transaction batching
- Chunks of 10 entities per transaction
- Atomic within chunk, graceful across chunks
6. Fixed VFS tree traversal N+1 pattern
- Graph traversal + ONE batch fetch
- 111 calls → 1 call (111x reduction)
7. Removed VFS updateAccessTime() on reads
- Eliminated 50-100ms write per read
- Follows modern filesystem noatime practice
**Performance Impact (Production GCS):**
| Operation | Before | After | Speedup |
|-----------|--------|-------|---------|
| find() 10 results | 500ms | 50ms | 10x |
| batchGet() 10 vectors | 500ms | 50ms | 10x |
| executeGraphSearch() 20 | 1000ms | 50ms | 20x |
| relate() duplicate (5) | 250ms | 50ms | 5x |
| deleteMany() 10 entities | 2000ms | 200ms | 10x |
| VFS tree loading | 5304ms | 100ms | 53x |
| VFS readFile() | 100-150ms | 50ms | 2-3x |
**Architecture:**
- All batch methods use readBatchWithInheritance() for COW/fork/asOf support
- Works with all storage adapters (GCS, S3, Azure, R2, OPFS, FileSystem)
- Cache-aware with proper UnifiedCache integration
- Transaction-safe with atomic chunked operations
- Fully backward compatible
**Files Modified:**
- src/brainy.ts: Fixed find(), batchGet(), relate(), deleteMany(), executeGraphSearch()
- src/storage/baseStorage.ts: Added getNounBatch(), getVerbsBatch()
- src/graph/graphAdjacencyIndex.ts: Added getVerbsBatchCached()
- src/vfs/VirtualFileSystem.ts: Fixed tree traversal, removed updateAccessTime()
- src/coreTypes.ts: Added batch method signatures to StorageAdapter
- src/types/brainy.types.ts: Added continueOnError to DeleteManyParams
- tests/: Added comprehensive regression tests
**Overall Impact:**
- 10-20x faster batch operations on cloud storage
- 50-90% cost reduction (fewer storage API calls)
- Production-ready with clean architecture
- Zero breaking changes - automatic performance improvement
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 15:18:26 -08:00
// Access time updates caused 50-100ms GCS write on EVERY file/dir read
// Modern file systems use 'noatime' for same reason
// Field 'accessed' still exists in metadata for backward compat but won't update
2025-09-24 17:31:48 -07:00
private async countRelationships ( entityId : string ) : Promise < number > {
2026-06-11 14:51:00 -07:00
const relations = await this . brain . related ( { from : entityId } )
const relationsTo = await this . brain . related ( { to : entityId } )
2025-09-24 17:31:48 -07:00
return relations . length + relationsTo . length
}
private filterDirectoryEntries ( entries : VFSEntity [ ] , filter : any ) : VFSEntity [ ] {
return entries . filter ( entry = > {
if ( filter . type && entry . metadata . vfsType !== filter . type ) return false
if ( filter . pattern && ! this . matchGlob ( entry . metadata . name , filter . pattern ) ) return false
if ( filter . minSize && entry . metadata . size < filter . minSize ) return false
if ( filter . maxSize && entry . metadata . size > filter . maxSize ) return false
if ( filter . modifiedAfter && entry . metadata . modified < filter . modifiedAfter . getTime ( ) ) return false
if ( filter . modifiedBefore && entry . metadata . modified > filter . modifiedBefore . getTime ( ) ) return false
return true
} )
}
private sortDirectoryEntries ( entries : VFSEntity [ ] , sort : string , order ? : 'asc' | 'desc' ) : VFSEntity [ ] {
const sorted = [ . . . entries ] . sort ( ( a , b ) = > {
let comparison = 0
switch ( sort ) {
case 'name' :
comparison = a . metadata . name . localeCompare ( b . metadata . name )
break
case 'size' :
comparison = a . metadata . size - b . metadata . size
break
case 'modified' :
comparison = a . metadata . modified - b . metadata . modified
break
case 'created' :
comparison = a . createdAt - b . createdAt
break
}
return order === 'desc' ? - comparison : comparison
} )
return sorted
}
private matchGlob ( name : string , pattern : string ) : boolean {
// Simple glob matching (in production, use proper glob library)
const regex = pattern
. replace ( /\*/g , '.*' )
. replace ( /\?/g , '.' )
return new RegExp ( ` ^ ${ regex } $ ` ) . test ( name )
}
2026-01-31 09:09:12 -08:00
private invalidateCaches ( path : string , recursive = false ) : void {
2025-09-24 17:31:48 -07:00
this . contentCache . delete ( path )
this . statCache . delete ( path )
2026-01-31 09:09:12 -08:00
if ( recursive ) {
const prefix = path . endsWith ( '/' ) ? path : path + '/'
for ( const cachedPath of this . contentCache . keys ( ) ) {
if ( cachedPath . startsWith ( prefix ) ) {
this . contentCache . delete ( cachedPath )
}
}
for ( const cachedPath of this . statCache . keys ( ) ) {
if ( cachedPath . startsWith ( prefix ) ) {
this . statCache . delete ( cachedPath )
}
}
}
2025-09-24 17:31:48 -07:00
}
private triggerWatchers ( path : string , event : 'rename' | 'change' ) : void {
const watchers = this . watchers . get ( path )
if ( watchers ) {
for ( const listener of watchers ) {
listener ( event , path )
}
}
}
private async updateChildrenPaths ( parentId : string , oldParentPath : string , newParentPath : string ) : Promise < void > {
// Get all children recursively
const children = await this . pathResolver . getChildren ( parentId )
for ( const child of children ) {
const oldChildPath = child . metadata . path as string
const relativePath = oldChildPath . substring ( oldParentPath . length )
const newChildPath = newParentPath + relativePath
feat(8.0): API simplification — remove neural()/Db.search, one storage `path` key, integration→0
8.0 RC cleanup toward "one place per thing, zero-config, no deprecation":
- Remove the `brain.neural()` clustering namespace (ImprovedNeuralAPI + the dead
legacy NeuralAPI + the neural CLI + neural-only types). Similarity is `find({vector})`
/ `similar({to})`; attribute grouping is the aggregation `GROUP BY` engine. The separate
entity-extraction / smart-import feature (NeuralImport, NeuralEntityExtractor, SmartExtractor,
NaturalLanguageProcessor, `brain.extract()`/`brain.nlp()`) is kept.
- Remove `Db.search()`; `find()` is the one query verb (accepts a bare string or FindParams).
Fix the bundled MCP client, which called a non-existent `brain.search(query, limit)` →
now `find({ query, limit })`.
- Storage config: collapse to one canonical top-level `path` key. The pre-8.0 aliases
(`rootDirectory`, `options.*`, `fileSystemStorage.*`) are removed and now THROW with the
exact rename instead of silently defaulting to `./brainy-data` on upgrade. A single resolver
feeds createStorage, the 7.x→8.0 migration probe, and the plugin-factory handoff, so a native
storage provider resolves the identical root (no split-brain).
- Fix `similar({ threshold })`: the min-similarity filter was silently dropped; it is now
applied as a post-filter on `result.score` (the documented way to bound semantic results).
- Fix `vfs.rename()` on a directory: child path updates spread the entity vector into `update()`
and failed dimension validation; they are metadata-only updates now.
- Fix `vfs.move()`: copy+delete orphaned the content-addressed content blob (the destination
shared the source hash, then unlink removed it). `move()` now delegates to `rename()` — an
in-place path change that preserves the blob and the entity id, for files and directories.
- Fix streaming import: the bulk fast path never flushed mid-import nor signalled queryability.
Entity writes are now chunked by a progressive flush interval (100 → 1000 → 5000); each chunk
flushes and emits `progress.queryable`, so imported data is queryable during the import.
- Sweep all docs, comments, and JSDoc for the removed/changed APIs.
Integration suite: 49 files / 588 passed / 0 failed. Unit: 80 files / 1456 passed, no type errors.
2026-06-20 13:31:11 -07:00
// Update child entity — metadata-only (mirrors rename() above). Spreading
// the whole child forwards its `vector` field into update(), which fails
// dimension validation when the child was fetched without vectors (and
// would needlessly touch the vector index when it wasn't).
await this . brain . update ( {
id : child.id ,
2025-09-24 17:31:48 -07:00
metadata : {
. . . child . metadata ,
path : newChildPath ,
modified : Date.now ( )
}
} )
// Update path cache
this . pathResolver . invalidatePath ( oldChildPath )
await this . pathResolver . createPath ( newChildPath , child . id )
// Recursively update if it's a directory
if ( child . metadata . vfsType === 'directory' ) {
await this . updateChildrenPaths ( child . id , oldChildPath , newChildPath )
}
}
}
private startBackgroundTasks ( ) : void {
// Clean up caches periodically
this . backgroundTimer = setInterval ( ( ) = > {
const now = Date . now ( )
// Clean content cache
for ( const [ path , entry ] of this . contentCache ) {
if ( now - entry . timestamp > ( this . config . cache ? . ttl || 300000 ) ) {
this . contentCache . delete ( path )
}
}
// Clean stat cache
for ( const [ path , entry ] of this . statCache ) {
if ( now - entry . timestamp > 5000 ) {
this . statCache . delete ( path )
}
}
} , 60000 ) // Every minute
fix: no script shape can hang on brainy's internals — unref every maintenance timer + one-shot beforeExit
A consumer's clean-room verification of the 8.0.10 fix found relate() still
hanging their scripts. Root-causing the CLASS instead of the repro found two
mechanisms:
1. The 'beforeExit' auto-flush hook looped forever on any script that never
reaches close(): Node re-emits beforeExit after every event-loop drain, and
the async flush schedules new work — flush, drain, flush, forever. The
listener now self-deregisters BEFORE its one flush, so the next drain exits.
(Empirically: process.on('beforeExit', async () => await anything) alone
never exits — this was the deepest root of the whole hang class.)
2. Every background-maintenance interval is now unref'd at creation — graph
auto-flush, LSM compaction, metadata write-buffer flush, VFS cache
maintenance, PathResolver cache maintenance, statistics debounce (the
writer-lock heartbeat, flush watcher, and cache monitors already were).
Durability is owned by close() and the beforeExit flush, both deterministic;
a best-effort interval must never keep the host process alive.
Proof: the reported shape (add x2 + relate, filesystem) exits cleanly BOTH
with close() (~0.5 s) and with NO teardown at all — and in the no-teardown
case the beforeExit flush still lands the data (verified by reopen: both
nouns + the edge present). New per-op-class sweep test asserts no ref'd
timer survives close() for add / relate / graph find / metadata update /
vfs — turning this bug class off permanently instead of per-repro.
2026-07-02 17:26:22 -07:00
// Cache maintenance must never keep the host process alive.
if ( this . backgroundTimer && typeof this . backgroundTimer . unref === 'function' ) {
this . backgroundTimer . unref ( )
}
2025-09-24 17:31:48 -07:00
}
private getDefaultConfig ( ) : Required < Omit < VFSConfig , 'rootEntityId' > > & { rootEntityId? : string } {
return {
root : '/' ,
rootEntityId : undefined ,
cache : {
enabled : true ,
maxPaths : 100_000 ,
maxContent : 100_000_000 , // 100MB
ttl : 5 * 60 * 1000 // 5 minutes
} ,
storage : {
inline : {
maxSize : 100_000 // 100KB
} ,
chunking : {
enabled : true ,
chunkSize : 5_000_000 , // 5MB
parallel : 4
} ,
compression : {
enabled : true ,
minSize : 10_000 , // 10KB
algorithm : 'gzip'
}
} ,
intelligence : {
enabled : true ,
autoEmbed : true ,
autoExtract : true ,
autoTag : false ,
autoConcepts : false
} ,
permissions : {
defaultFile : 0o644 ,
defaultDirectory : 0o755 ,
umask : 0o022
} ,
limits : {
maxFileSize : 1_000_000_000 , // 1GB
maxPathLength : 4096 ,
maxDirectoryEntries : 100_000
}
}
}
2026-06-29 10:04:19 -07:00
// ============= Lifecycle, POSIX & Extended Operations =============
2025-09-24 17:31:48 -07:00
2026-06-29 10:04:19 -07:00
/ * *
* Release all resources held by the VFS .
*
* Stops background cache eviction , tears down the path resolver , and clears the
* content cache and watcher registry . After close the VFS is marked
* uninitialized ; call { @link init } again before reusing it .
*
* @returns A promise that resolves once cleanup is complete .
* /
2025-09-24 17:31:48 -07:00
async close ( ) : Promise < void > {
// Cleanup PathResolver resources
if ( this . pathResolver ) {
this . pathResolver . cleanup ( )
}
// Stop background tasks
if ( this . backgroundTimer ) {
clearInterval ( this . backgroundTimer )
this . backgroundTimer = null
}
// Clear caches
this . contentCache . clear ( )
// Clear watchers
this . watchers . clear ( )
this . initialized = false
}
2026-06-29 10:04:19 -07:00
/ * *
* Change the permission bits of a file or directory ( POSIX ` chmod ` ) .
*
* @param path - The VFS path whose permissions should change .
* @param mode - The new permission bits ( e . g . ` 0o644 ` ) , stored in entity metadata .
* @returns A promise that resolves once the permissions are persisted .
* /
2025-09-24 17:31:48 -07:00
async chmod ( path : string , mode : number ) : Promise < void > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Update permissions in metadata
await this . brain . update ( {
. . . entity ,
id : entityId ,
metadata : {
. . . entity . metadata ,
permissions : mode ,
modified : Date.now ( )
}
} )
// Invalidate caches
this . invalidateCaches ( path )
}
2026-06-29 10:04:19 -07:00
/ * *
* Change the owner and group of a file or directory ( POSIX ` chown ` ) .
*
* @param path - The VFS path whose ownership should change .
* @param uid - The new owner user id , stored in entity metadata .
* @param gid - The new owning group id , stored in entity metadata .
* @returns A promise that resolves once the ownership is persisted .
* /
2025-09-24 17:31:48 -07:00
async chown ( path : string , uid : number , gid : number ) : Promise < void > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Update ownership in metadata
await this . brain . update ( {
. . . entity ,
id : entityId ,
metadata : {
. . . entity . metadata ,
uid ,
gid ,
modified : Date.now ( )
}
} )
// Invalidate caches
this . invalidateCaches ( path )
}
2026-06-29 10:04:19 -07:00
/ * *
* Set the access and modification timestamps of a file or directory ( POSIX ` utimes ` ) .
*
* @param path - The VFS path to update .
* @param atime - The new access time .
* @param mtime - The new modification time .
* @returns A promise that resolves once the timestamps are persisted .
* /
2025-09-24 17:31:48 -07:00
async utimes ( path : string , atime : Date , mtime : Date ) : Promise < void > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Update timestamps in metadata
await this . brain . update ( {
. . . entity ,
id : entityId ,
metadata : {
. . . entity . metadata ,
accessed : atime.getTime ( ) ,
modified : mtime.getTime ( )
}
} )
// Invalidate caches
this . invalidateCaches ( path )
}
2026-06-29 10:04:19 -07:00
/ * *
* Rename or move a file or directory in place .
*
* Updates the entity ' s path metadata without rewriting content ( preserving the
* underlying blob and entity id ) . When the parent directory changes , a new
* ` Contains ` edge is added to the destination parent . Renaming a directory
* cascades the path change to every descendant .
*
* @param oldPath - The existing VFS path .
* @param newPath - The destination VFS path ; must not already exist .
* @returns A promise that resolves once the rename is complete .
* @throws { VFSError } ENOENT when ` oldPath ` does not exist .
* @throws { VFSError } EEXIST when ` newPath ` already exists .
* /
2025-09-24 17:31:48 -07:00
async rename ( oldPath : string , newPath : string ) : Promise < void > {
await this . ensureInitialized ( )
// Check if source exists
const entityId = await this . pathResolver . resolve ( oldPath )
const entity = await this . brain . get ( entityId )
if ( ! entity ) {
throw new VFSError ( VFSErrorCode . ENOENT , ` No such file or directory: ${ oldPath } ` , oldPath , 'rename' )
}
// Check if target already exists
try {
await this . pathResolver . resolve ( newPath )
throw new VFSError ( VFSErrorCode . EEXIST , ` File exists: ${ newPath } ` , newPath , 'rename' )
} catch ( err : any ) {
if ( err . code !== VFSErrorCode . ENOENT ) throw err
}
// Update parent relationships if needed
const oldParentPath = this . getParentPath ( oldPath )
const newParentPath = this . getParentPath ( newPath )
if ( oldParentPath !== newParentPath ) {
2026-07-15 09:25:21 -07:00
// Remove the OLD parent's containment edge(s) — by edge id, resolved from
// the graph's own adjacency (the removal law: a removal never requires
// reading the thing being removed). This step used to be skipped as "not
// critical", which left the moved entity a child of BOTH directories:
// readdir(oldDir) kept listing it, re-creating the old path showed the
// name twice, and tree-walking consumers saw the file in two places.
2025-09-24 17:31:48 -07:00
if ( oldParentPath ) {
const oldParentId = await this . pathResolver . resolve ( oldParentPath )
2026-07-15 09:25:21 -07:00
const staleEdges = await this . brain . related ( {
from : oldParentId ,
to : entityId ,
type : VerbType . Contains
} )
for ( const edge of staleEdges ) {
await this . brain . unrelate ( edge . id )
}
2025-09-24 17:31:48 -07:00
}
2026-07-15 09:25:21 -07:00
// Add to the new parent. The root ('/') is a REAL parent — skipping it
// orphaned a move-to-root out of readdir('/') entirely.
if ( newParentPath ) {
2025-09-24 17:31:48 -07:00
const newParentId = await this . pathResolver . resolve ( newParentPath )
2025-10-24 15:59:41 -07:00
await this . brain . relate ( {
from : newParentId ,
to : entityId ,
type : VerbType . Contains ,
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
subtype : 'vfs-contains' , // Standard subtype for VFS containment edges (7.30+)
2026-01-27 15:38:21 -08:00
metadata : { isVFS : true } // Mark as VFS relationship
2025-10-24 15:59:41 -07:00
} )
2025-09-24 17:31:48 -07:00
}
}
2026-06-11 15:05:12 -07:00
// A rename is a path/metadata change, never a content change — issue a
// metadata-only update. Spreading the whole entity here used to forward
// its vector field into update(), which failed dimension validation when
// the entity was fetched without vectors (and would needlessly touch the
// vector index when it wasn't).
2025-09-24 17:31:48 -07:00
await this . brain . update ( {
2026-06-11 15:05:12 -07:00
id : entityId ,
metadata : {
. . . entity . metadata ,
path : newPath ,
name : this.getBasename ( newPath ) ,
modified : Date.now ( )
}
2025-09-24 17:31:48 -07:00
} )
// Update path cache
this . pathResolver . invalidatePath ( oldPath , true )
await this . pathResolver . createPath ( newPath , entityId )
// If it's a directory, update all children paths
if ( entity . metadata . vfsType === 'directory' ) {
await this . updateChildrenPaths ( entityId , oldPath , newPath )
}
// Trigger watchers
this . triggerWatchers ( oldPath , 'rename' )
this . triggerWatchers ( newPath , 'rename' )
}
2026-07-15 09:25:21 -07:00
/ * *
* Reconcile every VFS entity ' s containment edges against its canonical
* ` metadata.path ` — the path is the truth ( maintained by write / rename ) ; the
* ` Contains ` edges are a projection of it . Heals the "cosmetic ghost" class
* left by pre - fix renames that added the new parent ' s edge without removing
* the old one ( an entity listed in TWO directories ; a re - created old path
* showing its name twice ) , plus duplicate edges from the same parent left by
* concurrent writers .
*
* CONSERVATIVE by design : only VFS containment edges ( subtype
* ` 'vfs-contains' ` or ` metadata.isVFS ` ) are ever touched — a user ' s own
* knowledge - graph ` Contains ` edge between the same entities is never
* removed . An entity whose expected parent path has no entity is logged
* loudly and left alone ( never orphaned further ) . Operator - invoked via
* ` brain.repairIndex() ` .
*
* The canonical pagination walk ( not an index query ) is deliberate : this is
* a repair op — the projections are the thing under suspicion , so the walk
* reads the source of truth .
*
* @returns Counts of stale edges removed and missing expected edges restored .
* /
async repairContainment ( ) : Promise < { removed : number ; restored : number } > {
await this . ensureInitialized ( )
// Pass 1: canonical walk → every VFS entity's id + path.
const idByPath = new Map < string , string > ( )
const vfsEntities : Array < { id : string ; path : string } > = [ ]
let cursor : string | undefined
for ( ; ; ) {
const page = await ( this . brain as any ) . storage . getNounsWithPagination ( { limit : 500 , cursor } )
for ( const noun of page . items ) {
const meta = ( noun as any ) . metadata ? ? noun
const p = meta ? . path
if ( meta ? . vfsType && typeof p === 'string' ) {
idByPath . set ( p , noun . id )
vfsEntities . push ( { id : noun.id , path : p } )
}
}
if ( ! page . hasMore ) break
cursor = page . nextCursor
}
let removed = 0
let restored = 0
for ( const { id , path } of vfsEntities ) {
if ( path === '/' ) continue // the root has no parent
const expectedParentId = idByPath . get ( this . getParentPath ( path ) )
if ( ! expectedParentId ) {
console . warn (
` [VFS] repairContainment: no entity found for parent of ${ path } — leaving its edges untouched. `
)
continue
}
const incoming = await this . brain . related ( { to : id , type : VerbType . Contains } )
let expectedSeen = false
for ( const edge of incoming ) {
const isVfsEdge = edge . subtype === 'vfs-contains' || ( edge . metadata as any ) ? . isVFS === true
if ( ! isVfsEdge ) continue // never touch user knowledge edges
if ( edge . from === expectedParentId && ! expectedSeen ) {
expectedSeen = true // keep exactly one correct edge
continue
}
// Stale parent (a pre-fix rename ghost) or a duplicate of the correct
// edge (concurrent-writer artifact) — remove it, loudly.
await this . brain . unrelate ( edge . id )
removed ++
console . warn (
` [VFS] repairContainment: removed ${ edge . from === expectedParentId ? 'duplicate' : 'stale' } ` +
` containment edge ${ edge . from } -> ${ id } ( ${ path } ) `
)
}
if ( ! expectedSeen ) {
await this . brain . relate ( {
from : expectedParentId ,
to : id ,
type : VerbType . Contains ,
subtype : 'vfs-contains' ,
metadata : { isVFS : true }
} )
restored ++
console . warn ( ` [VFS] repairContainment: restored missing containment edge for ${ path } ` )
}
}
return { removed , restored }
}
2026-06-29 10:04:19 -07:00
/ * *
* Copy a file or directory to a new path .
*
* Files are duplicated as new entities ( sharing content via the content - addressed
* blob store ) ; directories are copied recursively . Unless ` overwrite ` is set , an
* existing destination is rejected .
*
* @param src - The source VFS path to copy from .
* @param dest - The destination VFS path to copy to .
* @param options - Copy options such as ` overwrite ` , ` deepCopy ` , ` preserveVector ` ,
* and ` preserveRelationships ` .
* @returns A promise that resolves once the copy is complete .
* @throws { VFSError } ENOENT when ` src ` does not exist .
* @throws { VFSError } EEXIST when ` dest ` exists and ` overwrite ` is not set .
* /
2025-09-24 17:31:48 -07:00
async copy ( src : string , dest : string , options? : CopyOptions ) : Promise < void > {
await this . ensureInitialized ( )
// Get source entity
const srcEntityId = await this . pathResolver . resolve ( src )
const srcEntity = await this . brain . get ( srcEntityId )
if ( ! srcEntity ) {
throw new VFSError ( VFSErrorCode . ENOENT , ` No such file or directory: ${ src } ` , src , 'copy' )
}
// Check if destination already exists
if ( ! options ? . overwrite ) {
try {
await this . pathResolver . resolve ( dest )
throw new VFSError ( VFSErrorCode . EEXIST , ` File exists: ${ dest } ` , dest , 'copy' )
} catch ( err : any ) {
if ( err . code !== VFSErrorCode . ENOENT ) throw err
}
}
// Copy the entity
if ( srcEntity . metadata . vfsType === 'file' ) {
await this . copyFile ( srcEntity , dest , options )
} else if ( srcEntity . metadata . vfsType === 'directory' ) {
await this . copyDirectory ( src , dest , options )
}
}
private async copyFile ( srcEntity : Entity , destPath : string , options? : CopyOptions ) : Promise < void > {
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
// Create new entity with same content but different path. Preserve the source
// entity's subtype when it has one (so a vfs-file stays vfs-file); fall back
// to 'vfs-file' for the rare case of a VFS entity without subtype (pre-7.30
// legacy data path that hits the copy operation).
2025-09-24 17:31:48 -07:00
const newEntity = await this . brain . add ( {
type : srcEntity . type ,
2026-06-11 14:51:00 -07:00
subtype : srcEntity.subtype ? ? 'vfs-file' ,
2025-09-24 17:31:48 -07:00
data : srcEntity.data ,
vector : options?.preserveVector ? srcEntity.vector : undefined ,
metadata : {
. . . srcEntity . metadata ,
path : destPath ,
name : this.getBasename ( destPath ) ,
created : Date.now ( ) ,
modified : Date.now ( ) ,
copiedFrom : srcEntity.metadata.path
}
} )
// Add to parent directory
const parentPath = this . getParentPath ( destPath )
if ( parentPath && parentPath !== '/' ) {
const parentId = await this . pathResolver . resolve ( parentPath )
2025-10-24 15:59:41 -07:00
await this . brain . relate ( {
from : parentId ,
to : newEntity ,
type : VerbType . Contains ,
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
subtype : 'vfs-contains' , // Standard subtype for VFS containment edges (7.30+)
2026-01-27 15:38:21 -08:00
metadata : { isVFS : true } // Mark as VFS relationship
2025-10-24 15:59:41 -07:00
} )
2025-09-24 17:31:48 -07:00
}
// Update path cache
await this . pathResolver . createPath ( destPath , newEntity )
// Copy relationships if requested
if ( options ? . preserveRelationships ) {
2026-06-11 14:51:00 -07:00
const relations = await this . brain . related ( { from : srcEntity . id } )
2025-09-24 17:31:48 -07:00
for ( const relation of relations ) {
if ( relation . type !== VerbType . Contains ) {
// Skip relationship without Contains type
// Future: implement proper relation copying
}
}
}
}
2025-12-11 08:37:55 -08:00
/ * *
* Copy a directory recursively
*
2026-01-27 15:38:21 -08:00
* Optimized for cloud storage using batch operations
2025-12-11 08:37:55 -08:00
* - Uses gatherDescendants ( ) for efficient graph traversal + batch fetch
* - Uses addMany ( ) for batch entity creation
* - Uses relateMany ( ) for batch relationship creation
*
* Performance improvement : 3 - 6 x faster on cloud storage ( GCS , S3 , R2 , Azure )
* /
2025-09-24 17:31:48 -07:00
private async copyDirectory ( srcPath : string , destPath : string , options? : CopyOptions ) : Promise < void > {
2025-12-11 08:37:55 -08:00
// Shallow copy - just create directory
if ( options ? . deepCopy === false ) {
await this . mkdir ( destPath , { recursive : true } )
return
}
// OPTIMIZED: Batch fetch all source entities in ONE call
const srcEntityId = await this . pathResolver . resolve ( srcPath )
const descendants = await this . gatherDescendants ( srcEntityId , Infinity )
const srcEntity = await this . getEntityById ( srcEntityId )
const allEntities = [ srcEntity , . . . descendants ]
// Build path mapping: srcPath -> destPath
const pathMap = new Map < string , string > ( )
const idMap = new Map < string , string > ( ) // old ID -> new ID
for ( const entity of allEntities ) {
const relativePath = entity . metadata . path . substring ( srcPath . length )
const newPath = destPath + relativePath
pathMap . set ( entity . metadata . path , newPath )
}
// Phase 1: Create all directories first (maintain hierarchy)
// Sort by path length to ensure parents are created before children
const directories = allEntities
. filter ( e = > e . metadata . vfsType === 'directory' )
. sort ( ( a , b ) = > a . metadata . path . length - b . metadata . path . length )
for ( const dir of directories ) {
const newPath = pathMap . get ( dir . metadata . path ) !
await this . mkdir ( newPath ) // mkdir is relatively fast
const newId = await this . pathResolver . resolve ( newPath )
idMap . set ( dir . id , newId )
}
// Phase 2: Batch-create all files using addMany
const files = allEntities . filter ( e = > e . metadata . vfsType === 'file' )
if ( files . length > 0 ) {
const items = files . map ( srcFile = > {
const newPath = pathMap . get ( srcFile . metadata . path ) !
return {
type : srcFile . type ,
data : srcFile.data ,
vector : options?.preserveVector ? srcFile.vector : undefined ,
metadata : {
. . . srcFile . metadata ,
path : newPath ,
name : this.getBasename ( newPath ) ,
parent : undefined , // Will be set via relationship
created : Date.now ( ) ,
modified : Date.now ( ) ,
copiedFrom : srcFile.metadata.path
}
}
} )
const result = await this . brain . addMany ( { items , continueOnError : false } )
// Build ID mapping for new files
for ( let i = 0 ; i < files . length ; i ++ ) {
idMap . set ( files [ i ] . id , result . successful [ i ] )
}
// Phase 3: Batch-create parent relationships using relateMany
const relations = files . map ( ( srcFile , i ) = > {
const newPath = pathMap . get ( srcFile . metadata . path ) !
const parentPath = this . getParentPath ( newPath )
// Find parent ID from directories we created
let parentId : string
if ( parentPath === '/' ) {
parentId = VirtualFileSystem . VFS_ROOT_ID
} else {
// Find the source directory that maps to this parent path
const srcParentDir = directories . find ( d = > pathMap . get ( d . metadata . path ) === parentPath )
parentId = srcParentDir ? idMap . get ( srcParentDir . id ) ! : VirtualFileSystem . VFS_ROOT_ID
2025-09-24 17:31:48 -07:00
}
2025-12-11 08:37:55 -08:00
return {
from : parentId ,
to : result.successful [ i ] ,
type : VerbType . Contains ,
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
subtype : 'vfs-contains' , // Standard subtype for VFS containment edges (7.30+)
2025-12-11 08:37:55 -08:00
metadata : { isVFS : true }
}
} )
await this . brain . relateMany ( { items : relations } )
// Phase 4: Update path resolver cache for all new files
for ( let i = 0 ; i < files . length ; i ++ ) {
const newPath = pathMap . get ( files [ i ] . metadata . path ) !
await this . pathResolver . createPath ( newPath , result . successful [ i ] )
2025-09-24 17:31:48 -07:00
}
}
}
2026-06-29 10:04:19 -07:00
/ * *
* Move a file or directory to a new path .
*
* Implemented as an in - place { @link rename } rather than copy - then - delete : on the
* content - addressed blob store a copy would share the source content hash , so
* deleting the source would orphan the destination . Renaming preserves the blob
* and entity id and handles directory descendants .
*
* @param src - The source VFS path to move from .
* @param dest - The destination VFS path to move to ; must not already exist .
* @returns A promise that resolves once the move is complete .
* @throws { VFSError } ENOENT when ` src ` does not exist .
* @throws { VFSError } EEXIST when ` dest ` already exists .
* /
2025-09-24 17:31:48 -07:00
async move ( src : string , dest : string ) : Promise < void > {
await this . ensureInitialized ( )
feat(8.0): API simplification — remove neural()/Db.search, one storage `path` key, integration→0
8.0 RC cleanup toward "one place per thing, zero-config, no deprecation":
- Remove the `brain.neural()` clustering namespace (ImprovedNeuralAPI + the dead
legacy NeuralAPI + the neural CLI + neural-only types). Similarity is `find({vector})`
/ `similar({to})`; attribute grouping is the aggregation `GROUP BY` engine. The separate
entity-extraction / smart-import feature (NeuralImport, NeuralEntityExtractor, SmartExtractor,
NaturalLanguageProcessor, `brain.extract()`/`brain.nlp()`) is kept.
- Remove `Db.search()`; `find()` is the one query verb (accepts a bare string or FindParams).
Fix the bundled MCP client, which called a non-existent `brain.search(query, limit)` →
now `find({ query, limit })`.
- Storage config: collapse to one canonical top-level `path` key. The pre-8.0 aliases
(`rootDirectory`, `options.*`, `fileSystemStorage.*`) are removed and now THROW with the
exact rename instead of silently defaulting to `./brainy-data` on upgrade. A single resolver
feeds createStorage, the 7.x→8.0 migration probe, and the plugin-factory handoff, so a native
storage provider resolves the identical root (no split-brain).
- Fix `similar({ threshold })`: the min-similarity filter was silently dropped; it is now
applied as a post-filter on `result.score` (the documented way to bound semantic results).
- Fix `vfs.rename()` on a directory: child path updates spread the entity vector into `update()`
and failed dimension validation; they are metadata-only updates now.
- Fix `vfs.move()`: copy+delete orphaned the content-addressed content blob (the destination
shared the source hash, then unlink removed it). `move()` now delegates to `rename()` — an
in-place path change that preserves the blob and the entity id, for files and directories.
- Fix streaming import: the bulk fast path never flushed mid-import nor signalled queryability.
Entity writes are now chunked by a progressive flush interval (100 → 1000 → 5000); each chunk
flushes and emits `progress.queryable`, so imported data is queryable during the import.
- Sweep all docs, comments, and JSDoc for the removed/changed APIs.
Integration suite: 49 files / 588 passed / 0 failed. Unit: 80 files / 1456 passed, no type errors.
2026-06-20 13:31:11 -07:00
// A move is a RENAME (in-place path change), NOT copy + delete. The old
// copy+delete path was broken on the content-addressed blob store: copy()
// makes the destination reference the SAME content-hash as the source, then
// unlink(src) deletes that shared blob — orphaning the destination
// ("Blob metadata not found" on the next read). rename() updates the path
// in place (preserving the blob, keeping the same entity id) and already
// handles both files and directories, including child path updates.
await this . rename ( src , dest )
2025-09-24 17:31:48 -07:00
}
2026-06-29 10:04:19 -07:00
/ * *
* Create a symbolic link at ` path ` that points to ` target ` ( POSIX ` symlink ` ) .
*
* The link is stored as a dedicated ` vfs-symlink ` entity whose
* ` metadata.symlinkTarget ` records the target path ; it is linked into its parent
* directory with a ` Contains ` edge .
*
* @param target - The path the symlink should resolve to .
* @param path - The VFS path at which to create the symlink ; must not already exist .
* @returns A promise that resolves once the symlink is created .
* @throws { VFSError } EEXIST when ` path ` already exists .
* /
2025-09-24 17:31:48 -07:00
async symlink ( target : string , path : string ) : Promise < void > {
await this . ensureInitialized ( )
// Check if symlink already exists
try {
await this . pathResolver . resolve ( path )
throw new VFSError ( VFSErrorCode . EEXIST , ` File exists: ${ path } ` , path , 'symlink' )
} catch ( err : any ) {
if ( err . code !== VFSErrorCode . ENOENT ) throw err
}
// Parse path to get parent and name
const parentPath = this . getParentPath ( path )
const name = this . getBasename ( path )
// Ensure parent directory exists
const parentId = await this . ensureDirectory ( parentPath )
// Create symlink entity
const metadata : VFSMetadata = {
path ,
name ,
parent : parentId ,
vfsType : 'symlink' ,
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
isVFS : true , // Infrastructure-bypass marker for strict-mode enforcement
isVFSEntity : true ,
2025-09-24 17:31:48 -07:00
symlinkTarget : target ,
size : 0 ,
permissions : 0o777 ,
owner : 'user' ,
group : 'users' ,
accessed : Date.now ( ) ,
modified : Date.now ( )
}
const entity = await this . brain . add ( {
data : ` symlink: ${ target } ` ,
type : NounType . File , // Symlinks are special files
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
subtype : 'vfs-symlink' , // Distinct from 'vfs-file' so consumers can find symlinks (7.30.1+)
2025-09-24 17:31:48 -07:00
metadata
} )
// Create parent-child relationship
await this . brain . relate ( {
from : parentId ,
to : entity ,
2025-10-24 15:59:41 -07:00
type : VerbType . Contains ,
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
subtype : 'vfs-contains' , // Standard subtype for VFS containment edges (7.30+)
2026-01-27 15:38:21 -08:00
metadata : { isVFS : true } // Mark as VFS relationship
2025-09-24 17:31:48 -07:00
} )
// Update path resolver cache
await this . pathResolver . createPath ( path , entity )
}
2026-06-29 10:04:19 -07:00
/ * *
* Read the target of a symbolic link ( POSIX ` readlink ` ) .
*
* @param path - The VFS path of the symlink .
* @returns The stored target path , or an empty string when no target is recorded .
* @throws { VFSError } EINVAL when ` path ` is not a symbolic link .
* /
2025-09-24 17:31:48 -07:00
async readlink ( path : string ) : Promise < string > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Verify it's a symlink
if ( entity . metadata . vfsType !== 'symlink' ) {
throw new VFSError ( VFSErrorCode . EINVAL , ` Not a symbolic link: ${ path } ` , path , 'readlink' )
}
return entity . metadata . symlinkTarget || ''
}
2026-06-29 10:04:19 -07:00
/ * *
* Resolve a path to its canonical form , following symbolic links ( POSIX ` realpath ` ) .
*
* Symlinks are followed iteratively until a non - symlink target is reached , up to a
* fixed depth limit that guards against link cycles .
*
* @param path - The VFS path to resolve .
* @returns The fully resolved ( non - symlink ) path .
* @throws { VFSError } ENOENT when the path ( or a link in the chain ) does not exist .
* @throws { VFSError } ELOOP when too many symbolic links are encountered .
* /
2025-09-24 17:31:48 -07:00
async realpath ( path : string ) : Promise < string > {
await this . ensureInitialized ( )
// Resolve symlinks recursively
let currentPath = path
let depth = 0
const maxDepth = 20 // Prevent infinite loops
while ( depth < maxDepth ) {
try {
const entityId = await this . pathResolver . resolve ( currentPath )
const entity = await this . getEntityById ( entityId )
if ( entity . metadata . vfsType === 'symlink' ) {
// Follow the symlink
currentPath = entity . metadata . symlinkTarget || ''
depth ++
} else {
// Not a symlink, we have the real path
return currentPath
}
} catch ( err ) {
throw new VFSError ( VFSErrorCode . ENOENT , ` No such file or directory: ${ path } ` , path , 'realpath' )
}
}
throw new VFSError ( VFSErrorCode . ELOOP , ` Too many symbolic links: ${ path } ` , path , 'realpath' )
}
2026-06-29 10:04:19 -07:00
/ * *
* Read a single extended attribute of a file or directory ( POSIX ` getxattr ` ) .
*
* @param path - The VFS path to read .
* @param name - The extended - attribute key to fetch .
* @returns The attribute value , or ` undefined ` when the attribute is not set .
* /
2025-09-24 17:31:48 -07:00
async getxattr ( path : string , name : string ) : Promise < any > {
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
return entity . metadata . attributes ? . [ name ]
}
2026-06-29 10:04:19 -07:00
/ * *
* Set a single extended attribute on a file or directory ( POSIX ` setxattr ` ) .
*
* @param path - The VFS path to update .
* @param name - The extended - attribute key to set .
* @param value - The value to store under ` name ` .
* @returns A promise that resolves once the attribute is persisted .
* /
2025-09-24 17:31:48 -07:00
async setxattr ( path : string , name : string , value : any ) : Promise < void > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Create extended attributes object
const xattrs = entity . metadata . attributes || { }
xattrs [ name ] = value
// Update entity metadata
await this . brain . update ( {
id : entityId ,
metadata : {
. . . entity . metadata ,
attributes : xattrs
}
} )
// Invalidate caches
this . invalidateCaches ( path )
}
2026-06-29 10:04:19 -07:00
/ * *
* List the extended - attribute names set on a file or directory ( POSIX ` listxattr ` ) .
*
* @param path - The VFS path to inspect .
* @returns The list of extended - attribute keys ( empty when none are set ) .
* /
2025-09-24 17:31:48 -07:00
async listxattr ( path : string ) : Promise < string [ ] > {
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
return Object . keys ( entity . metadata . attributes || { } )
}
2026-06-29 10:04:19 -07:00
/ * *
* Remove a single extended attribute from a file or directory ( POSIX ` removexattr ` ) .
*
* @param path - The VFS path to update .
* @param name - The extended - attribute key to remove .
* @returns A promise that resolves once the attribute is removed .
* /
2025-09-24 17:31:48 -07:00
async removexattr ( path : string , name : string ) : Promise < void > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Remove from extended attributes
const xattrs = { . . . entity . metadata . attributes }
delete xattrs [ name ]
// Update entity metadata
await this . brain . update ( {
. . . entity ,
id : entityId ,
metadata : {
. . . entity . metadata ,
attributes : xattrs
}
} )
// Invalidate caches
this . invalidateCaches ( path )
}
2026-06-29 10:04:19 -07:00
/ * *
* List the paths related to a file or directory via graph edges ( both directions ) .
*
* Walks outgoing and incoming relationships and maps each connected entity back to
* its VFS path , recording the relationship type and direction .
*
* @param path - The VFS path whose relationships to list .
* @param options - Reserved relationship - filtering options .
* @returns Related entries , each with the related ` path ` , ` relationship ` type , and
* ` direction ` ( ` 'from' ` for outgoing edges , ` 'to' ` for incoming ) .
* /
2025-09-24 17:31:48 -07:00
async getRelated ( path : string , options? : RelatedOptions ) : Promise < Array < {
path : string
relationship : string
direction : 'from' | 'to'
} >> {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const results : Array < { path : string , relationship : string , direction : 'from' | 'to' } > = [ ]
2025-09-25 14:50:52 -07:00
// Use proper Brainy relationship API to get all relationships
2025-09-24 17:31:48 -07:00
const [ fromRelations , toRelations ] = await Promise . all ( [
2026-06-11 14:51:00 -07:00
this . brain . related ( { from : entityId } ) ,
this . brain . related ( { to : entityId } )
2025-09-24 17:31:48 -07:00
] )
// Add outgoing relationships
for ( const rel of fromRelations ) {
2025-09-25 14:50:52 -07:00
const targetEntity = await this . brain . get ( rel . to )
if ( targetEntity && targetEntity . metadata ? . path ) {
2025-09-24 17:31:48 -07:00
results . push ( {
2025-09-25 14:50:52 -07:00
path : targetEntity.metadata.path ,
relationship : rel.type || 'related' ,
2025-09-24 17:31:48 -07:00
direction : 'from'
} )
}
}
// Add incoming relationships
for ( const rel of toRelations ) {
2025-09-25 14:50:52 -07:00
const sourceEntity = await this . brain . get ( rel . from )
if ( sourceEntity && sourceEntity . metadata ? . path ) {
2025-09-24 17:31:48 -07:00
results . push ( {
2025-09-25 14:50:52 -07:00
path : sourceEntity.metadata.path ,
relationship : rel.type || 'related' ,
2025-09-24 17:31:48 -07:00
direction : 'to'
} )
}
}
return results
}
2026-06-29 10:04:19 -07:00
/ * *
* Get the non - hierarchical relationships of a file or directory .
*
* Returns both outgoing and incoming edges as { @link Relation } objects , excluding
* the ` Contains ` edges that model the directory tree itself .
*
* @param path - The VFS path whose relationships to return .
* @returns The list of semantic relationships connected to the path .
* /
2025-09-24 17:31:48 -07:00
async getRelationships ( path : string ) : Promise < Relation [ ] > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const relationships : Relation [ ] = [ ]
2025-09-25 14:50:52 -07:00
// Use proper Brainy relationship API
2025-09-24 17:31:48 -07:00
const [ fromRelations , toRelations ] = await Promise . all ( [
2026-06-11 14:51:00 -07:00
this . brain . related ( { from : entityId } ) ,
this . brain . related ( { to : entityId } )
2025-09-24 17:31:48 -07:00
] )
2025-09-25 14:50:52 -07:00
// Process outgoing relationships (excluding Contains for parent-child)
2025-09-24 17:31:48 -07:00
for ( const rel of fromRelations ) {
2025-09-25 14:50:52 -07:00
if ( rel . type !== VerbType . Contains ) { // Skip filesystem hierarchy
const targetEntity = await this . brain . get ( rel . to )
if ( targetEntity && targetEntity . metadata ? . path ) {
2025-09-24 17:31:48 -07:00
relationships . push ( {
2025-09-25 14:50:52 -07:00
id : rel.id || crypto . randomUUID ( ) ,
from : entityId ,
to : rel.to ,
type : rel . type ,
createdAt : rel.createdAt || Date . now ( )
2025-09-24 17:31:48 -07:00
} )
}
}
}
2025-09-25 14:50:52 -07:00
// Process incoming relationships (excluding Contains for parent-child)
2025-09-24 17:31:48 -07:00
for ( const rel of toRelations ) {
2025-09-25 14:50:52 -07:00
if ( rel . type !== VerbType . Contains ) { // Skip filesystem hierarchy
const sourceEntity = await this . brain . get ( rel . from )
if ( sourceEntity && sourceEntity . metadata ? . path ) {
2025-09-24 17:31:48 -07:00
relationships . push ( {
2025-09-25 14:50:52 -07:00
id : rel.id || crypto . randomUUID ( ) ,
from : rel . from ,
to : entityId ,
type : rel . type ,
createdAt : rel.createdAt || Date . now ( )
2025-09-24 17:31:48 -07:00
} )
}
}
}
return relationships
}
2026-06-29 10:04:19 -07:00
/ * *
* Create a graph relationship between two VFS paths .
*
* @param from - The source VFS path .
* @param to - The target VFS path .
* @param type - The relationship ( verb ) type to create ; coerced to a { @link VerbType } .
* @returns A promise that resolves once the relationship is created .
* /
2025-09-24 17:31:48 -07:00
async addRelationship ( from : string , to : string , type : string ) : Promise < void > {
await this . ensureInitialized ( )
const fromEntityId = await this . pathResolver . resolve ( from )
const toEntityId = await this . pathResolver . resolve ( to )
// Create relationship using brain
await this . brain . relate ( {
from : fromEntityId ,
to : toEntityId ,
2026-06-11 14:51:00 -07:00
type : type as VerbType , // Convert string to VerbType
2026-01-27 15:38:21 -08:00
metadata : { isVFS : true } // Mark as VFS relationship
2025-09-24 17:31:48 -07:00
} )
// Invalidate caches for both paths
this . invalidateCaches ( from )
this . invalidateCaches ( to )
}
2026-06-29 10:04:19 -07:00
/ * *
* Remove a graph relationship between two VFS paths .
*
* Deletes outgoing edges from ` from ` to ` to ` ; when ` type ` is given , only edges of
* that relationship type are removed .
*
* @param from - The source VFS path .
* @param to - The target VFS path .
* @param type - Optional relationship type to match ; when omitted , all matching
* edges to ` to ` are removed .
* @returns A promise that resolves once the relationship ( s ) are removed .
* /
2025-09-24 17:31:48 -07:00
async removeRelationship ( from : string , to : string , type ? : string ) : Promise < void > {
await this . ensureInitialized ( )
const fromEntityId = await this . pathResolver . resolve ( from )
const toEntityId = await this . pathResolver . resolve ( to )
2025-09-25 10:47:44 -07:00
// Find and delete the relationship
2026-06-11 14:51:00 -07:00
const relations = await this . brain . related ( { from : fromEntityId } )
2025-09-24 17:31:48 -07:00
for ( const relation of relations ) {
if ( relation . to === toEntityId && ( ! type || relation . type === type ) ) {
2025-09-25 10:47:44 -07:00
// Delete the relationship using brain.unrelate
if ( relation . id ) {
await this . brain . unrelate ( relation . id )
}
2025-09-24 17:31:48 -07:00
}
}
// Invalidate caches
this . invalidateCaches ( from )
this . invalidateCaches ( to )
}
2026-06-29 10:04:19 -07:00
/ * *
* Get the todo items attached to a file or directory .
*
* @param path - The VFS path whose todos to read .
* @returns The stored todo list , or ` undefined ` when none are attached .
* /
2025-09-24 17:31:48 -07:00
async getTodos ( path : string ) : Promise < VFSMetadata [ 'todos' ] > {
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
return entity . metadata . todos
}
2026-06-29 10:04:19 -07:00
/ * *
* Replace the entire todo list attached to a file or directory .
*
* @param path - The VFS path to update .
* @param todos - The full set of todo items to store ( overwrites any existing list ) .
* @returns A promise that resolves once the todos are persisted .
* /
2025-09-24 17:31:48 -07:00
async setTodos ( path : string , todos : VFSTodo [ ] ) : Promise < void > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Update todos in metadata
await this . brain . update ( {
. . . entity ,
id : entityId ,
metadata : {
. . . entity . metadata ,
todos ,
modified : Date.now ( )
}
} )
// Invalidate caches
this . invalidateCaches ( path )
}
2026-06-29 10:04:19 -07:00
/ * *
* Append a single todo item to a file or directory .
*
* Generates an id when one is not supplied and applies default ` priority `
* ( ` 'medium' ` ) and ` status ` ( ` 'pending' ` ) .
*
* @param path - The VFS path to attach the todo to .
* @param todo - The todo to add ; ` id ` , ` priority ` , and ` status ` are optional .
* @returns A promise that resolves once the todo is persisted .
* /
2025-09-24 17:31:48 -07:00
async addTodo ( path : string , todo : VFSTodo ) : Promise < void > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Get existing todos
const todos = entity . metadata . todos || [ ]
// Add new todo with ID if not provided
const newTodo : VFSTodo = {
id : todo.id || crypto . randomUUID ( ) ,
task : todo.task ,
priority : todo.priority || 'medium' ,
status : todo.status || 'pending' ,
assignee : todo.assignee ,
due : todo.due
}
todos . push ( newTodo )
// Update entity metadata
await this . brain . update ( {
id : entityId ,
metadata : {
. . . entity . metadata ,
todos
}
} )
// Invalidate caches
this . invalidateCaches ( path )
}
2025-09-25 10:47:44 -07:00
/ * *
* Get metadata for a file or directory
* /
async getMetadata ( path : string ) : Promise < VFSMetadata | undefined > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
return entity . metadata
}
/ * *
* Set custom metadata for a file or directory
* Merges with existing metadata
* /
async setMetadata ( path : string , metadata : Partial < VFSMetadata > ) : Promise < void > {
await this . ensureInitialized ( )
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Merge with existing metadata
await this . brain . update ( {
id : entityId ,
metadata : {
. . . entity . metadata ,
. . . metadata ,
modified : Date.now ( )
}
} )
// Invalidate caches
this . invalidateCaches ( path )
}
2025-09-24 17:31:48 -07:00
2025-09-25 11:04:36 -07:00
/ * *
* Set the current user for tracking who makes changes
* /
setUser ( username : string ) : void {
this . currentUser = username || 'system'
}
/ * *
* Get the current user
* /
getCurrentUser ( ) : string {
return this . currentUser
}
2025-09-25 12:12:20 -07:00
/ * *
* Search for entities with filters
* /
async searchEntities ( query : {
type ? : string
name? : string
where? : Record < string , any >
limit? : number
} ) : Promise < Array < {
id : string
path : string
type : string
metadata : any
} >> {
await this . ensureInitialized ( )
// Build query for brain.find()
const searchQuery : any = {
where : {
. . . query . where ,
vfsType : 'entity'
} ,
2025-10-27 10:44:06 -07:00
limit : query.limit || 100
2025-09-25 12:12:20 -07:00
}
if ( query . type ) {
searchQuery . where . entityType = query . type
}
if ( query . name ) {
searchQuery . query = query . name
}
const results = await this . brain . find ( searchQuery )
return results . map ( result = > ( {
id : result.id ,
path : result.entity?.metadata?.path || '' ,
type : result . entity ? . metadata ? . type || result . entity ? . metadata ? . entityType || 'unknown' ,
metadata : result.entity?.metadata || { }
} ) )
}
2025-12-11 13:26:07 -08:00
/ * *
* Sort bulk operations to prevent race conditions
*
* Strategy :
* 1 . mkdir operations first , sorted by path depth ( shallowest first )
* 2 . Other operations ( write , delete , update ) after , in original order
*
* This ensures parent directories exist before files are written ,
* preventing duplicate entity creation from concurrent mkdir calls .
* /
private sortBulkOperations ( operations : Array < {
type : 'write' | 'delete' | 'mkdir' | 'update'
path : string
data? : Buffer | string
options? : any
} > ) : Array < typeof operations [ number ] > {
const mkdirOps : typeof operations = [ ]
const otherOps : typeof operations = [ ]
for ( const op of operations ) {
if ( op . type === 'mkdir' ) {
mkdirOps . push ( op )
} else {
otherOps . push ( op )
}
}
// Sort mkdir by path depth (shallowest first)
mkdirOps . sort ( ( a , b ) = > {
const depthA = ( a . path . match ( /\//g ) || [ ] ) . length
const depthB = ( b . path . match ( /\//g ) || [ ] ) . length
return depthA !== depthB ? depthA - depthB : a.path.localeCompare ( b . path )
} )
return [ . . . mkdirOps , . . . otherOps ]
}
2025-09-25 12:12:20 -07:00
/ * *
* Bulk write operations for performance
2025-12-11 13:26:07 -08:00
*
2026-01-27 15:38:21 -08:00
* Prevents race condition by processing mkdir operations
2025-12-11 13:26:07 -08:00
* sequentially before parallel batch processing of other operations .
2025-09-25 12:12:20 -07:00
* /
async bulkWrite ( operations : Array < {
type : 'write' | 'delete' | 'mkdir' | 'update'
path : string
data? : Buffer | string
options? : any
} > ) : Promise < {
successful : number
failed : Array < { operation : any , error : string } >
} > {
await this . ensureInitialized ( )
const result = {
successful : 0 ,
failed : [ ] as Array < { operation : any , error : string } >
}
2025-12-11 13:26:07 -08:00
// Sort operations: mkdirs first (by depth), then others
const sortedOps = this . sortBulkOperations ( operations )
// Separate mkdir operations for sequential processing
const mkdirOps = sortedOps . filter ( op = > op . type === 'mkdir' )
const otherOps = sortedOps . filter ( op = > op . type !== 'mkdir' )
// Phase 1: Process mkdir operations SEQUENTIALLY
// This prevents the race condition where parallel mkdir calls
// create duplicate directory entities due to mutex timing window
for ( const op of mkdirOps ) {
try {
await this . mkdir ( op . path , op . options )
result . successful ++
} catch ( error : any ) {
result . failed . push ( {
operation : op ,
error : error.message || 'Unknown error'
} )
}
}
// Phase 2: Process other operations in parallel batches
// These can safely run in parallel since parent directories now exist
2025-09-25 12:12:20 -07:00
const batchSize = 10
2025-12-11 13:26:07 -08:00
for ( let i = 0 ; i < otherOps . length ; i += batchSize ) {
const batch = otherOps . slice ( i , i + batchSize )
2025-09-25 12:12:20 -07:00
// Process batch in parallel
const promises = batch . map ( async ( op ) = > {
try {
switch ( op . type ) {
case 'write' :
await this . writeFile ( op . path , op . data || '' , op . options )
break
case 'delete' :
await this . unlink ( op . path )
break
2025-09-25 12:54:35 -07:00
case 'update' : {
2025-09-25 12:12:20 -07:00
// Update only metadata without changing content
const entityId = await this . pathResolver . resolve ( op . path )
await this . brain . update ( {
id : entityId ,
metadata : op.options?.metadata
} )
break
2025-09-25 12:54:35 -07:00
}
2025-09-25 12:12:20 -07:00
}
result . successful ++
} catch ( error : any ) {
result . failed . push ( {
operation : op ,
error : error.message || 'Unknown error'
} )
}
} )
await Promise . all ( promises )
}
return result
}
2025-09-25 11:04:36 -07:00
/ * *
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
* Calculate disk usage for a path ( POSIX du command )
* Returns total bytes used by files in directory tree
*
* @param path - Path to calculate usage for
* @param options - Options including maxDepth for safety
2025-09-25 11:04:36 -07:00
* /
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
async du ( path : string = '/' , options ? : {
maxDepth? : number
humanReadable? : boolean
} ) : Promise < {
bytes : number
files : number
directories : number
formatted? : string
2025-09-25 11:04:36 -07:00
} > {
await this . ensureInitialized ( )
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
const maxDepth = options ? . maxDepth ? ? 100 // Safety limit
let totalBytes = 0
let fileCount = 0
let dirCount = 0
2025-09-25 11:04:36 -07:00
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
const traverse = async ( currentPath : string , depth : number ) = > {
if ( depth > maxDepth ) {
throw new Error ( ` Maximum depth ${ maxDepth } exceeded. Use maxDepth option to increase limit. ` )
}
2025-09-25 11:04:36 -07:00
try {
const entityId = await this . pathResolver . resolve ( currentPath )
const entity = await this . getEntityById ( entityId )
if ( entity . metadata . vfsType === 'directory' ) {
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
dirCount ++
2025-09-25 11:04:36 -07:00
const children = await this . readdir ( currentPath )
for ( const child of children ) {
const childPath = currentPath === '/' ? ` / ${ child } ` : ` ${ currentPath } / ${ child } `
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
await traverse ( childPath , depth + 1 )
2025-09-25 11:04:36 -07:00
}
} else if ( entity . metadata . vfsType === 'file' ) {
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
fileCount ++
totalBytes += entity . metadata . size || 0
2025-09-25 11:04:36 -07:00
}
} catch ( error ) {
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
// Skip inaccessible paths
2025-09-25 11:04:36 -07:00
}
}
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
await traverse ( path , 0 )
2025-09-25 11:04:36 -07:00
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
const result : any = {
bytes : totalBytes ,
files : fileCount ,
directories : dirCount
2025-09-25 11:04:36 -07:00
}
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
if ( options ? . humanReadable ) {
const units = [ 'B' , 'KB' , 'MB' , 'GB' , 'TB' ]
let size = totalBytes
let unitIndex = 0
while ( size >= 1024 && unitIndex < units . length - 1 ) {
size /= 1024
unitIndex ++
2025-09-25 11:04:36 -07:00
}
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
result . formatted = ` ${ size . toFixed ( 2 ) } ${ units [ unitIndex ] } `
2025-09-25 11:04:36 -07:00
}
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
return result
}
/ * *
* Check file access permissions ( POSIX access command )
* Verifies if path exists and is accessible with specified mode
*
* @param path - Path to check
* @param mode - Access mode : 'r' ( read ) , 'w' ( write ) , 'x' ( execute ) , or 'f' ( exists only )
* /
async access ( path : string , mode : 'r' | 'w' | 'x' | 'f' = 'f' ) : Promise < boolean > {
await this . ensureInitialized ( )
try {
const entityId = await this . pathResolver . resolve ( path )
const entity = await this . getEntityById ( entityId )
// Path exists
if ( mode === 'f' ) {
return true
}
// Check permissions based on mode
const permissions = entity . metadata . permissions || 0 o644
switch ( mode ) {
case 'r' :
// Check read permission (owner, group, or other)
return ( permissions & 0 o444 ) !== 0
case 'w' :
// Check write permission
return ( permissions & 0 o222 ) !== 0
case 'x' :
// Check execute permission (only meaningful for directories)
return entity . metadata . vfsType === 'directory' || ( permissions & 0 o111 ) !== 0
default :
return false
}
} catch ( error ) {
// Path doesn't exist or not accessible
return false
}
2025-09-25 11:04:36 -07:00
}
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
/ * *
* Find files matching patterns ( Unix find command )
* Pattern - based file search ( complements semantic search ( ) )
*
* @param path - Starting path for search
* @param options - Search options including pattern matching
* /
async find ( path : string = '/' , options ? : {
name? : string | RegExp
type ? : 'file' | 'directory' | 'both'
maxDepth? : number
minSize? : number
maxSize? : number
modified ? : { after? : Date , before? : Date }
limit? : number
} ) : Promise < Array < {
path : string
type : 'file' | 'directory'
size? : number
modified? : Date
} >> {
await this . ensureInitialized ( )
const maxDepth = options ? . maxDepth ? ? 100 // Safety limit
const limit = options ? . limit ? ? 1000 // Prevent unbounded results
const results : Array < {
path : string
type : 'file' | 'directory'
size? : number
modified? : Date
} > = [ ]
const namePattern = options ? . name
const nameRegex = namePattern instanceof RegExp
? namePattern
: namePattern
? new RegExp ( namePattern . replace ( /\*/g , '.*' ) . replace ( /\?/g , '.' ) )
: null
2025-09-24 17:31:48 -07:00
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
const traverse = async ( currentPath : string , depth : number ) = > {
if ( depth > maxDepth || results . length >= limit ) {
return
}
2025-09-24 17:31:48 -07:00
feat: ID-first storage architecture + remove memory-unsafe APIs (v6.0.0)
BREAKING CHANGES:
**ID-First Storage Paths**
- Direct O(1) entity access without type lookups
- Before: entities/nouns/{TYPE}/metadata/{SHARD}/{ID}.json
- After: entities/nouns/{SHARD}/{ID}/metadata.json
- Migration handled automatically on first init()
**Removed Memory-Unsafe APIs**
- Removed brain.merge() - loaded all entities into memory
- Removed brain.diff() - loaded all entities into memory
- Removed brain.data().backup() - loaded all entities into memory
- Removed brain.data().restore() - depended on backup()
- Removed CLI commands: backup, restore, cow merge
**Migration Paths**
- merge() → Use checkout() or manually copy entities with pagination
- diff() → Use asOf() with manual paginated comparison
- backup() → Use fork() for instant COW snapshots
- restore() → Use checkout() to switch to snapshot branch
Core Improvements:
- ✅ All 8 storage adapters properly call super.init()
- ✅ GraphAdjacencyIndex integration in BaseStorage.init()
- ✅ Fixed ID-first path bugs (vector.json → vectors.json)
- ✅ Fixed MemoryStorage.initializeCounts() for ID-first paths
- ✅ New VFS APIs: du(), access(), find()
- ✅ Comprehensive documentation with migration guides
Storage Adapters Fixed:
- MemoryStorage, FileSystemStorage, AzureBlobStorage
- GCSStorage, R2Storage, S3CompatibleStorage
- OPFSStorage, HistoricalStorageAdapter
Files Changed: 28 files, +1,075/-1,933 lines (net -858)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 16:46:11 -08:00
try {
const entityId = await this . pathResolver . resolve ( currentPath )
const entity = await this . getEntityById ( entityId )
const vfsType = entity . metadata . vfsType
const fileName = currentPath . split ( '/' ) . pop ( ) || ''
// Check if this file matches criteria
let matches = true
// Type filter
if ( options ? . type && options . type !== 'both' ) {
matches = matches && vfsType === options . type
}
// Name pattern filter
if ( nameRegex ) {
matches = matches && nameRegex . test ( fileName )
}
// Size filters (files only)
if ( vfsType === 'file' ) {
const size = entity . metadata . size || 0
if ( options ? . minSize !== undefined ) {
matches = matches && size >= options . minSize
}
if ( options ? . maxSize !== undefined ) {
matches = matches && size <= options . maxSize
}
}
// Modified time filter
if ( options ? . modified && entity . metadata . modified ) {
const modifiedTime = new Date ( entity . metadata . modified )
if ( options . modified . after ) {
matches = matches && modifiedTime >= options . modified . after
}
if ( options . modified . before ) {
matches = matches && modifiedTime <= options . modified . before
}
}
// Add to results if matches
if ( matches && currentPath !== path ) {
results . push ( {
path : currentPath ,
type : vfsType as 'file' | 'directory' ,
size : entity.metadata.size ,
modified : entity.metadata.modified ? new Date ( entity . metadata . modified ) : undefined
} )
}
// Recurse into directories
if ( vfsType === 'directory' && results . length < limit ) {
const children = await this . readdir ( currentPath )
for ( const child of children ) {
if ( results . length >= limit ) break
const childPath = currentPath === '/' ? ` / ${ child } ` : ` ${ currentPath } / ${ child } `
await traverse ( childPath , depth + 1 )
}
}
} catch ( error ) {
// Skip inaccessible paths
}
}
await traverse ( path , 0 )
return results
}
2025-09-24 17:31:48 -07:00
2026-06-29 10:04:19 -07:00
/ * *
* Open a file as a Node - compatible readable stream .
*
* @param path - The VFS path of the file to read .
* @param options - Stream options ( e . g . byte range , chunk size ) .
* @returns A readable stream that yields the file ' s bytes .
* /
2025-09-24 17:31:48 -07:00
createReadStream ( path : string , options? : ReadStreamOptions ) : NodeJS . ReadableStream {
// Lazy import to avoid circular dependencies
const { VFSReadStream } = require ( './streams/VFSReadStream.js' )
return new VFSReadStream ( this , path , options )
}
2026-06-29 10:04:19 -07:00
/ * *
* Open a file as a Node - compatible writable stream .
*
* @param path - The VFS path of the file to write .
* @param options - Stream options controlling how buffered data is flushed .
* @returns A writable stream whose contents are persisted to the file on finish .
* /
2025-09-24 17:31:48 -07:00
createWriteStream ( path : string , options? : WriteStreamOptions ) : NodeJS . WritableStream {
// Lazy import to avoid circular dependencies
const { VFSWriteStream } = require ( './streams/VFSWriteStream.js' )
return new VFSWriteStream ( this , path , options )
}
2026-06-29 10:04:19 -07:00
/ * *
* Watch a path for changes and invoke a listener on filesystem events .
*
* @param path - The VFS path to watch .
* @param listener - Called with the event type ( ` 'rename' ` or ` 'change' ` ) and the path .
* @returns A handle whose ` close() ` method deregisters the listener .
* /
2025-09-24 17:31:48 -07:00
watch ( path : string , listener : WatchListener ) : { close ( ) : void } {
if ( ! this . watchers . has ( path ) ) {
this . watchers . set ( path , new Set ( ) )
}
this . watchers . get ( path ) ! . add ( listener )
return {
close : ( ) = > {
const watchers = this . watchers . get ( path )
if ( watchers ) {
watchers . delete ( listener )
if ( watchers . size === 0 ) {
this . watchers . delete ( path )
}
}
}
}
}
// ============= Import/Export Operations =============
/ * *
2025-09-25 10:47:44 -07:00
* Import a single file from the real filesystem into VFS
* /
async importFile ( sourcePath : string , targetPath : string ) : Promise < void > {
const fs = await import ( 'fs/promises' )
const pathModule = await import ( 'path' )
// Read file from local filesystem
const content = await fs . readFile ( sourcePath )
const stats = await fs . stat ( sourcePath )
// Ensure parent directory exists in VFS
const parentPath = pathModule . dirname ( targetPath )
if ( parentPath !== '/' && parentPath !== '.' ) {
try {
await this . mkdir ( parentPath , { recursive : true } )
} catch ( error : any ) {
if ( error . code !== 'EEXIST' ) throw error
}
}
// Write to VFS with metadata from source
await this . writeFile ( targetPath , content , {
metadata : {
imported : true ,
importedFrom : sourcePath ,
sourceSize : stats.size ,
sourceMtime : stats.mtime.getTime ( ) ,
sourceMode : stats.mode
}
} )
}
/ * *
* Import a directory from the real filesystem into VFS
2025-09-24 17:31:48 -07:00
* /
async importDirectory ( sourcePath : string , options? : any ) : Promise < any > {
const { DirectoryImporter } = await import ( './importers/DirectoryImporter.js' )
const importer = new DirectoryImporter ( this , this . brain )
return await importer . import ( sourcePath , options )
}
/ * *
* Import a directory with progress tracking
* /
async * importStream ( sourcePath : string , options? : any ) : AsyncGenerator < any > {
const { DirectoryImporter } = await import ( './importers/DirectoryImporter.js' )
const importer = new DirectoryImporter ( this , this . brain )
yield * importer . importStream ( sourcePath , options )
}
2026-06-29 10:04:19 -07:00
/ * *
* Register a change listener for a path ( Node ` fs.watchFile ` - style convenience ) .
*
* Delegates to { @link watch } without returning the watcher handle ; remove the
* listener with { @link unwatchFile } .
*
* @param path - The VFS path to watch .
* @param listener - Called with the event type and path on each change .
* /
2025-09-24 17:31:48 -07:00
watchFile ( path : string , listener : WatchListener ) : void {
this . watch ( path , listener )
}
2026-06-29 10:04:19 -07:00
/ * *
* Stop watching a path , removing all listeners registered for it .
*
* @param path - The VFS path to stop watching .
* /
2025-09-24 17:31:48 -07:00
unwatchFile ( path : string ) : void {
this . watchers . delete ( path )
}
2026-06-29 10:04:19 -07:00
/ * *
* Resolve a path and return its backing { @link VFSEntity } .
*
* @param path - The VFS path to resolve .
* @returns The entity ( with normalized VFS metadata ) for the path .
* @throws { VFSError } ENOENT when the path cannot be resolved .
* /
2025-09-24 17:31:48 -07:00
async getEntity ( path : string ) : Promise < VFSEntity > {
const entityId = await this . pathResolver . resolve ( path )
return this . getEntityById ( entityId )
}
2025-10-07 11:51:17 -07:00
/ * *
* Resolve a path to its normalized form
* Returns the normalized absolute path ( e . g . , '/foo/bar/file.txt' )
* /
2025-09-24 17:31:48 -07:00
async resolvePath ( path : string , from ? : string ) : Promise < string > {
// Handle relative paths
if ( ! path . startsWith ( '/' ) && from ) {
path = ` ${ from } / ${ path } `
}
2025-10-07 11:51:17 -07:00
// Normalize path: remove multiple slashes, trailing slashes
return path . replace ( /\/+/g , '/' ) . replace ( /\/$/ , '' ) || '/'
}
/ * *
* Resolve a path to its entity ID
* Returns the UUID of the entity representing this path
* /
async resolvePathToId ( path : string , from ? : string ) : Promise < string > {
// Handle relative paths
if ( ! path . startsWith ( '/' ) && from ) {
path = ` ${ from } / ${ path } `
}
2025-09-24 17:31:48 -07:00
// Normalize path
2025-09-26 15:12:04 -07:00
const normalizedPath = path . replace ( /\/+/g , '/' ) . replace ( /\/$/ , '' ) || '/'
// Special case for root
if ( normalizedPath === '/' ) {
return this . rootEntityId !
}
// Resolve the path to an entity ID
return await this . pathResolver . resolve ( normalizedPath )
2025-09-24 17:31:48 -07:00
}
}