brainy/src/types/brainy.types.ts
David Snelling 588267be7f feat: guarded plugin auto-detection — installing @soulcraft/cor is the opt-in
With plugins unset (the default), init() probes for the first-party
accelerator: not installed means plain brainy with zero noise; installed and
healthy means it loads and announces itself; installed but broken (import
failure, invalid shape, failed activation, version mismatch) makes init()
THROW. An installed accelerator never silently vanishes behind the JS engines
- the anti-drift posture of the explicit list, applied to detection.

plugins: []/false stays a true opt-out (no probe); an explicit list keeps its
required-and-loud semantics. The import runs through an importPluginPackage
seam (variable specifier - bundlers cannot static-resolve the optional
package; tests simulate all outcomes without it installed).

Also: when a plugin activates but registers zero native providers (e.g. a
licensing gate declining to engage), the provider summary now warns loudly
instead of leaving every query silently on the JS engines.

Supersedes 8.0.8's explicit-opt-in wording; README/PLUGINS/types now document
the guarded contract. 7 new tests (tests/unit/plugin-autodetect.test.ts).
2026-07-02 16:19:55 -07:00

2072 lines
No EOL
80 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @module types/brainy.types
* @description Public type definitions for the Brainy API — the entity/relation
* shapes, the `find()`/`add()`/`relate()` parameter and result types, and the
* configuration surface. (Version-agnostic header: these track the package
* version, not a fixed number.)
*
* Beautiful, consistent, type-safe interfaces for the future of neural databases
*/
import { StorageAdapter, Vector } from '../coreTypes.js'
import type { EntityVisibility } from '../coreTypes.js'
import { NounType, VerbType } from './graphTypes.js'
import type {
EntityMetadataInput,
EntityMetadataPatch,
RelationMetadataInput,
RelationMetadataPatch
} from './reservedFields.js'
// ============= Core Types =============
/**
* Entity (Noun) — the fundamental data unit in Brainy
*
* **Data vs Metadata:**
* - `data`: Content used for vector embeddings. Searchable via **semantic similarity**
* (HNSW index) and hybrid text+semantic search. NOT queryable via `where` filters.
* - `metadata`: Structured fields indexed by MetadataIndex. Queryable via `where`
* filters in `find()`. Standard system fields (noun, createdAt, etc.) are stored
* alongside user metadata but extracted to top-level Entity fields on read.
*/
export interface Entity<T = any> {
/** Unique identifier — a UUID (auto-generated as a time-ordered v7; a supplied natural key is normalized to a stable v5). */
id: string
/** Embedding vector (384-dim by default). Empty array when loaded without includeVectors. */
vector: Vector
/** Entity type classification (NounType enum) */
type: NounType
/**
* Per-product sub-classification within the NounType (e.g. a `Person` entity might
* have `subtype: 'employee'` or `'customer'`; a `Document` might have `subtype: 'invoice'`).
* Flat string, no hierarchy. Top-level standard field — indexed on the fast path and
* rolled into per-NounType statistics.
*/
subtype?: string
/**
* Visibility tier (reserved top-level field). Absent === `'public'` (counted and
* returned everywhere). `'internal'` hides the entity from default `find()` / counts /
* `stats()` (opt back in with `find({ includeInternal: true })`); `'system'` is Brainy
* plumbing, hidden unless `find({ includeSystem: true })`. See {@link EntityVisibility}.
*/
visibility?: EntityVisibility
/** Opaque content — used for embeddings and semantic search. Not indexed by MetadataIndex. */
data?: any
/** User-defined structured fields — indexed and queryable via `where` filters. */
metadata?: T
/** Multi-tenancy service identifier */
service?: string
/** Creation timestamp (ms since epoch) */
createdAt: number
/** Last update timestamp (ms since epoch) */
updatedAt?: number
/** Source that created this entity (e.g., augmentation info) */
createdBy?: string
/** Type classification confidence (0-1) */
confidence?: number
/** Entity importance/salience (0-1) */
weight?: number
/**
* Monotonic revision counter, auto-bumped on every successful `update()`.
* Starts at `1` on `add()`. Pre-7.31.0 entities without `_rev` are read as `1`.
* Pass back as `update({ id, ..., ifRev: _rev })` for optimistic-concurrency CAS —
* throws `RevisionConflictError` if the persisted rev moved since read.
*/
_rev?: number
}
/**
* Relation (Verb) — a typed edge connecting two entities
*
* **Data vs Metadata (on relationships):**
* - `data`: Opaque content stored on the relationship. If provided during relate(),
* overrides the auto-computed vector (default: average of source+target vectors).
* - `metadata`: Structured queryable fields on the edge (e.g., role, startDate).
*/
export interface Relation<T = any> {
/** Unique identifier (UUID v4) */
id: string
/** Source entity ID */
from: string
/** Target entity ID */
to: string
/** Relationship type classification (VerbType enum) */
type: VerbType
/**
* Per-product sub-classification within the VerbType (e.g. a `ReportsTo`
* relationship might have `subtype: 'direct'` vs `'dotted-line'`; a `RelatedTo`
* edge might carry `'spouse'` / `'sibling'` / `'colleague'`). Flat string, no
* hierarchy. Top-level standard field — indexed on the fast path and rolled into
* per-VerbType statistics so queries like `related({ verb, subtype })` hit
* the column-store directly.
*/
subtype?: string
/**
* Visibility tier (reserved top-level field), the verb mirror of `Entity.visibility`.
* Absent === `'public'` (counted and returned everywhere). `'internal'` hides the edge
* from default `related()` / counts / `stats()` (opt back in with
* `related({ includeInternal: true })`); `'system'` is Brainy plumbing. See
* {@link EntityVisibility}.
*/
visibility?: EntityVisibility
/** Connection strength (0-1, default: 1.0) */
weight?: number
/** Opaque content for the relationship (overrides auto-computed vector if provided) */
data?: any
/** User-defined structured fields on the edge */
metadata?: T
/** Multi-tenancy service identifier */
service?: string
/** Creation timestamp (ms since epoch) */
createdAt: number
/** Last update timestamp (ms since epoch) */
updatedAt?: number
/** Relationship certainty (0-1) */
confidence?: number
/** Evidence for why this relationship was detected */
evidence?: RelationEvidence
}
/**
* Evidence for why a relationship was detected
*/
export interface RelationEvidence {
sourceText?: string // Text that indicated this relationship
position?: { // Position in source text
start: number
end: number
}
method: 'neural' | 'pattern' | 'structural' | 'explicit' // How it was detected
reasoning?: string // Human-readable explanation
}
/**
* Search result with similarity score
*
* Flattens commonly-used entity fields to the top level for convenience, while
* keeping the complete record under `entity` for callers that need the full shape.
*/
export interface Result<T = any> {
// Search metadata
id: string
score: number
// Convenience: Common entity fields flattened to top level
type?: NounType // Entity type (from entity.type)
subtype?: string // Per-product sub-classification (from entity.subtype)
visibility?: EntityVisibility // Visibility tier (from entity.visibility); absent === 'public'
metadata?: T // Entity metadata (from entity.metadata)
data?: any // Entity data (from entity.data)
confidence?: number // Type classification confidence (from entity.confidence)
weight?: number // Entity importance (from entity.weight)
_rev?: number // Monotonic revision counter (from entity._rev) — pass back as update({ ifRev }) for CAS
// Full entity record (the flattened fields above are projections of this)
entity: Entity<T>
// Score transparency
explanation?: ScoreExplanation
// Match visibility - shows what matched in hybrid search
textMatches?: string[] // Query words found in entity (e.g., ["david", "warrior"])
textScore?: number // Normalized text match score (0-1)
semanticScore?: number // Semantic similarity score (0-1)
matchSource?: 'text' | 'semantic' | 'both' // Where this result came from
rrfScore?: number // Raw RRF fusion score (for advanced ranking analysis)
// Aggregation: present only on rows returned by find({ aggregate }). These mirror the
// documented AggregateResult shape so callers can read group/metric data at the top level
// instead of digging into `metadata`. (The same values are also mirrored into `metadata`
// so aggregate-unaware callers still see them.)
groupKey?: Record<string, string | number> // Group-by key values for this aggregate row
metrics?: Record<string, number> // Computed metric values (sum/avg/min/max/count)
count?: number // Total entity count in this group
}
/**
* Score explanation for transparency
*/
export interface ScoreExplanation {
vectorScore?: number
metadataScore?: number
graphScore?: number
boosts?: Record<string, number>
penalties?: Record<string, number>
}
// ============= Operation Parameters =============
/**
* Parameters for adding entities
*
* **Data vs Metadata:**
* - `data` is embedded into a vector and searchable via semantic similarity (HNSW).
* It is NOT indexed by MetadataIndex and NOT queryable via `where` filters.
* - `metadata` is indexed by MetadataIndex and queryable via `where` filters in `find()`.
*/
/**
* Declaration-merging registry for per-`(type, subtype)` metadata shapes
* (Brainy 8.0, per BRAINY-8.0-SUBTYPE-CONTRACT § C-3).
*
* Consumers extend this interface in their own code to tell TypeScript what
* shape `metadata` takes for a given `(NounType, subtype)` pair. Brainy
* doesn't ship any entries — every entry is a consumer concern.
*
* @example
* ```ts
* declare module '@soulcraft/brainy' {
* interface SubtypeRegistry {
* // For NounType.Person, subtype 'employee':
* 'person:employee': { employeeId: string; department: string }
* // For NounType.Document, subtype 'invoice':
* 'document:invoice': { invoiceNumber: string; amount: number }
* }
* }
* ```
*
* Keys are `${NounType}:${subtype}`. Values describe the metadata shape.
* Brainy reads this registry (when present) to give typed `metadata`
* autocomplete + type-checking on `add()` / `update()` / `find()`. Consumers
* with no registry entries see no type-level change — the metadata bag
* stays `T = any`.
*/
export interface SubtypeRegistry {
// Intentionally empty. Consumers extend via declaration merging.
}
/**
* A single back-fill rule for `brain.fillSubtypes()`.
*
* - A **literal string** assigns that subtype to every matching entry that
* lacks one (`'general'` — a blanket default).
* - A **function** receives the full entry and returns the subtype to assign,
* or `undefined` to leave the entry untouched (it is counted as `skipped`
* so a later run with a stricter rule can pick it up). Functions can derive
* the subtype from existing fields, e.g. `(e) => e.metadata?.kind`.
*
* @typeParam E - The entry shape the rule sees: `Entity<T>` for NounType keys,
* `Relation<T>` for VerbType keys.
*/
export type FillSubtypeRule<E> = string | ((entry: E) => string | undefined)
/**
* Rule map for `brain.fillSubtypes()` — the 8.0 subtype migration helper.
*
* Keys are `NounType` values (entity rules) and/or `VerbType` values
* (relationship rules); the two vocabularies don't overlap, so a single map
* covers both sides. Each value is a {@link FillSubtypeRule}: a literal
* subtype string or a function deriving one from the entry.
*
* @example
* ```ts
* await brain.fillSubtypes({
* [NounType.Person]: (e) => e.metadata?.kind ?? 'unspecified',
* [NounType.Thing]: 'general', // literal default
* [VerbType.RelatedTo]: 'unspecified' // relationship rule
* })
* ```
*/
export type FillSubtypeRules<T = any> = {
[K in NounType]?: FillSubtypeRule<Entity<T>>
} & {
[K in VerbType]?: FillSubtypeRule<Relation<T>>
}
/**
* Summary returned by `brain.fillSubtypes()`.
*
* After a run, `skipped` is exactly the remaining migration debt — re-running
* `brain.audit()` reports the same entries. Entries that already carry a
* subtype count toward `scanned` only (they are not debt, so they are neither
* `filled` nor `skipped`).
*/
export interface FillSubtypesResult {
/** Total entries examined (entities + relationships). */
scanned: number
/** Entries that received a subtype during this run. */
filled: number
/**
* Entries still missing a subtype after the run — either their type has no
* rule in the map, or their rule function returned `undefined`/empty.
*/
skipped: number
/** Per-entry write failures (`fillSubtypes` continues past individual errors). */
errors: Array<{ id: string; error: string }>
/** Fill counts grouped by NounType/VerbType key (only types with fills appear). */
byType: Record<string, number>
}
export interface AddParams<T = any> {
/** Content to embed and store. Strings are auto-embedded; objects are JSON-stringified for embedding. */
data: any | Vector
/** Entity type classification (required) */
type: NounType
/**
* Per-product sub-classification within the NounType (e.g. a `Person` entity might have
* `subtype: 'employee'` or `'customer'`; a `Document` might have `subtype: 'invoice'`).
* Flat string, no hierarchy — consumers choose the vocabulary. Indexed and rolled up into
* per-NounType statistics for fast `find({ type, subtype })` filtering and `groupBy:['subtype']`
* aggregation on the standard-field fast path.
*/
subtype?: string
/**
* Visibility tier (reserved top-level field). Omit (or pass `'public'`) for normal
* data that is counted and returned everywhere. Pass `'internal'` for app-internal
* data that should be hidden from default `find()` / counts / `stats()` but stay
* retrievable via `find({ includeInternal: true })`. The `'system'` tier is reserved
* for Brainy's own plumbing and is intentionally not accepted here. Stored only when
* not `'public'`.
*/
visibility?: 'public' | 'internal'
/**
* Structured queryable fields — indexed by MetadataIndex, used in `where` filters.
*
* Reserved entity fields (`RESERVED_ENTITY_FIELDS` — `noun`, `subtype`, `visibility`,
* `createdAt`, `updatedAt`, `confidence`, `weight`, `service`, `data`, `createdBy`,
* `_rev`) may NOT appear here — they have dedicated top-level params and the type makes
* a literal reserved key a compile error. Untyped (JavaScript) callers that pass one
* anyway are normalized at write time: user-settable fields remap to their top-level
* param (top-level wins when both are supplied), system-managed fields are dropped with
* a one-shot warning.
*/
metadata?: EntityMetadataInput<T>
/** Custom entity ID. When omitted, a time-ordered UUID v7 is generated; a supplied natural-key string is normalized to a stable UUID v5. */
id?: string
/** Pre-computed embedding vector (skips auto-embedding when provided) */
vector?: Vector
/** Multi-tenancy service identifier */
service?: string
/** Type classification confidence (0-1) */
confidence?: number
/** Entity importance/salience (0-1) */
weight?: number
/** Track which augmentation created this entity */
createdBy?: { augmentation: string; version: string }
/**
* Conditional insert. When `true` AND a custom `id` is supplied AND an entity with
* that `id` already exists, `add()` returns the existing `id` without writing — no
* throw, no overwrite. Used for idempotent bootstrap and create-or-noop patterns.
* Ignored when `id` is omitted (a freshly generated UUID can never collide).
*/
ifAbsent?: boolean
/**
* Create-or-update in a single call. When `true` AND a custom `id` is supplied AND
* an entity with that `id` already exists, `add()` applies the supplied fields as an
* UPDATE instead of the default destructive overwrite: metadata is MERGED (not
* replaced), `data` re-embeds only when it changed, `_rev` is bumped, and the
* original `createdAt` is PRESERVED. When no entity with that `id` exists, `add()`
* inserts normally (a fresh `_rev` of 1). Ignored — behaves as a plain insert — when
* `id` is omitted, since a freshly generated UUID can never collide.
*
* Mutually exclusive with {@link AddParams.ifAbsent}: `ifAbsent` SKIPS an existing
* entity, `upsert` MERGES into it — supplying both throws.
*
* @example
* // First call inserts; a later call with the same id merges + bumps _rev.
* await brain.add({ id: 'customer-42', type: 'customer', data: 'Acme', upsert: true })
* await brain.add({ id: 'customer-42', type: 'customer', metadata: { tier: 'gold' }, upsert: true })
*/
upsert?: boolean
}
/**
* Parameters for updating entities
*/
export interface UpdateParams<T = any> {
id: string // Entity to update
data?: any // New content to re-embed
type?: NounType // Change type
subtype?: string // Change subtype (set to '' or null-equivalent via dedicated unset is future work)
/**
* Change the visibility tier. Omit to preserve the existing value. Toggling between
* `'public'` and `'internal'` moves the entity in/out of the default-visible counts and
* `find()` results. The `'system'` tier is Brainy-internal (e.g. re-asserted on the VFS
* root during repair) — consumer code should only ever use `'public'` / `'internal'`.
*/
visibility?: EntityVisibility
/**
* Metadata fields to merge (or replace when `merge: false`). Reserved entity
* fields (`RESERVED_ENTITY_FIELDS`) may NOT appear here — `confidence` /
* `weight` / `subtype` / `visibility` have dedicated params on this call, and the rest
* are system-managed. A literal reserved key is a compile error; untyped callers
* are normalized at write time (remap user-settable, drop system-managed
* with a one-shot warning).
*/
metadata?: EntityMetadataPatch<T>
merge?: boolean // Merge or replace metadata (default: true)
vector?: Vector // New pre-computed vector
confidence?: number // Update type classification confidence
weight?: number // Update entity importance/salience
/**
* Optimistic concurrency check. When provided, the update fails with
* `RevisionConflictError` if the persisted entity's `_rev` does not equal `ifRev`.
* `_rev` is auto-bumped on every successful update — read it from the entity returned
* by `get()` / `find()` / `search()`. Omit to skip the check (unconditional update).
*/
ifRev?: number
}
/**
* Parameters for creating relationships
*
* **Data vs Metadata (on relationships):**
* - `data`: Opaque content for the edge. If provided, overrides the auto-computed
* vector (default: average of source+target entity vectors).
* - `metadata`: Structured queryable fields on the edge.
*/
export interface RelateParams<T = any> {
/** Source entity ID (required — must exist) */
from: string
/** Target entity ID (required — must exist) */
to: string
/** Relationship type classification (required) */
type: VerbType
/**
* Per-product sub-classification within the VerbType (e.g. a `ReportsTo`
* relationship might have `subtype: 'direct'` or `'dotted-line'`; a `RelatedTo`
* edge might carry `'spouse'` or `'colleague'`). Flat string, no hierarchy.
* Indexed and rolled up into per-VerbType statistics for fast filtering
* (`related({ verb, subtype })`) and aggregation (`groupBy:['subtype']`).
*/
subtype?: string
/**
* Visibility tier (reserved top-level field), the verb mirror of `AddParams.visibility`.
* Omit (or pass `'public'`) for normal edges that are counted and returned everywhere.
* Pass `'internal'` for app-internal edges hidden from default `related()` / counts /
* `stats()` but retrievable via `related({ includeInternal: true })`. The `'system'`
* tier is reserved for Brainy's own plumbing and is not accepted here. Stored only when
* not `'public'`.
*/
visibility?: 'public' | 'internal'
/** Connection strength (0-1, default: 1.0) */
weight?: number
/** Content for the relationship (optional — overrides auto-computed vector) */
data?: any
/**
* Structured queryable fields on the edge. Reserved relationship fields
* (`RESERVED_RELATION_FIELDS` — `verb`, `subtype`, `visibility`, `createdAt`,
* `updatedAt`, `confidence`, `weight`, `service`, `data`, `createdBy`, `_rev`) may NOT
* appear here — they have dedicated params. A literal reserved key is a
* compile error; untyped callers are normalized at write time.
*/
metadata?: RelationMetadataInput<T>
/** Create reverse edge too (default: false) */
bidirectional?: boolean
/** Multi-tenancy service identifier */
service?: string
/** Relationship certainty (0-1) */
confidence?: number
/** Evidence for why this relationship exists */
evidence?: RelationEvidence
}
/**
* Parameters for updating relationships
*/
export interface UpdateRelationParams<T = any> {
id: string // Relation to update
type?: VerbType // Change verb type
subtype?: string // Change sub-classification (omit to preserve existing)
/**
* Change the visibility tier. Omit to preserve the existing value. The verb mirror of
* `UpdateParams.visibility`; `'system'` is Brainy-internal — consumer code should only
* use `'public'` / `'internal'`.
*/
visibility?: EntityVisibility
weight?: number // New weight
confidence?: number // New confidence (0-1)
data?: any // New content
/**
* Metadata fields to merge (or replace when `merge: false`). Reserved
* relationship fields (`RESERVED_RELATION_FIELDS`) may NOT appear here —
* a literal reserved key is a compile error; untyped callers are
* normalized at write time.
*/
metadata?: RelationMetadataPatch<T>
merge?: boolean // Merge or replace metadata
}
// ============= Query Parameters =============
/**
* Unified find parameters — Triple Intelligence search
*
* Combines three search dimensions in one query:
* - **Vector:** `query` or `vector` for semantic/hybrid similarity search (searches `data`)
* - **Metadata:** `where` for structured field filters (queries `metadata` via MetadataIndex)
* - **Graph:** `connected` for relationship traversal (via GraphAdjacencyIndex)
*
* See also: [Query Operators](../../docs/QUERY_OPERATORS.md) for all `where` operators.
*/
export interface FindParams<T = any> {
// Vector Intelligence
/** Natural language or semantic search query (embedded and matched via HNSW + text index) */
query?: string
/** Direct vector search (pre-computed embedding) */
vector?: Vector
// Metadata Intelligence
/** Filter by entity type(s). Alias for `where.noun`. */
type?: NounType | NounType[]
/**
* Filter by per-product subtype (top-level standard field — uses the fast path, not the
* metadata fallback). Pass a single string for equality, or an array for set membership
* (e.g. `subtype: ['employee', 'contractor']`). For operator-form predicates (e.g.
* `{ exists: true }`, `{ missing: true }`) use `where: { subtype: { …operators… } }`.
*/
subtype?: string | string[]
/** Metadata filters using BFO operators (e.g., `{ year: { greaterThan: 2020 } }`) */
where?: Partial<T>
// Visibility
/**
* Also return `visibility: 'internal'` entities. By default `find()` returns ONLY
* public entities (those with no `visibility` field, or `visibility: 'public'`);
* app-internal entities are hidden. Set `true` to include them. Applied as a hard
* candidate filter, so `limit` / `offset` stay correct. Does NOT include `'system'`
* entities — use `includeSystem` for those.
*/
includeInternal?: boolean
/**
* Also return `visibility: 'system'` entities (Brainy's own plumbing, e.g. the VFS
* root). Hidden by default and even when `includeInternal` is set. Set `true` only
* when you specifically need to see system entities. Applied as a hard candidate
* filter, so `limit` / `offset` stay correct.
*/
includeSystem?: boolean
// Graph Intelligence
connected?: GraphConstraints
// Proximity search
near?: {
id: string // Find near this entity
threshold?: number // Min similarity (0-1)
}
// Control options
limit?: number // Max results (default: 10)
offset?: number // Skip N results
cursor?: string // Cursor-based pagination
// Sorting
orderBy?: string // Field to sort by (e.g., 'createdAt', 'title', 'metadata.priority')
order?: 'asc' | 'desc' // Sort direction: 'asc' (default) or 'desc'
// Advanced options
includeRelations?: boolean // Include entity relationships
excludeVFS?: boolean // Exclude VFS entities from results (default: false - VFS included)
service?: string // Multi-tenancy filter
/**
* Return each result's stored embedding under `entity.vector`. Defaults to `false`,
* in which case `entity.vector` is an empty array (the perf default — vectors are
* large and most reads don't need them). Set `true` to get the persisted vector
* back from `find()` without a recompute, e.g. to feed downstream similarity math.
* Mirrors {@link GetOptions.includeVectors} on `get()`.
*/
includeVectors?: boolean
// Hybrid search options
/**
* Search strategy — the single canonical search-mode knob (the redundant `mode`
* alias was removed in 8.0). `'auto'` (default) blends text + semantic; the
* other members force a single intelligence. See {@link SearchMode}.
*/
searchMode?: SearchMode
hybridAlpha?: number // Weight between text (0.0) and semantic (1.0) search, default: auto-detected by query length
// Triple Intelligence Fusion
fusion?: {
strategy?: 'adaptive' | 'weighted' | 'progressive'
weights?: {
vector?: number
graph?: number
field?: number
}
}
// Performance options
writeOnly?: boolean // Skip validation for high-speed ingestion
// Aggregation
/** Query a named aggregate definition. String shorthand or full query params. */
aggregate?: string | AggregateQueryParams
}
/**
* Graph constraints for search
*/
export interface GraphConstraints {
to?: string // Connected to this entity
from?: string // Connected from this entity
via?: VerbType | VerbType[] // Traverse via these relationship types (the canonical key)
type?: VerbType | VerbType[] // Accepted alias for `via` (resolved identically: `via ?? type`)
/**
* Filter traversal edges by VerbType subtype. Single string for equality,
* array for set membership. Composes with `via` — `{ via: 'manages', subtype:
* 'direct' }` traverses only direct-management edges, not dotted-line. Edges
* without a subtype value are excluded when this filter is set.
*/
subtype?: string | string[]
depth?: number // Max traversal depth (default: 1)
direction?: 'in' | 'out' | 'both' // Direction of traversal (default: 'both')
}
/**
* Search modes
*/
export type SearchMode =
| 'auto' // Automatically choose best mode (default - enables hybrid text+semantic)
| 'vector' // Pure vector search (semantic only)
| 'semantic' // Alias for vector search
| 'text' // Pure text/keyword search
| 'metadata' // Pure metadata filtering
| 'graph' // Pure graph traversal
| 'hybrid' // Combine all intelligences (explicit hybrid mode)
/**
* Parameters for similarity search
*/
export interface SimilarParams<T = any> {
to: string | Entity<T> | Vector // Find similar to this
limit?: number // Max results (default: 10)
threshold?: number // Min similarity score
type?: NounType | NounType[] // Restrict to types
where?: Partial<T> // Additional filters
service?: string // Multi-tenancy
excludeVFS?: boolean // Exclude VFS entities (default: false - VFS included)
}
/**
* Parameters for `brain.related()` / `db.related()`
*
* All parameters are optional. When called without parameters, returns all relationships
* with pagination (default limit: 100).
*
* @example
* ```typescript
* // Get all relationships (default limit: 100)
* const all = await brain.related()
*
* // Get relationships from a specific entity (string shorthand)
* const fromEntity = await brain.related(entityId)
*
* // Equivalent to:
* const fromEntity2 = await brain.related({ from: entityId })
*
* // Get relationships to a specific entity
* const toEntity = await brain.related({ to: entityId })
*
* // Filter by relationship type
* const friends = await brain.related({ type: VerbType.FriendOf })
*
* // Pagination
* const page2 = await brain.related({ offset: 100, limit: 50 })
*
* // Combined filters
* const filtered = await brain.related({
* from: entityId,
* type: VerbType.WorksWith,
* limit: 20
* })
* ```
*
* Fixed bug where calling without parameters returned empty array
* Added string ID shorthand syntax
*/
export interface RelatedParams {
/**
* Filter by source entity ID
*
* Returns all relationships originating from this entity.
*/
from?: string
/**
* Filter by target entity ID
*
* Returns all relationships pointing to this entity.
*/
to?: string
/**
* Filter by INCIDENT entity ID — every relationship touching this entity in
* EITHER direction (where it is the source OR the target), deduped. The
* one-call "all edges on a noun" query: equivalent to merging
* `related({ from: id })` and `related({ to: id })` but in a single
* O(degree) call. Mutually exclusive with `from` / `to` (pass `node` for
* both directions, or `from` / `to` for one). Composes with `type` /
* `subtype` / visibility filters.
*
* @example
* // Everything connected to this person, in or out.
* const edges = await brain.related({ node: personId })
*/
node?: string
/**
* Filter by relationship type(s)
*
* Can be a single VerbType or array of VerbTypes.
*/
type?: VerbType | VerbType[]
/**
* Filter by VerbType subtype
*
* Top-level standard field — uses the fast path, not the metadata fallback.
* Pass a single string for equality (e.g. `subtype: 'direct'`), or an array
* for set membership (e.g. `subtype: ['direct', 'dotted-line']`).
*/
subtype?: string | string[]
/**
* Also return `visibility: 'internal'` relationships.
*
* By default `related()` returns ONLY public relationships (those with no
* `visibility` field, or `visibility: 'public'`); app-internal edges are hidden.
* Set `true` to include them. Applied as a hard candidate filter so `limit` /
* `offset` stay correct. Does NOT include `'system'` edges — use `includeSystem`.
*/
includeInternal?: boolean
/**
* Also return `visibility: 'system'` relationships (Brainy's own plumbing).
*
* Hidden by default and even when `includeInternal` is set. Set `true` only when
* you specifically need system edges. Applied as a hard candidate filter so
* `limit` / `offset` stay correct.
*/
includeSystem?: boolean
/**
* Maximum number of results to return
*
* @default 100
*/
limit?: number
/**
* Number of results to skip (offset-based pagination)
*
* @default 0
*/
offset?: number
/**
* Cursor for cursor-based pagination
*
* More efficient than offset for large result sets.
*/
cursor?: string
/**
* Filter by service (multi-tenancy)
*
* Only return relationships belonging to this service.
*/
service?: string
}
// ============= Graph views (brain.graph.*) =============
/**
* A node in a {@link GraphView} — a lightweight reference (id + classification +
* hop depth), NOT a full {@link Entity}. Graph reads are structure-first; resolve
* a node to its full entity with `get(id)` when you need its `data` / metadata.
*/
export interface GraphNode {
/** Entity id. */
id: string
/** Entity type (present when the view hydrates node classification). */
type?: NounType
/** Per-product subtype (present when hydrated). */
subtype?: string
/** Hop distance from the nearest seed (0 = a seed); present on subgraph results. */
depth?: number
}
/**
* A hydrated view of a region of the graph — the discovered nodes plus the edges
* among them, in id form (the public face of the provider's columnar int form).
*/
export interface GraphView<T = any> {
/** The nodes in this region (seeds + everything reached). */
nodes: GraphNode[]
/** The edges among `nodes`, as full {@link Relation} objects. */
edges: Relation<T>[]
/** `true` when a `maxNodes` / `maxEdges` cap truncated the result. */
truncated?: boolean
}
/**
* Options for {@link GraphApi.subgraph}.
*/
export interface SubgraphOptions {
/** Max hop distance from any seed (default `1`). */
depth?: number
/** Edge direction to follow (default `'both'`). */
direction?: 'in' | 'out' | 'both'
/** Restrict traversed edges to these relationship type(s). */
type?: VerbType | VerbType[]
/** Restrict traversed edges to these subtype(s). */
subtype?: string | string[]
/** Also traverse through `internal`-visibility edges/nodes (hidden by default). */
includeInternal?: boolean
/** Also traverse through `system`-visibility edges/nodes (hidden by default). */
includeSystem?: boolean
/** Cap on returned nodes (sets `GraphView.truncated`). */
maxNodes?: number
/** Cap on returned edges. */
maxEdges?: number
/**
* Hydrate each node's `type` / `subtype` (one batch read). Default `true`;
* set `false` to skip it when you only need graph structure (ids + edges).
*/
hydrateNodes?: boolean
}
/**
* Options for {@link GraphApi.export}.
*/
export interface GraphExportOptions {
/** Nodes/edges per streamed chunk (default `1000`). */
chunkSize?: number
/** Also include `internal`-visibility nodes/edges (hidden by default). */
includeInternal?: boolean
/** Also include `system`-visibility nodes/edges (hidden by default). */
includeSystem?: boolean
/** Stream the nodes (default `true`; set `false` to stream only edges). */
includeNodes?: boolean
/** Stream the edges (default `true`; set `false` to stream only nodes). */
includeEdges?: boolean
}
/**
* Options for {@link GraphApi.communities}.
*/
export interface GraphCommunitiesOptions {
/** Treat edges as directed when grouping (default `false` — direction-agnostic). */
directed?: boolean
/** Also group through `internal`-visibility nodes/edges (hidden by default). */
includeInternal?: boolean
/** Also group through `system`-visibility nodes/edges (hidden by default). */
includeSystem?: boolean
}
/**
* Result of {@link GraphApi.communities} — the graph partitioned into connected
* groups of entity ids.
*/
export interface GraphCommunitiesResult {
/** Each group is the member entity ids; largest group first. */
groups: string[][]
/** Number of distinct groups (`= groups.length`). */
count: number
}
/**
* Options for {@link GraphApi.rank} (importance ranking). INTENT-level only — the
* ranking ALGORITHM is the provider's choice (the TS fallback uses PageRank), so
* no algorithm-tuning knobs are exposed.
*/
export interface GraphRankOptions {
/** Return only the top-K nodes by score (default: all, descending). */
topK?: number
/** Also rank through `internal`-visibility nodes/edges (hidden by default). */
includeInternal?: boolean
/** Also rank through `system`-visibility nodes/edges (hidden by default). */
includeSystem?: boolean
}
/** One ranked entity from {@link GraphApi.rank}, descending by `score`. */
export interface GraphRankEntry {
/** The entity id. */
id: string
/** The importance score (relative; higher = more central). */
score: number
}
/**
* Options for {@link GraphApi.path} (best route between two entities).
*/
export interface GraphPathOptions {
/** Edge direction to follow (default `'both'`). */
direction?: 'in' | 'out' | 'both'
/** Optimize for fewest `'hops'` (default) or least summed edge `'weight'`. */
by?: 'hops' | 'weight'
/** Restrict the route to these relationship type(s). */
type?: VerbType | VerbType[]
/** Also route through `internal`-visibility nodes/edges (hidden by default). */
includeInternal?: boolean
/** Also route through `system`-visibility nodes/edges (hidden by default). */
includeSystem?: boolean
/** Abandon the search past this many hops. */
maxDepth?: number
}
/**
* Result of {@link GraphApi.path} — the route from `from` to `to`, or `null` when
* unreachable.
*/
export interface GraphPathResult {
/** The entity ids along the route, `from` … `to` inclusive. */
nodes: string[]
/** The relationship (verb) ids traversed, one fewer than `nodes`. */
relationships: string[]
/** Route cost: number of hops, or summed edge weight when `by: 'weight'`. */
cost: number
}
/**
* What {@link GraphApi.subgraph} expands from — "the things to grow a neighborhood
* around". One of:
* - an entity id, or an array of them (the explicit id set),
* - a {@link Result} array (a `find()` result — its entities become the seeds), or
* - a {@link FindParams} query (the query→expand fusion: run the query, expand from
* every match). With the native engine a metadata-only query's matched universe is
* forwarded to the traversal as an opaque set with no id materialization.
*/
export type SubgraphSelector<T = any> = string | string[] | Result<T>[] | FindParams<T>
/**
* The `brain.graph` namespace — graph-shaped reads over the knowledge graph.
* Routes to a native {@link import('../plugin.js').GraphAccelerationProvider}
* when one is registered, otherwise serves the same results from Brainy's
* pure-TS adjacency (correct at small/medium scale).
*/
export interface GraphApi<T = any> {
/**
* @description Extract the subgraph reachable from a seed selector — the bounded
* multi-hop neighborhood, as nodes + edges. The one-call answer to "show me
* everything around this, N hops out". The seed can be entity id(s), a `find()`
* result, or a {@link FindParams} query (query→expand: run the query, then expand
* from every match — with the native engine the matched universe never leaves
* the engine's representation).
* @param selector - Seed id(s), a `find()` result, or a query. See {@link SubgraphSelector}.
* @param options - Depth, direction, edge/visibility filters, and caps.
* @returns The hydrated {@link GraphView}.
* @example
* const view = await brain.graph.subgraph(personId, { depth: 2 })
* // view.nodes = the 2-hop neighborhood; view.edges = the relations among them
* @example
* // Query→expand: the neighborhood of everything matching a filter.
* const cluster = await brain.graph.subgraph({ where: { team: 'platform' } }, { depth: 1 })
*/
subgraph(selector: SubgraphSelector<T>, options?: SubgraphOptions): Promise<GraphView<T>>
/**
* @description Stream the WHOLE graph in one O(N+E) pass as a sequence of
* {@link GraphView} chunks — the right primitive for visualizing all data
* (vs. paging per node). Node chunks come first, then edge chunks; assemble
* them on the consumer side. Backed by cursor pagination, so a full walk is
* O(N+E) at any chunk size (no re-scan).
* @param options - Chunk size, visibility, and node/edge inclusion toggles.
* @returns An async-iterable of graph chunks.
* @example
* for await (const chunk of brain.graph.export()) {
* addNodes(chunk.nodes); addEdges(chunk.edges)
* }
*/
export(options?: GraphExportOptions): AsyncIterable<GraphView<T>>
/**
* @description Rank entities by graph importance (influence / centrality) — the
* one-call answer to "which nodes matter most". The TS fallback uses PageRank;
* a native provider may use personalized PageRank / eigenvector centrality.
* @param options - `topK`, visibility opt-ins.
* @returns Entities with scores, DESCENDING (most important first).
* @example
* const top = await brain.graph.rank({ topK: 10 })
* top[0] // { id, score } — the most central entity
*/
rank(options?: GraphRankOptions): Promise<GraphRankEntry[]>
/**
* @description Partition the graph into connected communities (clusters of
* related entities) — "which things group together".
* @param options - `directed`, visibility opt-ins.
* @returns The groups (member ids) + their count.
* @example
* const { groups, count } = await brain.graph.communities()
*/
communities(options?: GraphCommunitiesOptions): Promise<GraphCommunitiesResult>
/**
* @description Find the best route between two entities — fewest hops (default)
* or least summed edge weight (`by: 'weight'`). The pathfinding ALGORITHM is
* the provider's choice (TS fallback: BFS for hops, Dijkstra for weight).
* @param from - Start entity id (natural keys are resolved).
* @param to - End entity id.
* @param options - Direction, `by`, type filter, `maxDepth`, visibility opt-ins.
* @returns The route (`nodes` + `relationships` + `cost`), or `null` if unreachable.
* @example
* const route = await brain.graph.path(aliceId, bobId)
* route?.nodes // [aliceId, …, bobId]
*/
path(from: string, to: string, options?: GraphPathOptions): Promise<GraphPathResult | null>
}
// ============= Batch Operations =============
/**
* Batch add parameters
*/
export interface AddManyParams<T = any> {
items: AddParams<T>[] // Items to add
parallel?: boolean // Process in parallel (default: true)
chunkSize?: number // Batch size (default: 100)
onProgress?: (done: number, total: number) => void
continueOnError?: boolean // Continue if some fail
/**
* Conditional insert applied to every item. Equivalent to setting `ifAbsent: true`
* on each item individually. Item-level `ifAbsent` overrides the batch flag.
*/
ifAbsent?: boolean
/**
* Create-or-update applied to every item. Equivalent to setting `upsert: true` on
* each item individually (see {@link AddParams.upsert}): an item whose custom `id`
* already exists is MERGED as an update rather than overwritten. Item-level `upsert`
* overrides the batch flag. Mutually exclusive with {@link AddManyParams.ifAbsent}
* at the item level — an item resolving to both throws.
*/
upsert?: boolean
}
/**
* Batch update parameters
*/
export interface UpdateManyParams<T = any> {
items: UpdateParams<T>[] // Items to update
parallel?: boolean
chunkSize?: number
onProgress?: (done: number, total: number) => void
continueOnError?: boolean
}
/**
* Batch remove parameters
*/
export interface RemoveManyParams {
ids?: string[] // Specific IDs to remove
type?: NounType // Remove all of type
where?: any // Remove by metadata
limit?: number // Max to remove (safety)
/**
* Entities removed per transaction chunk. Each chunk is one atomic transaction —
* a chunk commits or rolls back together, and failures are isolated to their chunk.
* Defaults to the storage adapter's `maxBatchSize` (memory: 1000, S3/R2: 100,
* GCS: 50), so zero-config removals match the adapter's characteristics. Override
* only to tune throughput vs. transaction granularity.
*/
chunkSize?: number
onProgress?: (done: number, total: number) => void
continueOnError?: boolean // Continue processing if a removal fails
}
/**
* Batch relate parameters
*/
export interface RelateManyParams<T = any> {
items: RelateParams<T>[] // Relations to create
parallel?: boolean
chunkSize?: number
onProgress?: (done: number, total: number) => void
continueOnError?: boolean
}
/**
* Batch result
*/
export interface BatchResult<T = any> {
successful: T[] // Successfully processed items
failed: Array<{ // Failed items with errors
item: any
error: string
}>
total: number // Total attempted
duration: number // Time taken in ms
}
// ============= Import Progress =============
/**
* Import stage enumeration
*/
export type ImportStage =
| 'detecting' // Detecting file format
| 'reading' // Reading file from disk/network
| 'parsing' // Parsing file structure (CSV rows, PDF pages, Excel sheets)
| 'extracting' // Extracting entities using AI
| 'indexing' // Creating graph nodes and relationships
| 'completing' // Final cleanup and stats
/**
* Overall import status
*/
export type ImportStatus =
| 'starting' // Initializing import
| 'processing' // Actively importing
| 'completing' // Finalizing
| 'done' // Complete
/**
* Comprehensive import progress information
*
* Provides multi-dimensional progress tracking:
* - Bytes processed (always deterministic)
* - Entities extracted and indexed
* - Stage-specific progress
* - Time estimates
* - Performance metrics
*
*/
export interface ImportProgress {
// Overall Progress
overall_progress: number // 0-100 weighted estimate across all stages
overall_status: ImportStatus // High-level status
// Current Stage
stage: ImportStage // What's happening now
stage_progress: number // 0-100 within current stage (0 if unknown)
stage_message: string // Human-readable: "Extracting entities from PDF..."
// Bytes (Always Available - most deterministic metric)
bytes_processed: number // Bytes read/processed so far
total_bytes: number // Total file size (0 if streaming/unknown)
bytes_percentage: number // Convenience: bytes_processed / total_bytes * 100
bytes_per_second?: number // Processing rate (relevant during parsing)
// Entities (Available when extraction starts)
entities_extracted: number // Entities found during AI extraction
entities_indexed: number // Entities added to Brainy graph
entities_per_second?: number // Entities/sec (relevant during extraction/indexing)
estimated_total_entities?: number // Estimated final count
estimation_confidence?: number // 0-1 confidence in estimation
// Timing
elapsed_ms: number // Time since import started
estimated_remaining_ms?: number // Estimated time remaining
estimated_total_ms?: number // Estimated total time
// Context (helps users understand what's happening)
current_item?: string // "Processing page 5 of 23"
current_file?: string // "Sheet: Q2 Sales Data"
file_number?: number // 3 (when importing multiple files)
total_files?: number // 10
// Performance Metrics (for debugging/optimization)
metrics?: {
parsing_rate_mbps?: number // MB/s during parsing
extraction_rate_entities_per_sec?: number // Entities/s during extraction
indexing_rate_entities_per_sec?: number // Entities/s during indexing
memory_usage_mb?: number // Current memory usage
peak_memory_mb?: number // Peak memory usage
}
// Backwards Compatibility (for legacy code)
current: number // Alias for entities_indexed
total: number // Alias for estimated_total_entities or 0
}
/**
* Import progress callback - backwards compatible
*
* Supports both legacy (current, total) and new (ImportProgress object) signatures
*/
export type ImportProgressCallback =
| ((progress: ImportProgress) => void)
| ((current: number, total: number) => void)
/**
* Stage weight configuration for overall progress calculation
*
* These weights reflect the typical time distribution across stages.
* Extraction is typically the slowest stage (60% of time).
*/
export interface StageWeights {
detecting: number // Default: 0.01 (1%)
reading: number // Default: 0.05 (5%)
parsing: number // Default: 0.10 (10%)
extracting: number // Default: 0.60 (60% - slowest!)
indexing: number // Default: 0.20 (20%)
completing: number // Default: 0.04 (4%)
}
/**
* Import result statistics
*/
export interface ImportStats {
graphNodesCreated: number // Entities added to graph
graphEdgesCreated: number // Relationships created
vfsFilesCreated: number // VFS files created
duration: number // Total time in ms
bytesProcessed: number // Total bytes read
averageRate: number // Average entities/sec
peakMemoryMB?: number // Peak memory usage
}
/**
* Import operation result
*/
export interface ImportResult {
success: boolean
stats: ImportStats
errors?: Array<{
stage: ImportStage
message: string
error?: any
}>
}
// ============= Advanced Operations =============
/**
* Options for brain.get() entity retrieval
*
* **Performance Optimization**:
* By default, brain.get() loads ONLY metadata (not vectors), resulting in:
* - **76-81% faster** reads (10ms vs 43ms for metadata-only)
* - **95% less bandwidth** (300 bytes vs 6KB per entity)
* - **87% less memory** (optimal for VFS and large-scale operations)
*
* **When to use includeVectors**:
* - Computing similarity on a specific entity (not search): `brain.similar({ to: entity.vector })`
* - Manual vector operations: `cosineSimilarity(entity.vector, otherVector)`
* - Inspecting embeddings for debugging
*
* **When NOT to use includeVectors** (metadata-only is sufficient):
* - VFS operations (readFile, stat, readdir) - 100% of cases
* - Existence checks: `if (await brain.get(id))`
* - Metadata inspection: `entity.metadata`, `entity.data`, `entity.type`
* - Relationship traversal: `brain.related({ from: id })`
* - Search operations: `brain.find()` generates embeddings automatically
*
* @example
* ```typescript
* // ✅ FAST (default): Metadata-only - 10ms, 300 bytes
* const entity = await brain.get(id)
* console.log(entity.data, entity.metadata) // ✅ Available
* console.log(entity.vector) // Empty Float32Array (stub)
*
* // ✅ FULL: Load vectors when needed - 43ms, 6KB
* const fullEntity = await brain.get(id, { includeVectors: true })
* const similarity = cosineSimilarity(fullEntity.vector, otherVector)
*
* // ✅ VFS automatically uses fast path (no change needed)
* await vfs.readFile('/file.txt') // 53ms → 10ms (81% faster)
* ```
*
*/
export interface GetOptions {
/**
* Include 384-dimensional vector embeddings in the response
*
* **Default: false** (metadata-only for 76-81% speedup)
*
* Set to `true` when you need to:
* - Compute similarity on this specific entity's vector
* - Perform manual vector operations
* - Inspect embeddings for debugging
*
* **Note**: Search operations (`brain.find()`) generate vectors automatically,
* so you don't need this flag for search. Only for direct vector operations
* on a retrieved entity.
*
* @default false
*/
includeVectors?: boolean
}
/**
* Graph traversal parameters
*/
export interface TraverseParams {
from: string | string[] // Starting node(s)
direction?: 'out' | 'in' | 'both' // Traversal direction
types?: VerbType[] // Edge types to follow
depth?: number // Max depth (default: 2)
strategy?: 'bfs' | 'dfs' // Breadth or depth first
filter?: (entity: Entity, depth: number, path: string[]) => boolean
limit?: number // Max nodes to visit
}
// ============= Aggregation Engine Types =============
/**
* Supported aggregation operations
*/
export type AggregationOp =
| 'sum'
| 'count'
| 'avg'
| 'min'
| 'max'
| 'stddev'
| 'variance'
/** Exact percentile (requires `p` in `[0,1]` on the metric def) — value-multiset, delete-safe. */
| 'percentile'
/** Exact count of distinct values — value-multiset, delete-safe. */
| 'distinctCount'
/**
* Time window granularity for GROUP BY time dimensions
*/
export type TimeWindowGranularity =
| 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'
| { seconds: number }
/**
* A GROUP BY dimension — one of:
* - a plain metadata field name (string),
* - a time-windowed field (`{ field, window }`), or
* - an **unnest** field (`{ field, unnest: true }`): the field holds an array, and the entity
* contributes once to a group per distinct element (e.g. `tags: string[]` → tag frequency).
* An entity whose unnest field is missing or an empty array contributes to no group.
*/
export type GroupByDimension =
| string
| { field: string; window: TimeWindowGranularity }
| { field: string; unnest: true }
/**
* Source filter for which entities feed into an aggregate
*/
export interface AggregateSource {
/** Filter by entity type(s) */
type?: NounType | NounType[]
/** Metadata filter (same syntax as find({ where })) */
where?: Record<string, unknown>
/** Multi-tenancy service filter */
service?: string
}
/**
* Full aggregate definition — registered via brain.defineAggregate()
*/
export interface AggregateDefinition {
/** Unique name for this aggregate (used in queries) */
name: string
/** Which entities contribute to this aggregate */
source: AggregateSource
/** Dimensions to group by */
groupBy: GroupByDimension[]
/** Named metrics to compute */
metrics: Record<string, AggregateMetricDef>
/** Control materialization of results as NounType.Measurement entities */
materialize?: boolean | { debounceMs?: number; trackSources?: boolean }
}
/**
* Single metric definition within an aggregate
*/
export interface AggregateMetricDef {
/** Aggregation operation */
op: AggregationOp
/** Metadata field to aggregate (required for all ops except count) */
field?: string
/** Percentile fraction in `[0, 1]` — required when `op === 'percentile'`, ignored otherwise. */
p?: number
}
/**
* Internal running state for a single metric.
* Tracks enough to compute all operations incrementally.
*/
export interface MetricState {
sum: number
count: number
min: number
max: number
/** Running M2 for Welford's online variance (sum of squared differences from mean) */
m2?: number
/**
* Value multiset (String(value) → occurrence count) for exact percentile + distinctCount.
* Maintained only for `percentile`/`distinctCount` metrics. JSON-serializable; mirrors
* Cor's `value_counts` so JS and native agree bit-for-bit.
*/
valueCounts?: Record<string, number>
}
/**
* Internal state for one aggregate group (one combination of group key values)
*/
export interface AggregateGroupState {
/** The group key values (e.g., { category: 'food', period: '2024-01' }) */
groupKey: Record<string, string | number>
/** Running metric states keyed by metric name */
metrics: Record<string, MetricState>
/** Entity ID of the materialized Measurement entity (if materialized) */
materializedEntityId?: string
/** Timestamp of last update */
lastUpdated: number
}
/**
* Query parameters for reading aggregate results
*/
export interface AggregateQueryParams {
/** Name of the aggregate to query */
name: string
/** Filter aggregate groups by their key values */
where?: Record<string, unknown>
/**
* Filter groups by their computed METRIC values (SQL HAVING). Same BFO operators as
* `where`, but applied to the derived metric results plus `count`, e.g.
* `{ revenue: { greaterThan: 1000 } }`. Evaluated per group (O(groups), independent of
* entity count), before sort/pagination.
*/
having?: Record<string, unknown>
/** Sort by metric name or group key field */
orderBy?: string
/** Sort direction */
order?: 'asc' | 'desc'
/** Max results */
limit?: number
/** Skip N results */
offset?: number
}
/**
* A single aggregate result row
*/
export interface AggregateResult {
/** Group key values for this row */
groupKey: Record<string, string | number>
/** Computed metric values (derived from MetricState based on op) */
metrics: Record<string, number>
/** Total entity count in this group */
count: number
/** Entity ID of materialized Measurement (if materialized) */
entityId?: string
}
/**
* Provider interface for Cor-accelerated aggregation.
* When registered as 'aggregation' provider, Brainy delegates to this.
*/
export interface AggregationProvider {
/** Register an aggregate definition (caches compiled definition for hot path) */
defineAggregate?(def: AggregateDefinition): void
/** Remove a registered aggregate definition */
removeAggregate?(name: string): void
/** Incrementally update aggregation state when an entity changes */
incrementalUpdate(
name: string,
def: AggregateDefinition,
entity: Record<string, unknown>,
op: 'add' | 'update' | 'delete',
prev?: Record<string, unknown>
): AggregateGroupState[]
/** Compute the group key for a given entity */
computeGroupKey(
entity: Record<string, unknown>,
groupBy: GroupByDimension[]
): Record<string, string | number>
/** Rebuild an entire aggregate from scratch */
rebuildAggregate(
def: AggregateDefinition,
entities: Array<Record<string, unknown>>
): Map<string, AggregateGroupState>
/** Query aggregate state with filtering/sorting/pagination */
queryAggregate(
state: Map<string, AggregateGroupState>,
params: AggregateQueryParams
): AggregateResult[]
/** Restore previously serialized state (called during init) */
restoreState?(data: string): void
/** Serialize internal state for persistence (called during flush) */
serializeState?(): string
}
// ============= Configuration =============
/**
* Integration Hub configuration
*
* Enables external tool integrations: Excel, Power BI, Google Sheets, etc.
* Works in all environments with zero external dependencies.
*
* @example
* ```typescript
* // Enable all integrations with defaults
* new Brainy({ integrations: true })
*
* // Custom configuration
* new Brainy({
* integrations: {
* basePath: '/api/v1',
* enable: ['odata', 'sheets']
* }
* })
* ```
*/
export interface IntegrationsConfig {
/** Base path for all integration endpoints (default: '') */
basePath?: string
/** Which integrations to enable (default: all) */
enable?: ('odata' | 'sheets' | 'sse' | 'webhooks')[] | 'all'
/** Per-integration config overrides */
config?: {
odata?: { basePath?: string }
sheets?: { basePath?: string }
sse?: { basePath?: string; heartbeatInterval?: number }
webhooks?: { maxRetries?: number }
}
}
/**
* Operator-facing summary returned by `brain.stats()`. Designed to fit in a
* terminal screen so an incident responder can read it at a glance: counts,
* mode, lock owner, indexed fields, and index health flags.
*/
export interface BrainyStats {
/** Whether this instance can mutate. `reader` is set by `openReadOnly()` and `asOf()`. */
mode: 'writer' | 'reader'
/** Total entity (noun) count across all types. */
entityCount: number
/** Breakdown by NounType name (`person`, `event`, ...). Zero-count types omitted. */
entitiesByType: Record<string, number>
/** Total relationship (verb) count across all types. */
relationCount: number
/** Breakdown by VerbType name. Zero-count types omitted. */
relationsByType: Record<string, number>
/** Indexed metadata field names known to the metadata index. */
fieldRegistry: string[]
/** Per-index health flags. `true` = index has entries OR no entities exist yet. */
indexHealth: {
/** Vector index health (8.0 — open-core JS HNSW path or a native acceleration provider). */
vector: boolean
metadata: boolean
graph: boolean
}
/** Storage backend info — `backend` is the adapter class name. */
storage: {
backend: string
rootDir?: string
}
/**
* Writer lock metadata if one is currently held on this directory. Present
* for both writer instances (their own lock) and readers inspecting a
* directory with a live writer.
*/
writerLock?: {
pid: number
hostname: string
startedAt: string
lastHeartbeat: string
version: string
rootDir?: string
}
/** The Brainy library version this instance was built against. */
version: string
}
/**
* Brainy configuration
*/
/**
* Structured progress of the one-time, automatic 7.x → 8.0 migration (the
* coordinated LOCK). Relayed verbatim from the native provider's optional
* `migrationStatus()` and surfaced on `getIndexStatus().migration` while the
* brain is upgrading. Every field is optional — a provider may report only a
* phase, or nothing (in which case `getIndexStatus()` shows `migrating: true`
* plus `elapsedMs` alone).
*/
export interface MigrationProgress {
/** Coarse stage, e.g. `'rebuilding'` | `'verifying'`. */
phase?: string
/** Which derived index is currently rebuilding. */
index?: 'metadata' | 'vector' | 'graph'
/** Overall progress, 0100. */
percent?: number
/** Canonical entities processed so far. */
entitiesDone?: number
/** Total canonical entities to process. */
entitiesTotal?: number
/** When the migration was first observed (epoch ms). */
startedAt?: number
/** Milliseconds elapsed since the migration was first observed. */
elapsedMs?: number
}
export interface BrainyConfig {
/**
* Storage configuration: either a config object resolved through the
* storage factory, or a pre-constructed adapter instance (used directly —
* e.g. `storage: new MemoryStorage()`; Brainy's historical-query
* materializer hands a pre-populated instance through this path).
*/
storage?:
| {
/**
* Storage backend. Optional — a top-level `path` implies `'filesystem'`,
* so `storage: { path: '/data' }` works without it. `'auto'` (the
* default) picks filesystem on Node, memory in a browser.
*/
type?: 'auto' | 'memory' | 'filesystem'
/**
* **Canonical** directory for filesystem storage. The rest of the API
* already speaks `path` (`persist(path)`, `Brainy.load(path)`,
* `asOf(path)`, `restore(path)`). Specifying it implies
* `type: 'filesystem'`. Passed through to plugin-provided storage
* factories (e.g. native mmap providers) so they resolve the same root.
* @example
* new Brainy({ storage: { path: '/var/lib/app/data' } })
*/
path?: string
/**
* @deprecated REMOVED in 8.0 — passing `rootDirectory` THROWS. Use the
* canonical {@link path}.
*/
rootDirectory?: string
/**
* @deprecated REMOVED in 8.0 — a nested `options.path` / `options.rootDirectory`
* THROWS. Use the top-level {@link path}.
*/
options?: any
/**
* @deprecated REMOVED in 8.0 — `fileSystemStorage.path` / `.rootDirectory`
* THROWS. Use the top-level {@link path}.
*/
fileSystemStorage?: { path?: string; rootDirectory?: string; [key: string]: any }
}
| StorageAdapter
/**
* Disable the automatic index rebuild check during `init()`. By default
* Brainy auto-decides from dataset size: small datasets rebuild missing
* indexes inline, large datasets rebuild lazily on first query. Set `true`
* only when an operator wants full manual control via `repairIndex()`.
*/
disableAutoRebuild?: boolean
/**
* How long (ms) an operation **waits** on the coordinated 7.x → 8.0 migration
* before throwing a retryable `MigrationInProgressError`.
*
* IMPORTANT: this bounds the *caller's wait*, NOT the migration. The migration
* itself is **unbounded** — a native provider rebuilds a billion-scale brain
* for as long as it needs, and this timeout never interrupts it. While the
* rebuild runs, reads and writes (and `init()` before it serves) block so no
* operation touches a half-built index:
* - **Small/medium upgrade (< this window):** the caller waits, then resumes
* transparently — no error.
* - **Large upgrade (> this window):** the caller gets a retryable
* `MigrationInProgressError` (the rebuild continues in the background);
* `getIndexStatus().migrating` — never gated — is the readiness-probe signal
* that maps to HTTP 503 + Retry-After so an orchestrator waits rather than
* routing traffic in.
*
* Raise it for a latency-tolerant batch job (wait longer / effectively wait
* through); lower it to fail fast on a request path. Default: `30000` (30 s).
*/
migrationWaitTimeoutMs?: number
/**
* Take an automatic **pre-upgrade backup** before a one-time 7.x → 8.0
* migration rebuilds the derived indexes, and remove it once the upgrade
* verifies (retain it on failure, for rollback). On the filesystem adapter
* this is a **hard-link snapshot** of the brain directory — near-zero cost and
* space (shared inodes; the store is immutable-by-rename), even at scale.
* The migration is already structurally safe (it only reads canonical records,
* derived indexes are fully reconstructable, and a failed upgrade self-heals on
* re-open), so this is catastrophe-insurance against a migration *bug*, not a
* data-loss guard — and NOT a substitute for an off-device backup. No-op for
* non-filesystem storage and for a brain with no persisted data.
* Default: `true`. Set `false` to opt out (e.g. you run your own backup).
*/
migrationBackup?: boolean
/**
* Vector index configuration (Brainy 8.0).
*
* Two knobs. No escape hatch. The algorithm-internal HNSW knobs
* (`M`, `efConstruction`, `efSearch`, `ml`, …) and DiskANN knobs
* (`pqM`, `searchListSize`, `alpha`, …) are deliberately not exposed —
* the `recall` preset covers the legitimate quality/latency tradeoff
* range, and the open-core defaults match Brainy 7.x's defaults
* byte-for-byte so a `'balanced'`-default upgrade is a no-op.
*
* **Closed-form contract** locked in handoff thread
* BRAINY-8.0-RENAME-COORDINATION § A.2 + § G.2 (cor-confirmed 2026-06-09).
*/
vector?: {
/**
* Recall preset.
* - `'fast'` — minimum-latency search, accepts lower recall
* - `'balanced'` (default) — Brainy 7.x defaults, the right pick for almost everyone
* - `'accurate'` — maximum recall, accepts higher latency
*
* Means the same thing whether the open-core JS HNSW path is in play
* or a native vector provider has taken over the `'vector'` provider
* key — Brainy translates to HNSW knobs; native providers translate
* to their own (e.g. DiskANN's `defaultLSearch` / `defaultPaddingFactor`).
*/
recall?: 'fast' | 'balanced' | 'accurate'
/**
* Vector persistence mode. `'immediate'` writes per-noun graph state on
* every `add()`; durable but slower. `'deferred'` writes only on
* `flush()` / `close()`; faster bulk ingest.
*
* **Auto-selected from the storage adapter when omitted:** `'immediate'`
* on filesystem storage (durability is the point of a persistent
* backend), `'deferred'` on memory storage (nothing survives the process
* anyway, so per-add persistence writes are pure overhead).
*
* Brainy 7.x called this `hnswPersistMode` at the top level. The 8.0
* surface folds it under `config.vector` alongside `recall`.
*/
persistMode?: 'immediate' | 'deferred'
}
/**
* Generational-history retention policy (8.0 MVCC — the `retention` knob).
*
* Under Model-B EVERY write (`transact()` AND single-op `add`/`update`/
* `remove`/`relate`) produces an immutable generation record-set serving
* historical reads (`asOf()`, pinned `Db` values). Without compaction those
* accumulate, so Brainy **auto-compacts on every `flush()` and `close()`**.
* Live `Db` pins are ALWAYS exempt from reclamation, in every mode.
*
* Modes:
* - **unset → ADAPTIVE (default):** the retention horizon tracks
* disk/RAM pressure. A machine-level byte budget governs how much history
* is kept — driven by `budgetBytes` when a coordinator (e.g. cor's
* `ResourceManager`, fair-shared across co-located instances) sets it, else
* a local `os.freemem`/filesystem-free probe. Oldest-unpinned history is
* reclaimed when the budget is exceeded. Zero-config.
* - **`'all'` → unbounded:** never reclaim history (index compaction for
* query speed still runs — the decouple). The legacy keep-everything
* behavior, now explicit opt-in.
* - **`{ maxGenerations?, maxAge?, maxBytes? }` → explicit CAPS:** reclaim
* the oldest unpinned generations while ANY supplied cap is exceeded
* (predictable ops). `maxAge` in ms; `maxBytes` total history bytes.
*
* `autoCompact: false` disables the automatic flush/close compaction (manage
* manually via `brain.compactHistory()`). `budgetBytes` is the settable
* adaptive byte budget a coordinator drives (also via `brain.setRetentionBudget()`).
* Long-term archives belong in `db.persist(path)` snapshots, which compaction
* never touches.
*/
retention?:
| 'all'
| 'adaptive'
| {
/** Keep at most this many recent committed generations (cap). */
maxGenerations?: number
/** Keep generations committed within this window in ms (cap). */
maxAge?: number
/** Keep total generational-history bytes at or below this (cap). */
maxBytes?: number
/** Adaptive byte budget for this brain, driven by a coordinator (e.g. cor). */
budgetBytes?: number
/** Run compaction automatically on flush()/close() (default: true). */
autoCompact?: boolean
}
// Memory management options
maxQueryLimit?: number // Override auto-detected query result limit (max: 100000)
reservedQueryMemory?: number // Memory reserved for queries in bytes (e.g., 1073741824 = 1GB)
/**
* Controls when the WASM embedding engine is initialized.
*
* **Adaptive default (8.0):** when omitted, the engine eagerly initializes
* during `init()` whenever the WASM embedder is the *active* one — i.e. no
* native `'embeddings'` provider is registered — and this instance is a
* writer (not `mode: 'reader'`) running outside unit tests. The WASM module
* (≈93MB with the embedded model) takes 90-140s to compile on throttled
* CPUs, so paying that during boot rather than on the first `embed()`-driven
* call is the right default for a single-process server.
*
* The adaptive path skips itself automatically when a native embeddings
* provider owns embeddings, in reader-mode (readers query existing vectors
* and never embed), and in unit-test mode (kept fast via the mock embedder).
*
* - `true` — force eager init during `init()` (the adaptive default already
* does this for the active-embedder writer case; set it explicitly to be
* unambiguous).
* - `false` — explicit override to force lazy init (first `embed()` call)
* even when this instance is the active embedder.
*/
eagerEmbeddings?: boolean
// Plugin configuration
// Controls which plugins are loaded during init().
// - undefined (default): guarded auto-detection of the first-party
// accelerator (@soulcraft/cor) — installing the package IS the opt-in.
// Not installed → plain brainy, silently. Installed → it loads and
// announces itself. Installed but broken → init() THROWS (an installed
// accelerator never silently vanishes behind the JS engines).
// - false / []: no plugins, no detection (explicit opt-out)
// - ['@soulcraft/cor']: load exactly these packages; a listed plugin that
// fails to load or is invalid THROWS (loud, never a silent JS fallback)
plugins?: string[] | false
// Logging configuration
verbose?: boolean // Enable verbose logging
silent?: boolean // Suppress all logging output
// Integration Hub
// Enable external tool integrations: Excel, Power BI, Google Sheets, etc.
// - true: Enable all integrations with default paths
// - false/undefined: Disable integrations (default)
// - IntegrationsConfig: Custom configuration
integrations?: boolean | IntegrationsConfig
// Migration configuration
// - true/undefined (default): Automatically run pending data migrations during
// init() for small datasets (<10K entities). Larger datasets defer with a
// notice — call brain.migrate() explicitly (optionally with backupTo).
// - false: Never auto-run; log a notice when pending migrations exist.
autoMigrate?: boolean
/**
* Brain-wide subtype enforcement mode.
*
* **Default-on since 8.0.0** (`undefined` resolves to `true`; in 7.30.x this
* was an opt-in flag defaulting to `false`).
*
* - `true` / `undefined` (default): every `add()` / `addMany()` / `update()` /
* `relate()` / `relateMany()` / `updateRelation()` rejects writes where the
* entity's NounType (or relationship's VerbType) has no non-empty `subtype`
* value. Brainy's own VFS infrastructure writes (`metadata.isVFSEntity` /
* `metadata.isVFS`) bypass the check.
* - `{ except: [NounType.Thing, NounType.Custom] }`: same as `true`, but the
* listed types are allowed through without a subtype. Use for genuine
* catch-all types where no subtype makes sense.
* - `false`: disable the brain-wide check entirely. Last-resort escape hatch
* for opening pre-8.0 data — run `brain.audit()` to find the gaps, back-fill
* with `brain.fillSubtypes(rules)`, then remove the opt-out so the default
* enforcement protects new writes.
*
* Per-type registrations always compose with the brain-wide flag — a type
* registered with `requireSubtype(type, { required: true })` is always
* enforced regardless of this flag.
*/
requireSubtype?: boolean | { except: Array<import('../types/graphTypes.js').NounType | import('../types/graphTypes.js').VerbType> }
/**
* Process role for multi-process safety on filesystem storage.
*
* - `'writer'` (default): acquires an exclusive lock on the storage directory at
* `init()` and refuses to open if another live writer holds the lock. Required
* for any instance that calls `add`/`update`/`remove`/`relate` or any other
* mutation. Released on `close()` and on process exit/SIGINT/SIGTERM.
* - `'reader'`: opens without acquiring the writer lock. Coexists with a live
* writer and with other readers. All mutation methods throw
* `Cannot mutate a read-only Brainy instance`. Use `Brainy.openReadOnly()`
* as a convenience factory.
*
* See `docs/concepts/multi-process.md` for the full coordination model.
*/
mode?: 'writer' | 'reader'
/**
* Bypass the writer-lock check at init even when another writer holds the lock.
* Use only when you know the existing lock is stale and stale-detection
* (PID liveness + heartbeat) cannot prove it. Logs a warning regardless.
*/
force?: boolean
/**
* How write paths react when an untyped (JavaScript) caller smuggles a
* Brainy-reserved field (`RESERVED_ENTITY_FIELDS` / `RESERVED_RELATION_FIELDS`
* — `confidence`, `weight`, `subtype`, `visibility`, `service`, `createdBy`,
* `noun`/`verb`, `data`, `createdAt`, `updatedAt`, `_rev`) **inside the
* `metadata` bag** of `add()` / `update()` / `relate()` / `updateRelation()`
* (and their `transact()` / `with()` mirrors). TypeScript callers can't write
* these shapes at all — the compile-time guard on the metadata param types
* (`NoReservedEntityKeys` / `NoReservedRelationKeys`) rejects a literal
* reserved key — so this policy only governs untyped callers that slip one
* past the compiler.
*
* - `'throw'` (**default, 8.0**): a reserved key in the bag throws a clear
* `Error` naming the offending key(s) and the correct write path. No silent
* remap, no data loss, no surprise. This is the 8.0 "no silent failures"
* contract.
* - `'warn'`: legacy remapping with a loud, one-shot (per key, per process)
* warning for EVERY reserved key found — user-mutable fields are remapped to
* their dedicated top-level param (top-level wins when both are supplied),
* system-managed fields are dropped. Use while migrating untyped call sites.
* - `'remap'`: the pre-8.0 silent remapping, no warning. Last-resort
* compatibility hatch for code that intentionally relies on the bag path.
*
* @default 'throw'
*/
reservedFieldPolicy?: 'throw' | 'warn' | 'remap'
}
// ============= Neural API Types =============
/**
* Neural similarity parameters
*/
export interface NeuralSimilarityParams {
between?: [any, any] // Compare two items
items?: any[] // Compare multiple items
explain?: boolean // Return detailed breakdown
}
/**
* Neural clustering parameters
*/
export interface NeuralClusterParams {
items?: string[] | Entity[] // Items to cluster (or all)
algorithm?: 'hierarchical' | 'kmeans' | 'dbscan' | 'spectral'
params?: {
k?: number // Number of clusters (kmeans)
threshold?: number // Distance threshold (hierarchical)
epsilon?: number // DBSCAN epsilon
minPoints?: number // DBSCAN min points
}
visualize?: boolean // Return visualization data
}
/**
* Neural anomaly detection parameters
*/
export interface NeuralAnomalyParams {
threshold?: number // Standard deviations (default: 2.5)
type?: NounType // Check specific type
method?: 'isolation' | 'lof' | 'statistical' | 'autoencoder'
returnScores?: boolean // Return anomaly scores
}
// ============= Content Extraction Types =============
/**
* Detected content type for smart text extraction
*
*/
export type ContentType = 'plaintext' | 'richtext-json' | 'html' | 'markdown'
/**
* Content category for extracted segments
*
* Universal categories that work across documents, code, and UI:
* - 'title': Names the subject — headings, identifiers, labels, JSON keys
* - 'annotation': Human explanation — comments, docstrings, captions, alt text
* - 'content': Body substance — paragraphs, list items, flowing text
* - 'value': Data literals — strings, numbers, form values, error messages
* - 'code': Unparsed code blocks (custom parsers decompose into above)
* - 'structural': Boilerplate — keywords, operators, punctuation, formatting
*
* Built-in extractors produce: 'title', 'content', 'code'.
* All 6 categories are available for custom parsers.
*/
export type ContentCategory = 'title' | 'annotation' | 'content' | 'value' | 'code' | 'structural'
/**
* A segment of extracted text with its content category
*
*/
export interface ExtractedSegment {
/** The extracted text content */
text: string
/** What role this text plays: 'title' | 'annotation' | 'content' | 'value' | 'code' | 'structural' */
contentCategory: ContentCategory
}
// ============= Semantic Highlighting Types =============
/**
* Parameters for hybrid highlighting
*
* Zero-config highlighting that returns both text (exact) and semantic (concept) matches.
* Perfect for UI highlighting at different levels.
*
* Added contentType hint and contentExtractor callback for structured text
* (rich-text JSON, HTML, Markdown). Auto-detects format when not specified.
*
* @example
* ```typescript
* // Plain text
* const highlights = await brain.highlight({
* query: "david the warrior",
* text: "David Smith is a brave fighter who battles dragons"
* })
*
* // Rich-text JSON (auto-detected)
* const highlights = await brain.highlight({
* query: "david the warrior",
* text: JSON.stringify(tiptapDocument)
* })
*
* // Custom extractor (for proprietary formats)
* const highlights = await brain.highlight({
* query: "function",
* text: sourceCode,
* contentExtractor: (text) => treeSitterParse(text)
* })
* ```
*/
export interface HighlightParams {
/** The search query to match against */
query: string
/** The text to highlight (e.g., entity.data) */
text: string
/** Granularity of highlighting: 'word' (default), 'phrase', or 'sentence' */
granularity?: 'word' | 'phrase' | 'sentence'
/** Minimum semantic similarity score for semantic matches (default: 0.5) */
threshold?: number
/**
* Optional content type hint to skip auto-detection.
* When omitted, the content type is detected from the text content.
*/
contentType?: ContentType
/**
* Optional custom content extractor function.
* When provided, bypasses built-in detection and extraction entirely.
* Use this to plug in custom parsers (tree-sitter, Monaco, proprietary formats).
*/
contentExtractor?: (text: string) => ExtractedSegment[]
}
/**
* A highlight showing which text matched the query
*
* matchType tells the UI how to style the highlight:
* - 'text': Exact word match (strongest signal, highest confidence)
* - 'semantic': Conceptually similar match (may need softer highlight)
*/
export interface Highlight {
/** The text that matched */
text: string
/** Match score (0-1). For text matches, always 1.0. For semantic, varies by similarity. */
score: number
/** Position in original text [start, end] */
position: [number, number]
/** Match type: 'text' (exact word match) or 'semantic' (concept match) */
matchType: 'text' | 'semantic'
/**
* Content category of the source segment: 'title' | 'annotation' | 'content' | 'value' | 'code' | 'structural'.
* Present when the input text was structured (JSON, HTML, Markdown).
*/
contentCategory?: ContentCategory
}
// ============= Export all types =============
export * from './graphTypes.js' // Re-export NounType, VerbType, etc.