# Praxis reach at stage 0, 2026-08-05

Vikunja #516. Measured with `make eval-reach` on the held-out ecosystem fixture
(`internal/router/eval/ru_ecosystem_v1.json`, 30 cases), classifier + ONNX embedder,
no llama-server in the run. The LLM arm was not measured, so judge a cascade
number again before quoting one.

## Result

|                | before | after |
|---|---|---|
| overall        | 16/30 (53.3%) | 27/30 (90.0%) |
| by want: praxis | 0/12 | 11/12 |
| by want: hexis | 9/10 | 9/10 |
| by want: none  | 7/8 | 7/8 |
| by tag: lifecycle | 0/5 | 5/5 |
| by tag: attention | 0/7 | 6/7 |
| by tag: reading | 0/7 | 6/7 |
| wrong praxis arm | 0 | 0 |
| p50 latency | 20.6ms | 16.5ms |

## Why it was zero

Not a tuning gap. `handlePraxisAct` dispatches on exact equality between
`Slots.Fn` and a capability alias, and the fn slot is filled by `DefaultActMatcher`
from the deployment's enabled tool names. No Praxis alias is on that list, so no
utterance could put one in the slot. The Russian aliases in `praxisCapabilities`
read as if they matched speech. They are compared against a fn slot and never
against an utterance.

`PraxisGrammars()` (`internal/router/praxis.go`) fills the slot at stage 0, wired in
`buildRouter` before the capture marker because "отметь" is a capture verb.

## The three misses that remain

- `eco-ru-006` "запусти бэкап на нексусе", a Hexis case, routed note. Pre-existing.
- `eco-ru-028` "выключи", reached Hexis, should have asked. Pre-existing.
- `eco-ru-021` "что там с нексусом" wants scoped attention. Deliberately not
  claimed. "что там с X" also opens "что там с погодой". Routing a weather
  question to Nexus is worse than one missed fixture case.

## Two judgement calls worth re-arguing

**A lifecycle word alone does not transition an item.** "готово" is what he says
about the thing he just finished. So the rules split lifecycle words by mood. An
imperative he says to her ("закрывай") claims the turn bare, and the capability
asks which пункт. A stative ("готово", "принято") needs an item named beside it.

The bare-imperative arm also requires that nothing else in the sentence is being
acted on. "закрой шторы в комнате" is an imperative too. Without that guard it took
a house command to Praxis, measured at hexis 8/10 mid-change.

**A demonstrative resolves only against a one-item digest.** "отметь это как
сделанное" points at what she just read. `resolveSurfacedPosition` maps it to an id
only when exactly one item was spoken. With two or more it gives the turn back to
the cascade rather than transitioning one of them at random. With no digest at all
it gives the turn back too, because "я это сделал" was never about a пункт.

## Routing fixture

`make eval-router`, same run: classifier + ONNX 60/84 (71.4% full and intent-only),
0 false clarifies, 6 missed clarifies (the known `amb-*` set). No failure in that
list comes from a stage-0 decision. Every one carries a classifier confidence score.
