The survey this file held was reviewed against the code and two of its four recommendations rested on things that are not in the codebase. Rather than leave a document nobody should execute sitting in plans/ under a `reference` status, it is replaced by the work that survived the review plus three defects found while checking it. Items 1-3 are live defects with a reproduction measured on the live DB: the TUI renders the literal "None" in three cells (4 passthrough rows have no classification), CLAUDE.md's "zero such rows exist today" claim about the demand-ceiling TypeError is now false, and config/ holds 45 unpruned overlay backups. Item 4 is the one structural change the review endorsed. Item 4 also carries a correction: the review priced it as "112 lines out", but load_config and summary_lines are imported by 49 files, and the obvious re-export mitigation is a module-level import cycle. It is marked optional and last on that basis rather than quietly carrying the old estimate. The demand-ceiling item is careful to state that the bug has NOT fired -- all four NULL rows are kind='passthrough' with a NULL task_tier, and the query filters on both, so none reach _percentile and /metrics returns 200. A plan that overstates the urgency gets the fix rushed and the test wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
8.4 KiB
Four small fixes, scoped down from the refactoring survey
Status: planned -- decision-complete; execute in order, each item lands on its own
Superseded scope. This file used to hold a four-item refactoring survey of
src/. That survey was reviewed against the code
(review-code-analysis-refactoring-opportunities.md) and two of its four
recommendations rested on things that are not in the codebase -- a pinch()
function that does not exist, and six config.py classes that do not exist.
The survey is kept in git history; do not execute it.
What replaces it is the work that survived the review, plus three defects found while doing it. Nothing here is a refactor for its own sake: items 1-3 are live defects with a reproduction, and item 4 is the one structural change the review endorsed.
Intended executor: opencode (Atlas), directly. These are small and independent; no Prometheus plan artifact is needed beyond this file. Land each item as its own commit so any one can be reverted alone.
Every figure below was measured on the live database and the live service at
1e2e806 on 2026-09-12. Re-check before trusting any of them.
Item 1 -- the TUI renders the literal string "None" in three cells
Priority: high (visible defect, one-line fix, already has a precedent in the same function)
src/tui.py:763-775 builds each decision row by wrapping every value in
str(). Three of those columns are nullable, so a NULL renders as the literal
None:
| cell | source column | NULL rows today |
|---|---|---|
| category | task_category |
4 |
| tier | task_tier |
4 |
| ctx | required_context_tokens |
4 |
The rows are kind='passthrough', which is why this shows up now: passthrough
decisions do not carry a classification.
The fix is already written two lines below, for the column someone hit first:
# `or ""` so an absent profile is blank, never the literal "None".
str(r.get("profile") or ""),
Apply the same treatment to category, tier and required_context_tokens.
Do not touch selected: tui_model.py:218 already maps it to "none"
via or "none", deliberately, and that is a different string from the Python
repr.
Recorded previously in .omo/notepads/tui-overhaul/issues.md -- close that
note out as part of this item.
Verify: add a row to the existing TUI table test asserting that a decision
with NULL task_category / task_tier / required_context_tokens renders
empty cells and that the string "None" appears nowhere in the rendered row.
Item 2 -- harden the demand-ceiling comparison, and correct CLAUDE.md
Priority: high (the documented safety claim is now false)
CLAUDE.md says of the demand-ceiling TypeError:
Latent only: zero such rows exist today, checked on the live DB.
That is no longer true. There are 4 rows with NULL required_context_tokens
as of 2026-09-12.
It has not fired, and the plan must not claim it has. metrics.py:1433-1441
filters kind = 'chat' AND task_tier = ?, and all four NULL rows are
kind='passthrough' with a NULL task_tier, so none reach _percentile.
/metrics returns 200. Two filters are the only thing standing between the
current data and the crash.
Two changes:
src/metrics.py-- make_percentile(line 46) and the threerequired_context_tokenscollection sites NULL-safe. Filter NULLs out in SQL (AND required_context_tokens IS NOT NULL) rather than in Python: theMAX(...)sites at lines 1374, 1382, 1406, 1644, 1652, 1670 already ignore NULLs the way SQL aggregates do, so filtering in the one row-returning query at line 1436 makes every site consistent. A NULL is "this request did not state a context requirement", which is not a zero and must not be averaged in as one.CLAUDE.md-- replace the "zero such rows exist today" sentence with what is actually true: NULL rows exist, they arekind='passthrough', and thekind/task_tierfilters are what keep them out of the comparison.
Verify: a test that inserts a kind='chat' decision with NULL
required_context_tokens at a tier that has a ceiling, then calls the warning
function and asserts it returns without raising. That test must fail before
the fix.
Item 3 -- cap the config backup retention
Priority: medium (unbounded growth in the directory holding the irreplaceable file)
config/ holds 45 config.local.yaml.bak.<epoch> files. Two write paths
create them and neither prunes:
src/admin.py:467-470--config.yaml.bak.<epoch>(0 on disk today)src/admin.py:587-591--config.local.yaml.bak.<epoch>(45 on disk today)
config.local.yaml is gitignored and holds the live classifier settings, so
this is the one directory where backups genuinely matter -- which is the
argument for making them legible, not for keeping every one forever.
Add a retention cap after each successful write: keep the newest N (N = 10,
as a named module constant, not a literal), delete older ones, matching only
<name>.bak.<digits> so nothing else in config/ can be caught. Deleting a
backup must never fail the write -- wrap the prune so an error is logged and
swallowed; the write has already succeeded by that point and reporting it as
failed would be worse than the clutter.
Also worth fixing while in there: the filename is int(time.time()), so
two writes in the same second silently overwrite each other's backup. The
existing files show consecutive-second writes (...877, ...878), so this is
reachable. Either widen to a monotonic counter or accept it and say so in a
comment -- decide and record which.
Verify: a test that performs 15 overlay writes and asserts exactly 10 backups remain, that they are the 10 newest, and that no non-matching file in the directory was touched.
Item 4 -- move the loader out of config.py (OPTIONAL, do last)
Priority: low, and more expensive than the review first estimated.
The review proposed lifting _merge_overlay, load_config and summary_lines
(src/config.py:1489-1601, 112 lines) into src/config_loader.py, leaving
config.py as schema only. The reasoning stands: the schema is declarative and
rarely read top to bottom, while the loader has real control flow (overlay
merging, ROUTER_IGNORE_LOCAL_CONFIG, backup rotation) and is read when
something has gone wrong.
The cost was under-stated and this is the correction. load_config /
summary_lines are imported by 49 files (9 in src/, 39 in tests/, plus
baseline_report.py). The obvious mitigation -- re-exporting from config.py
-- does not work: config_loader must import RouterConfig from config, so
a re-export in the other direction is a module-level import cycle.
So this item is genuinely "move 112 lines and update 49 import statements". The edit is mechanical and the test suite covers it completely, but it is churn bought for readability alone.
Do this only if items 1-3 land clean and there is appetite. If it is skipped, say so and close the item rather than leaving it open -- an indefinitely-deferred refactor in a plans file is noise.
Explicitly out of scope
Carried over from the review, so nobody re-derives them:
- Splitting
config.pyby config section (the survey's nine-file tree). Of 72 validators, eleven hang offRouterConfigand cross-check sections against each other; those 380 lines stay in one file under any split. The proposal moved the easy 1108 lines and left the hard third untouched. Sourceas a plain Enum. The strings appear at 14 comparison sites insrc/and 32 intests/, against 3 annotation sites. A plainEnummakes all of them silentlyFalse. If ever done, it must beclass Source(str, Enum).- Extracting
_summarize_prose()from the pinch stage. There is no prose summarization; the stage trims tool results only, on purpose.prune_contextat 216 lines (context_prune.py:240-455) is a real extract candidate, but it is the money path and has a documentedprotected_max_charsgotcha indocs/pinch.md. Not now.
Definition of done
- Items 1-3 each landed as their own commit, each with the test described.
CLAUDE.md's demand-ceiling paragraph reflects the live database..omo/notepads/tui-overhaul/issues.mdnote closed.- Item 4 either done or explicitly closed as skipped.
- Full suite green (1918 at the time of writing) on Python 3.10 and 3.14.
- No change to
config/config.local.yaml--md5sumit before and after.