# Slice 6b: Capability Selection Boundary *2026-09-06, from 05f79173* ## 1. CapabilitySelection / selector contract ```go type CapabilitySelection struct { Fn string Args []string Resolved bool Method ActionResolutionMethod InputKind SelectionInputKind Producer RouteProducer Confidence float64 } type SelectionInputKind string const ( SelectionRaw SelectionInputKind = "raw" SelectionLLMText SelectionInputKind = "llm_text" SelectionDeterministic SelectionInputKind = "deterministic" ) func SelectCapability(dec Decision, m ActMatcher) CapabilitySelection func applyCapabilityToSlots(dec *Decision, sel CapabilitySelection) ``` `SelectCapability` is the single entry point for capability selection. It sits between route resolution and action candidate production. `applyCapabilityToSlots` propagates the selection into `Decision.Slots.Fn/Args/HasFn` for backward compatibility. `CapabilitySelection` is the authoritative record. ## 2. Where the selector lives `internal/router/capability.go` — new file, 115 lines. Called from `Router.Route` in `internal/router/router.go` after each cascade path: - Stage-0 grammar path (line ~108) - Stage-0b heads path (line ~148) - Stage-1a LLM path (line ~190) - Stage-1 classifier path (line ~250) ## 3. Old vs new ownership | Concern | Before | After | |---|---|---| | Raw capability extraction | `Extractor.Extract(IntentAct)` inside `fillMatchedSlots` | Same extractor, but `CapabilitySelection` is the authoritative record | | LLM text capability backfill | Embedded in `fillSlots` (lines 319-324) | Moved to `SelectCapability` | | Fallback matcher | `ResolveActionCandidate` (lines 160-171) | Same location, unchanged | | Resolution method tracking | `Slots.ResolvedBy` only | `CapabilitySelection.Method` (authoritative), `Slots.ResolvedBy` (compatibility) | ## 4. Compatibility fields `Decision.Slots.Fn/Args/HasFn/ResolvedBy` are still populated by `applyCapabilityToSlots` from the `CapabilitySelection` result. They exist for backward compatibility with code that reads slots directly (tests, rebuilt decisions). The ownership distinction is documented on the `Decision` struct. ## 5. Before/after flow **Before:** ``` Route → Decision (Slots.Fn set by extractor/fillSlots) ↓ resolveAction → ResolveActionCandidate(dec, matcher) → if Slots.HasFn: candidate from slots → else: fallback matcher ``` **After:** ``` Route → Decision ↓ fillMatchedSlots (time, key extraction) ↓ SelectCapability(dec, matcher) → CapabilitySelection ↓ applyCapabilityToSlots(dec, sel) → populates Slots.Fn/Args/HasFn for compat ↓ resolveAction → ResolveActionCandidate(dec, matcher) → if CapabilitySelection.Resolved: candidate from selection → else if Slots.HasFn: backward compat path → else: fallback matcher ``` ## 6. Fixture matrix before/after ``` Routing fixture (11 act cases): grammar_fixed=2, grammar_matcher=2, extractor_raw=4, extractor_llm_text=0, fallback_matcher=0 resolved: 8, unresolved: 3 Ecosystem fixture (24 act cases): grammar_fixed=12, grammar_matcher=0, extractor_raw=11, extractor_llm_text=0, fallback_matcher=0 resolved: 23, unresolved: 1 ``` Identical before and after. Zero distribution change. ## 7. Fallback matcher usage The fallback matcher in `ResolveActionCandidate` is unchanged. It still runs when `CapabilitySelection.Resolved == false` and `Slots.HasFn == false`. The fallback matcher fires zero times in the eval fixture, consistent with previous measurements. ## 8. Tests New test file: `internal/router/capability_test.go` — 14 tests. | Test | What it pins | |---|---| | `TestSelectCapability_DeterministicBypass` | Grammar-fixed acts bypass selector | | `TestSelectCapability_ExtractorRawBypass` | Extractor-raw acts bypass selector | | `TestSelectCapability_GrammarMatcherBypass` | Grammar-matcher acts bypass selector | | `TestSelectCapability_LLMTextMatch` | LLM cleaned text resolves when raw misses | | `TestSelectCapability_LLMTextSameAsUtterance` | No double-match when Text == Utterance | | `TestSelectCapability_Unresolved` | No match on any input | | `TestSelectCapability_NonActIntent` | Non-act returns empty selection | | `TestSelectCapability_NilMatcher` | Nil matcher does not panic | | `TestApplyCapabilityToSlots_PopulatesCompatibilityFields` | Compat fields populated from selection | | `TestApplyCapabilityToSlots_UnresolvedDoesNotSetSlots` | Unresolved selection does not overwrite slots | | `TestResolveActionCandidate_CapabilitySelectionSource` | Selection produces route-sourced candidate | | `TestResolveActionCandidate_CapabilitySelectionOverSlots` | CapabilitySelection takes precedence over Slots.HasFn | | `TestResolveActionCandidate_BackwardCompatSlotsHasFn` | Decisions with HasFn but no CapabilitySelection still work | | `TestRouterRoute_CapabilitySelectionPopulated` | Router.Route sets CapabilitySelection on Decision | | `TestSelectionInputKindConstants` | Three input kind constants are distinct | Modified tests: `actioncandidate_test.go` — 8 tests updated to set `CapabilitySelection` alongside `Slots.HasFn`. ## 9. Fn/Args result confirmation Every `Fn` and `Args` value is unchanged: - Routing fixture: identical distribution (8 resolved, 3 unresolved) - Ecosystem fixture: identical distribution (23 resolved, 1 unresolved) - All 57 router tests pass - All cmd/mavend tests pass - All eval fixture tests pass ## 10. Newly exposed architectural problems None. The extraction is clean and mechanical. The backward compatibility path in `ResolveActionCandidate` (checking `Slots.HasFn` when `CapabilitySelection` is not set) is a temporary bridge that should be removed when all call sites produce decisions through the router. ## 11. Commit hash Pending commit on branch `task/slice-6b-capability-selection`.