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>
CLAUDE.md, internal/config/voice.go and docs/routing.md all say the routing
heads graph is a fine-tuned copy of the embedder, never the embedder's own file.
Nothing enforced it. The daemon loaded whatever the key pointed at, so pointing
both keys at one file cost recall with no error and no log line, which reads as
ordinary drift rather than as a misconfiguration.
validateVoice now refuses it at load. Both paths are cleaned and made absolute
first, so "./m.onnx" and "$PWD/m.onnx" are one path, and then compared with
os.SameFile, which catches a copy that is a symlink or a hard link. A path that
does not stat is left to the loader, whose error message is better than this
check can give.
Refusing to start is deliberate and it differs from the loader's treatment of a
broken weights file, which logs and leaves the heads nil on purpose. That case
is a missing accelerator. This one is a working file in the wrong role, and a
daemon that cannot route well should say so rather than answer worse.
deploy/mavend.json points the two keys at different files, so the live config
still starts.