package memory import ( "context" "encoding/json" "errors" "fmt" "io" "strings" "time" "unicode/utf8" "github.com/kami/maven/internal/llm" ) // LocativeCompleter is the narrow resident-model seam the verifier needs. // *llm.Client satisfies it. Keeping the interface here lets the safety and // malformed-output paths run without a server in unit tests. type LocativeCompleter interface { Complete(context.Context, llm.Req) (string, error) } // LocativeAnswerVerifier is the model-backed second opinion for candidates the // deterministic locative identity gate rejected. It never replaces that gate: // exact structural accepts do not call it, and a missing model, timeout, error, // or malformed verdict remains an abstention in the daemon. type LocativeAnswerVerifier struct { c LocativeCompleter } // LocativeVerdict keeps the two extracted referents as auditable evidence. // The daemon consumes only Answerable; the live eval records all three fields // so a yes/no score cannot hide what the model thought it was comparing. type LocativeVerdict struct { Target string `json:"target"` MemorySubject string `json:"memory_subject"` Answerable bool `json:"-"` Raw string `json:"-"` } // NewLocativeAnswerVerifier returns nil when no resident completion seam exists. // That is the ordinary no-model deployment and deliberately means abstain. func NewLocativeAnswerVerifier(c LocativeCompleter) *LocativeAnswerVerifier { if c == nil { return nil } return &LocativeAnswerVerifier{c: c} } // locativeVerifierGrammar fixes both the shape and every variable-width field. // target and memory_subject come before answer deliberately: the small resident // model must identify the two referents before choosing the verdict instead of // emitting an unconstrained first-token hunch. const locativeVerifierGrammar = ` root ::= "{" ws "\"target\"" ws ":" ws string "," ws "\"memory_subject\"" ws ":" ws string "," ws "\"answer\"" ws ":" ws answer ws "}" answer ::= "\"no\"" | "\"yes\"" string ::= "\"" ([^"\\\x00-\x1F] | "\\" ["\\/bfnrt] | "\\u" [0-9a-fA-F]{4}){1,80} "\"" ws ::= [ \t\n]{0,2} ` // locativeVerifierSystem teaches a relation, not the held-out fixture. The // examples use different entities from the measured nginx/token/disk/key and // adversarial passport/shirt/box cases. Two negative examples pin the dangerous // distinctions: a location object is not the proposition subject, and a shared // generic noun with a conflicting complement is not the same referent. const locativeVerifierSystem = `Ты — строгий классификатор логического следования для личной памяти. Вход — JSON с одним question и одним memory. Сначала выдели target: конкретный предмет/событие, чьё место или источник спрашивают. Затем memory_subject: предмет/событие, МЕСТО КОТОРОГО сообщает память. Предмет после слов места (в, на, под, рядом с, inside, at, under) — это место/контейнер, а НЕ memory_subject. answer=yes только если target и memory_subject — один и тот же конкретный референт и память прямо сообщает запрошенное место/источник. Настоящие синонимы и контекстные названия допустимы. Совпадение цвета, свойства, общего слова, контейнера, места или действия недостаточно. Уточнения принадлежности/состава не должны конфликтовать. Не используй внешние знания. Сомнение => no. Примеры: input: {"question":"где красная тетрадь?","memory":"зарядка лежит на красной тетради"} output: {"target":"красная тетрадь","memory_subject":"зарядка","answer":"no"} input: {"question":"где ключ от гаража?","memory":"ключ от офиса лежит под ковриком"} output: {"target":"ключ от гаража","memory_subject":"ключ от офиса","answer":"no"} input: {"question":"где дубликат ключа от мастерской?","memory":"запасной ключ мастерской лежит в ящике"} output: {"target":"дубликат ключа от мастерской","memory_subject":"запасной ключ мастерской","answer":"yes"} input: {"question":"where are the database settings?","memory":"the database configuration is in /etc/db"} output: {"target":"database settings","memory_subject":"database configuration","answer":"yes"} Верни только JSON требуемой формы.` const ( locativeVerifierTimeout = 8 * time.Second locativeVerifierMaxTokens = 128 locativeVerifierMaxField = 80 ) // Answerable implements the daemon's deliberately tiny verifier interface. func (v *LocativeAnswerVerifier) Answerable(ctx context.Context, question, candidate string) (bool, error) { verdict, err := v.Evaluate(ctx, question, candidate) return verdict.Answerable, err } // Evaluate returns the bounded model verdict and its extracted referents. // Callers must treat every error as false; it never manufactures a fallback. func (v *LocativeAnswerVerifier) Evaluate(ctx context.Context, question, candidate string) (LocativeVerdict, error) { if v == nil || v.c == nil { return LocativeVerdict{}, errors.New("locative verifier: resident model unavailable") } input, err := json.Marshal(struct { Question string `json:"question"` Memory string `json:"memory"` }{Question: question, Memory: candidate}) if err != nil { return LocativeVerdict{}, fmt.Errorf("locative verifier: encode input: %w", err) } callCtx, cancel := context.WithTimeout(ctx, locativeVerifierTimeout) defer cancel() raw, err := v.c.Complete(callCtx, llm.Req{ System: locativeVerifierSystem, User: string(input), Grammar: locativeVerifierGrammar, MaxTokens: locativeVerifierMaxTokens, RepeatPenalty: 1.1, }) if err != nil { return LocativeVerdict{}, fmt.Errorf("locative verifier: complete: %w", err) } return parseLocativeVerdict(raw) } func parseLocativeVerdict(raw string) (LocativeVerdict, error) { trimmed := strings.TrimSpace(raw) var wire struct { Target string `json:"target"` MemorySubject string `json:"memory_subject"` Answer string `json:"answer"` } dec := json.NewDecoder(strings.NewReader(trimmed)) dec.DisallowUnknownFields() if err := dec.Decode(&wire); err != nil { return LocativeVerdict{}, fmt.Errorf("locative verifier: parse %q: %w", boundedRaw(trimmed), err) } var trailing any if err := dec.Decode(&trailing); !errors.Is(err, io.EOF) { if err == nil { return LocativeVerdict{}, fmt.Errorf("locative verifier: trailing JSON in %q", boundedRaw(trimmed)) } return LocativeVerdict{}, fmt.Errorf("locative verifier: trailing data in %q: %w", boundedRaw(trimmed), err) } wire.Target = strings.TrimSpace(wire.Target) wire.MemorySubject = strings.TrimSpace(wire.MemorySubject) if wire.Target == "" || wire.MemorySubject == "" || utf8.RuneCountInString(wire.Target) > locativeVerifierMaxField || utf8.RuneCountInString(wire.MemorySubject) > locativeVerifierMaxField { return LocativeVerdict{}, fmt.Errorf("locative verifier: empty or oversized referent in %q", boundedRaw(trimmed)) } var answerable bool switch wire.Answer { case "yes": answerable = true case "no": answerable = false default: return LocativeVerdict{}, fmt.Errorf("locative verifier: invalid answer %q", wire.Answer) } return LocativeVerdict{ Target: wire.Target, MemorySubject: wire.MemorySubject, Answerable: answerable, Raw: trimmed, }, nil } func boundedRaw(s string) string { const max = 240 if len(s) <= max { return s } return s[:max] + "…" }