154 lines
5.8 KiB
Markdown
154 lines
5.8 KiB
Markdown
# 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`.
|