2026-02-28 10:07:34 -08:00
|
|
|
# @soulcraft/brainy — Release Notes for Consumers
|
|
|
|
|
|
|
|
|
|
This file is the **quick reference for Soulcraft product sessions** tracking Brainy changes.
|
|
|
|
|
Full auto-generated changelog: `CHANGELOG.md` · Releases: https://github.com/soulcraftlabs/brainy/releases
|
|
|
|
|
|
|
|
|
|
**How to use:** Brainy is the underlying data engine for Workshop, Venue, Academy, and
|
|
|
|
|
Collective. The SDK wraps it — most products never call Brainy directly. Read this when:
|
|
|
|
|
- Upgrading `@soulcraft/brainy` in the SDK or a product
|
|
|
|
|
- Debugging data, query, or storage behaviour
|
|
|
|
|
- A new Brainy feature is available that SDK should expose
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
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
|
|
|
## v7.30.0 — 2026-06-05
|
|
|
|
|
|
|
|
|
|
**Affected products:** consumers modeling typed relationships with sub-classification
|
|
|
|
|
(direct vs dotted-line management; spouse / sibling / colleague; collaborator vs competitor;
|
|
|
|
|
etc.), and anyone wanting to enforce the pairing of `type` + `subtype` on every write.
|
|
|
|
|
Additive; drop-in from 7.29.x. No deprecations.
|
|
|
|
|
|
|
|
|
|
### Symmetric — `subtype` on relationships (parity with 7.29.0 nouns)
|
|
|
|
|
|
|
|
|
|
`subtype?: string` is now a first-class standard field on every relationship, mirroring the
|
|
|
|
|
noun-side work shipped in 7.29.0. Verbs and nouns are now first-class peers — every API
|
|
|
|
|
available on the noun side has a verb-side mirror.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const ceoId = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'Avery' })
|
|
|
|
|
const vpId = await brain.add({ type: NounType.Person, subtype: 'employee', data: 'Jordan' })
|
|
|
|
|
|
|
|
|
|
await brain.relate({
|
|
|
|
|
from: ceoId,
|
|
|
|
|
to: vpId,
|
|
|
|
|
type: VerbType.ReportsTo,
|
|
|
|
|
subtype: 'direct' // sub-classification on the edge
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**Read & filter:**
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Fast-path filter — column-store hit, not metadata fallback
|
|
|
|
|
const direct = await brain.getRelations({
|
|
|
|
|
from: ceoId,
|
|
|
|
|
type: VerbType.ReportsTo,
|
|
|
|
|
subtype: 'direct'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Set membership
|
|
|
|
|
const all = await brain.getRelations({
|
|
|
|
|
from: ceoId,
|
|
|
|
|
type: VerbType.ReportsTo,
|
|
|
|
|
subtype: ['direct', 'dotted-line']
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Traversal filter (depth-1 in JS; multi-hop lands on Cortex native)
|
|
|
|
|
const reports = await brain.find({
|
|
|
|
|
connected: { from: ceoId, via: VerbType.ReportsTo, subtype: 'direct', depth: 1 }
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### New — `updateRelation()` closes a long-standing gap
|
|
|
|
|
|
|
|
|
|
Verbs previously had no update method — the only way to change a relationship was
|
|
|
|
|
delete-then-recreate, which lost the relation id. 7.30 ships `brain.updateRelation()`:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
await brain.updateRelation({ id: relId, subtype: 'dotted-line' })
|
|
|
|
|
await brain.updateRelation({ id: relId, weight: 0.5, confidence: 0.9 })
|
|
|
|
|
|
|
|
|
|
// Change verb type — re-indexes in graph adjacency, id preserved
|
|
|
|
|
await brain.updateRelation({ id: relId, type: VerbType.WorksWith })
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### New — O(1) verb subtype counts via the persisted rollup
|
|
|
|
|
|
|
|
|
|
`_system/verb-subtype-statistics.json` mirrors the noun-side rollup shipped in 7.29.0.
|
|
|
|
|
Per-VerbType-per-subtype counts are maintained incrementally and persisted; reads are O(1):
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
brain.counts.byRelationshipSubtype(VerbType.ReportsTo)
|
|
|
|
|
// → { direct: 12, 'dotted-line': 3 }
|
|
|
|
|
|
|
|
|
|
brain.counts.byRelationshipSubtype(VerbType.ReportsTo, 'direct') // O(1) point
|
|
|
|
|
// → 12
|
|
|
|
|
|
|
|
|
|
brain.counts.topRelationshipSubtypes(VerbType.ReportsTo, 3)
|
|
|
|
|
// → [['direct', 12], ['dotted-line', 3]]
|
|
|
|
|
|
|
|
|
|
brain.relationshipSubtypesOf(VerbType.ReportsTo)
|
|
|
|
|
// → ['direct', 'dotted-line']
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### New — `brain.requireSubtype(type, options)` per-type enforcement
|
|
|
|
|
|
|
|
|
|
Unified API for noun OR verb types — register specific types as requiring a subtype,
|
|
|
|
|
optionally with a fixed vocabulary:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
brain.requireSubtype(NounType.Person, {
|
|
|
|
|
values: ['employee', 'customer', 'vendor'],
|
|
|
|
|
required: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
brain.requireSubtype(VerbType.ReportsTo, {
|
|
|
|
|
values: ['direct', 'dotted-line'],
|
|
|
|
|
required: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Now this throws — Person requires subtype:
|
|
|
|
|
await brain.add({ type: NounType.Person, data: 'no subtype' })
|
|
|
|
|
|
|
|
|
|
// And this throws — 'matrix' isn't in the registered vocabulary:
|
|
|
|
|
await brain.relate({ from: a, to: b, type: VerbType.ReportsTo, subtype: 'matrix' })
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### New — Brain-wide strict mode
|
|
|
|
|
|
|
|
|
|
`new Brainy({ requireSubtype: true })` enforces subtype on every public write across the
|
|
|
|
|
whole brain. Composes with per-type rules; per-type rules win when both apply.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Every write must include subtype
|
|
|
|
|
const brain = new Brainy({ requireSubtype: true })
|
|
|
|
|
|
|
|
|
|
// Exempt specific types (e.g. catch-all Thing)
|
|
|
|
|
const brain2 = new Brainy({
|
|
|
|
|
requireSubtype: { except: [NounType.Thing, NounType.Custom] }
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
When strict mode is on:
|
|
|
|
|
- Every `add()` / `addMany()` / `update()` / `relate()` / `relateMany()` / `updateRelation()`
|
|
|
|
|
rejects writes missing a subtype on a non-exempt type.
|
|
|
|
|
- `addMany()` and `relateMany()` validate every item BEFORE any storage write —
|
|
|
|
|
atomic-fail semantics, no partial writes.
|
|
|
|
|
- Brainy's own infrastructure writes (VFS root, directories, files) bypass via the
|
|
|
|
|
`metadata.isVFSEntity: true` marker so existing consumers' VFS usage continues to work.
|
|
|
|
|
|
|
|
|
|
The brain-wide flag becomes the default in 8.0.0; the type-level `subtype` field becomes
|
|
|
|
|
required at the type system level. The full 8.0 contract upgrade is coordinated through the
|
|
|
|
|
internal platform handoff (`CTX-SUBTYPE-8.0-CONTRACT`).
|
|
|
|
|
|
|
|
|
|
### `migrateField()` extended to verbs
|
|
|
|
|
|
|
|
|
|
The migration helper shipped in 7.29.0 now walks verbs too via the new `entityKind` option:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Migrate verb-side metadata.kind → top-level subtype
|
|
|
|
|
await brain.migrateField({
|
|
|
|
|
from: 'metadata.kind',
|
|
|
|
|
to: 'subtype',
|
|
|
|
|
entityKind: 'verb'
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Or walk nouns and verbs in one pass
|
|
|
|
|
await brain.migrateField({
|
|
|
|
|
from: 'metadata.kind',
|
|
|
|
|
to: 'subtype',
|
|
|
|
|
entityKind: 'both'
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Default is `entityKind: 'noun'` (backward-compatible).
|
|
|
|
|
|
|
|
|
|
### Symmetry — noun + verb capability matrix
|
|
|
|
|
|
|
|
|
|
7.30 closes every gap between nouns and verbs. Every capability available on the noun side
|
|
|
|
|
has a verb-side mirror:
|
|
|
|
|
|
|
|
|
|
| Capability | Nouns | Verbs |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `subtype` top-level field | ✓ (7.29) | ✓ (7.30 new) |
|
|
|
|
|
| Standard-field set | `STANDARD_ENTITY_FIELDS` | `STANDARD_VERB_FIELDS` (new) |
|
|
|
|
|
| Field resolver helper | `resolveEntityField` | `resolveVerbField` (new) |
|
|
|
|
|
| Statistics rollup | `_system/subtype-statistics.json` | `_system/verb-subtype-statistics.json` (new) |
|
|
|
|
|
| Counts breakdown | `counts.bySubtype` / `topSubtypes` / `subtypesOf` | `counts.byRelationshipSubtype` / `topRelationshipSubtypes` / `relationshipSubtypesOf` (new) |
|
|
|
|
|
| Fast-path filter | `find({type, subtype})` | `getRelations({type, subtype})` + `find({connected, subtype})` (new) |
|
|
|
|
|
| Update method | `update()` | `updateRelation()` (new — closed pre-7.30 gap) |
|
|
|
|
|
| Migration helper | `migrateField()` | `migrateField({entityKind: 'verb'\|'both'})` (new) |
|
|
|
|
|
| Enforcement | `requireSubtype(NounType, ...)` | `requireSubtype(VerbType, ...)` — one unified API |
|
|
|
|
|
| Brain-wide strict mode | `new Brainy({ requireSubtype })` covers both | same |
|
|
|
|
|
|
|
|
|
|
### Cortex compatibility
|
|
|
|
|
|
|
|
|
|
Verb subtype works under Cortex out of the box via auto-field-indexing. Cortex parity items
|
|
|
|
|
for the verb side (native query planner recognition, `verbSubtypeCountsByType` native rollup,
|
|
|
|
|
per-edge subtype on native graph adjacency for multi-hop traversal filtering) ship in the
|
|
|
|
|
next Cortex release. Not a Brainy blocker.
|
|
|
|
|
|
|
|
|
|
### Docs
|
|
|
|
|
|
|
|
|
|
Full guide: `docs/guides/subtypes-and-facets.md` (extended with Layer V + Enforcement
|
|
|
|
|
sections). The verb subtype + enforcement APIs are documented in `docs/api/README.md`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
feat: subtype top-level field + trackField + migrateField
Promotes `subtype?: string` to a top-level standard field on every entity,
alongside `type` / `confidence` / `weight`. Flat string, no hierarchy — the
consumer-chosen vocabulary for sub-classifying entities within a NounType
(Person → employee/customer, Document → invoice/contract, etc.).
Layer 1 — subtype field + rollup
- HNSWNounWithMetadata.subtype + STANDARD_ENTITY_FIELDS entry
- Entity / Result / AddParams / UpdateParams / FindParams threading
- add()/update() persist subtype on storageMetadata + entityForIndexing
- get()/find() route through the standard-field fast path
- subtypeCountsByType (Map<NounTypeIdx, Map<subtype, count>>) on
BaseStorage, mirrored after nounCountsByType with the same self-heal
rebuild and persisted to _system/subtype-statistics.json
- brain.counts.bySubtype(type, subtype?) — O(1) point + breakdown
- brain.counts.topSubtypes(type, n) — top-N by count
- brain.subtypesOf(type) — distinct subtypes seen
- find({ type, subtype }) and find({ subtype: ['a','b'] }) on the fast path
Layer 2 — trackField for other facets
- brain.trackField(name, { perType?, values? }) registers a field for
cardinality + per-NounType breakdown stats. Backed by the aggregation
engine (auto-defines __fieldCounts__<name>), backfill-on-define applies.
- brain.counts.byField(name, { type? }) returns value frequencies
- Optional vocabulary whitelist rejects off-vocabulary writes at add/update
Layer 3 — generic migrateField
- brain.migrateField({ from, to, readBoth?, batchSize?, onProgress? })
streams every entity, copies the value from one path to another, and
(unless readBoth) clears the source. Supports top-level standard fields,
metadata.X, and data.X paths. Idempotent — safe to re-run.
Docs
- New guide: docs/guides/subtypes-and-facets.md (Layer 1 + 2 + 3)
- README, DATA_MODEL, QUERY_OPERATORS, api/README, finite-type-system,
quick-start all treat subtype as a core primitive with anonymous example
vocabularies (employee/customer/invoice/milestone).
Tests
- 26 new integration tests covering write/read/update/delete round-trips,
counts rollup decrement + re-route on mutation, trackField + byField
with and without perType, vocabulary whitelist enforcement, and
migrateField for metadata.X → subtype and data.X → subtype paths
including readBoth deprecation-window semantics.
Unit suite: 1468/1468 passing. Type-check + build clean.
2026-06-04 17:24:36 -07:00
|
|
|
## v7.29.0 — 2026-06-04
|
|
|
|
|
|
|
|
|
|
**Affected products:** anyone modeling entities with per-product sub-classification — every
|
|
|
|
|
consumer that's been reaching for `metadata.kind`, a custom `metadata.subtype` field, a
|
|
|
|
|
`data.kind` shape inside the payload, or similar ad-hoc conventions. Additive; drop-in from
|
|
|
|
|
7.28.x. No deprecations.
|
|
|
|
|
|
|
|
|
|
### New — `subtype` promoted to a top-level standard field
|
|
|
|
|
|
|
|
|
|
`subtype?: string` is now a first-class standard field on every entity, alongside `type` /
|
|
|
|
|
`confidence` / `weight`. Use it as the platform-standard primitive for sub-classifying entities
|
|
|
|
|
within a NounType (`Person` → `'employee'` / `'customer'`; `Document` → `'invoice'` / `'contract'`;
|
|
|
|
|
`Event` → `'milestone'` / `'meeting'`):
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
await brain.add({
|
|
|
|
|
data: 'Avery Brooks — runs the AI lab',
|
|
|
|
|
type: NounType.Person,
|
|
|
|
|
subtype: 'employee', // top-level write param
|
|
|
|
|
metadata: { department: 'ai-lab' }
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Fast-path filter — column-store hit, not metadata fallback:
|
|
|
|
|
const employees = await brain.find({ type: NounType.Person, subtype: 'employee' })
|
|
|
|
|
|
|
|
|
|
// Set membership:
|
|
|
|
|
const internal = await brain.find({
|
|
|
|
|
type: NounType.Person,
|
|
|
|
|
subtype: ['employee', 'contractor']
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Flat string, no hierarchy — your vocabulary, your choice. Brainy stores and counts, never validates.
|
|
|
|
|
|
|
|
|
|
### New — O(1) subtype counts via the persisted rollup
|
|
|
|
|
|
|
|
|
|
A new `_system/subtype-statistics.json` rollup is maintained incrementally as entities are added,
|
|
|
|
|
updated, and deleted — mirroring the existing `nounCountsByType` machinery with the same self-heal
|
|
|
|
|
behavior on poison detection. Counts are O(1) at billion scale:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
brain.counts.bySubtype(NounType.Person)
|
|
|
|
|
// → { employee: 12, customer: 847, vendor: 34 }
|
|
|
|
|
|
|
|
|
|
brain.counts.bySubtype(NounType.Person, 'employee') // O(1) point count
|
|
|
|
|
// → 12
|
|
|
|
|
|
|
|
|
|
brain.counts.topSubtypes(NounType.Person, 3)
|
|
|
|
|
// → [['customer', 847], ['employee', 12], ['vendor', 34]]
|
|
|
|
|
|
|
|
|
|
brain.subtypesOf(NounType.Person)
|
|
|
|
|
// → ['customer', 'employee', 'vendor']
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### New — `brain.trackField()` for other metadata facets
|
|
|
|
|
|
|
|
|
|
For facets that aren't the *primary* sub-classification (`status`, `source`, `role`, `paradigm`),
|
|
|
|
|
`trackField` registers a field for cardinality + per-NounType breakdown stats without promoting
|
|
|
|
|
each one to a top-level slot. Piggybacks on the existing aggregation engine, so backfill-on-define
|
|
|
|
|
applies — registering on a populated brain scans existing entities on the first query:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
brain.trackField('status', { perType: true })
|
|
|
|
|
|
|
|
|
|
await brain.counts.byField('status')
|
|
|
|
|
// → { todo: 12, doing: 3, done: 47 }
|
|
|
|
|
|
|
|
|
|
await brain.counts.byField('status', { type: NounType.Task })
|
|
|
|
|
// → { todo: 8, doing: 2, done: 30 }
|
|
|
|
|
|
|
|
|
|
// Opt-in vocabulary validation:
|
|
|
|
|
brain.trackField('priority', { values: ['low', 'medium', 'high'] })
|
|
|
|
|
// brain.add({ ..., metadata: { priority: 'urgent' } }) → throws
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### New — generic `brain.migrateField()` for one-shot rewrites
|
|
|
|
|
|
|
|
|
|
Streams every entity and copies a value from one field path to another. Supports top-level
|
|
|
|
|
standard fields, `metadata.*`, and `data.*` paths; idempotent; with optional `readBoth: true`
|
|
|
|
|
deprecation window that preserves the source field alongside the new one:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
// Phase 1: dual-populate (legacy readers still work)
|
|
|
|
|
await brain.migrateField({
|
|
|
|
|
from: 'metadata.kind',
|
|
|
|
|
to: 'subtype',
|
|
|
|
|
readBoth: true
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// ...readers migrate to subtype at their own pace...
|
|
|
|
|
|
|
|
|
|
// Phase 2: clear the source field
|
|
|
|
|
await brain.migrateField({ from: 'metadata.kind', to: 'subtype' })
|
|
|
|
|
// → { scanned: 1500, migrated: 1500, skipped: 0, errors: [] }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Supports `batchSize` and `onProgress` for large brains.
|
|
|
|
|
|
|
|
|
|
### Aggregation composition
|
|
|
|
|
|
|
|
|
|
`subtype` is a standard-field group-by dimension out of the box — no `where:` wrapper, no
|
|
|
|
|
metadata-fallback overhead:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
await brain.find({
|
|
|
|
|
aggregate: {
|
|
|
|
|
groupBy: ['type', 'subtype'],
|
|
|
|
|
metrics: { count: { op: 'COUNT' } }
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
// [
|
|
|
|
|
// { groupKey: { type: 'person', subtype: 'customer' }, count: 847 },
|
|
|
|
|
// { groupKey: { type: 'person', subtype: 'employee' }, count: 12 },
|
|
|
|
|
// { groupKey: { type: 'document', subtype: 'invoice' }, count: 2103 },
|
|
|
|
|
// ...
|
|
|
|
|
// ]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Cortex compatibility
|
|
|
|
|
|
|
|
|
|
Subtype works under Cortex out of the box via auto-field-indexing. Cortex will land its own
|
|
|
|
|
native-side query-planner recognition + `subtypeCountsByType` native rollup in the next Cortex
|
|
|
|
|
release for full parity with `type`'s fast path. Not a Brainy blocker — subtype reads/writes
|
|
|
|
|
function correctly with current Cortex.
|
|
|
|
|
|
|
|
|
|
### Docs
|
|
|
|
|
|
|
|
|
|
Full guide: `docs/guides/subtypes-and-facets.md`. Subtype is documented as a core primitive
|
|
|
|
|
throughout `README.md`, `docs/DATA_MODEL.md`, `docs/architecture/finite-type-system.md`,
|
|
|
|
|
`docs/api/README.md`, and `docs/QUERY_OPERATORS.md`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-26 14:20:40 -07:00
|
|
|
## v7.24.0 — 2026-05-26
|
|
|
|
|
|
|
|
|
|
**Affected products:** aggregation/reporting users + anyone calling `extractEntities` on
|
|
|
|
|
multi-entity text. Additive; drop-in from 7.23.x.
|
|
|
|
|
|
|
|
|
|
### New — array-unnest `groupBy` (tag frequency / faceted counts)
|
|
|
|
|
|
|
|
|
|
A `groupBy` dimension can now be `{ field, unnest: true }`: the field holds an array and the
|
|
|
|
|
entity contributes once per **distinct** element. Enables tag-frequency / label-count style
|
|
|
|
|
aggregates:
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
brain.defineAggregate({
|
|
|
|
|
name: 'tag_frequency',
|
|
|
|
|
source: { type: NounType.Document },
|
|
|
|
|
groupBy: [{ field: 'tags', unnest: true }],
|
|
|
|
|
metrics: { count: { op: 'count' } }
|
|
|
|
|
})
|
|
|
|
|
// queryAggregate('tag_frequency', { orderBy: 'count', order: 'desc' })
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Duplicate elements on one entity count once; an entity with an empty/missing array joins no group.
|
|
|
|
|
|
|
|
|
|
### Perf — entity extraction batch-embeds candidates
|
|
|
|
|
|
|
|
|
|
`extractEntities` / `extractConcepts` now embed all unique candidate spans in a single
|
|
|
|
|
`embedBatch` call instead of one `embed()` per candidate (N sequential model calls before). No
|
|
|
|
|
behavior change — and with vectors reliably available, the embedding signal more consistently
|
|
|
|
|
reinforces correct types (e.g. clearer Organization confidence). Falls back to per-candidate
|
|
|
|
|
embedding if a batch call fails.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
feat: queryAggregate() + HAVING, plus aggregate backfill, traversal depth/via, extraction typing (BR-ADV-FEATURES-BUN)
New report APIs:
- brain.queryAggregate(name, params) returns the clean AggregateResult[] shape
({ groupKey, metrics, count }) directly, instead of the search-Result wrapper that
find({ aggregate }) returns.
- HAVING: find({ aggregate, having }) and queryAggregate filter groups by computed
metric values (e.g. revenue > 1000), complementing where (which filters group keys).
Evaluated per group: O(groups), independent of entity count.
Fixes (all reproducible on Node; surfaced under Cortex+Bun by Memory):
- Aggregate backfill-on-define: defining an aggregate over a store that already holds
matching entities returned []. It now backfills from existing entities on first query
(storage-agnostic via getNouns), so it works under durable backends that reopen
pre-populated. groupBy:['noun'] resolves to the entity type; find({ aggregate }) rows
expose groupKey/metrics/count at the top level.
- Multi-hop find({ connected: { depth, via } }) honors depth and via at every hop
(was 1-hop only, with verb filtering applied to hop 1 only); BFS bounded by limit.
- extractEntities no longer bleeds a neighbour's type indicator across candidates; each
candidate is typed by its own span. Also fixes the "Dr." title pattern.
Adds real-embedding regression tests; full unit suite green.
2026-05-26 13:55:43 -07:00
|
|
|
## v7.23.0 — 2026-05-26
|
|
|
|
|
|
|
|
|
|
**Affected products:** anyone using aggregation (stats/dashboards), graph traversal, or entity
|
|
|
|
|
extraction. Adds two report APIs and fixes three correctness gaps surfaced by BR-ADV-FEATURES-BUN.
|
|
|
|
|
Drop-in upgrade from 7.22.x.
|
|
|
|
|
|
|
|
|
|
### New — `brain.queryAggregate(name, params)`
|
|
|
|
|
|
|
|
|
|
A first-class report API returning the clean `AggregateResult[]` shape
|
|
|
|
|
(`{ groupKey, metrics, count }[]`) directly, instead of the search-`Result` wrapper that
|
|
|
|
|
`find({ aggregate })` returns. Accepts `where` / `having` / `orderBy` / `order` / `limit` / `offset`.
|
|
|
|
|
|
|
|
|
|
### New — HAVING (filter groups by metric value)
|
|
|
|
|
|
|
|
|
|
`find({ aggregate, having: { revenue: { greaterThan: 1000 } } })` (and the same on
|
|
|
|
|
`queryAggregate`) filters groups by their computed metrics — the analytics equivalent of SQL
|
|
|
|
|
`HAVING`, complementing `where` (which filters group keys). Evaluated per group: **O(groups),
|
|
|
|
|
independent of entity count.**
|
|
|
|
|
|
|
|
|
|
### Fix — aggregates now backfill when defined over existing data (R1)
|
|
|
|
|
|
|
|
|
|
Defining an aggregate on a store that already holds matching entities returned `[]` because
|
|
|
|
|
write-time hooks only saw *future* writes. It now backfills from existing entities on first query
|
|
|
|
|
(one-time scan via `getNouns`, then incremental) — storage-agnostic, so it works under durable
|
|
|
|
|
backends (Cortex) where a brain reopens pre-populated. Also: `groupBy:['noun']` now resolves to
|
|
|
|
|
the entity type instead of a single null group. `find({ aggregate })` rows now expose
|
|
|
|
|
`groupKey`/`metrics`/`count` at the top level (previously only under `.metadata`).
|
|
|
|
|
|
|
|
|
|
### Fix — multi-hop `find({ connected: { depth, via } })` (traversal)
|
|
|
|
|
|
|
|
|
|
`depth` and `via` are now honored at **every hop** (previously only the immediate neighbour was
|
|
|
|
|
returned, and verb filtering applied to hop 1 only). The BFS is bounded by `limit`.
|
|
|
|
|
|
|
|
|
|
### Fix — entity extraction type accuracy (R3)
|
|
|
|
|
|
|
|
|
|
`extractEntities` no longer lets a type indicator in one candidate's surrounding text bleed onto
|
|
|
|
|
neighbours (e.g. "Corp" in "Sarah Chen founded Acme Corp" no longer types "Sarah Chen" as
|
|
|
|
|
Organization). Each candidate is typed by its own span; context may only reinforce the same type.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-26 11:32:46 -07:00
|
|
|
## v7.22.1 — 2026-05-26
|
|
|
|
|
|
|
|
|
|
**Affected products:** Anyone using `extractEntities()` / `extractConcepts()`, native
|
|
|
|
|
aggregation (`find({ aggregate })`), or multi-hop graph traversal
|
|
|
|
|
(`find({ connected: { depth } })`). Three advanced-API correctness fixes — all reproduce on
|
|
|
|
|
Node, so they were **not** Bun-specific despite the original report (BR-ADV-FEATURES-BUN).
|
|
|
|
|
|
|
|
|
|
### Entity/concept extraction no longer returns `[]`
|
|
|
|
|
|
|
|
|
|
`extractEntities()` / `extractConcepts()` returned empty for entity-rich text. The SmartExtractor
|
|
|
|
|
ensemble scored agreeing signals with a weighted **sum** against an absolute 0.60 gate, so a
|
|
|
|
|
confident low-weight signal (e.g. a name pattern at 0.82) lost selection to a mediocre
|
|
|
|
|
high-weight signal that then failed the gate — dropping the whole result. It now selects and
|
|
|
|
|
gates on a normalized weighted **average**. Also: the `confidence` option now actually controls
|
|
|
|
|
the threshold (was a dead hardcoded 0.60), and the embedding-signal timeout was raised
|
|
|
|
|
100ms → 2000ms so the neural signal isn't silently dropped on slower runtimes.
|
|
|
|
|
|
|
|
|
|
*Known limitation:* type accuracy on dense multi-entity sentences is still imperfect — a strong
|
|
|
|
|
indicator in one entity's surrounding text can bleed onto neighbours. Tracked separately.
|
|
|
|
|
|
|
|
|
|
### `find({ aggregate })` exposes `groupKey` / `metrics` / `count`
|
|
|
|
|
|
|
|
|
|
Aggregation always computed correct values, but rows nested them under `.metadata`, so callers
|
|
|
|
|
expecting the documented `AggregateResult` shape saw empty-looking rows. Those three fields are
|
|
|
|
|
now also present at the top level of each result row.
|
|
|
|
|
|
|
|
|
|
### Multi-hop `find({ connected: { depth } })` honours depth
|
|
|
|
|
|
|
|
|
|
Previously returned only the immediate (1-hop) neighbour at any depth because the graph-search
|
|
|
|
|
path ignored `depth` / `via`. It now performs the full depth-aware traversal.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-05-15 12:31:28 -07:00
|
|
|
## v7.22.0 — 2026-05-15
|
|
|
|
|
|
|
|
|
|
**Affected products:** All. Fixes a silent-data-loss class affecting `find()` and
|
|
|
|
|
`brain.stats()`, plus Cortex-compatibility regression from 7.21.0. Recommended
|
|
|
|
|
upgrade for everyone on 7.20.x or 7.21.x.
|
|
|
|
|
|
|
|
|
|
### Two correctness fixes
|
|
|
|
|
|
|
|
|
|
#### 1. `find({ where })` no longer silently returns `[]` (BR-FIND-WHERE-ZERO)
|
|
|
|
|
|
|
|
|
|
The 7.20.0 column-store refactor deleted the legacy sparse-index write path
|
|
|
|
|
but left `getStats()` and `getIds()` reading from sparse indices. New
|
|
|
|
|
workspaces had no sparse-index files, so:
|
|
|
|
|
- `brain.stats().entityCount` was `0` regardless of actual entity count
|
|
|
|
|
- `find({ where: { ... } })` returned `[]` regardless of actual matches
|
|
|
|
|
- `rebuildIndexesIfNeeded()` saw `0` entries → fired the `[Brainy] CRITICAL ...
|
|
|
|
|
Second rebuild result: 0 entries` log → application saw no indexed data
|
|
|
|
|
|
|
|
|
|
7.22.0 makes the ColumnStore the single source of truth post-7.20.0:
|
|
|
|
|
- `MetadataIndex.getStats()` reads from `ColumnStore.getIndexedFields()` and
|
|
|
|
|
`idMapper.size`. Entity count is now accurate across writer → close →
|
|
|
|
|
reader-open cycles.
|
|
|
|
|
- `MetadataIndex.getIdsFromChunks()` throws `BrainyError(FIELD_NOT_INDEXED)`
|
|
|
|
|
when neither column store nor legacy sparse index has the field.
|
|
|
|
|
- `MetadataIndex.getIdsForFilter()` catches per-clause, logs
|
|
|
|
|
`[brainy] find() where-clause referenced unindexed field(s): ...`, returns
|
|
|
|
|
`[]`. Production `find()` is now consistent with `brain.explain()`'s
|
|
|
|
|
`path: 'none'` diagnostic.
|
|
|
|
|
|
|
|
|
|
Separately, `BaseStorage.getNounType()` was hardcoded to return `'thing'`
|
|
|
|
|
since a type-cache removal in commit `42ae5be`. Every noun save attributed
|
|
|
|
|
the entity to `'thing'` regardless of its actual type, poisoning
|
|
|
|
|
`_system/type-statistics.json` and `brain.stats().entitiesByType`. 7.22.0
|
|
|
|
|
restores type-correct attribution:
|
|
|
|
|
- New `nounTypeByIdCache: Map<string, NounType>` on `BaseStorage`, populated
|
|
|
|
|
in `saveNounMetadata_internal` and consumed in `saveNoun_internal`.
|
|
|
|
|
- New `flushCounts()` override that persists `nounCountsByType` /
|
|
|
|
|
`verbCountsByType` alongside the per-entity counter. Readers opening the
|
|
|
|
|
same directory after a writer flush now see correct per-type counts.
|
|
|
|
|
|
|
|
|
|
**Self-heal at init.** If `loadTypeStatistics()` detects the poisoned-state
|
|
|
|
|
signature (all nouns attributed to `'thing'` with multiple non-`thing`
|
|
|
|
|
entities on disk), it auto-runs `rebuildTypeCounts()` once and rewrites the
|
|
|
|
|
file. A `[BaseStorage] Detected poisoned type-statistics.json signature ...`
|
|
|
|
|
warning is logged. Operators don't need to take any action — the first 7.22
|
|
|
|
|
open of a directory poisoned by 7.20.0/7.21.0 fixes it.
|
|
|
|
|
|
|
|
|
|
**`brainy inspect repair`** can also be used to manually trigger
|
|
|
|
|
`rebuildTypeCounts()`. It's now public on `BaseStorage`.
|
|
|
|
|
|
|
|
|
|
#### 2. Cortex 2.2.x no longer crashes 7.21.0 boot (BR-DEFENSIVE-INTERFACE)
|
|
|
|
|
|
|
|
|
|
7.21.0 added new storage-adapter methods (`supportsMultiProcessLocking`,
|
|
|
|
|
`acquireWriterLock`, etc.) and called them unconditionally. Cortex 2.2.0's
|
|
|
|
|
mmap storage adapter bundles a pre-7.21 `BaseStorage` and doesn't define
|
|
|
|
|
them, so Venue's 7.21.0 upgrade attempt threw
|
|
|
|
|
`TypeError: this.storage.supportsMultiProcessLocking is not a function`
|
|
|
|
|
at boot.
|
|
|
|
|
|
|
|
|
|
7.22.0 introduces `hasStorageMethod(name)` on Brainy and guards every new-
|
|
|
|
|
method call site. Older adapters degrade with a one-line warning at init:
|
|
|
|
|
```
|
|
|
|
|
[brainy] Storage adapter `CortexMmapStorage` predates the 7.21
|
|
|
|
|
multi-process methods. Writer locking and the flush-request RPC are
|
|
|
|
|
disabled for this directory. Upgrade the plugin (e.g.
|
|
|
|
|
`@soulcraft/cortex` ≥2.3.0) for full enforcement.
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### Dead-code cleanup
|
|
|
|
|
|
|
|
|
|
Removed `MetadataIndexManager.dirtyChunks`, `dirtySparseIndices`, and the
|
|
|
|
|
`flushDirtyMetadata()` no-op machinery. These accumulators stopped being
|
|
|
|
|
populated when the sparse-index write path was deleted in 7.20.0; the
|
|
|
|
|
remaining 6 callers wasted async hops on an empty Map iteration.
|
|
|
|
|
|
|
|
|
|
### Upgrade notes
|
|
|
|
|
|
|
|
|
|
- **Behaviour change:** `find({ where: { unindexedField: ... } })` previously
|
|
|
|
|
returned `[]` silently. It now logs a one-line warning and still returns
|
|
|
|
|
`[]`. The diagnostic message names the field — use
|
|
|
|
|
`brain.explain({ where: { ... } })` for full diagnosis. No code change
|
|
|
|
|
needed for callers that already handle empty results.
|
|
|
|
|
- **Behaviour change (good):** `brain.stats()` and `brain.health()` now
|
|
|
|
|
report accurate per-type counts. If you previously relied on the buggy
|
|
|
|
|
`entitiesByType.thing` over-count, update consumers.
|
|
|
|
|
- **No API breaks.** Pure additive on the public surface.
|
|
|
|
|
|
|
|
|
|
### Cortex consumers
|
|
|
|
|
|
|
|
|
|
Cortex 2.2.x continues to work against 7.22.0 thanks to the defensive
|
|
|
|
|
guards, but for full multi-process protection on Cortex-backed stores and
|
|
|
|
|
to make sure Cortex's native MetadataIndex picks up the find-where-zero
|
|
|
|
|
fix, Cortex 2.3.0 is recommended. Tracked as `CX-MULTIPROC-METHOD` in the
|
|
|
|
|
internal platform handoff.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
feat: multi-process safety + read-only inspector mode
Filesystem storage now enforces single-writer, many-reader semantics.
A second writer on the same data directory throws at init time with the
holder's PID, hostname, and heartbeat — replacing the previous silent
stale-reads failure mode.
- New: `Brainy.openReadOnly()` — coexists with a live writer, every
mutation throws clearly.
- New: writer lock at `<rootDir>/locks/_writer.lock` with 10s heartbeat
and stale-detection (PID liveness + heartbeat freshness).
- New: cross-process flush-request RPC (filesystem-based, no signals)
so inspectors can force fresh state on demand.
- New: `brain.stats()`, `brain.explain(findParams)`, `brain.health()`
for operator-facing introspection.
- New: `brainy inspect` CLI with 13 subcommands (stats, find, get,
relations, explain, health, sample, fields, dump, watch, backup,
repair, diff), all read-only by default.
- Same-PID re-opens allowed with a warning (preserves test "simulate
restart" patterns).
- Storage instances passed directly via `storage: new MemoryStorage()`
are now honoured instead of silently falling through to the
filesystem auto-detect path.
Brainy + Cortex compose under this model — the lock covers both because
they share `rootDir`, Cortex segments are immutable mmap files, and
MANIFEST updates use atomic-rename.
2026-05-15 11:25:05 -07:00
|
|
|
## v7.21.0 — 2026-05-15
|
|
|
|
|
|
|
|
|
|
**Affected products:** All consumers using filesystem storage (Venue, Workshop dev,
|
|
|
|
|
local development).
|
|
|
|
|
|
|
|
|
|
### Multi-process safety + first-class read-only inspector
|
|
|
|
|
|
|
|
|
|
Filesystem storage now enforces **single-writer, many-reader**. Previously, opening
|
|
|
|
|
a second writer process against a live data directory silently produced wrong query
|
|
|
|
|
results — no error, no warning, just empty or stale data. This release replaces
|
|
|
|
|
that footgun with a hard refusal at init time and a dedicated read-only inspection
|
|
|
|
|
mode.
|
|
|
|
|
|
|
|
|
|
#### New: `Brainy.openReadOnly()`
|
|
|
|
|
```ts
|
|
|
|
|
const reader = await Brainy.openReadOnly({
|
|
|
|
|
storage: { type: 'filesystem', rootDirectory: '/data/brain' }
|
|
|
|
|
})
|
|
|
|
|
const bookings = await reader.find({ where: { entityType: 'booking' } })
|
|
|
|
|
```
|
|
|
|
|
- Does NOT acquire the writer lock — coexists with a live writer.
|
|
|
|
|
- Every mutation method throws `Cannot mutate a read-only Brainy instance`.
|
|
|
|
|
- `flush()` and `close()` are safe (no-op + clean shutdown).
|
|
|
|
|
|
|
|
|
|
#### New: writer lock at init
|
|
|
|
|
- `new Brainy({ ... })` (default `mode: 'writer'`) acquires
|
|
|
|
|
`<rootDir>/locks/_writer.lock` containing `{ pid, hostname, startedAt, lastHeartbeat, version }`.
|
|
|
|
|
- A second writer on the same directory **throws** with the PID, hostname,
|
|
|
|
|
heartbeat, and a pointer to `openReadOnly()`.
|
|
|
|
|
- Stale-lock detection: same hostname + dead PID OR heartbeat > 60s ago → overwritten with a warning.
|
|
|
|
|
- Heartbeat: writer rewrites `lastHeartbeat` every 10s (unref'd timer).
|
|
|
|
|
- Override: pass `{ force: true }` to bypass when you've verified the existing lock is stale.
|
|
|
|
|
|
|
|
|
|
#### New: `brain.requestFlush({ timeoutMs })`
|
|
|
|
|
Cross-process RPC for inspectors to force a fresh snapshot:
|
|
|
|
|
- In-process: just calls `flush()`.
|
|
|
|
|
- Out-of-process: writes a request file, polls for ack. Times out gracefully.
|
|
|
|
|
- Watcher runs in every writer instance (filesystem only).
|
|
|
|
|
|
|
|
|
|
#### New: `brain.stats()`
|
|
|
|
|
Operator-facing summary: `entityCount`, `entitiesByType`, `relationCount`,
|
|
|
|
|
`relationsByType`, `fieldRegistry`, `indexHealth`, `storage.backend`, `writerLock`, `version`.
|
|
|
|
|
Designed for `/api/health` endpoints and incident triage.
|
|
|
|
|
|
|
|
|
|
#### New: `brain.explain(findParams)`
|
|
|
|
|
Shows which index path serves each `where` clause: `column-store` | `sparse-chunked` | `none`.
|
|
|
|
|
**This is the answer to "why is `find()` returning empty?"** — `path: "none"` means
|
|
|
|
|
the field has no index entries and the query will silently return `[]`. Includes
|
|
|
|
|
notes on likely causes (writer hasn't flushed; typo; field genuinely absent).
|
|
|
|
|
|
|
|
|
|
#### New: `brain.health()`
|
|
|
|
|
Invariant-check battery: HNSW vs metadata count parity, field registry sanity,
|
|
|
|
|
`_seeded` entity sweep, writer heartbeat freshness. Returns `{ overall: 'pass' | 'warn' | 'fail', checks: [...] }`.
|
|
|
|
|
|
|
|
|
|
#### New: `brainy inspect` CLI (13 subcommands)
|
|
|
|
|
All read-only by default (internally uses `openReadOnly()`):
|
|
|
|
|
```
|
|
|
|
|
brainy inspect stats <path>
|
|
|
|
|
brainy inspect find <path> --type Event --where '{"status":"paid"}' --limit 20
|
|
|
|
|
brainy inspect get <path> <id>
|
|
|
|
|
brainy inspect relations <path> <id> --direction both
|
|
|
|
|
brainy inspect explain <path> --where '{"entityType":"booking"}'
|
|
|
|
|
brainy inspect health <path>
|
|
|
|
|
brainy inspect sample <path> --type Event --n 20
|
|
|
|
|
brainy inspect fields <path>
|
|
|
|
|
brainy inspect dump <path> --type Event > backup.jsonl
|
|
|
|
|
brainy inspect watch <path> --type Event
|
|
|
|
|
brainy inspect backup <path> /backups/brain.tar
|
|
|
|
|
brainy inspect repair <path> # writer mode — stop live writer first
|
|
|
|
|
brainy inspect diff <pathA> <pathB>
|
|
|
|
|
```
|
|
|
|
|
Default behaviour: ask the writer to flush via the RPC first (skip with `--no-fresh`).
|
|
|
|
|
|
|
|
|
|
### Brainy + Cortex
|
|
|
|
|
|
|
|
|
|
Cortex segments (`*.cidx`) are immutable mmap files; `MANIFEST.json` uses atomic
|
|
|
|
|
rename. The single writer lock at `<rootDir>/locks/_writer.lock` covers both Brainy
|
|
|
|
|
and Cortex. Read-only inspectors mmap Cortex segments with zero coordination.
|
|
|
|
|
|
|
|
|
|
### What's NOT enforced yet
|
|
|
|
|
|
|
|
|
|
- **Cloud storage backends** (S3, GCS, R2, Azure) — no multi-process locking.
|
|
|
|
|
A best-effort warning logs in writer mode against non-filesystem backends.
|
|
|
|
|
- **The `find({ where })`-returns-0 root-cause bug** — tracked separately. The
|
|
|
|
|
new `brain.explain()` and `brain.health()` surface it loudly
|
|
|
|
|
(`path: 'none'`, `index-parity warn`) instead of letting it be silent.
|
|
|
|
|
|
|
|
|
|
### Upgrade notes
|
|
|
|
|
|
|
|
|
|
- **Default behaviour change:** opening a Brainy directory in writer mode while
|
|
|
|
|
another writer is live now THROWS where it previously silently produced wrong
|
|
|
|
|
results. If you have scripts that intentionally co-opened a writer directory,
|
|
|
|
|
switch them to `Brainy.openReadOnly()` or pass `{ force: true }`.
|
|
|
|
|
- **Cloud backends unchanged** — no lock acquisition for S3/GCS/R2/Azure.
|
|
|
|
|
- All existing APIs unchanged. Pure addition.
|
|
|
|
|
|
|
|
|
|
### Docs
|
|
|
|
|
|
|
|
|
|
- README "Single-Writer Model" section.
|
|
|
|
|
- `docs/concepts/multi-process.md` — full model + Cortex compatibility.
|
|
|
|
|
- `docs/guides/inspection.md` — operator recipes.
|
|
|
|
|
- JSDoc on every new API.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-02-28 10:07:34 -08:00
|
|
|
## v7.19.10 — 2026-02-24
|
|
|
|
|
|
|
|
|
|
**Affected products:** All Bun/ESM consumers (Workshop, Venue, Academy, SDK)
|
|
|
|
|
|
|
|
|
|
### ESM crypto fix in SSTable
|
|
|
|
|
|
|
|
|
|
Replaced `require('crypto')` with `import { createHash } from 'node:crypto'` in the
|
|
|
|
|
SSTable implementation. Fixes a crash in Bun and strict ESM environments where
|
|
|
|
|
CommonJS `require` is unavailable.
|
|
|
|
|
|
|
|
|
|
No API changes — upgrade and redeploy.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## v7.19.2 — 2026-02-18
|
|
|
|
|
|
|
|
|
|
**Affected products:** All
|
|
|
|
|
|
|
|
|
|
### Metadata index cleanup on delete
|
|
|
|
|
|
|
|
|
|
Fixed: metadata indexes were not cleaned up after `delete()` / `deleteMany()`. Stale
|
|
|
|
|
index entries could cause phantom results in metadata-filtered queries after deletion.
|
|
|
|
|
|
|
|
|
|
No API changes. If you were seeing ghost results in filtered queries, this fixes it.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## v7.18.0 — 2026-02-16
|
|
|
|
|
|
|
|
|
|
**Affected products:** Workshop, Venue, Academy (analytics, reporting, session summaries)
|
|
|
|
|
|
|
|
|
|
### Aggregation engine
|
|
|
|
|
|
|
|
|
|
New `brain.aggregate()` API — incremental SUM, COUNT, AVG, MIN, MAX with GROUP BY
|
|
|
|
|
and time window support. Computes over entity collections without loading all records
|
|
|
|
|
into memory.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
const result = await brain.aggregate({
|
|
|
|
|
collection: 'bookings',
|
|
|
|
|
metrics: [
|
|
|
|
|
{ field: 'revenue', fn: 'SUM' },
|
|
|
|
|
{ field: 'id', fn: 'COUNT' },
|
|
|
|
|
],
|
|
|
|
|
groupBy: 'staffId',
|
|
|
|
|
timeWindow: { field: 'createdAt', from: startOfMonth, to: now },
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
SDK exposure: `sdk.brainy.aggregate()` — available once SDK is updated to pass through.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## v7.17.0 — 2026-02-09
|
|
|
|
|
|
|
|
|
|
**Affected products:** All (schema evolution, data migrations)
|
|
|
|
|
|
|
|
|
|
### Migration system
|
|
|
|
|
|
|
|
|
|
New `brain.migrate()` API with error handling, validation, and enterprise hardening.
|
|
|
|
|
Run schema migrations reliably across Brainy data directories.
|
|
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
|
await brain.migrate({
|
|
|
|
|
version: 3,
|
|
|
|
|
up: async (brain) => {
|
|
|
|
|
// transform entities, rename fields, etc.
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## v7.16.0 — 2026-02-09
|
|
|
|
|
|
|
|
|
|
**Affected products:** All
|
|
|
|
|
|
|
|
|
|
### Data/metadata separation enforced + numeric range queries
|
|
|
|
|
|
|
|
|
|
- Entity `data` and `metadata` fields are now strictly separated at the storage layer
|
|
|
|
|
- Numeric range queries now supported in metadata filters: `{ age: { $gte: 18, $lt: 65 } }`
|
|
|
|
|
- Fixes edge cases where mixed data/metadata storage caused inconsistent query results
|
|
|
|
|
|
|
|
|
|
**Breaking for anyone storing numeric values in metadata and relying on range queries:**
|
|
|
|
|
verify your filter syntax matches the new `$gte/$lte/$gt/$lt` operators.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## v7.15.5 — 2026-02-02
|
|
|
|
|
|
|
|
|
|
**Affected products:** Anyone using `@soulcraft/cortex` plugin
|
|
|
|
|
|
|
|
|
|
### Plugin opt-in clarified
|
|
|
|
|
|
|
|
|
|
Cortex and other plugins are opt-in. Pass explicitly:
|
|
|
|
|
```typescript
|
|
|
|
|
new Brainy({ plugins: ['@soulcraft/cortex'] })
|
|
|
|
|
```
|
|
|
|
|
Without `plugins`, no external plugins are loaded regardless of what's installed.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## v7.15.2 — 2026-02-01
|
|
|
|
|
|
|
|
|
|
**Affected products:** All (data safety)
|
|
|
|
|
|
|
|
|
|
### Graph LSM flush on close
|
|
|
|
|
|
|
|
|
|
Fixed: graph LSM-trees were not flushed on `brain.close()`, risking data loss across
|
|
|
|
|
restarts. Graph edges written in the final seconds before shutdown are now guaranteed
|
|
|
|
|
to be persisted.
|
|
|
|
|
|
|
|
|
|
No API changes — upgrade immediately if running Brainy in a long-lived server process.
|