Files
6krrt/plans/code-analysis-refactoring-opportunities.md
adlee-was-taken 26398d7a79 docs(plans): replace the refactoring survey with four scoped, verified items
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
2026-09-12 19:54:31 -04:00

192 lines
8.4 KiB
Markdown

# 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:
```python
# `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:
1. `src/metrics.py` -- make `_percentile` (line 46) and the three
`required_context_tokens` collection sites NULL-safe. Filter NULLs out in
SQL (`AND required_context_tokens IS NOT NULL`) rather than in Python: the
`MAX(...)` 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.
2. `CLAUDE.md` -- replace the "zero such rows exist today" sentence with what
is actually true: NULL rows exist, they are `kind='passthrough'`, and the
`kind`/`task_tier` filters 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.py` by config section** (the survey's nine-file tree).
Of 72 validators, eleven hang off `RouterConfig` and 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.
- **`Source` as a plain Enum.** The strings appear at 14 comparison sites in
`src/` and 32 in `tests/`, against 3 annotation sites. A plain `Enum` makes
all of them silently `False`. If ever done, it must be
`class Source(str, Enum)`.
- **Extracting `_summarize_prose()` from the pinch stage.** There is no prose
summarization; the stage trims tool results only, on purpose. `prune_context`
at 216 lines (`context_prune.py:240-455`) is a real extract candidate, but it
is the money path and has a documented `protected_max_chars` gotcha in
`docs/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.md` note 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` -- `md5sum` it before and after.