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.
161 lines
7.3 KiB
TypeScript
161 lines
7.3 KiB
TypeScript
/**
|
|
* @module db/errors
|
|
* @description Error types for the 8.0 generational-MVCC Db API.
|
|
*
|
|
* Three failure modes have dedicated classes so callers can branch on them
|
|
* with `instanceof` instead of string matching:
|
|
*
|
|
* - {@link GenerationConflictError} — the store's current generation differs
|
|
* from what the caller's operation requires: a `transact()`
|
|
* compare-and-swap (`ifAtGeneration`) observed a different generation than
|
|
* expected, or `db.persist()` was called on a view that history has moved
|
|
* past (snapshots capture current bytes, so persisting requires the view
|
|
* to still be the latest generation). The standard retry pattern is:
|
|
* re-read via `brain.now()`, re-derive the operation, and re-submit.
|
|
* - {@link SpeculativeOverlayError} — an index-accelerated query (vector /
|
|
* hybrid / graph-traversal search, cursors, aggregation) or `persist()`
|
|
* was issued against a speculative `db.with()` overlay. Overlays are pure
|
|
* in-memory values whose entities carry no embeddings (`with()` never
|
|
* invokes the embedder), so a "full" index query over one would silently
|
|
* miss the overlay's own entities — an honest error beats silently-wrong
|
|
* results. Commit the operations with `brain.transact()` to get the full
|
|
* query surface. Historical (non-overlay) views do NOT throw this: they
|
|
* serve the full query surface via at-generation index materialization.
|
|
* - {@link GenerationCompactedError} — `asOf()` asked for a generation whose
|
|
* immutable records were reclaimed by `compactHistory()`.
|
|
*
|
|
* All three are exported from the package root (`@soulcraft/brainy`).
|
|
*/
|
|
|
|
/**
|
|
* @description Thrown when an operation requires the store to be at a
|
|
* specific generation and it is not:
|
|
*
|
|
* - `brain.transact(ops, { ifAtGeneration })` — the whole-store
|
|
* compare-and-swap counterpart to the per-entity `ifRev` /
|
|
* `RevisionConflictError` pair: it guarantees that *nothing* was committed
|
|
* between the caller's read (`brain.now()`) and this transaction. The
|
|
* transaction is rejected before any record is staged — the store is
|
|
* untouched and the generation counter is unchanged.
|
|
* - `db.persist(path)` — snapshots capture current bytes, so persisting
|
|
* requires the view to still be the store's **latest** generation. A view
|
|
* that history has moved past throws this error: pin with `brain.now()`
|
|
* and persist before further writes.
|
|
*
|
|
* @example
|
|
* const db = brain.now()
|
|
* try {
|
|
* await brain.transact(ops, { ifAtGeneration: db.generation })
|
|
* } catch (err) {
|
|
* if (err instanceof GenerationConflictError) {
|
|
* // Someone committed since we pinned — re-read and retry.
|
|
* console.log(`expected ${err.expected}, store is at ${err.actual}`)
|
|
* }
|
|
* }
|
|
*/
|
|
export class GenerationConflictError extends Error {
|
|
/** The generation the operation required the store to be at. */
|
|
public readonly expected: number
|
|
/** The generation the store was actually at. */
|
|
public readonly actual: number
|
|
|
|
/**
|
|
* @param expected - The required generation (`ifAtGeneration`, or the
|
|
* pinned generation of the view being persisted).
|
|
* @param actual - The store's current generation.
|
|
*/
|
|
constructor(expected: number, actual: number) {
|
|
super(
|
|
`Generation conflict: the operation requires the store at generation ${expected}, ` +
|
|
`but it is at generation ${actual}. Another write committed in between. ` +
|
|
`Re-read with brain.now(), rebuild the operation, and retry.`
|
|
)
|
|
this.name = 'GenerationConflictError'
|
|
this.expected = expected
|
|
this.actual = actual
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @description Thrown when an operation on a speculative `db.with()` overlay
|
|
* requires index machinery (vector / hybrid / graph-traversal search, cursor
|
|
* pagination, aggregation) or durability (`persist()`).
|
|
*
|
|
* Why this single boundary exists: overlays are pure in-memory values —
|
|
* `with()` never touches disk, the generation counter, or the embedder, so
|
|
* overlay entities carry **no embeddings**. A "full" vector or hybrid query
|
|
* over an overlay would silently exclude the overlay's own entities, and a
|
|
* persisted overlay would be a store whose entities cannot be semantically
|
|
* searched. An honest error beats silently-wrong results. Commit the
|
|
* operations with `brain.transact()` to get the full query surface, or query
|
|
* the overlay's base view (historical views serve the complete surface via
|
|
* at-generation index materialization).
|
|
*
|
|
* `get()`, metadata-filter `find()`, and filter-based `related()` remain
|
|
* fully supported on overlays.
|
|
*
|
|
* @example
|
|
* const spec = await brain.now().with([{ op: 'add', type, data: 'draft' }])
|
|
* await spec.find({ where: { draft: true } }) // ✅ metadata find — supported
|
|
* await spec.find({ query: 'semantic query' }) // ❌ throws this error
|
|
* await brain.transact([{ op: 'add', type, data: 'draft' }]) // → full surface
|
|
*/
|
|
export class SpeculativeOverlayError extends Error {
|
|
/** The capability that is not supported on speculative overlays. */
|
|
public readonly capability: string
|
|
/** The overlay's pinned base generation. */
|
|
public readonly generation: number
|
|
|
|
/**
|
|
* @param capability - Human-readable name of the unsupported capability
|
|
* (e.g. `'vector search'`, `'persist'`).
|
|
* @param generation - The overlay's pinned base generation.
|
|
*/
|
|
constructor(capability: string, generation: number) {
|
|
super(
|
|
`${capability} is not supported on a speculative with() overlay ` +
|
|
`(pinned at base generation ${generation}). Overlays are pure in-memory ` +
|
|
`values whose entities carry no embeddings, so index-accelerated reads ` +
|
|
`over them would be silently incomplete. Supported on overlays: get(), ` +
|
|
`metadata-filter find(), and filter-based related(). Commit the operations ` +
|
|
`with brain.transact() for the full query surface, or query the base view.`
|
|
)
|
|
this.name = 'SpeculativeOverlayError'
|
|
this.capability = capability
|
|
this.generation = generation
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @description Thrown by `brain.asOf(generationOrTimestamp)` when the
|
|
* requested generation's history was reclaimed by `brain.compactHistory()`.
|
|
* The store records the compaction horizon (the highest reclaimed
|
|
* generation) in its manifest; any generation BELOW the horizon is
|
|
* unreachable — its reads would need the reclaimed before-images. The
|
|
* horizon itself stays reachable, resolved from the record-sets above it.
|
|
*
|
|
* To keep a generation readable forever, `persist()` it to a snapshot
|
|
* directory before compacting — snapshots are self-contained and unaffected
|
|
* by compaction of the source store.
|
|
*/
|
|
export class GenerationCompactedError extends Error {
|
|
/** The generation that was requested. */
|
|
public readonly requested: number
|
|
/** The compaction horizon — generations < this value are unreachable. */
|
|
public readonly horizon: number
|
|
|
|
/**
|
|
* @param requested - The generation the caller asked for.
|
|
* @param horizon - The store's current compaction horizon.
|
|
*/
|
|
constructor(requested: number, horizon: number) {
|
|
super(
|
|
`Generation ${requested} has been compacted away (compaction horizon: ${horizon}). ` +
|
|
`Only generations at or above the horizon are reachable via asOf(). ` +
|
|
`Use db.persist(path) before compactHistory() to keep a generation readable.`
|
|
)
|
|
this.name = 'GenerationCompactedError'
|
|
this.requested = requested
|
|
this.horizon = horizon
|
|
}
|
|
}
|