Files
Maven/internal/claim/claim.go
T
claude 72aa97dae8 sweep claim, decision, netscan and vision (V-581)
The decision ring kept evicted turn records reachable. Push resliced the
backing array forward without clearing the dropped pointers, so up to
ringSize records stayed addressable until the next append reallocated. The
package holds this store in memory precisely so his words do not outlive the
diagnosis, and the reslice quietly broke that. Push now nils the dropped
slots first.

internal/claim carried three drifted counts in its package doc. The cascade
has twenty-two stage-0 grammars and not ten, and seven stateful pre-emptors
and not four. The band ordering itself did not drift: bandOf still maps stage
0 to anchored, the LLM router to structural and the classifier to nearest,
which is the order buildRouter and querySources actually run in.
BandStructural now names preRouteLadder as the roster so the next count is
checkable rather than remembered.

netscan formatted a port with fmt.Sprintf once per probe. A default scan is
1016 probes, so strconv.Itoa is the same string for less work, and the local
itoa helper goes with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 03:28:23 +04:00

194 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package claim is the common unit for the many claimants that compete for one
// utterance (V-565, umbrella V-558, design in
// docs/plans/19-dialogue-arbitration.md).
//
// Maven's cascade has twenty-two stage-0 grammars, seven router intents,
// twenty-two query sources and seven stateful pre-emptors, and every one of them
// answers "is this mine?" alone. None can answer "is this more mine than
// yours?", because their scores are not comparable: stage 0 asserts 1.0 by
// fiat, the classifier reports a cosine, the LLM router derives one from
// structure. So list order is the whole arbitration.
//
// A Claim carries evidence rather than a verdict. Two things read that evidence
// and neither needs a float:
//
// - specificity — a claim explaining more of the utterance is preferred, and
// that is Consumed against Unexplained;
// - negative constraint — a claimant may veto itself and say why, and that is
// Veto.
//
// Where a number is unavoidable, it is an ordinal Band and not a probability.
// The band set is argued from measurement in the plan doc: the classifier's
// cosine is flat against correctness (62% at both ends of a spread 0.083 wide)
// and its top-two margin is p50 0.009, so no claimant Maven has today can
// produce a graded confidence.
//
// This package deliberately imports nothing from the rest of Maven.
// internal/dialogue must not import internal/router, so a shared unit that
// pulled in router.Intent would smuggle that edge back in. Intent is a plain
// string and the conversion happens at each edge.
package claim
import "strings"
// Band — the kind of evidence behind a claim, ordinal and comparable. Higher
// wins a tie. Four values, because four is what the claimants can report.
type Band int
const (
// BandUnknown — the zero value. A claim that never set a band is a bug in
// its builder, not a weak claim, so it must not silently rank as one.
BandUnknown Band = iota
// BandVetoed — the claimant will take the turn only if nobody else will,
// and Veto says why it should not. The three arms of gateLLMDecision (a
// fact with no key, an act with no allowlisted fn, a reminder with no
// subject) land here. Still a claim: asking "о чём напомнить?" beats
// silence.
BandVetoed
// BandNearest — the claim rests only on resemblance to something else,
// with no anchor in the utterance and no structural check behind it. The
// nearest-centroid classifier. One band and not a graded scale, because
// the cosine measured flat against correctness.
BandNearest
// BandStructural — the claimant read the whole sentence and produced a
// complete route, every slot its intent requires filled. The LLM router at
// full confidence, and a stateful claimant holding a pending question.
// Below BandAnchored on purpose: the stateful claimants pre-empt
// unconditionally today, and that is the V-558 defect. There are seven of
// them and preRouteLadder in cmd/mavend/decisiontrace.go is the roster.
BandStructural
// BandAnchored — a literal pattern anchored in the utterance matched, and
// the matched span is what decides the intent. Stage 0 grammars and
// query-source matchers. Certainty about the shape of the sentence, which
// is not certainty about the answer.
BandAnchored
)
// String — the band's name, for a trace line and for a test failure that has to
// say which band it got.
func (b Band) String() string {
switch b {
case BandVetoed:
return "vetoed"
case BandNearest:
return "nearest"
case BandStructural:
return "structural"
case BandAnchored:
return "anchored"
default:
return "unknown"
}
}
// Claim — one claimant's bid for one utterance.
type Claim struct {
// Claimant — who wants the turn. A grammar name, a query source name, a
// stage label. Read by the trace and by a test naming a loser.
Claimant string
// Intent — the route this claim would take. A plain string and not
// router.Intent: see the package comment.
Intent string
// Filled — the slot names this claim would fill ("time", "fn", "key",
// "text"). Names and not values, because arbitration compares shape.
Filled []string
// Consumed — the utterance tokens this claim explains, in the order they
// appear. The numerator of specificity.
Consumed []string
// Unexplained — the tokens this claim does not explain, in order. Carried
// rather than derived, so a claimant may decline a span it did match.
Unexplained []string
// Band — the kind of evidence. The tie-break, after coverage.
Band Band
// Veto — why this claim should NOT win, empty when there is none. A
// non-empty Veto and a Band above BandVetoed is legal: a claim can be
// well-evidenced and still name a reason to prefer somebody else.
Veto string
}
// Coverage — the fraction of the utterance this claim explains, in [0,1]. A
// claim with no tokens either way covers nothing; it is not division by zero
// and it is not a full claim.
func (c Claim) Coverage() float64 {
total := len(c.Consumed) + len(c.Unexplained)
if total == 0 {
return 0
}
return float64(len(c.Consumed)) / float64(total)
}
// Vetoed reports whether the claimant named a reason against itself.
func (c Claim) Vetoed() bool { return c.Veto != "" }
// MoreSpecificThan — the ordering V-560's arbiter will read. Coverage first,
// because that is what fixes the failure this program opened with: a pending
// reminder ate "какая сейчас погода в Риме?" as a time answer while explaining
// none of it. Band only breaks a coverage tie.
//
// Deliberately NOT wired into the cascade by V-565. It is here so the ordering
// is one function with tests on it, rather than a rule restated at each of the
// sites that will eventually call it.
func (c Claim) MoreSpecificThan(other Claim) bool {
cc, oc := c.Coverage(), other.Coverage()
if cc != oc {
return cc > oc
}
return c.Band > other.Band
}
// Tokens — the utterance split for coverage accounting. Whitespace, then
// trailing and leading punctuation, then lowercased.
//
// This is tokenization over the raw string and not a Russian pattern: it
// contains no word list, and its output is a count rather than a fact or a
// route (CLAUDE.md § Russian patterns). Lowercasing is Unicode-aware, so
// Cyrillic folds the same way Latin does.
func Tokens(utterance string) []string {
fields := strings.FieldsFunc(utterance, func(r rune) bool {
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
})
out := make([]string, 0, len(fields))
for _, f := range fields {
t := strings.Trim(strings.ToLower(f), ".,!?;:()\"'«»…-–—")
if t == "" {
continue
}
out = append(out, t)
}
return out
}
// Split partitions the utterance's tokens into the ones a claim explains and
// the rest, preserving order in both. A token is explained when it appears in
// one of the spans the claimant filled (a slot value, a matched substring).
//
// Duplicates are handled by membership and not by count: "напомни напомни
// позвонить" with span "напомни" explains both copies. The alternative is a
// multiset, and no claimant Maven has can say which copy it meant.
func Split(utterance string, spans ...string) (consumed, unexplained []string) {
explained := map[string]bool{}
for _, s := range spans {
for _, t := range Tokens(s) {
explained[t] = true
}
}
for _, t := range Tokens(utterance) {
if explained[t] {
consumed = append(consumed, t)
} else {
unexplained = append(unexplained, t)
}
}
return consumed, unexplained
}