package router import ( "context" "time" ) // FastPathResult is the outcome of the stage-0 deterministic fast path. // Matched is true when a grammar accepted the input. Decision carries the // full routing result for a match. When Matched is false the caller falls // through to the general cascade (heads → LLM → classifier → extraction → gate). type FastPathResult struct { Matched bool Decision Decision } // TryFastPath runs the stage-0 exact-match grammars. It owns the ordered // grammar list, first-match-wins semantics, wake-token alternate handling, // slot filling, capability selection, and per-grammar trace recording. // // When no grammar matches, Matched is false and the caller falls through // to the general cascade. // // TryFastPath receives NormalizedInput so later recognizers can opt into // MatchText, but no existing grammar switches in this slice — they all // receive input.Text (the raw utterance) exactly as they do today. func (r *Router) TryFastPath( ctx context.Context, input NormalizedInput, now time.Time, ) (FastPathResult, 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(input.Text) // 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(input.Text) 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 = input.Text 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 FastPathResult{Matched: true, Decision: d}, nil } r.noteGrammarOutcomes(ctx, len(r.grammars), declinedBuild, "", "") return FastPathResult{}, nil }