package router import "github.com/kami/maven/internal/claim" // ClaimOf — build a claim.Claim from a Decision (V-565, design in // docs/plans/19-dialogue-arbitration.md). // // Additive and beside the existing path. Decision.Confidence keeps its float // and keeps working: r.threshold and gateLLMDecision read it, and the // classifier cascade is the failure floor. Nothing in Route calls this yet. // The arbiter that reads claims is V-560. // // claimant names who produced the decision. The cascade does not record which // stage-0 grammar matched, so the caller passes what it knows and the builder // does not guess. func ClaimOf(claimant string, d Decision) claim.Claim { consumed, unexplained := claim.Split(d.Utterance, claimSpans(d)...) return claim.Claim{ Claimant: claimant, Intent: string(d.Intent), Filled: filledSlots(d.Slots), Consumed: consumed, Unexplained: unexplained, Band: bandOf(d), Veto: vetoOf(d), } } // claimSpans — the parts of the utterance the decision says it read. Slot // values, not the utterance, because coverage is the question of how much of // the sentence the claim actually explains. // // A stage-0 grammar reports whatever its Build put in the slots, which for the // reminder rule is the text after "напомни" and not the verb itself. That // under-reports coverage rather than over-reporting it, which is the safe // direction: a claim that overstates what it explains wins arbitrations it // should lose. func claimSpans(d Decision) []string { spans := []string{d.Slots.Text, d.Slots.Key, d.Slots.Value, d.Slots.Fn} return append(spans, d.Slots.Args...) } // filledSlots — the slot names this decision would fill. Text counts only when // it differs from the whole utterance: fillSlots backfills the raw utterance // into Text for a note, a query and a chat turn, so a set Text is not by itself // evidence that anything was extracted. func filledSlots(s Slots) []string { var out []string if s.HasTime { out = append(out, "time") } if s.HasFn { out = append(out, "fn") } if s.HasKey { out = append(out, "key") } if s.Text != "" { out = append(out, "text") } return out } // bandOf — which kind of evidence this decision rests on. // // Stage 0 is anchored: a literal pattern matched and its span decided the // intent. The LLM path (stage 1) is structural: the model read the whole // sentence, and gateLLMDecision already checked the route for structural // holes. The classifier (stages 2 and 3) is nearest, and the measurement is why // it is one band rather than a scale — on the 91-case RU fixture its cosine // spans 0.859 to 0.942 and scores 62% at both ends. // // A decision carrying a veto lands in BandVetoed regardless of who produced it. // That is the point of the band: a self-vetoed claim should lose to any claim // that is not, whatever machinery built it. func bandOf(d Decision) claim.Band { if vetoOf(d) != "" { return claim.BandVetoed } switch d.Stage { case 0: return claim.BandAnchored case 1: return claim.BandStructural default: return claim.BandNearest } } // vetoOf — why this decision should not win, recovered as a reason rather than // a number. // // gateLLMDecision flattens three named structural holes into // llmThinConfidence, and the reason is lost at that point: 0.3 tells a reader // that something was wrong and never which thing. The same three conditions are // checked here so the claim carries the sentence a trace can print and the // owner can be told. // // Clarify is checked last and is the general case. A decision below threshold // has already asked to be doubted, whichever path set it. func vetoOf(d Decision) string { switch { case d.Intent == IntentFact && !d.Slots.HasKey: return "fact with no key: nothing to write, or a confident write under the wrong key" case d.Intent == IntentAct && !d.Slots.HasFn: return "act with no allowlisted fn: running an unlisted command or silently doing nothing" case d.Intent == IntentReminder && !reminderHasSubject(d.Slots.Text): return "reminder with no subject: it would fire empty at the hour" case d.Clarify: return "below the confidence gate" } return "" }