From 80f807d2ad71fa872c0444987bd0ffd4e12fafb5 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 7 Sep 2026 01:24:19 +0400 Subject: [PATCH] router: extract stage-0 grammars into TryFastPath (slice 10) --- internal/router/router.go | 48 ++++++--------------------------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/internal/router/router.go b/internal/router/router.go index 93265a3..70a3f77 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -72,48 +72,14 @@ func New(cfg Config) *Router { // since(key)==null → don't fire: a misrouted fact is a confident wrong write, // worse than a gap). func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (Decision, error) { - // stage 0 — exact match / grammar. First match wins; grammars are ordered. - // Grammars like time/date/reminder don't expect a wake-word prefix, but - // the STT often includes one (transcribed phonetically, any script) — try - // the wake-stripped utterance too so those grammars still fire. - stripped, hadWake := StripWakeToken(utterance) - // declinedBuild — the grammars that matched the shape and refused the - // content, kept for the decision record (V-564) so a reader can tell that - // rule from one whose pattern never fired. - var declinedBuild map[int]bool - for i, g := range r.grammars { - d, matched, ok := g.Evaluate(utterance) - if !matched && hadWake { - d, matched, ok = g.Evaluate(stripped) - } - if !matched { - continue - } - if !ok { - if declinedBuild == nil { - declinedBuild = map[int]bool{} - } - declinedBuild[i] = true - continue // grammar matched shape but not content → fall through - } - d.Utterance = utterance - d.Producer = RouteProducerGrammar - // A literal pattern named that destination, which is the one provenance - // allowed to take the personal boundary off a turn (V-666). Set here and - // nowhere else, so no other arm of the cascade can claim it. - d.SourceAnchored = d.Source != SourceUnknown - // The grammar decided the intent; the extractor fills the slots it did - // not match (V-572). See fillMatchedSlots for why every grammar gets it. - r.fillMatchedSlots(ctx, &d, now) - // Capability selection: deterministic grammars that already resolved Fn - // (wakeword-act, praxis, task-status) bypass the general selector. - // SelectCapability records the existing result with - // InputKind=SelectionDeterministic. - applyCapabilityToSlots(&d, SelectCapability(d, r.extractor.Acts)) - r.noteGrammarOutcomes(ctx, i+1, declinedBuild, g.Name, d.Intent) - return d, nil + // stage 0 — deterministic fast path. First match wins; grammars are ordered. + fast, err := r.TryFastPath(ctx, NormalizedInput{Text: utterance}, now) + if err != nil { + return Decision{}, err + } + if fast.Matched { + return fast.Decision, nil } - r.noteGrammarOutcomes(ctx, len(r.grammars), declinedBuild, "", "") // stage 0b — routing heads (when wired). A softmax over the label set, so // it cannot name an intent or a destination that does not exist, and its