A runtime admin write goes straight past Pydantic, so the registry's bounds
are the only check between a request body and a field the warning reads. The
validators inlined `0 < v <= 1` and `v >= 1`, so a registry entry would have
had to restate them and could drift: it would accept 0.0 where load refuses.
Name the three edges (exclusive low, high, min floor), use them in the
validators, and pin them to the validators' actual boundary behaviour.
No behaviour change.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkCGRantZsSwmcFpet6FTa
A declined answer left one number behind and it was in a log line, so
confidence_min could not be tuned from data. route_decisions.confidence cannot
say it either: the chat path re-routes through the override branch, which
hard-codes 1.0, and 43,804 of 43,856 live rows hold exactly that.
Measured on 2026-10-04 under local_decision (qwen3.5:4b): 769 of 2,557 turns
(30%) had no fresh classification, against 0% for local_llm and local_encoder.
The cause was recorded in only 4 of them.
- classifier_confidence, classifier_coverage and classifier_reject on
route_decisions, filled from a ClassifierAttempt carried on Classification.
Both accepted and declined answers carry one, so the two distributions can be
compared around the floor. Reason codes are listed in docs/data-model.md.
- ClassifierRejected (a RuntimeError subclass, messages unchanged) replaces the
plain RuntimeErrors at the six floor-miss sites and the two local_decision
refusals, so the number and reason travel out of the raise site.
- The chat path captures the classifier's verdict before the re-route and passes
it to persist_route_decision (attempt_of), like it already does for source.
- Admin decisions page: the source badge's tooltip shows the attempt, and
session_history / session_stale get an amber badge instead of neutral grey.
TUI detail popup and the live event carry the same three keys.
- The /metrics degraded-share warning lists the recorded reasons instead of
claiming the classifier "has been failing", which was wrong for a classifier
that answers and is declined.
- degraded_warn_threshold must be in (0, 1] and degraded_warn_min at least 1,
refused at load: a value above 1 could never fire.
The three columns arrive by ALTER and are NULL on every earlier row; metrics
selects them only when present, so the live DB reads NULL until its restart.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkCGRantZsSwmcFpet6FTa