feat(memory-b): port V1 exact vector pin adjustment
This commit is contained in:
@ -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<string, number>} [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,
|
||||
|
||||
@ -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 }) }
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user