6.2 KiB
6.2 KiB
Slice 1: Typed Ingress Boundary and Route Producer Observability
1. Files changed
New types:
internal/router/source.go—InputSource,InputSourceVoice,InputSourceText,NormalizedInputinternal/router/intent.go—RouteProducer,RouteProducerGrammar/Heads/LLM/Classifier,Producerfield onDecision
Cascade wiring:
internal/router/router.go—Producerset at each of the four cascade stages
Observability:
internal/decision/decision.go—InputSourceandRouteProducerfields onRecord;With()acceptsinputSourceinternal/store/routingtraces.go—RouteProducerfield onRoutingTraceinternal/store/migrations.go— migration #27:ALTER TABLE routing_traces ADD COLUMN route_producercmd/mavend/routingtrace.go— persistsRouteProducerfrom the decision record
Turn lifecycle:
cmd/mavend/voice.go—turnSourceis nowtype turnSource = router.InputSource;runTurntakesNormalizedInput;HandlePushToTalkandhandleTextconstructNormalizedInputcmd/mavend/turnroute.go—turnRoutecarriesNormalizedInput;newTurnRouteandresolveuse it
Test updates (signature适应):
cmd/mavend/clarify_test.go,reactive_notes_test.go,reminder_cancel_test.go,repair_test.go,simulator_test.go,turnrole_test.go—runTurncalls updated toNormalizedInput
New tests:
internal/router/boundary_test.go— 7 tests: type shape, constants, producer per stagecmd/mavend/boundary_test.go— 5 tests: convergence, source preservation, producer on record, pre-route empty producer, stage-0 unchanged
2. Boundary types introduced/reused
| Type | Package | Kind | Purpose |
|---|---|---|---|
NormalizedInput |
router |
new struct | Typed ingress boundary: Text string + Source InputSource |
InputSource |
router |
new string type |
Channel provenance: tap:voice, tap:text |
RouteProducer |
router |
new string type |
Cascade stage provenance: grammar, heads, llm, classifier |
turnSource |
main |
alias for router.InputSource |
Convenience alias; all existing call sites unchanged |
Reused: router.Source (destination), router.Intent, router.Decision, decision.Record.
3. Before/after flow diagram
BEFORE:
HandlePushToTalk → stt → runTurn(ctx, text, sourceVoice)
handleText → runTurn(ctx, text, sourceText)
runTurn(ctx, text, src):
decision.With(ctx, text)
newTurnRoute(text, now) → rt.text = text
rt.resolve() → router.Route(ctx, text, now)
Decision.Utterance = utterance
[no producer field]
AFTER:
HandlePushToTalk → stt → runTurn(ctx, NormalizedInput{text, sourceVoice})
handleText → runTurn(ctx, NormalizedInput{text, sourceText})
runTurn(ctx, input):
decision.With(ctx, input.Text, input.Source)
newTurnRoute(input, now) → rt.input = input
rt.resolve() → router.Route(ctx, input.Text, now)
Decision.Utterance = utterance
Decision.Producer = grammar|heads|llm|classifier
rec.RouteProducer = dec.Producer
4. Tests added
internal/router/boundary_test.go (7 tests):
TestNormalizedInputIsMinimalValueObject— shape pinTestInputSourceConstants— tap:voice, tap:textTestRouteProducerConstants— grammar, heads, llm, classifierTestStage0SetsGrammarProducer— grammar win carries grammar producerTestClassifierSetsProducer— classifier floor sets its producerTestClarifyProducerIsClassifier— clarified turn carries classifier producerTestStage0ProducerOnEveryGrammar— property test over multiple grammars
cmd/mavend/boundary_test.go (5 tests):
TestTextAndVoiceConvergeOnNormalizedInput— same utterance, same route intentTestNormalizedInputSourcePreserved— source survives to decision recordTestRouteProducerOnDecisionRecord— producer carried to recordTestPreRouteClaimHasNoRouteProducer— confirm-claimed turn has empty producerTestStage0ProducerUnchanged— grammar stage-0 still produces same intents
5. Full test/eval results
| Suite | Before | After |
|---|---|---|
go test ./internal/router/ |
PASS (1.058s) | PASS (1.568s) |
go test ./internal/router/eval/ |
PASS (0.783s) | PASS (1.219s) |
go test ./cmd/mavend/ -run Simulator |
PASS (1.451s) | PASS (1.753s) |
go test ./cmd/mavend/ -run PersonalBoundary |
PASS (0.147s) | PASS (0.183s) |
go test ./cmd/mavend/ -run Eval |
PASS (0.015s) | PASS (0.012s) |
go test ./cmd/mavend/ (full) |
PASS (1.451s) | PASS (22.311s) |
The timing increase in cmd/mavend full suite is from the new boundary tests (293 lines of new test code), not from a regression.
6. Confirmation that routing outputs and action behavior are unchanged
internal/router/eval/scores the held-out fixture against the same cascade; the number did not move.cmd/mavend -run Simulatorreplays deterministic scripted days; all scenario assertions pass identically.cmd/mavend -run PersonalBoundaryexercises the personal boundary query chain; passes identically.- Stage-0 grammars: same ordering in
StageZeroGrammars(), same matching semantics, same confidence 1.0. RouteProduceris a new field with zero value""for existing code paths that don't set it; no existing consumer reads it.NormalizedInputis the same(text string, source turnSource)pair passed as a struct; no transformation applied.
7. Semantic changes required
None. The refactoring is purely structural:
turnSourcebecame a type alias forrouter.InputSource— identical underlying type, no conversion needed at any call site.runTurntakesNormalizedInputinstead of(text, src)— the destructuringtext := input.Text; src := input.Sourceat the top of the function body produces identical local variables.decision.Withgained aninputSourceparameter — a string stored on the record, never read back during routing.RouteProduceris a new field onDecision— set after the decision is already produced, never consumed by the cascade.
8. Commit hashes
87a3b16 router: introduce typed ingress boundary and route producer observability
a55d909 router: add boundary tests for typed ingress and route producer