Files
Maven/docs/reports/2026-09-05-slice1-ingress-boundary.md
T

6.2 KiB

Slice 1: Typed Ingress Boundary and Route Producer Observability

1. Files changed

New types:

  • internal/router/source.goInputSource, InputSourceVoice, InputSourceText, NormalizedInput
  • internal/router/intent.goRouteProducer, RouteProducerGrammar/Heads/LLM/Classifier, Producer field on Decision

Cascade wiring:

  • internal/router/router.goProducer set at each of the four cascade stages

Observability:

  • internal/decision/decision.goInputSource and RouteProducer fields on Record; With() accepts inputSource
  • internal/store/routingtraces.goRouteProducer field on RoutingTrace
  • internal/store/migrations.go — migration #27: ALTER TABLE routing_traces ADD COLUMN route_producer
  • cmd/mavend/routingtrace.go — persists RouteProducer from the decision record

Turn lifecycle:

  • cmd/mavend/voice.goturnSource is now type turnSource = router.InputSource; runTurn takes NormalizedInput; HandlePushToTalk and handleText construct NormalizedInput
  • cmd/mavend/turnroute.goturnRoute carries NormalizedInput; newTurnRoute and resolve use 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.gorunTurn calls updated to NormalizedInput

New tests:

  • internal/router/boundary_test.go — 7 tests: type shape, constants, producer per stage
  • cmd/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 pin
  • TestInputSourceConstants — tap:voice, tap:text
  • TestRouteProducerConstants — grammar, heads, llm, classifier
  • TestStage0SetsGrammarProducer — grammar win carries grammar producer
  • TestClassifierSetsProducer — classifier floor sets its producer
  • TestClarifyProducerIsClassifier — clarified turn carries classifier producer
  • TestStage0ProducerOnEveryGrammar — property test over multiple grammars

cmd/mavend/boundary_test.go (5 tests):

  • TestTextAndVoiceConvergeOnNormalizedInput — same utterance, same route intent
  • TestNormalizedInputSourcePreserved — source survives to decision record
  • TestRouteProducerOnDecisionRecord — producer carried to record
  • TestPreRouteClaimHasNoRouteProducer — confirm-claimed turn has empty producer
  • TestStage0ProducerUnchanged — 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 Simulator replays deterministic scripted days; all scenario assertions pass identically.
  • cmd/mavend -run PersonalBoundary exercises the personal boundary query chain; passes identically.
  • Stage-0 grammars: same ordering in StageZeroGrammars(), same matching semantics, same confidence 1.0.
  • RouteProducer is a new field with zero value "" for existing code paths that don't set it; no existing consumer reads it.
  • NormalizedInput is the same (text string, source turnSource) pair passed as a struct; no transformation applied.

7. Semantic changes required

None. The refactoring is purely structural:

  • turnSource became a type alias for router.InputSource — identical underlying type, no conversion needed at any call site.
  • runTurn takes NormalizedInput instead of (text, src) — the destructuring text := input.Text; src := input.Source at the top of the function body produces identical local variables.
  • decision.With gained an inputSource parameter — a string stored on the record, never read back during routing.
  • RouteProducer is a new field on Decision — 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