fix(vectors): a zero-norm vector is not a vector, canonical side included, plus the sanctioned unvector door
The engine-pair seam law: a zero-norm vector never crosses an engine
boundary. The index belt already refused to insert one, but the canonical
write and the vectored-noun ledger still counted it, so a near-empty store
whose only vectored row was zero-norm read "1 canonical vectored vs 0
indexed" and threw a not-ready error at open, and a store's own legacy
zero-norm VFS root could trip the same gate before its VFS-init-time cure
ever ran.
- add()/update() (single and transact()) now normalize an explicit
real all-zero vector to the unvectored [] shape before the dimension
pin, the ledger flag, and the index ops ever see it (loud, one warn per
write, canonical write still succeeds).
- The legacy counts.json derivation walk (scanVectoredNounCount) excludes
a persisted zero-norm row, matching the live ledger's definition.
- A legacy zero-norm VFS root now migrates at open, before the vector-leg
gate evaluates, via one O(1) fixed-path read (torn-tolerant — skips
rather than aborting init on a torn root, letting the recovery walk
heal it) — independent of whether a VirtualFileSystem is ever
constructed this session.
- update({ id, vector: [] }) (and the same op inside transact()) is now
the sanctioned, idempotent unvector door: index removal, exactly-once
ledger decrement, no re-embed, and it clears a pending deferred-embed
marker rather than leaving it to re-vectorize the row later. The
combination with deferEmbedding is a typed refusal.
- JsHnswVectorIndex.rebuild() now skips a zero-norm/empty persisted
vector when repopulating from canonical (the same belt the live
add/replace paths already had), and health()'s index-parity check now
compares HNSW size against the vectored-noun ledger rather than the
raw metadata-entry count, since a store's VFS root is permanently
unvectored by design.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fd6b4ce4ff
commit
0de7665930
6 changed files with 810 additions and 53 deletions
|
|
@ -613,6 +613,17 @@ export function validateUpdateParams(params: UpdateParams): void {
|
|||
// null/undefined means "no new data was given".
|
||||
const hasData = params.data !== undefined && params.data !== null
|
||||
if ((params as UpdateParams & { deferEmbedding?: boolean }).deferEmbedding === true) {
|
||||
if (params.vector && params.vector.length === 0) {
|
||||
// The nonsensical combination Leg D of the zero-norm/unvector-door law
|
||||
// refuses: `vector: []` is the SANCTIONED UNVECTOR DOOR — an explicit
|
||||
// instruction to remove the vector NOW, never "please embed" — so it
|
||||
// cannot be paired with a request to defer an embed.
|
||||
throw new Error(
|
||||
`update(): 'vector: []' (the unvector door) cannot be combined with ` +
|
||||
`'deferEmbedding: true' — an unvector is an explicit instruction to remove ` +
|
||||
`the vector now, not a request to defer an embed. Drop one of the two.`
|
||||
)
|
||||
}
|
||||
if (params.vector) {
|
||||
throw new Error(
|
||||
`update(): deferEmbedding cannot be combined with an explicit 'vector' — ` +
|
||||
|
|
@ -649,8 +660,16 @@ export function validateUpdateParams(params: UpdateParams): void {
|
|||
throw new Error(`invalid NounType: ${params.type}`)
|
||||
}
|
||||
|
||||
// Validate vector dimensions if provided
|
||||
if (params.vector) {
|
||||
// Validate vector dimensions if provided. A length-0 vector is the
|
||||
// SANCTIONED UNVECTOR DOOR (see brainy.ts update()'s matching comment): an
|
||||
// explicit `vector: []` — or a real all-zero vector, normalized to `[]`
|
||||
// upstream by the zero-norm law — carries no dimension information,
|
||||
// exactly like validateAddParams's identical exemption, so it is exempt
|
||||
// from the dimension check rather than refused as a "0-dimensional
|
||||
// vector". (The `deferEmbedding` combination above already refuses
|
||||
// `vector: []` paired with `deferEmbedding: true` — an empty array is
|
||||
// truthy, so that guard fires unconditionally on any explicit `vector`.)
|
||||
if (params.vector && params.vector.length > 0) {
|
||||
const config = ValidationConfig.getInstance()
|
||||
if (params.vector.length !== config.maxVectorDimensions) {
|
||||
throw new Error(`vector must have exactly ${config.maxVectorDimensions} dimensions`)
|
||||
|
|
|
|||
Reference in a new issue