feat: vector provider identity is a required name field (hnsw-js), rendered [vector-index:<name>]
Reconciles the vector-index rename to the ruled three-layer naming: the provider contract's identity field is now a REQUIRED readonly name (was optional providerId), self-reported and truthful, rendered as [vector-index:<name>] where the index identifies itself and stamped into the op-name strings journals already parse (AddToVectorIndex(<name>)). The built-in JS engine names itself hnsw-js. A runtime provider instance compiled against the previous optional contract is tolerated - never crashed on, never silently mislabeled: it stamps unknown-provider and emits one loud warning naming the missing field. Graph index operations keep their static names (they never interpolate provider identity), and no public API exports a provider-routed hnsw-carrying name, so no deprecation shim is required.
This commit is contained in:
parent
16832671fb
commit
ec57174760
6 changed files with 92 additions and 38 deletions
|
|
@ -6,12 +6,14 @@
|
|||
* journals and timings — a fossil "HNSW" name misdirects an operator running
|
||||
* a native (non-HNSW) vector provider. Verifies:
|
||||
* 1. rollback wiring stayed byte-identical under the rename,
|
||||
* 2. the emitted `name` stamps the active backend — `'js-hnsw'` for
|
||||
* Brainy's own JS index, a provider's own `providerId` when it
|
||||
* self-identifies, and the honest `'unknown-provider'` literal when
|
||||
* neither applies (never a silently-wrong guess).
|
||||
* 2. the emitted `name` stamps the active backend — `'hnsw-js'` for
|
||||
* Brainy's own JS index, a provider's own required `name` when it
|
||||
* self-identifies, and the tolerant-loud `'unknown-provider'` literal
|
||||
* (plus one console.warn) when a runtime instance lacks `name`
|
||||
* altogether (an older native provider compiled against the previous,
|
||||
* optional `providerId` contract) — never a silently-wrong guess.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest'
|
||||
import {
|
||||
AddToVectorIndexOperation,
|
||||
RemoveFromVectorIndexOperation,
|
||||
|
|
@ -28,7 +30,7 @@ import { JsHnswVectorIndex } from '../../../src/hnsw/hnswIndex.js'
|
|||
*/
|
||||
class FakeVectorProvider implements VectorIndexProvider {
|
||||
readonly items = new Map<string, Vector>()
|
||||
constructor(readonly providerId?: string) {}
|
||||
constructor(readonly name: string) {}
|
||||
|
||||
async addItem(item: VectorDocument): Promise<string> {
|
||||
this.items.set(item.id, item.vector)
|
||||
|
|
@ -115,16 +117,20 @@ describe('Vector index transaction operations — backend-neutral rename', () =>
|
|||
})
|
||||
|
||||
describe('backend stamping in the emitted op name', () => {
|
||||
it('stamps the real JS HNSW index as js-hnsw (self-identifying providerId)', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('stamps the real JS HNSW index as hnsw-js (self-identifying name)', () => {
|
||||
const index = new JsHnswVectorIndex()
|
||||
const addOp = new AddToVectorIndexOperation(index, 'id-1', [1, 2, 3])
|
||||
const removeOp = new RemoveFromVectorIndexOperation(index, 'id-1', [1, 2, 3])
|
||||
|
||||
expect(addOp.name).toBe('AddToVectorIndex(js-hnsw)')
|
||||
expect(removeOp.name).toBe('RemoveFromVectorIndex(js-hnsw)')
|
||||
expect(addOp.name).toBe('AddToVectorIndex(hnsw-js)')
|
||||
expect(removeOp.name).toBe('RemoveFromVectorIndex(hnsw-js)')
|
||||
})
|
||||
|
||||
it('stamps a self-identifying provider\'s own providerId, never "HNSW"', () => {
|
||||
it('stamps a self-identifying provider\'s own name, never "HNSW"', () => {
|
||||
const provider = new FakeVectorProvider('acme-diskann')
|
||||
const addOp = new AddToVectorIndexOperation(provider, 'id-1', [1, 2, 3])
|
||||
const removeOp = new RemoveFromVectorIndexOperation(provider, 'id-1', [1, 2, 3])
|
||||
|
|
@ -137,16 +143,34 @@ describe('Vector index transaction operations — backend-neutral rename', () =>
|
|||
expect(removeOp.name).not.toContain('HNSW')
|
||||
})
|
||||
|
||||
it('honestly reports "unknown-provider" rather than guessing when a provider omits providerId', () => {
|
||||
const provider = new FakeVectorProvider(undefined)
|
||||
const addOp = new AddToVectorIndexOperation(provider, 'id-1', [1, 2, 3])
|
||||
const removeOp = new RemoveFromVectorIndexOperation(provider, 'id-1', [1, 2, 3])
|
||||
it('tolerant-loud: stamps "unknown-provider" and warns exactly once when a runtime provider lacks the required `name` (an older native provider compiled against the previous optional `providerId` contract)', () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
// Simulate a native provider compiled before `name` was required — the
|
||||
// TypeScript contract requires `name`, but `as unknown as` mirrors what
|
||||
// actually reaches this code at runtime from an un-rebuilt native addon.
|
||||
const legacyProvider = {
|
||||
addItem: async (item: VectorDocument) => item.id,
|
||||
removeItem: async () => true,
|
||||
search: async () => [],
|
||||
size: () => 0,
|
||||
clear: () => {},
|
||||
rebuild: async () => {},
|
||||
flush: async () => 0,
|
||||
getPersistMode: () => 'immediate' as const
|
||||
} as unknown as VectorIndexProvider
|
||||
|
||||
const addOp = new AddToVectorIndexOperation(legacyProvider, 'id-1', [1, 2, 3])
|
||||
const removeOp = new RemoveFromVectorIndexOperation(legacyProvider, 'id-1', [1, 2, 3])
|
||||
|
||||
expect(addOp.name).toBe('AddToVectorIndex(unknown-provider)')
|
||||
expect(removeOp.name).toBe('RemoveFromVectorIndex(unknown-provider)')
|
||||
// Never silently falls back to the JS engine's name for a provider it
|
||||
// knows nothing about — that's the exact fossil-naming bug being fixed.
|
||||
expect(addOp.name).not.toContain('js-hnsw')
|
||||
expect(addOp.name).not.toContain('hnsw-js')
|
||||
// Loud, not silent — but exactly once per provider instance, not once
|
||||
// per op stamped against it.
|
||||
expect(warnSpy).toHaveBeenCalledTimes(1)
|
||||
expect(warnSpy.mock.calls[0][0]).toContain('name')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue