"code": ["technologycode/codegeneration", "technologycode/codeanalysis"],
Root cause: docs/CONFIG.md taught short-form keys (code, analysis, reasoning), but selector.py's PATH_PATTERNS + _resolve_path only matched long-form hierarchical paths (technology_code/code_generation/python). Any short-form config key resolved to [] → every model scored 0.0 → select() picked uniformly at random.
Fix (3 parts):
1. Module-level alias map in selector.py — short-form key → canonical long-form paths:
PATH_PATTERNS = {
"technology_code": "technology_code",
"code_generation": "technology_code/code_generation",
"python": "technology_code/code_generation/python",
"code_analysis": "technology_code/code_analysis",
"reasoning": "technology_code/code_analysis/reasoning",
"architecture": "technology_code/code_analysis/reasoning/architecture",
}
# Short-form keys taught by pre-2.0 docs. Resolved ONLY as a LAST RESORT,
# after exact and parent-prefix checks, so canonical paths keep precedence.
CATEGORY_ALIASES = {
"code": ["technology_code/code_generation", "technology_code/code_analysis"],
"generation": ["technology_code/code_generation"],
"analysis": ["technology_code/code_analysis"],
"reasoning": ["technology_code/code_analysis/reasoning"],
"arch": ["technology_code/code_analysis/reasoning/architecture"],
}
2. _resolve_path resolves aliases as last resort — precedence preserved exactly (exact → parent-prefix → alias):
def _resolve_path(path, patterns=None, aliases=None):
patterns = PATH_PATTERNS if patterns is None else patterns
aliases = CATEGORY_ALIASES if aliases is None else aliases
if not path:
return []
path = str(path).strip().strip("/")
if not path:
return []
# 1) exact match on a canonical pattern value
exact = [v for v in patterns.values() if path == v]
if exact:
return exact
# 2) parent-prefix: ancestor/descendant containment, deepest first
prefix = [v for v in patterns.values()
if path.startswith(v + "/") or v.startswith(path + "/")]
if prefix:
prefix.sort(key=lambda v: v.count("/"), reverse=True)
return prefix
# 3) LAST RESORT: legacy short-form alias -> canonical paths
if path in aliases:
return list(aliases[path])
return [] # caller scores 0.0 — no silent random fallthrough
Scoring then works off canonical paths only: exact 1.0, ancestor/descendant 0.75, alias-bridged sibling 0.5, unresolvable 0.0.
3. docs/CONFIG.md rewritten to long-form examples + deprecation notice:
models:
python-helper:
category: technology_code/code_generation/python # was: code
reviewer:
category: technology_code/code_analysis/reasoning # was: reasoning
⚠️ Do not use short-form keys in new configs; they map to multiple canonical paths and can tie (e.g.
codematches both generation and analysis).
Reusable lesson: when docs teach config keys, grep the consuming matcher for the exact key format before shipping; add alias maps at the resolution point so exact/parent precedence is preserved.
Ran in a scratch repo (`/tmp/kc-mismatch`): fixed `selector.py`, pre-fix `before.py` for comparison, rewritten `docs/CONFIG.md`, 8 offline tests, and a demo script.
**Test run — `python3 -m pytest tests/ -q`:** `8 passed in 0.01s`
| # | Test | Verifies |
|---|---|---|
| 1 | exact long-form path resolves to itself | exact match |
| 2 | `technology_code/code_generation/go` → ancestors deepest-first | parent-prefix |
| 3 | `code` → both generation + analysis paths | alias last resort |
| 4 | `analysis`, `reasoning` → correct canonical path | alias coverage |
| 5 | injected patterns where `code` is a real path → exact wins | alias never shadows exact |
| 6 | `bogus/nowhere`, `""`, `" "` → score `0.0` | no random fallthrough |
| 7 | `score("code", [analysis-model]) == 1.0`; `0.75` for nested | regression: no more 0.0 |
| 8 | `select` picks best long-form match; short-form alias picks deterministic across same seed | selection no longer random |
**Before/after demo (short-form configs, scores py/arch):**
```
BEFORE: 'code' py=0.00 arch=0.00 'analysis' py=0.00 arch=0.00
AFTER: 'code' py=0.75 arch=0.75 'analysis' py=0.50 arch=0.75
AFTER: 'reasoning' py=0.50 arch=0.75 -> stable pick: architecture-reviewer
```
**Edge cases covered:** empty/whitespace path → `[]`; alias-bridged sibling scoring (0.5) vs containment (0.75) vs exact (1.0); the broad `code` alias intentionally ties (documented in CONFIG.md); canonical long-form paths select correctly for every `PATH_PATTERNS` value; tie-break deterministic via `sorted()` + seeded `rng`.
---{"model": "deepseek-v4-flash", "problem_class": "python-config-keys-doc-code-mismatch", "result": "passed", "tests": 8}