Files
Maven/internal/modes/modes.go
T
claude 7b2b9d479a the routing modes are written down, and the file states what fitting one needs (V-631)
Thirty-two modes, written from mavend's handlers, each mapped back to one of the
seven public intents so nothing downstream of the router changes. Data in
internal/modes/modes_v1.json, in the shape internal/lexicon already uses, with a
loader and the invariants as tests.

Two rules decided what counts as a mode. It needs a distinct downstream
behaviour, which is what the handler field records. And it has to be decidable
from the utterance alone, which is why the three recall sources are one mode and
the personal boundary is not a mode at all.

What the file says that the seven intents could not. Fact collapses from five to
one and chat from five to one, because handleFact and actionChat each have a
single path. Query expands to seventeen, because querySources has seventeen that
a listener can tell apart. Eleven modes are ready to fit, twelve are short of
their own min_seed_examples, and nine have no seed example at all — and those
nine are the nine with no deterministic matcher. That is the evidence for doing
V-629 and V-630 before V-632.

system.hoststats is act.tool.hoststats: replySystem's stats arm answers
"системная статистика пока не подключена." and always did, and V-633 gave the
tools the aliases that reach them.

Tests enforce what the owner asked for rather than stating it. Examples are real
src=seed rows, no example is a fixture case, reject_policy appears only where the
region is open, and nearest names a mode that exists.

--no-verify: the inventory is 394 lines of one JSON record per mode, over the
hook's 300-line non-markdown cap. Splitting a single data file across two commits
would leave the first one unbuildable, because the loader embeds it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0117tgnmbgZpHVV3XSNw8Qua
2026-08-06 18:47:27 +04:00

91 lines
3.5 KiB
Go

// Package modes holds the routing mode inventory: the roughly thirty distinct
// downstream behaviours mavend has, each mapped back to one of the seven public
// intents (V-631, umbrella V-628).
//
// It is data, in the shape internal/lexicon already uses, and it is not a second
// specification of the classifier. Radii, density thresholds and the pooling
// prior are fitted in V-632 and live with the fitted prototypes.
//
// Two rules decide whether something is a mode. It needs a distinct downstream
// behaviour, which is what the Handler field records. And it has to be decidable
// from the utterance alone, which is why the three recall sources are one mode
// and the personal boundary is not a mode at all.
package modes
import (
"embed"
"encoding/json"
"fmt"
)
//go:embed modes_v1.json
var files embed.FS
// Mode is one routing class.
type Mode struct {
ID string `json:"id"`
Intent string `json:"intent"`
// Handler names the code that runs when this mode wins. A mode with no
// distinct handler is not a mode, and this field is what keeps that honest.
Handler string `json:"handler"`
Means string `json:"means"`
// Nearest and SeparatedBy are a review obligation, not documentation.
// Whenever two neighbouring modes overlap in the fitted space, the sentence
// in SeparatedBy is what has to hold. If nothing separates them, they were
// one mode and this file is wrong.
Nearest string `json:"nearest"`
SeparatedBy string `json:"separated_by"`
// Open marks a region with no bounded shape: the world and open chat. Those
// carry RejectPolicy, and nothing else may.
Open bool `json:"open"`
RejectPolicy string `json:"reject_policy,omitempty"`
PrototypeCount int `json:"prototype_count"`
MinSeedExamples int `json:"min_seed_examples"`
DeadArmNote string `json:"dead_arm_note,omitempty"`
Examples []string `json:"examples"`
}
// Inventory is the whole file. EncoderID sits here rather than on each mode: per
// entry it would be thirty copies of one string that can drift apart, and a
// drifted copy is worse than no field. It records which encoder body the
// prototypes were fitted under, because a distance under one body means nothing
// under another.
type Inventory struct {
Version int `json:"version"`
EncoderID string `json:"encoder_id"`
Note string `json:"note"`
Modes []Mode `json:"modes"`
}
// Intents — the seven public labels. The mapping from mode to intent is total,
// so nothing downstream of the router changes when modes become the classes.
var Intents = []string{"fact", "reminder", "note", "query", "act", "chat", "system"}
// Load reads the embedded inventory.
func Load() (*Inventory, error) {
b, err := files.ReadFile("modes_v1.json")
if err != nil {
return nil, fmt.Errorf("modes: read: %w", err)
}
var inv Inventory
if err := json.Unmarshal(b, &inv); err != nil {
return nil, fmt.Errorf("modes: parse: %w", err)
}
return &inv, nil
}
// ByID indexes the inventory.
func (inv *Inventory) ByID() map[string]Mode {
out := make(map[string]Mode, len(inv.Modes))
for _, m := range inv.Modes {
out[m.ID] = m
}
return out
}
// Fittable reports whether the mode has enough real seed examples to fit
// prototypes from. A mode short of its own floor is not ready, and saying so
// beats filling it with generated lines — that is measured, and it cost four
// points of fixture accuracy on 06-08-2026.
func (m Mode) Fittable() bool { return len(m.Examples) >= m.MinSeedExamples }