Files
Maven/internal/memory/locativeverify_test.go
claude 5b0b29dfad Make locative recall prove identity, not overlap (V-719)
The spare-key note scored 0.832 to 0.867 against a spare passport, a blue
shirt, a blue document box and a car key. Score and margin cannot separate
those: the right note runs 0.817 to 0.892 and the silent cases 0.787 to
0.874, so the ranges overlap and structure has to decide.

RecallAllowed now takes two structural facts from the router. A locative
question must corroborate every identity term against the candidate's
subject, read up to its first dictionary-proven verb, so a location object
in the note cannot answer for the thing being located. A turn that is not
question-shaped needs a named shared topic even when it ends in '?', which
is what "я отменил напоминание про молоко" lacked when it recalled an
unrelated note at 0.825 with no runner-up to fail the margin.

query_min_score moves 0.55 to 0.80 for tokenizer rev 2. The held-out
fixture answers 14/27 real recalls and 0/14 false ones.

LocativeAnswerVerifier is the resident-model second opinion, kept behind
the deterministic gate and wired into nothing. The measurement that says
why is docs/evals/2026-08-15-locative-answerability-verifier.md.

--no-verify: master is the working branch this session by the owner's call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 17:19:01 +04:00

107 lines
3.9 KiB
Go

package memory
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"github.com/kami/maven/internal/llm"
)
type locativeCompleteFunc func(context.Context, llm.Req) (string, error)
func (f locativeCompleteFunc) Complete(ctx context.Context, req llm.Req) (string, error) {
return f(ctx, req)
}
func TestLocativeAnswerVerifierRequestAndVerdict(t *testing.T) {
var got llm.Req
v := NewLocativeAnswerVerifier(locativeCompleteFunc(func(_ context.Context, req llm.Req) (string, error) {
got = req
return `{"target":"настройки nginx","memory_subject":"конфиг nginx","answer":"yes"}`, nil
}))
verdict, err := v.Evaluate(context.Background(), "где настройки nginx?", "конфиг nginx лежит в /etc/nginx")
if err != nil {
t.Fatalf("Evaluate: %v", err)
}
if !verdict.Answerable || verdict.Target != "настройки nginx" || verdict.MemorySubject != "конфиг nginx" {
t.Fatalf("verdict = %+v", verdict)
}
if got.Grammar != locativeVerifierGrammar || got.MaxTokens != locativeVerifierMaxTokens || got.RepeatPenalty != 1.1 {
t.Fatalf("request bounds drifted: %+v", got)
}
var input map[string]string
if err := json.Unmarshal([]byte(got.User), &input); err != nil {
t.Fatalf("user input is not JSON: %v", err)
}
if len(input) != 2 || input["question"] != "где настройки nginx?" || input["memory"] != "конфиг nginx лежит в /etc/nginx" {
t.Fatalf("model saw fields outside question+memory: %#v", input)
}
if strings.Contains(got.System, "nginx") {
t.Fatal("held-out entity leaked into the static verifier prompt")
}
}
func TestLocativeAnswerVerifierNoAndMalformedFailClosed(t *testing.T) {
for _, tc := range []struct {
name string
raw string
want bool
err bool
}{
{"no", `{"target":"паспорт","memory_subject":"ключ","answer":"no"}`, false, false},
{"bare yes", `yes`, false, true},
{"unknown answer", `{"target":"a","memory_subject":"b","answer":"maybe"}`, false, true},
{"empty target", `{"target":"","memory_subject":"b","answer":"yes"}`, false, true},
{"extra field", `{"target":"a","memory_subject":"b","answer":"yes","why":"guess"}`, false, true},
{"trailing object", `{"target":"a","memory_subject":"b","answer":"yes"}{}`, false, true},
} {
t.Run(tc.name, func(t *testing.T) {
v := NewLocativeAnswerVerifier(locativeCompleteFunc(func(context.Context, llm.Req) (string, error) {
return tc.raw, nil
}))
got, err := v.Answerable(context.Background(), "q", "m")
if got != tc.want || (err != nil) != tc.err {
t.Fatalf("Answerable = %v, %v; want %v, err=%v", got, err, tc.want, tc.err)
}
})
}
}
func TestLocativeAnswerVerifierUnavailableErrorAndTimeout(t *testing.T) {
if v := NewLocativeAnswerVerifier(nil); v != nil {
t.Fatal("nil resident model produced a verifier")
}
boom := errors.New("llama down")
v := NewLocativeAnswerVerifier(locativeCompleteFunc(func(context.Context, llm.Req) (string, error) {
return "", boom
}))
if ok, err := v.Answerable(context.Background(), "q", "m"); ok || !errors.Is(err, boom) {
t.Fatalf("model error = %v, %v; want false wrapping %v", ok, err, boom)
}
v = NewLocativeAnswerVerifier(locativeCompleteFunc(func(ctx context.Context, _ llm.Req) (string, error) {
<-ctx.Done()
return "", ctx.Err()
}))
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
defer cancel()
if ok, err := v.Answerable(ctx, "q", "m"); ok || !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("timeout = %v, %v; want false deadline", ok, err)
}
}
func TestLocativeVerifierGrammarIsFullyBounded(t *testing.T) {
if strings.Contains(locativeVerifierGrammar, "*") || strings.Contains(locativeVerifierGrammar, "+") {
t.Fatalf("grammar contains an unbounded repetition:\n%s", locativeVerifierGrammar)
}
for _, bound := range []string{"{1,80}", "{0,2}"} {
if !strings.Contains(locativeVerifierGrammar, bound) {
t.Errorf("grammar missing bound %s", bound)
}
}
}