2025-11-14 10:26:23 -08:00
/ * *
* Duplicate Relationship Check Optimization Tests
*
* Tests for v5 . 8.0 optimization that uses GraphAdjacencyIndex
* for O ( log n ) duplicate detection instead of O ( n ) storage scan .
* /
import { describe , it , expect , beforeEach , afterEach } from 'vitest'
import { Brainy } from '../../../src/brainy.js'
import { NounType , VerbType } from '../../../src/types/graphTypes.js'
describe ( 'Duplicate Check Optimization' , ( ) = > {
let brain : Brainy
beforeEach ( async ( ) = > {
feat(8.0)!: flip requireSubtype default to true (BRAINY-8.0-SUBTYPE-CONTRACT § C-1)
Brainy 8.0 makes subtype required by default on every public write path
(`add`, `addMany`, `update`, `relate`, `relateMany`, `updateRelation`,
import). Per the locked C-1 contract, every entity and relation gets a
non-empty subtype string by the time the storage layer sees it.
OPT-OUT REMAINS FULLY SUPPORTED
The runtime flag is still consumer-controlled. Three opt-out paths
cover migration / legacy fixtures / typed escape:
- `new Brainy({ requireSubtype: false })` — last-resort: turn off the
contract entirely. Recommended only for migration windows or test
fixtures that legitimately can't supply a subtype.
- `new Brainy({ requireSubtype: { except: [NounType.Thing, ...] } })` —
per-type allowlist: strict everywhere except the listed types.
- `brain.requireSubtype(type, options)` — per-type registration with
optional vocabulary. Composes with the brain-wide flag.
Default is now `true`. Opt-out is explicit and documented; nothing
silently degrades.
TEST SWEEP
Bulk-applied `requireSubtype: false` to every `new Brainy({...})` call
site across 120 test files. Three sed patterns covered the shapes:
- `new Brainy({` → `new Brainy({ requireSubtype: false,`
- `new Brainy<T>({` → `new Brainy<T>({ requireSubtype: false,`
- `new Brainy()` → `new Brainy({ requireSubtype: false })`
tests/helpers/test-factory.ts → createTestConfig() defaults
`requireSubtype: false` so test files using the helper inherit the
opt-out without per-site edits.
The test sites that DO exercise subtype semantics (the
subtype-and-facets suite, the strict-mode-self-test suite, the verb-
subtype-and-enforcement suite, etc.) already pass real subtypes — they
were the 7.30.x acceptance tests for this contract. Those tests
continue to pass unchanged.
CHANGES
src/brainy.ts
- normalizeConfig() — `requireSubtype` default `false` → `true`.
Comment refreshed to document the three opt-out paths.
tests/* (120 files)
- Bulk-edited brain construction sites. No functional test changes; the
opt-out preserves the test author's original intent.
tests/helpers/test-factory.ts
- createTestConfig() base config gains `requireSubtype: false`.
NO-OP for consumers who were already passing subtype on every write.
For consumers who weren't, the upgrade path is one of the three opt-out
forms above. Migration recipe documented in 8.0 release notes (next
commit).
VERIFICATION
- npx tsc --noEmit: clean
- npm test: 1408 / 1409 (same pre-existing race-condition outstanding;
no other regressions from the flip)
2026-06-09 14:58:25 -07:00
brain = new Brainy ( { requireSubtype : false } )
2025-11-14 10:26:23 -08:00
await brain . init ( )
} )
afterEach ( async ( ) = > {
// Cleanup is automatic with memory storage
} )
it ( 'should detect duplicate relationships using GraphAdjacencyIndex' , async ( ) = > {
// Create two entities
const personId = await brain . add ( {
data : { name : 'Alice' } ,
type : NounType . Person
} )
const orgId = await brain . add ( {
data : { name : 'Acme Corp' } ,
type : NounType . Organization
} )
// Create first relationship
const relationId1 = await brain . relate ( {
from : personId ,
to : orgId ,
type : VerbType . ParticipatesIn
} )
// Attempt to create duplicate relationship
const relationId2 = await brain . relate ( {
from : personId ,
to : orgId ,
type : VerbType . ParticipatesIn
} )
// Should return the same ID (duplicate detected)
expect ( relationId2 ) . toBe ( relationId1 )
// Verify only one relationship exists
2026-06-11 14:51:00 -07:00
const relations = await brain . related ( { from : personId } )
2025-11-14 10:26:23 -08:00
expect ( relations ) . toHaveLength ( 1 )
expect ( relations [ 0 ] . id ) . toBe ( relationId1 )
} )
it ( 'should allow different relationship types between same entities' , async ( ) = > {
const personId = await brain . add ( {
data : { name : 'Bob' } ,
type : NounType . Person
} )
const projectId = await brain . add ( {
data : { name : 'Project X' } ,
type : NounType . Thing
} )
// Create first relationship
const relationId1 = await brain . relate ( {
from : personId ,
to : projectId ,
type : VerbType . Creates
} )
// Create second relationship with different type (not a duplicate)
const relationId2 = await brain . relate ( {
from : personId ,
to : projectId ,
type : VerbType . Modifies
} )
// Should be different IDs (different verb types)
expect ( relationId2 ) . not . toBe ( relationId1 )
// Verify both relationships exist
2026-06-11 14:51:00 -07:00
const relations = await brain . related ( { from : personId } )
2025-11-14 10:26:23 -08:00
expect ( relations ) . toHaveLength ( 2 )
// Both relations should exist with different IDs
const relationIds = relations . map ( r = > r . id )
expect ( relationIds ) . toContain ( relationId1 )
expect ( relationIds ) . toContain ( relationId2 )
} )
it ( 'should handle duplicate check with many relationships (performance)' , async ( ) = > {
// Create source entity
const sourceId = await brain . add ( {
data : { name : 'Hub Entity' } ,
type : NounType . Thing
} )
// Create 50 target entities and relationships
const targetIds : string [ ] = [ ]
for ( let i = 0 ; i < 50 ; i ++ ) {
const targetId = await brain . add ( {
data : { name : ` Target ${ i } ` } ,
type : NounType . Thing
} )
targetIds . push ( targetId )
await brain . relate ( {
from : sourceId ,
to : targetId ,
type : VerbType . RelatesTo
} )
}
// Now attempt to create duplicate with first target (should be fast with GraphIndex)
const startTime = performance . now ( )
const duplicateId = await brain . relate ( {
from : sourceId ,
to : targetIds [ 0 ] ,
type : VerbType . RelatesTo
} )
const elapsed = performance . now ( ) - startTime
// Should be fast with O(log n) GraphIndex lookup (< 10ms even with 50 relationships)
expect ( elapsed ) . toBeLessThan ( 10 )
// Verify duplicate was detected
2026-06-11 14:51:00 -07:00
const relations = await brain . related ( { from : sourceId } )
2025-11-14 10:26:23 -08:00
expect ( relations ) . toHaveLength ( 50 ) // No duplicate created
} )
it ( 'should use cached verb data for duplicate check' , async ( ) = > {
const entityA = await brain . add ( {
data : { name : 'Entity A' } ,
type : NounType . Thing
} )
const entityB = await brain . add ( {
data : { name : 'Entity B' } ,
type : NounType . Thing
} )
// Create relationship
const relationId1 = await brain . relate ( {
from : entityA ,
to : entityB ,
type : VerbType . RelatesTo
} )
feat(8.0): u64 BigInt graph provider contract — punch list a-d,g,h
GraphIndexProvider now speaks BigInt at the boundary (D.2 mirror):
- getNeighbors/getVerbIdsBySource/getVerbIdsByTarget take entity ints and
return entity/verb ints as bigint[]
- new REQUIRED verbIntsToIds(bigint[]) batch reverse resolver (L.7
identity-fingerprint design — verb ids are UUIDs by contract, so the
provider-side interning is losslessly reversible)
- addVerb(verb, sourceInt, targetInt) returns the interned verb int;
removeVerb(verbId) joins the contract
Coordinator (brainy.ts) owns ALL UUID <-> int conversion: getOrAssign on
writes, getInt on reads (unmapped UUID -> empty result without calling the
provider), getUuid / verbIntsToIds on returns, plus a bounded ~100k-entry
insertion-order warm cache for verb-int -> verb-id pairs fed by addVerb
returns and resolver results. GraphVerb gains derived sourceInt/targetInt
(populated at add time, never persisted). findConnectedSubtype gains a
native fast path that routes single-type single-subtype outgoing BFS
through the provider when available.
JS GraphAdjacencyIndex satisfies the contract while staying string/u32-keyed
internally: entity ints resolve through the shared entity-id mapper (threaded
in by the coordinator on init/fork/checkout), verb ints come from an
in-process append-only interning map re-derived from storage on
rebuild/cold-start. ColumnStoreProvider widens the same way: addEntity/
removeEntity take bigint, sortTopK/filteredSortTopK return bigint[].
relate() now rejects a caller-supplied id with a teaching error — verb ids
are brainy-generated UUIDs by contract in 8.0 (previously a passed id was
silently ignored). No Roaring64 provider-boundary decode site exists yet;
the JS-internal column store stays Roaring32 and the Treemap decoder lands
with the first consumer of provider-returned filter buffers.
Public brain API unchanged. 1413 tests green (+10 new BigInt contract tests).
2026-06-10 10:45:45 -07:00
// Access GraphIndex to ensure verb is cached (8.0 BigInt boundary:
// UUID → entity int in, verb ints out, resolved back via verbIntsToIds)
const idMapper = ( brain as any ) . metadataIndex . getIdMapper ( )
const sourceInt = BigInt ( idMapper . getInt ( entityA ) ! )
const verbInts : bigint [ ] = await ( brain as any ) . graphIndex . getVerbIdsBySource ( sourceInt )
const verbIds = await ( brain as any ) . graphIndex . verbIntsToIds ( verbInts )
2025-11-14 10:26:23 -08:00
expect ( verbIds ) . toContain ( relationId1 )
// Attempt duplicate (should use cached verb)
const startTime = performance . now ( )
const relationId2 = await brain . relate ( {
from : entityA ,
to : entityB ,
type : VerbType . RelatesTo
} )
const elapsed = performance . now ( ) - startTime
// Should be very fast with cached verb (< 5ms)
expect ( elapsed ) . toBeLessThan ( 5 )
expect ( relationId2 ) . toBe ( relationId1 )
} )
it ( 'should handle duplicate check across multiple verb types efficiently' , async ( ) = > {
const person = await brain . add ( {
data : { name : 'Charlie' } ,
type : NounType . Person
} )
const org = await brain . add ( {
data : { name : 'BigCorp' } ,
type : NounType . Organization
} )
// Create different relationship types
const rel1 = await brain . relate ( {
from : person ,
to : org ,
type : VerbType . Affects
} )
const rel2 = await brain . relate ( {
from : person ,
to : org ,
type : VerbType . Owns
} )
// Verify both relationships exist
2026-06-11 14:51:00 -07:00
let relations = await brain . related ( { from : person } )
2025-11-14 10:26:23 -08:00
expect ( relations ) . toHaveLength ( 2 )
const relationIds = relations . map ( r = > r . id )
expect ( relationIds ) . toContain ( rel1 )
expect ( relationIds ) . toContain ( rel2 )
// Attempt duplicate of first relationship type
const startTime = performance . now ( )
const duplicate = await brain . relate ( {
from : person ,
to : org ,
type : VerbType . Affects
} )
const elapsed = performance . now ( ) - startTime
// Should detect duplicate efficiently (< 20ms)
expect ( elapsed ) . toBeLessThan ( 20 )
expect ( duplicate ) . toBe ( rel1 )
// Verify still same number of relationships (no duplicate added)
2026-06-11 14:51:00 -07:00
const finalRelations = await brain . related ( { from : person } )
2025-11-14 10:26:23 -08:00
expect ( finalRelations . length ) . toBe ( relations . length )
} )
} )