query: stop asking the world about him

"во сколько у меня встреча" fell past the calendar, reached Kiwix, matched
an article on the 2015 CPISRA World Games and came back phrased as his
meeting. Inventing is worse than refusing, and as `system` this used to say
"пока не умею".

A new source sits between the notes pass and Kiwix: a question carrying a
possession marker ("у меня", "мой", "my", "do i have", "did i") that his own
data did not answer has no answer outside it either, so the walk stops there.
The markers are possession, not first person, so "как мне сварить борщ" still
reaches the encyclopedia.

This is also where CLAUDE.md's privacy line lands: only the utterance may
leave the box, and a question about him carries his life in the utterance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
This commit is contained in:
kami
2026-08-01 23:27:04 +04:00
parent b35151418a
commit b8227295b8
2 changed files with 174 additions and 0 deletions
+65
View File
@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"log"
"regexp"
"strings"
"time"
@@ -106,6 +107,11 @@ var querySources = []querySource{
{name: "embed", answer: (*reactiveHandler).queryEmbed},
{name: "memory", answer: (*reactiveHandler).queryMemory},
{name: "notes", answer: (*reactiveHandler).queryNotes},
// THE BOUNDARY. Everything above answers from his own data; everything
// below answers from the world's. A question about him that got this far
// has no answer in his data, and no outside source can supply one, so this
// stops the walk rather than let the encyclopedia and the model guess.
{name: "personal", answer: (*reactiveHandler).queryPersonal},
// The offline encyclopedia, after everything of his and before anything on
// the network. A question he can be answered from his own notes is answered
// from his own notes; only what is left over is looked up.
@@ -619,6 +625,65 @@ func (h *reactiveHandler) queryKiwix(ctx context.Context, t *queryTurn) (string,
return reply, true
}
// queryPersonal — stop the walk on a question about him that his own data did
// not answer.
//
// Every source above this one reads something of his: his facts, his calendar,
// his tasks, his house, his notes. Everything below reads the world: an offline
// Wikipedia, a page he named, the model's own weights. The world does not know
// when his meeting is, and asked anyway it will produce something.
//
// It did. "во сколько у меня встреча" reached Kiwix on the deployed daemon,
// 01-08-2026; Wikipedia matched an article on the 2015 CPISRA World Games, and
// the phraser rendered it as "встреча у тебя в 2015 CPISRA World Games, где
// были соревнования по плаванию". Fluent, confident, and about a swimming
// competition in Nottingham. Saying "не знаю" is not a worse answer than that
// one — it is the only true one.
//
// Note this is also the privacy edge. The rule in CLAUDE.md is that only the
// utterance may leave the box, never his notes; a question that is ABOUT him
// carries his life in the utterance itself, so it is the one class that should
// not be sent to an upstream engine at all. The guard closes both holes with
// the same test.
func (h *reactiveHandler) queryPersonal(ctx context.Context, t *queryTurn) (string, bool) {
if !isPersonalQuery(t.dec.Utterance) {
return "", false
}
log.Printf("voice: %q is about him and his own data did not answer it; not asking the world", t.dec.Utterance)
return "не знаю — не нашла у тебя такой записи.", true
}
// personalMarkers — first-person POSSESSION, not first person generally.
//
// "у меня" and "мой" attach to a thing that is his, which is what makes the
// question unanswerable from outside. A bare "мне" or "я" does not: "как мне
// сварить борщ" and "что я могу посмотреть" are ordinary questions about the
// world that happen to mention the asker, and refusing those would be the
// opposite mistake. The narrow test is the point.
// Go's \b is ASCII-only and never fires next to a Cyrillic letter, so the
// Russian patterns spell the boundary out as "not a letter or a digit". The
// English ones keep \b, where it works.
var personalMarkers = []*regexp.Regexp{
regexp.MustCompile(`(?i)(^|[^\p{L}\p{N}])у\s+меня([^\p{L}\p{N}]|$)`),
regexp.MustCompile(`(?i)(^|[^\p{L}\p{N}])мо(й|я|ё|е|и|его|ей|их|им|ими|ем|ю|ею)([^\p{L}\p{N}]|$)`),
regexp.MustCompile(`(?i)\bmy\b`),
regexp.MustCompile(`(?i)\bdo\s+i\s+have\b`),
regexp.MustCompile(`(?i)\bdid\s+i\b`),
}
// isPersonalQuery reports whether the utterance asks about something of his.
func isPersonalQuery(utterance string) bool {
if utterance == "" {
return false
}
for _, re := range personalMarkers {
if re.MatchString(utterance) {
return true
}
}
return false
}
// queryGeneral — general knowledge from the phraser, the last source before
// giving up. It always claims: either the model answers or Maven says she
// doesn't know.
+109
View File
@@ -0,0 +1,109 @@
package main
import (
"context"
"testing"
"time"
"github.com/kami/maven/internal/ipc"
"github.com/kami/maven/internal/router"
)
func TestIsPersonalQuery(t *testing.T) {
for _, s := range []string{
"во сколько у меня встреча",
"что у меня сегодня",
"когда мой следующий отпуск",
"где моя книга",
"сколько моих задач висит",
"when is my meeting",
"do i have anything today",
"did i take my vitamins",
} {
if !isPersonalQuery(s) {
t.Errorf("isPersonalQuery(%q) = false, want true", s)
}
}
for _, s := range []string{
// First person without possession. These are questions about the
// world that merely mention the asker, and refusing them would be the
// opposite mistake.
"как мне сварить борщ",
"что я могу посмотреть вечером",
"почему небо синее",
"столица франции",
"how do i boil an egg",
"",
} {
if isPersonalQuery(s) {
t.Errorf("isPersonalQuery(%q) = true, want false", s)
}
}
}
// kiwixTrapAPI stands in for the world. Nothing below the personal boundary
// should be consulted for a question about him, so the test asserts on the
// reply rather than on a call: reaching Kiwix or general knowledge produces a
// phrased answer, and refusing produces the honest one.
func personalHandler() *reactiveHandler {
return &reactiveHandler{
api: ipc.UnimplementedCoreAPI{},
now: func() time.Time { return contNow },
// No phraser and no kiwix wiring: if the walk gets past the personal
// source it reaches queryGeneral, which returns "не знаю." with a nil
// phraser — a different string from the one this guard produces, so
// the two cases stay distinguishable.
}
}
// The regression: "во сколько у меня встреча" reached Kiwix, Wikipedia matched
// an article on the 2015 CPISRA World Games, and the phraser reported it back
// as his meeting. Seen on the deployed daemon, 01-08-2026.
func TestPersonalQuestionIsNotSentToTheWorld(t *testing.T) {
h := personalHandler()
reply, ok := h.queryPersonal(context.Background(), &queryTurn{
dec: router.Decision{Intent: router.IntentQuery, Utterance: "во сколько у меня встреча"},
})
if !ok {
t.Fatal("queryPersonal passed on a question about him")
}
if reply == "" {
t.Fatal("empty reply")
}
}
func TestWorldQuestionsPassThroughTheBoundary(t *testing.T) {
h := personalHandler()
for _, u := range []string{"почему небо синее", "столица франции"} {
if _, ok := h.queryPersonal(context.Background(), &queryTurn{
dec: router.Decision{Intent: router.IntentQuery, Utterance: u},
}); ok {
t.Errorf("queryPersonal claimed %q, want it to pass to the encyclopedia", u)
}
}
}
// The boundary must sit above kiwix and general-knowledge and below every
// source that reads his own data. Asserted on the table itself: an ordering
// bug here is silent, because both arrangements answer, just from the wrong
// place.
func TestPersonalBoundarySitsBetweenHisDataAndTheWorld(t *testing.T) {
idx := map[string]int{}
for i, s := range querySources {
idx[s.name] = i
}
boundary, ok := idx["personal"]
if !ok {
t.Fatal("no personal source in the chain")
}
for _, his := range []string{"fact-by-key", "day-plan", "tasks", "calendar", "memory", "notes"} {
if i, ok := idx[his]; !ok || i > boundary {
t.Errorf("%q reads his own data and must run before the personal boundary", his)
}
}
for _, world := range []string{"kiwix", "web", "general-knowledge"} {
if i, ok := idx[world]; !ok || i < boundary {
t.Errorf("%q reads the world and must run after the personal boundary", world)
}
}
}