diff --git a/README.md b/README.md index d47558d..21e1f68 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ address) and `mesh_runtimes` (which runtimes answered discovery: per runtime its answer took, its size in bytes, how many modules and tools it announced, whether it was shortened to fit the bus, when it was last heard — and who was expected and not heard). -`mesh_search` also finds a tool by the shell command it replaces (novox/hq ADR 0242): each seat verb and +`mesh_search` also finds a tool by the shell command it replaces (novox/hq ADR 0245): each seat verb and module tool says what it replaces in the controller's records (`replaces`, read from the controller's `tools` and `modules` answers), so `journalctl -u x`, `systemctl status x` or `docker ps` puts the verb for it first, and says the command it matched as `instead_of`. A word also matches through a short table diff --git a/merge-check.sh b/merge-check.sh index 5f91179..d04b8a5 100644 --- a/merge-check.sh +++ b/merge-check.sh @@ -31,7 +31,7 @@ if ! command -v node >/dev/null 2>&1 || [ ! -d node_modules/@novox/mesh-sdk ]; t echo "NOT TESTED HERE: internal/runtime and internal/console launch bundles that need node and @novox/mesh-sdk" packages=$(printf '%s\n' "$packages" | grep -v -e '/internal/runtime$' -e '/internal/console$') # What of the console launches no bundle runs anyway: how an address resolves (novox/hq issue 287), - # and how a search ranks (novox/hq ADR 0242). + # and how a search ranks (novox/hq ADR 0245). bundleless='^(TestASeatAndAModuleOfOneNameAreEachReached|TestSearch.*|TestASearchByCommandSaysWhatItReplaces)$' fi if command -v gcc >/dev/null 2>&1; then diff --git a/node-tools/internal/console/address.go b/node-tools/internal/console/address.go index 6ec497c..f216ddc 100644 --- a/node-tools/internal/console/address.go +++ b/node-tools/internal/console/address.go @@ -131,7 +131,7 @@ type index struct { // what it serves in time, or it answered before and not now. An address it might answer is never // called missing while it is here (2026-10-05). Unheard []unheard - // Replaces is what each verb and tool replaces, from the controller's records (novox/hq ADR 0242): + // Replaces is what each verb and tool replaces, from the controller's records (novox/hq ADR 0245): // `seat:.` and `.`. Replaces map[string][]string } @@ -225,7 +225,7 @@ type recordedModule struct { Module string `json:"module"` On []string `json:"on"` Tools bool `json:"tools"` - // Replaces is what each of its own tools replaces (novox/hq ADR 0242). + // Replaces is what each of its own tools replaces (novox/hq ADR 0245). Replaces map[string][]string `json:"replaces,omitempty"` } @@ -244,7 +244,7 @@ func indexOn(conn *bus.Conn) (*index, error) { wg.Add(3) go func() { defer wg.Done(); nodesOut, _ = controllerOutput(conn, "nodes") }() go func() { defer wg.Done(); modulesOut, _ = controllerOutput(conn, "modules") }() - // The seats' verbs as the records define them, for what each replaces (novox/hq ADR 0242): answered + // The seats' verbs as the records define them, for what each replaces (novox/hq ADR 0245): answered // in-process from the records, never a command. go func() { defer wg.Done() @@ -832,7 +832,7 @@ func (s *Surface) discover(name string, args map[string]any) map[string]any { if len(hits) == 0 { out["matches"] = []hit{} out["hint"] = "nothing matched every word; try fewer words, or mesh_overview and mesh_machine to browse. " + - "If no tool does it, one is created in the module that owns it, on its seat — never worked around (novox/hq ADR 0242)" + "If no tool does it, one is created in the module that owns it, on its seat — never worked around (novox/hq ADR 0245)" } if len(x.Unheard) > 0 { out["incomplete"] = "discovery did not hear in full from " + sayUnheard(x.Unheard) + diff --git a/node-tools/internal/console/search.go b/node-tools/internal/console/search.go index 66b0625..eaeaf1d 100644 --- a/node-tools/internal/console/search.go +++ b/node-tools/internal/console/search.go @@ -1,6 +1,6 @@ package console -// Finding a tool by the words an agent already has (novox/hq ADR 0242). +// Finding a tool by the words an agent already has (novox/hq ADR 0245). // // An agent that wants a unit's log reaches for `journalctl`, and one that wants a container's for `docker // logs`; it searches with those words, or with `logs`. Matched against names and descriptions alone, none @@ -12,10 +12,11 @@ package console // every word of the replaced command, in order, among the query's words. The most specific command wins. // 2. **Every word is in the tool's name, its replaced commands or its description**, a word also matching // through a short table of the words agents use for the mesh's (logs ⇄ journal, ps ⇄ list). A word in -// the name or a replaced command weighs more than one in the description. +// the name or a replaced command weighs more than one in the description, and a name the query says +// whole more than one it says in part. // 3. Nothing else: a search that matches nothing says so. // -// Ties keep the listing's order — seats first, the place tools belong — then the modules by name. +// Ties go to a seat — the place tools belong — then to the shorter name, then by address. import ( "encoding/json" @@ -149,6 +150,19 @@ func scoreOf(query string, qwords []string, name, description string, replaces [ score += found } } + // A name the query says whole — `docker logs` for docker_logs, not docker_secrets_in_logs — first. + own := name[strings.LastIndex(name, ".")+1:] + whole := true + for _, part := range strings.FieldsFunc(strings.ToLower(own), func(r rune) bool { return r == '_' || r == '-' }) { + in := false + for _, w := range qwords { + in = in || w == part + } + whole = whole && in + } + if whole { + score += 2 + } return score, "" } @@ -208,11 +222,22 @@ func (x *index) search(query string) []hit { hits = append(hits, h) } } - sort.SliceStable(hits, func(i, j int) bool { return hits[i].score > hits[j].score }) + // Best first; on a tie the shorter name — the plainer tool — then the address, so an answer never + // depends on the order discovery heard things in. + sort.SliceStable(hits, func(i, j int) bool { + if hits[i].score != hits[j].score { + return hits[i].score > hits[j].score + } + a, b := hits[i].Address[strings.LastIndex(hits[i].Address, ".")+1:], hits[j].Address[strings.LastIndex(hits[j].Address, ".")+1:] + if len(a) != len(b) { + return len(a) < len(b) + } + return hits[i].Address < hits[j].Address + }) return hits } -// replacesOf is what every verb and tool replaces, from the controller's records (novox/hq ADR 0242): +// replacesOf is what every verb and tool replaces, from the controller's records (novox/hq ADR 0245): // a seat's verbs from its `tools` answer, keyed `seat:.`, and a module's own tools from // its `modules` answer, keyed `.`. A controller older than the field answers neither, // and search matches names and descriptions as before. diff --git a/node-tools/internal/console/search_test.go b/node-tools/internal/console/search_test.go index 5683782..41ba59c 100644 --- a/node-tools/internal/console/search_test.go +++ b/node-tools/internal/console/search_test.go @@ -55,7 +55,7 @@ func meshToSearch(t *testing.T) *index { return x } -// **The words an agent has find the verb first** (novox/hq ADR 0242): the command it would have run, or +// **The words an agent has find the verb first** (novox/hq ADR 0245): the command it would have run, or // the word it would have used, puts the mesh's tool for it at the top. func TestSearchFindsTheToolThatReplacesACommandFirst(t *testing.T) { x := meshToSearch(t)