router/semantic: slice 23 corpus fast-path reconciliation — DeriveFastPath over the real router replaces the regex mirror, factory/merge validated, corpus rebuilt (dataset_hash unchanged), drift diagnostic and rerun tooling

This commit is contained in:
2026-09-08 04:02:00 +04:00
parent bb6bd8efb9
commit be71ac406b
12 changed files with 8460 additions and 7890 deletions
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -142,7 +142,7 @@ func TestLegacyBaseline(t *testing.T) {
func buildMinimalRouter(t *testing.T) LegacyRouter {
t.Helper()
// Use the same act matcher and seed loading as the eval package.
acts := router.DefaultActMatcher{Fns: actVerbList()}
acts := router.DefaultActMatcher{Fns: ExperimentActVerbs()}
cls := buildSeededClassifier(t, router.NewHashEmbedder(1024))
r := router.New(router.Config{
Grammars: router.StageZeroGrammars(acts),
+82
View File
@@ -0,0 +1,82 @@
package semantic
import (
"context"
"sync"
"time"
"github.com/kami/maven/internal/router"
)
// ExperimentActVerbs is the act allowlist shared by the experiment's legacy
// baseline (the eval fixture and the slice-22 harness), the routed-heads
// harness, and the corpus fast-path derivation. One non-test list so the
// corpus derivation can never drift from what the measurement router accepts.
func ExperimentActVerbs() []string {
return []string{
"перезапусти", "перезагрузи", "выключи", "включи", "останови", "запусти",
"закрой", "открой", "сделай", "поставь",
"restart", "reboot", "stop", "start", "turn off", "turn on", "open", "close",
}
}
func experimentActMatcher() router.ActMatcher {
return router.DefaultActMatcher{Fns: ExperimentActVerbs()}
}
// FastPathOutcome is the derived fast-path classification for one surface.
// Matched mirrors TryFastPath; Grammar names the winning stage-0 grammar for
// attribution, or the empty string when nothing resolved.
type FastPathOutcome struct {
Matched bool
Grammar string
}
var (
fastPathOnce sync.Once
fastPathRouter *router.Router
fastPathGrammars []router.Grammar
)
// DeriveFastPath mirrors the production fast path for a surface: it runs
// TryFastPath over the daemon's ordered stage-0 grammar list with the
// experiment's act allowlist — the exact router the legacy baseline measures —
// and reports whether a grammar resolved the utterance. This is the
// authoritative derivation for corpus fast_path_resolved metadata. The
// classifier, threshold and LLM never run on the fast path, so they are not
// wired here.
func DeriveFastPath(text string) FastPathOutcome {
fastPathOnce.Do(func() {
acts := experimentActMatcher()
fastPathGrammars = router.StageZeroGrammars(acts)
fastPathRouter = router.New(router.Config{
Grammars: fastPathGrammars,
Extractor: router.Extractor{
Time: router.StubDateTimeParser{},
Acts: acts,
Facts: router.DefaultFactParser{},
},
})
})
res, err := fastPathRouter.TryFastPath(context.Background(), router.NormalizedInput{
Text: text,
MatchText: router.NormalizeMatchText(text),
}, time.Now())
if err != nil || !res.Matched {
return FastPathOutcome{}
}
// Attribute the winner by replaying TryFastPath's ordered first-accept
// walk, including the wake-stripped alternate. A matcher that matched the
// shape but declined the content falls through, exactly as the router does.
stripped, hadWake := router.StripWakeToken(text)
for _, g := range fastPathGrammars {
_, matched, ok := g.Evaluate(text)
if !matched && hadWake {
_, matched, ok = g.Evaluate(stripped)
}
if matched && ok {
return FastPathOutcome{Matched: true, Grammar: g.Name}
}
}
return FastPathOutcome{Matched: true}
}
@@ -0,0 +1,36 @@
package semantic
import "testing"
// TestFastPathDerivationInvariant asserts that every development-pool row's
// stored fast-path flag exactly matches what the production fast path derives
// today (DeriveFastPath runs TryFastPath over the stage-0 grammars with the
// experiment's act allowlist). Frozen holdout rows are preserved verbatim
// across merges and are exempt — their drift is reported, never silently
// rewritten. A stale development value is a corpus-build error.
func TestFastPathDerivationInvariant(t *testing.T) {
exs, err := LoadCorpus()
if err != nil {
t.Fatalf("load corpus: %v", err)
}
_, dev, _ := FrozenHoldoutSplit(exs)
devSet := make(map[string]bool, len(dev))
for _, e := range dev {
devSet[e.SourceID] = true
}
stale := 0
for _, e := range exs {
if !devSet[e.SourceID] {
continue
}
if got := DeriveFastPath(e.Text).Matched; got != e.FastPathResolved {
t.Errorf("dev row %s (route=%s) fast_path_resolved=%v but router derives %v: %q",
e.SourceID, e.Route, e.FastPathResolved, got, e.Text)
stale++
}
}
if stale > 0 {
t.Fatalf("%d development rows have stale fast-path metadata", stale)
}
}
-8
View File
@@ -19,14 +19,6 @@ var seedIntents = []router.Intent{
router.IntentNote, router.IntentQuery, router.IntentChat, router.IntentSystem,
}
func actVerbList() []string {
return []string{
"перезапусти", "перезагрузи", "выключи", "включи", "останови", "запусти",
"закрой", "открой", "сделай", "поставь",
"restart", "reboot", "stop", "start", "turn off", "turn on", "open", "close",
}
}
func buildSeededClassifier(t *testing.T, emb router.Embedder) *router.Classifier {
t.Helper()
cls := router.NewClassifier(emb)