From fc92db9f5b0e140d430435db728e1cdff6e8524d Mon Sep 17 00:00:00 2001 From: jianf <2198972886@qq.com> Date: Sun, 1 Nov 2026 11:51:13 +0800 Subject: [PATCH] feat(memory-b): port V1 exact vector pin adjustment --- lib/recall.js | 12 +++++- test/phase-b-a.test.js | 98 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/lib/recall.js b/lib/recall.js index 176a91f..22d82db 100644 --- a/lib/recall.js +++ b/lib/recall.js @@ -217,10 +217,11 @@ function allIndexesOf(haystack, needle) { * @param {object} [options] * @param {number} [options.topK] max hits returned (default 5) * @param {number} [options.poolSize] FTS candidate pool before truncation (default 50) + * @param {Map} [options.vectorScores] optional vector scores for V1 exact-band tie adjustment * @returns {Array<{id: string, text: string, source: string, score: number, * exact: boolean, why: string}>} */ -export function recallBlocks(store, query, { topK = 5, poolSize = 50, fingerprint, dimension } = {}) { +export function recallBlocks(store, query, { topK = 5, poolSize = 50, fingerprint, dimension, vectorScores } = {}) { const text = String(query ?? '').trim() if (text === '') return [] @@ -399,8 +400,15 @@ export function recallBlocks(store, query, { topK = 5, poolSize = 50, fingerprin // [1.0+) exact numeric pin — must always outrank everything else // [0..1) non-exact match — lexical / word / CJK coverage const nonExact = Math.max(candidate.lexical, coverage * 0.5, cjkCoverage * 0.75) + // Ported from Go: internal/memory/fusion.go:425-428. + // ExactHit is a boolean pin; an explicitly supplied vector score only makes + // a same-band tie adjustment. Keep the old no-vector path byte-compatible + // for A2: it has no VectorHit to apply. + const exactScore = vectorScores instanceof Map + ? 1.0 + (vectorScores.get(candidate.id) ?? 0) * 0.01 + : 1.0 + candidate.lexical * 0.01 + boundaryBonus * 1e-4 const score = exact - ? 1.0 + candidate.lexical * 0.01 + boundaryBonus * 1e-4 + ? exactScore : nonExact + boundaryBonus * 1e-3 scored.push({ id: candidate.id, diff --git a/test/phase-b-a.test.js b/test/phase-b-a.test.js index 8f5bb75..106044b 100644 --- a/test/phase-b-a.test.js +++ b/test/phase-b-a.test.js @@ -271,3 +271,101 @@ test('A3c mutation: labelling the success path "hybrid" must fail', () => { 'if lib returns "hybrid", the A3c assertion must throw', ) }) + +// ── V1: exact hits stay BOOLEAN-pinned, vector only adjusts inside the band ─ +// +// Ported from Go `internal/memory/fusion.go:425-428`: +// +// if f.ExactHit.mutated { f.Score = 1.0 + f.VectorHit*0.01 } +// +// The blocked copy that must never come back is the one HA measured and +// recorded (`fusion.go:42-44`): folding a 1.0 exact hit into the weighted sum +// gives it 0.30 and lets a noise candidate at 0.534 win. +test('V1: an exact hit keeps the pin band and only takes a vector tie adjustment', () => { + const store = seededStore() + try { + // A query with an exact numeric symbol, so it is a real exact-band hit. + const query = '4324' + const plain = recallBlocks(store, query, { topK: 5 }) + const exact = plain.filter((hit) => hit.exact === true) + assert.ok(exact.length > 0, `the corpus must yield an exact hit for ${query}; got ${JSON.stringify(plain.map((h) => [h.id, h.exact]))}`) + + // Vector scores supplied for the exact hits only: same band, tiny nudge. + const scores = new Map(exact.map((hit) => [hit.id, 1])) + const withVector = recallBlocks(store, query, { topK: 5, vectorScores: scores }) + + const before = new Map(exact.map((hit) => [hit.id, hit.score])) + for (const hit of withVector.filter((h) => h.exact === true)) { + const previous = before.get(hit.id) + if (previous === undefined) continue + assert.equal(hit.score, 1.0 + 1 * 0.01, 'exact score must be 1.0 + vector*0.01 (go:fusion.go:427)') + assert.ok(hit.score >= 1.0, 'and stays in the pin band, never below 1.0') + } + + // The vector nudge must never outrank an exact hit: every exact hit still + // sorts above every non-exact hit. + const firstNonExact = withVector.findIndex((hit) => hit.exact !== true) + if (firstNonExact !== -1) { + for (let i = firstNonExact; i < withVector.length; i += 1) { + assert.ok(withVector[i].score < 1.0, 'non-exact hits stay strictly below the pin band') + } + } + } finally { + store.close() + } +}) + +test('V1: a low vector score cannot pull an exact hit out of the pin band', () => { + const store = seededStore() + try { + const query = '4324' + const plain = recallBlocks(store, query, { topK: 5 }) + const exact = plain.find((hit) => hit.exact === true) + assert.ok(exact, 'precondition: the corpus must produce an exact hit') + + const zero = recallBlocks(store, query, { topK: 5, vectorScores: new Map([[exact.id, 0]]) }) + const hit = zero.find((row) => row.id === exact.id) + assert.equal(hit.score, 1.0, 'vector 0 still pins at 1.0 — the pin never depends on the vector') + } finally { + store.close() + } +}) + + +// M-V1 mutation: fold exact into weights; the unchanged positive predicate must bite. +test('V1†: folding an exact hit into a weighted sum turns V1 red', async (t) => { + const { readFileSync, writeFileSync, mkdtempSync, rmSync, cpSync } = await import('node:fs') + const { tmpdir } = await import('node:os') + const { join } = await import('node:path') + const { fileURLToPath, pathToFileURL } = await import('node:url') + const libDir = fileURLToPath(new URL('../lib/', import.meta.url)) + const pkgRoot = fileURLToPath(new URL('..', import.meta.url)) + const dir = mkdtempSync(join(tmpdir(), 'mv1-')) + try { + cpSync(libDir, join(dir, 'lib'), { recursive: true }) + cpSync(join(pkgRoot, 'package.json'), join(dir, 'package.json')) + const target = join(dir, 'lib', 'recall.js') + let source = readFileSync(target, 'utf8') + const anchor = ` const exactScore = vectorScores instanceof Map + ? 1.0 + (vectorScores.get(candidate.id) ?? 0) * 0.01 + : 1.0 + candidate.lexical * 0.01 + boundaryBonus * 1e-4` + assert.equal(source.split(anchor).length - 1, 1, 'M-V1 anchor must occur exactly once') + source = source.replace(anchor, + ' const exactScore = 0.7 * (vectorScores instanceof Map ? (vectorScores.get(candidate.id) ?? 0) : 0) + 0.3 * (exact ? 1 : 0) // MUTATED') + writeFileSync(target, source) + const mod = await import(pathToFileURL(target).href) + const store = seededStore() + try { + const plain = mod.recallBlocks(store, '4324', { topK: 5 }) + const exact = plain.filter((hit) => hit.exact === true) + assert.ok(exact.length > 0, 'precondition: mutant corpus has an exact hit') + assert.throws(() => { + assert.ok(exact.every((hit) => hit.score >= 1.0), 'exact must stay in the pin band') + }, (err) => { + assert.equal(err.code, 'ERR_ASSERTION') + t.diagnostic(`V1† RAW RED >>> ${err.message}`) + return true + }, 'M-V1 must break the unchanged V1 positive predicate') + } finally { store.close() } + } finally { rmSync(dir, { recursive: true, force: true }) } +})