Only a stage 0 grammar may take the personal boundary off a turn #211

Merged
claude merged 1 commits from task/666-only-a-stage-0-grammar-may-take-the-pers into master 2026-08-09 00:01:52 +02:00
5 changed files with 84 additions and 21 deletions
+15 -7
View File
@@ -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
+18 -4
View File
@@ -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,
+32 -10
View File
@@ -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))
}
+15
View File
@@ -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
+4
View File
@@ -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)