98ee701e03
The memory pass ran only after the notes-only gate had already rejected the same note at the same score. Notes and facts share one vector index, so a note that failed there failed again — the branch could only ever return a fact. Now the memory pass runs first: one search over everything Maven remembers, one gate, and the memory that clearly matches best answers (a note gets phrased, a fact is read back). The notes-only pass stays behind it for notes the vector index does not hold. No threshold moved, so the set of questions answered is unchanged — only which memory answers them. Fixture gained two mixed note+fact cases, so the answerable count goes 25 -> 27: hash recall@1 36.0% -> 37.0% (ratchet 0.32 unchanged, comment updated), e5 recall@1 72.0% -> 70.4%, false recall still 1/5. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
78 lines
2.4 KiB
Go
78 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/kami/maven/internal/memory"
|
|
)
|
|
|
|
func TestBestRecall(t *testing.T) {
|
|
const min = 0.55
|
|
const margin = 0.008
|
|
|
|
t.Run("empty results", func(t *testing.T) {
|
|
if _, ok := bestRecall(nil, min, margin); ok {
|
|
t.Error("empty results returned ok")
|
|
}
|
|
})
|
|
|
|
t.Run("top below threshold", func(t *testing.T) {
|
|
res := []memory.Result{{Score: 0.4, Meta: map[string]string{"text": "выпил воды"}}}
|
|
if _, ok := bestRecall(res, min, margin); ok {
|
|
t.Error("below-threshold hit returned ok")
|
|
}
|
|
})
|
|
|
|
t.Run("hit without text meta", func(t *testing.T) {
|
|
res := []memory.Result{{Score: 0.9, Meta: map[string]string{"type": "fact"}}}
|
|
if _, ok := bestRecall(res, min, margin); ok {
|
|
t.Error("textless hit returned ok")
|
|
}
|
|
})
|
|
|
|
t.Run("clearing hit returns its text", func(t *testing.T) {
|
|
res := []memory.Result{
|
|
{Score: 0.82, Meta: map[string]string{"text": "выпил воды в три часа", "type": "fact"}},
|
|
{Score: 0.60, Meta: map[string]string{"text": "другое"}},
|
|
}
|
|
got, ok := bestRecall(res, min, margin)
|
|
if !ok {
|
|
t.Fatal("clearing hit not returned")
|
|
}
|
|
if got.Meta["text"] != "выпил воды в три часа" {
|
|
t.Errorf("wrong text: %q", got.Meta["text"])
|
|
}
|
|
if got.Meta["type"] != "fact" {
|
|
t.Errorf("kind lost: %q", got.Meta["type"])
|
|
}
|
|
})
|
|
|
|
// The index holds notes and facts together, so a note has to be able to win
|
|
// it — for a long time it could not (Vikunja #373).
|
|
t.Run("a note can win", func(t *testing.T) {
|
|
res := []memory.Result{
|
|
{Score: 0.86, Meta: map[string]string{"text": "молоко в холодильнике", "type": "note"}},
|
|
{Score: 0.61, Meta: map[string]string{"text": "выпил воды", "type": "fact"}},
|
|
}
|
|
got, ok := bestRecall(res, min, margin)
|
|
if !ok {
|
|
t.Fatal("clearly-best note not returned")
|
|
}
|
|
if got.Meta["type"] != "note" || got.Meta["text"] != "молоко в холодильнике" {
|
|
t.Errorf("got %v, want the note", got.Meta)
|
|
}
|
|
})
|
|
|
|
// The runner-up is almost as close, so the embedder cannot tell the two
|
|
// notes apart. Silence beats reading back a coin flip.
|
|
t.Run("runner-up too close", func(t *testing.T) {
|
|
res := []memory.Result{
|
|
{Score: 0.860, Meta: map[string]string{"text": "выпил воды в три часа"}},
|
|
{Score: 0.858, Meta: map[string]string{"text": "другое"}},
|
|
}
|
|
if _, ok := bestRecall(res, min, margin); ok {
|
|
t.Error("thin-margin hit returned ok")
|
|
}
|
|
})
|
|
}
|