From c9381486197c9d5b2dc074b117c7b850448f539b Mon Sep 17 00:00:00 2001 From: claude Date: Sun, 9 Aug 2026 01:56:46 +0400 Subject: [PATCH] Only a literal pattern may take the personal boundary off a turn (V-666) Naming a destination takes the guessing query sources off a turn, and the personal boundary is one of them. Every other guesser costs an answer when it is wrongly dropped. This one costs the rule that a question about him never reaches an upstream engine. Three deciders name a destination now and two of them infer it: the routing heads and the resident model. Decision.SourceAnchored says a stage 0 grammar read the words instead. queryWalk honours it for the source marked boundary: true and for no other, so the rest of the table is unchanged. Owner's call of 2026-08-09. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN --- CLAUDE.md | 22 +++++++++++++------ cmd/mavend/actions_query.go | 22 +++++++++++++++---- cmd/mavend/querywalk_test.go | 42 +++++++++++++++++++++++++++--------- internal/router/intent.go | 15 +++++++++++++ internal/router/router.go | 4 ++++ 5 files changed, 84 insertions(+), 21 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6fed4ab..e4806f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -510,13 +510,21 @@ they look rather than guess. **The personal boundary is the one exception and it is deliberate.** It guesses, so naming `SourceWorld` drops it. That is what stops it answering "кто такой Линус Торвальдс?" with "не нашла у тебя такой записи", which it did on -2026-08-07. The cost is that a destination a model wrote can now take the -boundary off a turn. A question about him that the model calls `world` reaches -SearXNG, where today the boundary stops it. Only the utterance leaves the box, -never his notes or history, so this widens what is asked and not what is sent. -`TestNamingRecallKeepsTheBoundary` pins the other half: naming `SourceRecall` -keeps the boundary in front of the world. Whether a model may drop it at all is -the owner's call and has not been made. +2026-08-07. `TestNamingRecallKeepsTheBoundary` pins the other half: naming +`SourceRecall` keeps the boundary in front of the world. + +**Only a stage 0 grammar may drop it** (owner's call, 09-08-2026, V-666). The +question of who is allowed to was open until then. Three deciders name a +destination and two of them infer it: the routing heads and the resident model. +An inferred `SourceWorld` on a question about him would reach SearXNG, and that +widens what is asked rather than costing a local answer. So `Decision.SourceAnchored` +carries the provenance. It is a field and not `Stage == 0`. Stage 0 also means +confidence 1.0 and an anchored claim band, and one of those could stop implying +the others. `queryWalk` reads it for the source marked `boundary: true` and for +no other. So every other guesser still comes off the turn, whoever named the +destination. `TestOnlyAGrammarMayDropTheBoundary` pins both directions. +`definitionQueryPattern` claims "кто такой X", so the 2026-08-07 case is still +anchored and still answered. What comes out is only the sources that **guess**. Those decide a turn is theirs by cosine against frozen seeds, then answer whatever they claimed. They hold no table diff --git a/cmd/mavend/actions_query.go b/cmd/mavend/actions_query.go index b89241c..a945805 100644 --- a/cmd/mavend/actions_query.go +++ b/cmd/mavend/actions_query.go @@ -79,6 +79,15 @@ type querySource struct { // not named do not get to try. The lookups still run, because a named // destination is evidence and not a promise. guesses bool + + // boundary — dropping this source widens what leaves the box, so only a + // literal pattern may do it (V-666, owner's call of 2026-08-09). + // + // Every other guesser costs an answer when it is wrongly taken off a turn. + // This one costs the rule that a question about him never reaches an + // upstream engine. A grammar read the words to name a destination. A model + // and a softmax both inferred one, and neither may spend that. + boundary bool } // querySources is the ordered chain actionQuery walks; first source to claim @@ -156,7 +165,7 @@ var querySources = []querySource{ // 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, dest: router.SourceRecall, guesses: true}, + {name: "personal", answer: (*reactiveHandler).queryPersonal, dest: router.SourceRecall, guesses: true, boundary: true}, // The world, read live. Owner's ruling of 2026-08-02: a metasearch hit beats // a frozen ZIM, so SearXNG asks before Kiwix does. Nothing of his is at // stake by this point — the boundary above already stopped every question @@ -198,12 +207,17 @@ var querySources = []querySource{ // No destination named ⇒ the table exactly as written, which is what shipped // before the field existed. That is the floor. The classifier arm names // nothing, so a box whose model is down routes queries the way it always did. -func queryWalk(dest router.Source) (walk, skipped []querySource) { +// The personal boundary is the one exception, and anchored is what buys it +// (V-666). A grammar matched a literal pattern to name the destination. The +// routing heads and the resident model inferred one, and an inferred SourceWorld +// takes the boundary off a question about him. That widens what is asked +// upstream rather than costing a local answer, so those two keep it. +func queryWalk(dest router.Source, anchored bool) (walk, skipped []querySource) { if dest == router.SourceUnknown { return querySources, nil } for _, s := range querySources { - if s.guesses && s.dest != dest { + if s.guesses && s.dest != dest && (anchored || !s.boundary) { skipped = append(skipped, s) continue } @@ -219,7 +233,7 @@ func (h *reactiveHandler) actionQuery(ctx context.Context, dec router.Decision) // (V-564). Finish names everyone below the winner. decision.Expect(ctx, decision.StageQuery, querySourceNames()) rec := decision.From(ctx) - walk, skipped := queryWalk(dec.Source) + walk, skipped := queryWalk(dec.Source, dec.SourceAnchored) for _, src := range skipped { rec.Note(decision.Claim{ Stage: decision.StageQuery, Claimant: src.name, Outcome: decision.NeverAsked, diff --git a/cmd/mavend/querywalk_test.go b/cmd/mavend/querywalk_test.go index 4476680..2c4b179 100644 --- a/cmd/mavend/querywalk_test.go +++ b/cmd/mavend/querywalk_test.go @@ -10,7 +10,7 @@ import ( // whose model is down names nothing, and naming nothing has to walk the chain // the way it walked before the field existed. func TestNoDestinationWalksTheWholeChain(t *testing.T) { - walk, skipped := queryWalk(router.SourceUnknown) + walk, skipped := queryWalk(router.SourceUnknown, false) if len(skipped) != 0 { t.Errorf("skipped %d sources with no destination named, want none", len(skipped)) } @@ -32,15 +32,16 @@ func TestANamedDestinationSilencesTheOtherGuessers(t *testing.T) { dest router.Source utterance string silenced string + anchored bool // a stage 0 grammar named the destination }{ - {router.SourceWorld, "что такое TCP?", "weather"}, - {router.SourceWorld, "сколько будет 17 на 23?", "weather"}, - {router.SourceWorld, "кто такой Линус Торвальдс?", "personal"}, - {router.SourceRecall, "какой у меня любимый язык?", "feeds"}, - {router.SourceCalendar, "что в календаре на завтра?", "weather"}, + {router.SourceWorld, "что такое TCP?", "weather", true}, + {router.SourceWorld, "сколько будет 17 на 23?", "weather", true}, + {router.SourceWorld, "кто такой Линус Торвальдс?", "personal", true}, + {router.SourceRecall, "какой у меня любимый язык?", "feeds", false}, + {router.SourceCalendar, "что в календаре на завтра?", "weather", true}, } for _, c := range cases { - walk, skipped := queryWalk(c.dest) + walk, skipped := queryWalk(c.dest, c.anchored) if inWalk(walk, c.silenced) { t.Errorf("%q named %q: %q is still asked", c.utterance, c.dest, c.silenced) } @@ -56,7 +57,7 @@ func TestANamedDestinationSilencesTheOtherGuessers(t *testing.T) { // data first, then the world", and a destination a model wrote must not be able // to reverse it. func TestNamingTheWorldStillReadsHisDataFirst(t *testing.T) { - walk, _ := queryWalk(router.SourceWorld) + walk, _ := queryWalk(router.SourceWorld, true) for _, look := range []string{"fact-by-key", "embed", "memory", "notes"} { if !inWalk(walk, look) { t.Errorf("%q was dropped; only the sources that guess may be dropped", look) @@ -74,7 +75,7 @@ func TestNamingTheWorldStillReadsHisDataFirst(t *testing.T) { // makes "какой у меня любимый язык?" answer "не нашла у тебя такой записи" // rather than reaching SearXNG once nothing local had it. func TestNamingRecallKeepsTheBoundary(t *testing.T) { - walk, _ := queryWalk(router.SourceRecall) + walk, _ := queryWalk(router.SourceRecall, true) if !inWalk(walk, "personal") { t.Fatal("the personal boundary was skipped on a turn named for his own data") } @@ -83,12 +84,33 @@ func TestNamingRecallKeepsTheBoundary(t *testing.T) { } } +// The owner's call of 2026-08-09 (V-666): only a stage 0 grammar may take the +// personal boundary off a turn. The routing heads and the resident model both +// name a destination by inference, and an inferred SourceWorld would send a +// question about him upstream. Every other guesser still goes. +func TestOnlyAGrammarMayDropTheBoundary(t *testing.T) { + walk, skipped := queryWalk(router.SourceWorld, false) + if !inWalk(walk, "personal") { + t.Error("an inferred destination took the boundary off the turn") + } + if !inWalk(skipped, "weather") { + t.Error("weather is still asked; the rule covers the boundary alone") + } + if posOf(walk, "personal") > posOf(walk, "search") { + t.Error("the boundary no longer sits in front of the world") + } + if anchored, _ := queryWalk(router.SourceWorld, true); inWalk(anchored, "personal") { + t.Error(`a grammar named the world and the boundary stayed: ` + + `"кто такой Линус Торвальдс?" is answered "не нашла у тебя такой записи" again`) + } +} + // Whatever the destination, the walk is a subsequence of the table. Every // comment on that table argues an order between two sources, and none of those // reasons is about this field. func TestTheWalkNeverReordersTheTable(t *testing.T) { for _, dest := range append([]router.Source{router.SourceUnknown}, router.Sources...) { - walk, skipped := queryWalk(dest) + walk, skipped := queryWalk(dest, true) if len(walk)+len(skipped) != len(querySources) { t.Errorf("%q: %d walked + %d skipped, want %d", dest, len(walk), len(skipped), len(querySources)) } diff --git a/internal/router/intent.go b/internal/router/intent.go index 43b0b1c..3cbac14 100644 --- a/internal/router/intent.go +++ b/internal/router/intent.go @@ -111,6 +111,21 @@ type Decision struct { // before this field existed. See source.go for why it is twelve values. Source Source + // SourceAnchored — a stage 0 grammar named that destination, matching a + // literal pattern to do it. Only the router sets this, and only there. + // + // It exists because one thing downstream is not reversible by evidence + // (V-666). Naming a destination normally takes guessing sources off a turn, + // and one of those is the personal boundary, which is what stops a question + // about him from reaching the world. A grammar that read "что такое X" may + // take it off. A model or a softmax may not, because a wrong destination + // there widens what leaves the box rather than costing an answer. + // + // Read Stage instead and the two decisions get coupled: stage 0 also means + // confidence 1.0 and an anchored claim band, and a later cascade change + // could make one true where the other is not. + SourceAnchored bool + // Continued — this decision was rebuilt from the previous turn rather // than routed, because the utterance was an ellipsis ("а завтра?"). // Handlers use it to know that Slots.Text is the PREVIOUS turn's topic diff --git a/internal/router/router.go b/internal/router/router.go index 3ad9653..d50b5f5 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -98,6 +98,10 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De continue // grammar matched shape but not content → fall through } d.Utterance = utterance + // A literal pattern named that destination, which is the one provenance + // allowed to take the personal boundary off a turn (V-666). Set here and + // nowhere else, so no other arm of the cascade can claim it. + d.SourceAnchored = d.Source != SourceUnknown // The grammar decided the intent; the extractor fills the slots it did // not match (V-572). See fillMatchedSlots for why every grammar gets it. r.fillMatchedSlots(ctx, &d, now) -- 2.52.0