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).
This commit is contained in:
parent
8c68396e71
commit
c0d326b36d
14 changed files with 2180 additions and 68 deletions
|
|
@ -221,6 +221,47 @@ await brain.add({
|
|||
|
||||
`subtype` lives at the **top level** — NOT inside `metadata`, NOT inside `data`. That's how `find({ type, subtype })` routes through the standard-field fast path (column-store hit) instead of the metadata fallback. See **[Subtypes & Facets](./guides/subtypes-and-facets.md)** for the full guide including `trackField()` and `migrateField()`.
|
||||
|
||||
### Subtype — sub-classification within a VerbType (7.30+)
|
||||
|
||||
Relationships are first-class citizens too. Every verb (`VerbType`) gets the same `subtype` primitive — a `ReportsTo` relationship might carry `subtype: 'direct'` vs `'dotted-line'`; a `RelatedTo` edge might carry `'spouse'` / `'sibling'` / `'colleague'`. Same shape as the noun side: flat string, no hierarchy, top-level standard field on `HNSWVerbWithMetadata` and on the public `Relation<T>`:
|
||||
|
||||
```typescript
|
||||
await brain.relate({
|
||||
from: ceoId,
|
||||
to: vpId,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct', // top-level standard field
|
||||
metadata: { since: '2025-Q1' } // user-custom fields stay in metadata
|
||||
})
|
||||
```
|
||||
|
||||
Fast-path filter on the verb side:
|
||||
|
||||
```typescript
|
||||
const direct = await brain.getRelations({
|
||||
from: ceoId,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct'
|
||||
})
|
||||
```
|
||||
|
||||
The verb-side rollup at `_system/verb-subtype-statistics.json` mirrors the noun-side `_system/subtype-statistics.json` — same shape, same self-heal machinery. Per-VerbType-per-subtype counts are O(1) via `brain.counts.byRelationshipSubtype()`.
|
||||
|
||||
Verbs and nouns now have full capability parity — every API on the noun side has a verb-side mirror, including the new `brain.updateRelation()` (which closed a pre-7.30 gap where relationships had no update path).
|
||||
|
||||
### Standard verb fields
|
||||
|
||||
The verb-side equivalent of `STANDARD_ENTITY_FIELDS` is `STANDARD_VERB_FIELDS`, exported from `src/coreTypes.ts`. Verb-specific standard fields:
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `verb` | The VerbType enum value |
|
||||
| `sourceId` / `targetId` | The two endpoints of the relationship |
|
||||
| `subtype` | Sub-classification within the VerbType (7.30+) |
|
||||
| `confidence`, `weight`, `createdAt`, `updatedAt`, `service`, `createdBy`, `data` | Same semantics as the noun-side standard fields |
|
||||
|
||||
The companion `resolveVerbField(verb, field)` helper resolves field paths the same way `resolveEntityField` does for nouns: standard fields first, metadata fallback for everything else.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
|
|
|||
|
|
@ -214,6 +214,37 @@ brain.find({
|
|||
|
||||
See the **[Subtypes & Facets guide](./guides/subtypes-and-facets.md)** for the full surface.
|
||||
|
||||
### Filter relationships by subtype (7.30+)
|
||||
|
||||
Verbs are first-class peers — `getRelations()` and graph traversal both honor subtype filters on the fast path:
|
||||
|
||||
```typescript
|
||||
// Filter relationships by VerbType subtype
|
||||
const direct = await brain.getRelations({
|
||||
from: ceoId,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct'
|
||||
})
|
||||
|
||||
// Set membership on verb subtype
|
||||
const all = await brain.getRelations({
|
||||
from: ceoId,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: ['direct', 'dotted-line']
|
||||
})
|
||||
|
||||
// Graph traversal — subtype filters traversal edges (depth-1 in 7.30 JS path;
|
||||
// multi-hop subtype filtering lands on Cortex native)
|
||||
const reports = await brain.find({
|
||||
connected: {
|
||||
from: ceoId,
|
||||
via: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
depth: 1
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Combine semantic search with filters
|
||||
|
||||
```typescript
|
||||
|
|
|
|||
|
|
@ -608,9 +608,10 @@ Create a typed relationship between entities.
|
|||
const relId = await brain.relate({
|
||||
from: sourceId,
|
||||
to: targetId,
|
||||
type: VerbType.RelatedTo,
|
||||
data: 'Collaborated on the research paper', // Optional: content for this edge
|
||||
metadata: { // Optional: structured edge fields
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct', // Optional: sub-classification
|
||||
data: 'Collaborated on the research paper', // Optional: content for this edge
|
||||
metadata: { // Optional: structured edge fields
|
||||
strength: 0.9,
|
||||
role: 'primary author'
|
||||
}
|
||||
|
|
@ -621,6 +622,7 @@ const relId = await brain.relate({
|
|||
- `from`: `string` - Source entity ID (must exist)
|
||||
- `to`: `string` - Target entity ID (must exist)
|
||||
- `type`: `VerbType` - Relationship type
|
||||
- `subtype?`: `string` - Per-product sub-classification within the VerbType (top-level standard field, fast-path indexed). See [Subtypes & Facets](../guides/subtypes-and-facets.md).
|
||||
- `data?`: `any` - Content for the relationship (overrides auto-computed vector)
|
||||
- `metadata?`: `object` - Structured edge fields
|
||||
- `weight?`: `number` - Connection strength (0-1, default: 1.0)
|
||||
|
|
@ -631,6 +633,35 @@ const relId = await brain.relate({
|
|||
|
||||
---
|
||||
|
||||
### `updateRelation(params)` → `Promise<void>`
|
||||
|
||||
Update an existing relationship. Mirror of `update()` for verbs — closed a long-standing gap (verbs had no update path before 7.30).
|
||||
|
||||
```typescript
|
||||
// Change the subtype on an existing relationship
|
||||
await brain.updateRelation({ id: relId, subtype: 'dotted-line' })
|
||||
|
||||
// Update weight + confidence
|
||||
await brain.updateRelation({ id: relId, weight: 0.7, confidence: 0.9 })
|
||||
|
||||
// Change verb type (re-indexes in graph adjacency, id preserved)
|
||||
await brain.updateRelation({ id: relId, type: VerbType.WorksWith })
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `id`: `string` - Relationship ID (required)
|
||||
- `type?`: `VerbType` - Change verb type (re-indexes in graph adjacency)
|
||||
- `subtype?`: `string` - Change sub-classification (omit to preserve existing)
|
||||
- `weight?`: `number` - New weight (0-1)
|
||||
- `confidence?`: `number` - New confidence (0-1)
|
||||
- `data?`: `any` - New content
|
||||
- `metadata?`: `object` - Metadata to merge (or replace with `merge: false`)
|
||||
- `merge?`: `boolean` - Merge or replace metadata (default: true)
|
||||
|
||||
**Returns:** `Promise<void>`
|
||||
|
||||
---
|
||||
|
||||
### `getRelations(params)` → `Promise<Relation[]>`
|
||||
|
||||
Get relationships for an entity.
|
||||
|
|
@ -647,14 +678,32 @@ const related = await brain.getRelations({
|
|||
from: entityId,
|
||||
type: VerbType.Contains
|
||||
})
|
||||
|
||||
// Filter by subtype (fast path, column-store hit)
|
||||
const direct = await brain.getRelations({
|
||||
from: entityId,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct'
|
||||
})
|
||||
|
||||
// Set membership on subtype
|
||||
const all = await brain.getRelations({
|
||||
from: entityId,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: ['direct', 'dotted-line']
|
||||
})
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `from?`: `string` - Source entity ID
|
||||
- `to?`: `string` - Target entity ID
|
||||
- `type?`: `VerbType` - Filter by relationship type
|
||||
- `type?`: `VerbType | VerbType[]` - Filter by relationship type
|
||||
- `subtype?`: `string | string[]` - Filter by VerbType subtype (top-level standard field, fast path)
|
||||
- `service?`: `string` - Multi-tenancy filter
|
||||
- `limit?`: `number` - Pagination limit (default: 100)
|
||||
- `offset?`: `number` - Pagination offset
|
||||
|
||||
**Returns:** `Promise<Relation[]>` - Matching relationships
|
||||
**Returns:** `Promise<Relation[]>` - Matching relationships (each with `subtype` at top level when set)
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -1970,6 +2019,81 @@ brain.subtypesOf(NounType.Person)
|
|||
// → ['customer', 'employee', 'vendor']
|
||||
```
|
||||
|
||||
#### `counts.byRelationshipSubtype(verb, subtype?)` → `Record<string, number> | number`
|
||||
|
||||
Verb-side mirror of `counts.bySubtype`. O(1) per-VerbType-per-subtype counts.
|
||||
|
||||
```typescript
|
||||
brain.counts.byRelationshipSubtype(VerbType.ReportsTo)
|
||||
// → { direct: 12, 'dotted-line': 3 }
|
||||
|
||||
brain.counts.byRelationshipSubtype(VerbType.ReportsTo, 'direct')
|
||||
// → 12
|
||||
```
|
||||
|
||||
#### `counts.topRelationshipSubtypes(verb, n=10)` → `Array<[subtype, count]>`
|
||||
|
||||
Top N subtypes for a `VerbType` ranked by count.
|
||||
|
||||
```typescript
|
||||
brain.counts.topRelationshipSubtypes(VerbType.ReportsTo, 3)
|
||||
// → [['direct', 12], ['dotted-line', 3]]
|
||||
```
|
||||
|
||||
#### `relationshipSubtypesOf(verb)` → `string[]`
|
||||
|
||||
Sorted distinct subtypes seen for a `VerbType`.
|
||||
|
||||
```typescript
|
||||
brain.relationshipSubtypesOf(VerbType.ReportsTo)
|
||||
// → ['direct', 'dotted-line']
|
||||
```
|
||||
|
||||
#### `requireSubtype(type, options?)` → `void`
|
||||
|
||||
Register subtype enforcement for a specific `NounType` or `VerbType`. Unified API for nouns and verbs. Composes with the brain-wide `requireSubtype` constructor flag.
|
||||
|
||||
```typescript
|
||||
// Lock down Person sub-classification
|
||||
brain.requireSubtype(NounType.Person, {
|
||||
values: ['employee', 'customer', 'vendor'],
|
||||
required: true
|
||||
})
|
||||
|
||||
// Lock down management edges
|
||||
brain.requireSubtype(VerbType.ReportsTo, {
|
||||
values: ['direct', 'dotted-line'],
|
||||
required: true
|
||||
})
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `type`: `NounType | VerbType` - The type to register
|
||||
- `options.values?`: `string[]` - Vocabulary whitelist (rejects off-vocab values)
|
||||
- `options.required?`: `boolean` - Whether subtype is required (default: `true`)
|
||||
|
||||
#### Brain-wide strict mode — `new Brainy({ requireSubtype })`
|
||||
|
||||
Constructor option that enforces subtype on every `add()` / `addMany()` / `update()` / `relate()` / `relateMany()` / `updateRelation()` for every type:
|
||||
|
||||
```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 public write path checks the pairing guarantee.
|
||||
- `addMany()` / `relateMany()` validate all items BEFORE any storage write — atomic-fail, no partial writes.
|
||||
- Brainy's own VFS infrastructure writes bypass via the `metadata.isVFSEntity: true` marker.
|
||||
- Per-type registrations always apply regardless of the brain-wide flag.
|
||||
|
||||
Becomes the default in 8.0.0.
|
||||
|
||||
#### `trackField(name, options?)` → `void`
|
||||
|
||||
Register a metadata field for cardinality + per-NounType breakdown stats. With `values: [...]`, validates against the whitelist on `add()`/`update()`.
|
||||
|
|
|
|||
|
|
@ -467,7 +467,11 @@ brain.counts.bySubtype(NounType.Person)
|
|||
// → { employee: 12, customer: 847 }
|
||||
```
|
||||
|
||||
Subtype has its own statistics rollup (`_system/subtype-statistics.json`) maintained alongside `nounCountsByType`, so per-subtype counts stay O(1) at billion scale. Full guide: **[Subtypes & Facets](../guides/subtypes-and-facets.md)**.
|
||||
Subtype has its own statistics rollup (`_system/subtype-statistics.json`) maintained alongside `nounCountsByType`, so per-subtype counts stay O(1) at billion scale.
|
||||
|
||||
The same principle applies to **VerbTypes** (7.30+): the 127-verb taxonomy is intentionally coarse, and `subtype` is the per-product axis for relationships too. A `ReportsTo` relationship might carry `subtype: 'direct'` vs `'dotted-line'`; a `RelatedTo` edge might carry `'spouse'` / `'colleague'`. Verb-side rollup lives at `_system/verb-subtype-statistics.json` with identical shape to the noun-side rollup. Per-VerbType-per-subtype counts are O(1) via `brain.counts.byRelationshipSubtype()`. Brainy's design is fully symmetric — nouns and verbs are first-class peers with identical capability surfaces.
|
||||
|
||||
Full guide: **[Subtypes & Facets](../guides/subtypes-and-facets.md)**.
|
||||
|
||||
### 2. Semantic not Structural
|
||||
|
||||
|
|
|
|||
|
|
@ -271,8 +271,199 @@ await brain.counts.byField('status', { type: NounType.Event })
|
|||
await brain.migrateField({ from: 'metadata.kind', to: 'subtype' })
|
||||
```
|
||||
|
||||
## Layer V — `subtype` on relationships (verbs)
|
||||
|
||||
Relationships are first-class citizens too. Every verb (`VerbType`) gets the same `subtype` primitive: a `ReportsTo` relationship might carry `subtype: 'direct'` vs `'dotted-line'`; a `RelatedTo` edge might carry `'spouse'` / `'sibling'` / `'colleague'`. Same shape as the noun side: flat string, no hierarchy, top-level standard field, indexed on the fast path.
|
||||
|
||||
### Write
|
||||
|
||||
```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' })
|
||||
const matrixId = await brain.add({ type: NounType.Person, subtype: 'contractor', data: 'Sam' })
|
||||
|
||||
await brain.relate({
|
||||
from: ceoId,
|
||||
to: vpId,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct'
|
||||
})
|
||||
|
||||
await brain.relate({
|
||||
from: ceoId,
|
||||
to: matrixId,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'dotted-line'
|
||||
})
|
||||
```
|
||||
|
||||
### Read & filter
|
||||
|
||||
```typescript
|
||||
// Direct reports — fast path filter (column-store hit, not metadata fallback)
|
||||
const direct = await brain.getRelations({
|
||||
from: ceoId,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: 'direct'
|
||||
})
|
||||
|
||||
// Set membership
|
||||
const allReports = await brain.getRelations({
|
||||
from: ceoId,
|
||||
type: VerbType.ReportsTo,
|
||||
subtype: ['direct', 'dotted-line']
|
||||
})
|
||||
```
|
||||
|
||||
### Update (new — `updateRelation()`)
|
||||
|
||||
Verbs previously had no update method — the only way to change a relationship was delete-then-recreate. 7.30 closes that gap:
|
||||
|
||||
```typescript
|
||||
// Promote a dotted-line report to direct without losing the edge id
|
||||
await brain.updateRelation({ id: relationId, subtype: 'direct' })
|
||||
|
||||
// Or change weight/confidence
|
||||
await brain.updateRelation({ id: relationId, weight: 0.5, confidence: 0.9 })
|
||||
```
|
||||
|
||||
### Traversal filter
|
||||
|
||||
`find({ connected, subtype })` filters traversal edges by their subtype. Composes with `via` (verb-type filter):
|
||||
|
||||
```typescript
|
||||
// All direct reports two hops deep (will be supported in Cortex; for now depth-1)
|
||||
const directChain = await brain.find({
|
||||
connected: {
|
||||
from: ceoId,
|
||||
via: VerbType.ReportsTo,
|
||||
subtype: 'direct',
|
||||
depth: 1
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Multi-hop subtype filtering (`depth > 1`) lights up on the Cortex native path; the JS path throws today rather than return incorrect partial results.
|
||||
|
||||
### Counts
|
||||
|
||||
Same shape as the noun-side counts API:
|
||||
|
||||
```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']
|
||||
```
|
||||
|
||||
These are O(1) lookups backed by the persisted `_system/verb-subtype-statistics.json` rollup — same self-heal machinery as the noun-side rollup.
|
||||
|
||||
### Migrate verb fields
|
||||
|
||||
`migrateField()` now walks verbs too:
|
||||
|
||||
```typescript
|
||||
// Migrate verb-side metadata.kind → top-level subtype
|
||||
await brain.migrateField({
|
||||
from: 'metadata.kind',
|
||||
to: 'subtype',
|
||||
entityKind: 'verb'
|
||||
})
|
||||
|
||||
// Or migrate both nouns and verbs in one pass
|
||||
await brain.migrateField({
|
||||
from: 'metadata.kind',
|
||||
to: 'subtype',
|
||||
entityKind: 'both'
|
||||
})
|
||||
```
|
||||
|
||||
Default is `entityKind: 'noun'` (backward-compatible with 7.29).
|
||||
|
||||
## Enforcement — `requireSubtype()` + brain-wide strict mode
|
||||
|
||||
By default (7.30) subtype is optional. Two complementary opt-in mechanisms let you enforce the pairing of type + subtype on every write:
|
||||
|
||||
### Per-type registration
|
||||
|
||||
Mark a specific `NounType` or `VerbType` 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 a 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'
|
||||
})
|
||||
```
|
||||
|
||||
### Brain-wide strict mode
|
||||
|
||||
Enforce on every write across the whole brain:
|
||||
|
||||
```typescript
|
||||
const brain = new Brainy({ requireSubtype: true })
|
||||
|
||||
// Allow specific types to omit subtype (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.
|
||||
- Per-type rules registered via `requireSubtype()` compose with the brain-wide flag; specific rules win when both apply.
|
||||
- Brainy's own internal writes (VFS root, VFS directories, VFS file entities) bypass enforcement via the `metadata.isVFSEntity: true` infrastructure marker.
|
||||
|
||||
### Migrating to strict mode on an existing brain
|
||||
|
||||
`brain.migrateField()` is your friend — populate `subtype` from an existing convention before flipping strict mode on:
|
||||
|
||||
```typescript
|
||||
// Step 1: backfill subtype from your existing metadata.kind convention
|
||||
await brain.migrateField({
|
||||
from: 'metadata.kind',
|
||||
to: 'subtype',
|
||||
entityKind: 'both'
|
||||
})
|
||||
|
||||
// Step 2: register the vocabulary
|
||||
brain.requireSubtype(NounType.Person, {
|
||||
values: ['employee', 'customer', 'vendor'],
|
||||
required: true
|
||||
})
|
||||
|
||||
// Step 3: future writes must have a subtype matching the vocabulary
|
||||
await brain.add({ type: NounType.Person, subtype: 'employee', data: '...' })
|
||||
```
|
||||
|
||||
## Reference
|
||||
|
||||
### Layer 1 — `subtype` (nouns)
|
||||
|
||||
- `brain.add({ ..., subtype: 'value' })` — write the field
|
||||
- `brain.update({ id, subtype: 'value' })` — change the field
|
||||
- `brain.find({ type, subtype })` — filter (fast path)
|
||||
|
|
@ -280,6 +471,29 @@ await brain.migrateField({ from: 'metadata.kind', to: 'subtype' })
|
|||
- `brain.counts.bySubtype(type, subtype?)` — O(1) counts
|
||||
- `brain.counts.topSubtypes(type, n?)` — top N by count
|
||||
- `brain.subtypesOf(type)` — distinct subtype list
|
||||
|
||||
### Layer V — `subtype` (verbs / relationships)
|
||||
|
||||
- `brain.relate({ ..., subtype: 'value' })` — write the field
|
||||
- `brain.updateRelation({ id, subtype, type?, weight?, ... })` — change a relationship
|
||||
- `brain.getRelations({ subtype })` — filter (fast path)
|
||||
- `brain.getRelations({ subtype: ['a', 'b'] })` — set membership
|
||||
- `brain.find({ connected: { via, subtype, depth } })` — traversal filter
|
||||
- `brain.counts.byRelationshipSubtype(verb, subtype?)` — O(1) counts
|
||||
- `brain.counts.topRelationshipSubtypes(verb, n?)` — top N by count
|
||||
- `brain.relationshipSubtypesOf(verb)` — distinct subtype list
|
||||
|
||||
### Layer 2 — generic facets
|
||||
|
||||
- `brain.trackField(name, { perType?, values? })` — register a facet
|
||||
- `brain.counts.byField(name, { type? })` — facet counts
|
||||
- `brain.migrateField({ from, to, readBoth?, batchSize?, onProgress? })` — rewrite a field
|
||||
|
||||
### Layer 3 — migration
|
||||
|
||||
- `brain.migrateField({ from, to, readBoth?, batchSize?, onProgress?, entityKind? })` — rewrite a field (nouns, verbs, or both)
|
||||
|
||||
### Enforcement
|
||||
|
||||
- `brain.requireSubtype(type, { values?, required })` — per-`NounType` / `VerbType` rule
|
||||
- `new Brainy({ requireSubtype: true })` — brain-wide strict mode
|
||||
- `new Brainy({ requireSubtype: { except: [type, ...] } })` — strict with exemptions
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue