feat: graph link compression — delta-varint connections (2.4.0 #3)
Cortex registers a graph:compression provider exposing `encode`/`decode`
for HNSW connection lists (delta-varint, ~4× smaller edges than the
JSON-UUID-array shape brainy has persisted since the beginning). This
wires the consumer side without changing the saveHNSWData/getHNSWData
adapter signatures.
Architecture:
- NEW ConnectionsCodec (src/hnsw/connectionsCodec.ts) — translates between
UUID-keyed Map<level, Set<UUID>> and a single compact per-node buffer
via the provider + stable EntityIdMapper. Custom wire format:
[count: u8] [[level: u8] [len: u32 LE] [bytes...]]*. One blob per node
regardless of level count, so the storage I/O is identical to the
legacy path (one saveHNSWData call) plus a single saveBinaryBlob.
- HNSWIndex changes — a `connectionsCodec: ConnectionsCodec | null` field
with `setConnectionsCodec()` setter. THREE save sites (deferred flush,
immediate-mode entity persist, immediate-mode neighbor updates) now go
through a single `persistNodeConnections(nodeId, noun)` helper: when the
codec is wired AND the storage adapter exposes saveBinaryBlob, it
encodes the connections, stores them at `_hnsw_conn/<nodeId>`, and
records `connections: {}` in saveHNSWData as the marker. Otherwise the
legacy JSON-array path is taken. TWO load sites use a matching
`restoreNodeConnections` helper that tries loadBinaryBlob first and
falls back to the legacy hnswData.connections field on miss / decode
error. Format convergence is lazy: pre-2.4.0 nodes still load via the
legacy path, then write the compressed form on next dirty save.
- brainy.ts wireConnectionsCodec — runs alongside wireMmapVectorBackend
during init. Activates when (a) the graph:compression provider is
registered and (b) the metadata index exposes its idMapper. Unlike the
mmap-vector backend, this layer engages on EVERY brainy 7.25.0
adapter — the blob primitive itself is universal; only the codec
presence gates activation.
- Provider interface GraphCompressionProvider in plugin.ts — encode +
decode static signatures. Brainy depends on the interface; cortex's
registered { encode: encodeConnections, decode: decodeConnections }
satisfies it structurally.
Tests (1437 total, +4 vs prior tip):
- tests/unit/hnsw/connections-codec.test.ts — 4 unit tests with a JSON
mock provider: single-level round-trip, empty-Map round-trip (one-byte
buffer), multi-level fan-out round-trip, and silent-drop of UUIDs the
decoding idMapper no longer knows. The real cross-language byte
format is exercised when cortex 2.4.0 wires its delta-varint
encode/decode in.
This completes the brainy half of the 2.4.0 storage foundation: stable
ids (#23), mmap vectors (#24), graph link compression (#25), and
column-store interchange (#26). Coordinated release as brainy 7.26.0 +
cortex 2.4.0 follows.
2026-05-28 10:37:01 -07:00
/ * *
* @module hnsw / connectionsCodec
* @description Bridge between brainy ' s UUID - keyed HNSW connection sets and
2026-07-02 15:11:41 -07:00
* cor ' s int - keyed delta - varint encode / decode ( the ` graph:compression `
feat: graph link compression — delta-varint connections (2.4.0 #3)
Cortex registers a graph:compression provider exposing `encode`/`decode`
for HNSW connection lists (delta-varint, ~4× smaller edges than the
JSON-UUID-array shape brainy has persisted since the beginning). This
wires the consumer side without changing the saveHNSWData/getHNSWData
adapter signatures.
Architecture:
- NEW ConnectionsCodec (src/hnsw/connectionsCodec.ts) — translates between
UUID-keyed Map<level, Set<UUID>> and a single compact per-node buffer
via the provider + stable EntityIdMapper. Custom wire format:
[count: u8] [[level: u8] [len: u32 LE] [bytes...]]*. One blob per node
regardless of level count, so the storage I/O is identical to the
legacy path (one saveHNSWData call) plus a single saveBinaryBlob.
- HNSWIndex changes — a `connectionsCodec: ConnectionsCodec | null` field
with `setConnectionsCodec()` setter. THREE save sites (deferred flush,
immediate-mode entity persist, immediate-mode neighbor updates) now go
through a single `persistNodeConnections(nodeId, noun)` helper: when the
codec is wired AND the storage adapter exposes saveBinaryBlob, it
encodes the connections, stores them at `_hnsw_conn/<nodeId>`, and
records `connections: {}` in saveHNSWData as the marker. Otherwise the
legacy JSON-array path is taken. TWO load sites use a matching
`restoreNodeConnections` helper that tries loadBinaryBlob first and
falls back to the legacy hnswData.connections field on miss / decode
error. Format convergence is lazy: pre-2.4.0 nodes still load via the
legacy path, then write the compressed form on next dirty save.
- brainy.ts wireConnectionsCodec — runs alongside wireMmapVectorBackend
during init. Activates when (a) the graph:compression provider is
registered and (b) the metadata index exposes its idMapper. Unlike the
mmap-vector backend, this layer engages on EVERY brainy 7.25.0
adapter — the blob primitive itself is universal; only the codec
presence gates activation.
- Provider interface GraphCompressionProvider in plugin.ts — encode +
decode static signatures. Brainy depends on the interface; cortex's
registered { encode: encodeConnections, decode: decodeConnections }
satisfies it structurally.
Tests (1437 total, +4 vs prior tip):
- tests/unit/hnsw/connections-codec.test.ts — 4 unit tests with a JSON
mock provider: single-level round-trip, empty-Map round-trip (one-byte
buffer), multi-level fan-out round-trip, and silent-drop of UUIDs the
decoding idMapper no longer knows. The real cross-language byte
format is exercised when cortex 2.4.0 wires its delta-varint
encode/decode in.
This completes the brainy half of the 2.4.0 storage foundation: stable
ids (#23), mmap vectors (#24), graph link compression (#25), and
column-store interchange (#26). Coordinated release as brainy 7.26.0 +
cortex 2.4.0 follows.
2026-05-28 10:37:01 -07:00
* provider ) . Translates UUIDs to stable int slots via the post - 2.4 . 0 # 1
* ` EntityIdMapper ` , batches all of a node ' s per - level connection lists into
* a single compact buffer , and reverses the path on load .
*
* Wire format of the per - node buffer the codec produces :
*
* [ num_levels : u8 ]
* repeated num_levels times :
* [ level : u8 ]
* [ encoded_len : u32 LE ]
* [ encoded_bytes : < encoded_len > bytes ] < - provider . encode ( ints )
*
* Why a single buffer per node and not one blob per level :
* - One ` saveBinaryBlob ` call per persisted node , regardless of level count .
* On hot insert paths an HNSW node may be at levels 0 . . 3 ; the per - node
* blob trades a one - byte count for 3 saved I / O round - trips .
* - Read path is symmetric : one ` loadBinaryBlob ` reconstructs the full
* connection set , so HNSW rebuild stays linear in the number of nodes .
*
* Loose UUIDs ( ints assigned to entities that no longer exist ) are silently
* dropped on decode . They can ' t legitimately participate in graph traversal
* because the entity is gone ; treating them as ` undefined ` UUIDs matches the
* pre - 2.4 . 0 behaviour of an ` idMapper.getUuid() ` miss in the legacy load path .
* /
import type {
EntityIdMapperProvider ,
GraphCompressionProvider
} from '../plugin.js'
export class ConnectionsCodec {
constructor (
private readonly provider : GraphCompressionProvider ,
private readonly idMapper : EntityIdMapperProvider
) { }
/ * *
* @description Encode all levels of a node ' s connections into one compact
* buffer . Empty input → an empty buffer ( zero levels , no body ) . The
* idMapper assigns stable ints on demand if a connection UUID has never
* been seen — append - only , matches the rest of the 2.4 . 0 contract .
* /
encode ( connectionsByLevel : Map < number , Set < string > > ) : Buffer {
if ( connectionsByLevel . size === 0 ) {
// Single byte: zero levels. Round-trips cleanly through decode.
return Buffer . from ( [ 0 ] )
}
if ( connectionsByLevel . size > 0xff ) {
throw new Error (
` ConnectionsCodec.encode: too many HNSW levels ( ${ connectionsByLevel . size } ); ` +
` the per-node header is one byte (max 255 levels). `
)
}
// First pass: encode each level into its own buffer.
const levelBuffers : Array < { level : number ; buf : Buffer } > = [ ]
let bodyBytes = 0
for ( const [ level , uuids ] of connectionsByLevel ) {
if ( level > 0xff ) {
throw new Error (
` ConnectionsCodec.encode: level ${ level } exceeds u8 header range (max 255). `
)
}
const ints : number [ ] = [ ]
for ( const uuid of uuids ) {
ints . push ( this . idMapper . getOrAssign ( uuid ) )
}
const buf = this . provider . encode ( ints )
levelBuffers . push ( { level , buf } )
bodyBytes += 1 + 4 + buf . length // level + len + payload
}
// Second pass: concatenate with header.
const out = Buffer . alloc ( 1 + bodyBytes )
out . writeUInt8 ( levelBuffers . length , 0 )
let offset = 1
for ( const { level , buf } of levelBuffers ) {
out . writeUInt8 ( level , offset )
offset += 1
out . writeUInt32LE ( buf . length , offset )
offset += 4
buf . copy ( out , offset )
offset += buf . length
}
return out
}
/ * *
* @description Decode a per - node buffer back into a connections Map . Ints
* for entities the idMapper no longer knows are silently dropped — see the
* module - level note on why .
* /
decode ( data : Buffer ) : Map < number , Set < string > > {
const out = new Map < number , Set < string > > ( )
if ( data . length === 0 ) return out
const count = data . readUInt8 ( 0 )
let offset = 1
for ( let i = 0 ; i < count ; i ++ ) {
if ( offset + 5 > data . length ) {
throw new Error ( ` ConnectionsCodec.decode: truncated header at level ${ i } ` )
}
const level = data . readUInt8 ( offset )
offset += 1
const len = data . readUInt32LE ( offset )
offset += 4
if ( offset + len > data . length ) {
throw new Error ( ` ConnectionsCodec.decode: truncated body at level ${ level } ` )
}
const levelBuf = data . slice ( offset , offset + len )
offset += len
const ints = this . provider . decode ( levelBuf )
const uuids = new Set < string > ( )
for ( const intId of ints ) {
const uuid = this . idMapper . getUuid ( intId )
if ( uuid !== undefined ) uuids . add ( uuid )
}
out . set ( level , uuids )
}
return out
}
}
/ * *
* @description Build the binary - blob storage key for a node ' s compressed
* connections . Suffix - free ; the adapter appends its own . Keys live under
* ` _hnsw_conn/ ` so they don ' t collide with the existing ` _column_index/ `
2026-07-02 15:11:41 -07:00
* blobs and so a cor - side reader knows exactly where to look .
feat: graph link compression — delta-varint connections (2.4.0 #3)
Cortex registers a graph:compression provider exposing `encode`/`decode`
for HNSW connection lists (delta-varint, ~4× smaller edges than the
JSON-UUID-array shape brainy has persisted since the beginning). This
wires the consumer side without changing the saveHNSWData/getHNSWData
adapter signatures.
Architecture:
- NEW ConnectionsCodec (src/hnsw/connectionsCodec.ts) — translates between
UUID-keyed Map<level, Set<UUID>> and a single compact per-node buffer
via the provider + stable EntityIdMapper. Custom wire format:
[count: u8] [[level: u8] [len: u32 LE] [bytes...]]*. One blob per node
regardless of level count, so the storage I/O is identical to the
legacy path (one saveHNSWData call) plus a single saveBinaryBlob.
- HNSWIndex changes — a `connectionsCodec: ConnectionsCodec | null` field
with `setConnectionsCodec()` setter. THREE save sites (deferred flush,
immediate-mode entity persist, immediate-mode neighbor updates) now go
through a single `persistNodeConnections(nodeId, noun)` helper: when the
codec is wired AND the storage adapter exposes saveBinaryBlob, it
encodes the connections, stores them at `_hnsw_conn/<nodeId>`, and
records `connections: {}` in saveHNSWData as the marker. Otherwise the
legacy JSON-array path is taken. TWO load sites use a matching
`restoreNodeConnections` helper that tries loadBinaryBlob first and
falls back to the legacy hnswData.connections field on miss / decode
error. Format convergence is lazy: pre-2.4.0 nodes still load via the
legacy path, then write the compressed form on next dirty save.
- brainy.ts wireConnectionsCodec — runs alongside wireMmapVectorBackend
during init. Activates when (a) the graph:compression provider is
registered and (b) the metadata index exposes its idMapper. Unlike the
mmap-vector backend, this layer engages on EVERY brainy 7.25.0
adapter — the blob primitive itself is universal; only the codec
presence gates activation.
- Provider interface GraphCompressionProvider in plugin.ts — encode +
decode static signatures. Brainy depends on the interface; cortex's
registered { encode: encodeConnections, decode: decodeConnections }
satisfies it structurally.
Tests (1437 total, +4 vs prior tip):
- tests/unit/hnsw/connections-codec.test.ts — 4 unit tests with a JSON
mock provider: single-level round-trip, empty-Map round-trip (one-byte
buffer), multi-level fan-out round-trip, and silent-drop of UUIDs the
decoding idMapper no longer knows. The real cross-language byte
format is exercised when cortex 2.4.0 wires its delta-varint
encode/decode in.
This completes the brainy half of the 2.4.0 storage foundation: stable
ids (#23), mmap vectors (#24), graph link compression (#25), and
column-store interchange (#26). Coordinated release as brainy 7.26.0 +
cortex 2.4.0 follows.
2026-05-28 10:37:01 -07:00
* /
export function compressedConnectionsKey ( nodeId : string ) : string {
return ` _hnsw_conn/ ${ nodeId } `
}