From 96b474223d462ba268c8fb62dbbe21b54ec279b7 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 4 Aug 2026 01:51:27 +0400 Subject: [PATCH] mavend: an unconfigured capability names the gap (V-479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Netscan and the crawler both declined their own turn when the wiring was nil, and the question fell through to the search leg. "какие устройства в сети?" came back as a paragraph about routers in general, and a question about his own LAN went to an upstream engine — the personal boundary exists to stop exactly that. A URL he named came back answered as though he had not named it. Both now claim the turn once their own recogniser has matched, and say which capability is missing: net_off and page_off in the query family. TestQueryWebPassesWhenNotConfigured encoded the old decision, that announcing a configuration status is only for a capability that exists and failed. It is rewritten, not deleted: the gap is the answer now. --- cmd/mavend/actions_query.go | 20 +++++++++++--------- cmd/mavend/crawls_test.go | 16 ++++++++++------ cmd/mavend/netscan_test.go | 21 +++++++++++++++++++++ internal/phraser/query.go | 6 +++++- internal/phraser/query_ru_v1.json | 8 ++++++++ 5 files changed, 55 insertions(+), 16 deletions(-) diff --git a/cmd/mavend/actions_query.go b/cmd/mavend/actions_query.go index f019816..a07d5fd 100644 --- a/cmd/mavend/actions_query.go +++ b/cmd/mavend/actions_query.go @@ -367,9 +367,12 @@ func (h *reactiveHandler) queryNetwork(ctx context.Context, t *queryTurn) (strin return "", false } if h.netscan == nil { - // Fall through, same as queryHome: an unconfigured scanner must not - // swallow "сколько устройств в сети?" before recall has looked. - return "", false + // The recogniser already matched, so this is a question about HIS LAN + // and there is no scanner to answer it. Falling through sent it to the + // search leg, which answered with a paragraph about routers in general + // and put his network question on an upstream engine (Vikunja #479). + // A missing capability names itself. + return phraser.Q(phraser.QueryNetOff, nil), true } return h.netscan.scanSummary(ctx) } @@ -524,12 +527,11 @@ func (h *reactiveHandler) queryWeb(ctx context.Context, t *queryTurn) (string, b return "", false } if h.crawler == nil { - // Fall through. Reading pages is off unless configured, and on a daemon - // where it was never turned on the older behaviour is right: the model - // answers the question as if the URL had not been said. Announcing a - // configuration status is for a capability that exists and failed, not - // for one he never asked for. - return "", false + // He named a URL, so the question is about that page and nothing else + // can answer it. The older comment here argued for falling through and + // letting the model answer as if the URL had not been said; that is a + // guess dressed as an answer (Vikunja #479). + return phraser.Q(phraser.QueryPageOff, nil), true } ctxFetch, cancel := context.WithTimeout(ctx, 30*time.Second) defer cancel() diff --git a/cmd/mavend/crawls_test.go b/cmd/mavend/crawls_test.go index 4423355..b6b1882 100644 --- a/cmd/mavend/crawls_test.go +++ b/cmd/mavend/crawls_test.go @@ -125,13 +125,17 @@ func TestQueryWebPassesWithoutAURL(t *testing.T) { } } -// A daemon where page reading was never turned on — the default — answers the -// question the way it did before the capability existed. Claiming the turn to -// report a configuration status is for something that exists and failed. -func TestQueryWebPassesWhenNotConfigured(t *testing.T) { +// A daemon where page reading was never turned on names the gap. He asked +// about one page, nothing else on the box can read it, and the old behaviour +// here was to answer as though the URL had not been said (Vikunja #479). +func TestQueryWebNamesTheGapWhenNotConfigured(t *testing.T) { h := buildWebHandler(nil) - if reply, ok := askWeb(h, "посмотри https://example.org/page"); ok { - t.Fatalf("an unconfigured crawler claimed the turn with %q", reply) + reply, ok := askWeb(h, "посмотри https://example.org/page") + if !ok { + t.Fatal("an unconfigured crawler let the page question fall through") + } + if !phraser.IsQ(phraser.QueryPageOff, nil, reply) { + t.Errorf("got %q, want the gap named", reply) } } diff --git a/cmd/mavend/netscan_test.go b/cmd/mavend/netscan_test.go index df87124..62362fb 100644 --- a/cmd/mavend/netscan_test.go +++ b/cmd/mavend/netscan_test.go @@ -10,6 +10,8 @@ import ( "github.com/kami/maven/internal/config" "github.com/kami/maven/internal/ipc" + "github.com/kami/maven/internal/phraser" + "github.com/kami/maven/internal/router" ) func TestWireNetScanOffUnlessEnabled(t *testing.T) { @@ -169,3 +171,22 @@ func TestScanSummarySpeaksACountAndWritesTheAddresses(t *testing.T) { t.Errorf("a repeat question rescanned and rewrote the record (%d notes)", api.n) } } + +// An unconfigured scanner names the gap instead of declining the turn. +// +// Falling through sent "какие устройства в сети?" to the search leg, which +// answered with a paragraph about routers in general — and put a question about +// his own LAN on an upstream engine, which the personal boundary exists to +// prevent (Vikunja #479). +func TestQueryNetworkNamesTheGapWhenNotConfigured(t *testing.T) { + h := &reactiveHandler{} + reply, ok := h.queryNetwork(context.Background(), &queryTurn{ + dec: router.Decision{Utterance: "какие устройства в сети?"}, + }) + if !ok { + t.Fatal("an unconfigured scanner let the question fall through to search") + } + if !phraser.IsQ(phraser.QueryNetOff, nil, reply) { + t.Errorf("got %q, want the gap named", reply) + } +} diff --git a/internal/phraser/query.go b/internal/phraser/query.go index f99d2c1..2ee3a39 100644 --- a/internal/phraser/query.go +++ b/internal/phraser/query.go @@ -45,6 +45,8 @@ const ( QueryWeatherOff = "weather_off" QueryWeatherWhere = "weather_nolocation" QueryNetEmpty = "net_empty" + QueryNetOff = "net_off" + QueryPageOff = "page_off" QueryFailPlan = "fail_plan" QueryFailNotes = "fail_notes" @@ -60,7 +62,7 @@ var queryKeys = []string{ QueryUnknown, QueryOtherDay, QueryPersonalNone, QueryFactWhen, QueryFactValue, QueryFound, QueryPageText, QueryPageBlocked, QueryPageEmpty, QueryFeedsOff, QueryFeedsNew, QueryFeedsEmpty, QueryFeedsTopic, - QueryWeatherNow, QueryWeatherOff, QueryWeatherWhere, QueryNetEmpty, + QueryWeatherNow, QueryWeatherOff, QueryWeatherWhere, QueryNetEmpty, QueryNetOff, QueryPageOff, QueryFailPlan, QueryFailNotes, QueryFailFeeds, QueryFailCalendar, QueryFailWeather, QueryFailAnswer, QueryFailPage, QueryFailNetscan, } @@ -85,6 +87,8 @@ var queryFloor = say.RegisterFloor(map[string]string{ QueryWeatherOff: "погода не настроена.", QueryWeatherWhere: "не знаю, для какого города — задай voice.weather.default_location или назови город.", QueryNetEmpty: "в сети никого не нашла{tail}.", + QueryNetOff: "сканирование сети не настроено.", + QueryPageOff: "я не читаю страницы — это не настроено.", QueryFailPlan: "не получилось собрать план.", QueryFailNotes: "не получилось посмотреть записи.", diff --git a/internal/phraser/query_ru_v1.json b/internal/phraser/query_ru_v1.json index 249294e..ac173f8 100644 --- a/internal/phraser/query_ru_v1.json +++ b/internal/phraser/query_ru_v1.json @@ -62,6 +62,14 @@ "fixed": true, "variants": ["не знаю, для какого города — задай voice.weather.default_location или назови город."] }, + "net_off": { + "fixed": true, + "variants": ["сканирование сети не настроено."] + }, + "page_off": { + "fixed": true, + "variants": ["я не читаю страницы — это не настроено."] + }, "net_empty": { "variants": ["в сети никого не нашла{tail}.", "никого в сети не видно{tail}."] },