Skip to content

Commit 5e7544d

Browse files
committed
docs: Add codebase health audit research brief
1 parent f678997 commit 5e7544d

File tree

1 file changed

+286
-0
lines changed
  • design/research/2026-02-17-codebase-health-audit

1 file changed

+286
-0
lines changed
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
# Codebase Health Audit
2+
3+
**Date**: 2026-02-17
4+
5+
**Status**: Complete
6+
7+
**Scope**: Full codebase audit — structure, churn, coupling, test coverage, code quality signals
8+
9+
## Context
10+
11+
### What prompted this
12+
13+
Routine health check to identify the highest-impact problems in the codebase before they compound. The project is ~6 months old with 202 commits (44 in the last 3 months), primarily single-contributor. The codebase has grown to ~20,800 lines of source across 153 Python files with 76 test files.
14+
15+
### Methodology
16+
17+
Five parallel analyses were run:
18+
19+
1. **Structure & size** — directory tree, line counts, large files/functions
20+
2. **Git churn & age** — hot spots, cold spots, net growth
21+
3. **Dependency & coupling** — import graph, fan-in/fan-out, circular deps
22+
4. **Test coverage & safety** — pytest --cov, untested paths, broad catches
23+
5. **Code quality signals** — nesting, duplication, hardcoded values, TODOs, inconsistencies
24+
25+
## Findings
26+
27+
### Critical: 67 Broad Exception Catches
28+
29+
**Impact**: Silently masks real bugs in an async framework where correctness of automation execution matters.
30+
31+
67 instances of `except Exception` (or `except Exception as e:`) across 22 source files. Many log-and-continue or silently swallow errors.
32+
33+
**Worst offenders by file:**
34+
35+
| File | Count | Context |
36+
| ------------------------------------ | ----- | ----------------------------------------- |
37+
| `bus/injection.py` | 5 | DI resolution — masks injection failures |
38+
| `core/app_handler.py` | 5 | App lifecycle — masks start/stop failures |
39+
| `resources/base.py` | 5 | Resource lifecycle — init/shutdown |
40+
| `task_bucket/task_bucket.py` | 5 | Task execution — masks task failures |
41+
| `core/app_factory.py` | 3 | App instantiation |
42+
| `core/bus_service.py` | 3 | Event dispatch |
43+
| `core/service_watcher.py` | 3 | Background service monitoring |
44+
| `utils/app_utils.py` | 5 | App loading/detection |
45+
| `web/routes/apps.py` | 3 | API endpoints |
46+
| `web/routes/ws.py` | 2 | WebSocket routes |
47+
| `conversion/annotation_converter.py` | 4 | Type annotation conversion |
48+
| `conversion/type_registry.py` | 2 | Type registry lookups |
49+
| `conversion/state_registry.py` | 1 | State registry |
50+
| `core/websocket_service.py` | 2 | WebSocket connection |
51+
| `core/data_sync_service.py` | 2 | Status collection — silently returns 0 |
52+
| `core/scheduler_service.py` | 2 | Job execution cleanup |
53+
| `state_manager/state_manager.py` | 2 | State access |
54+
| `core/state_proxy.py` | 2 | State proxy |
55+
| `core/core.py` | 1 | Main initialization |
56+
| `core/app_lifecycle.py` | 2 | App lifecycle hooks |
57+
| `test_utils/harness.py` | 3 | Test cleanup (acceptable) |
58+
| Others | 3 | Various |
59+
60+
**Patterns observed:**
61+
62+
1. **Log-and-continue** (most common): `except Exception as e: self.logger.exception(...)` — the error is logged but execution continues, potentially leaving the system in an inconsistent state.
63+
2. **Silent swallow**: `except Exception: pass` or `except Exception: return default` — error disappears entirely. Seen in `data_sync_service.py` (returns 0 for entity/app counts on error), `scheduler_service.py`, `app_factory.py`.
64+
3. **Cleanup guards**: `except Exception:` in shutdown/cleanup code — more defensible, seen in `resources/base.py` shutdown and `test_utils/harness.py`.
65+
66+
**Recommendation**: Audit each instance and narrow to specific exception types. Priority order:
67+
1. `bus/injection.py` — DI failures should surface, not be swallowed
68+
2. `core/app_handler.py` — app lifecycle errors need specific handling
69+
3. `core/bus_service.py` — event dispatch errors affect automation correctness
70+
4. `conversion/*.py` — type conversion failures mask data issues
71+
72+
---
73+
74+
### Critical: Test Coverage at 79% (target: 80%)
75+
76+
**Impact**: The safety net has holes in exactly the areas that change most.
77+
78+
**Test run summary** (2026-02-17, `pytest -n auto --dist loadscope`):
79+
80+
| Metric | Value |
81+
| -------- | ----------------- |
82+
| Passed | 825 |
83+
| Failed | 1 |
84+
| Errors | 0 |
85+
| xfailed | 2 |
86+
| Coverage | 79% (target: 80%) |
87+
| Duration | 85s |
88+
89+
**The 1 test failure** is in `tests/integration/test_listeners.py`:
90+
- `TestThrottleLogic::test_throttle_tracks_time_correctly` — flaky timing test, passes in isolation
91+
92+
Note: running with the default `--dist load` strategy produces 43 errors due to test isolation issues (shared env vars, global registries, event loops). Using `--dist loadscope` groups tests by module and eliminates these. Consider updating CI and `CLAUDE.md` to specify `--dist loadscope`.
93+
94+
**Lowest coverage modules** (source files with highest risk):
95+
96+
| Module | Coverage | Uncovered lines | Churn (6mo) | Risk |
97+
| -------------------------------- | -------- | --------------- | -------------------------- | ---------------------------------------- |
98+
| `scheduler/scheduler.py` | **53%** | 39 | 10 changes | HIGH — active development, half untested |
99+
| `utils/hass_utils.py` | **66%** | 10 || MEDIUM |
100+
| `state_manager/state_manager.py` | **66%** | 40 || MEDIUM — foundational module |
101+
| `utils/func_utils.py` | **66%** | 14 || MEDIUM |
102+
| `utils/request_utils.py` | **67%** | 7 || LOW — cold, small |
103+
| `utils/app_utils.py` | **70%** | 59 | 12 changes | HIGH — high churn + low coverage |
104+
| `resources/mixins.py` | **70%** | 34 || MEDIUM — used by all resources |
105+
| `utils/type_utils.py` | **70%** | 63 | 15 changes (recent growth) | HIGH — complex type introspection |
106+
| `utils/glob_utils.py` | **70%** | 3 || LOW — small file |
107+
| `task_bucket/task_bucket.py` | **70%** | 46 || MEDIUM |
108+
109+
The coverage numbers above are unchanged between `--dist load` and `--dist loadscope`.
110+
111+
---
112+
113+
### Concerning: Config Module Is the #1 Hotspot
114+
115+
**Impact**: Most frequently changed file drives framework behavior; bidirectional coupling with app module.
116+
117+
**Churn data (last 6 months):**
118+
119+
| File | Changes | Role |
120+
| ------------------------ | ------- | -------------------- |
121+
| `config/core_config.py` | **30** | #1 most-changed file |
122+
| `core/core.py` | **28** | Main coordinator |
123+
| `test_utils/harness.py` | **22** | Test harness |
124+
| `__init__.py` | **24** | Public API exports |
125+
| `test_utils/fixtures.py` | **17** | Test fixtures |
126+
| `models/states/base.py` | **16** | State model base |
127+
| `core/api.py` | **15** | API resource |
128+
129+
`core_config.py` at 30 changes is the single highest-churn source file. Every feature addition, behavior tweak, or default change touches this file. It also has coupling to `utils/app_utils.py` (for app detection/validation) and `config/classes.py` (for `AppManifest`), while the app module imports back from config.
130+
131+
**Net growth leaders (last 3 months):**
132+
133+
| Net lines | Added | Deleted | File |
134+
| --------- | ----- | ------- | ------------------------------------------------- |
135+
| +1,158 | 1,767 | 609 | `tests/integration/test_web_ui.py` |
136+
| +970 | 991 | 21 | `tests/integration/test_dependencies.py` |
137+
| +526 | 580 | 54 | `tests/integration/test_state_proxy.py` |
138+
| +505 | 505 | 0 | `tests/integration/test_app_factory_lifecycle.py` |
139+
| +498 | 516 | 18 | `tests/unit/core/test_data_sync_service.py` |
140+
| +491 | 491 | 0 | `tests/unit/core/test_app_registry.py` |
141+
| +460 | 486 | 26 | `src/hassette/core/data_sync_service.py` |
142+
| +415 | 495 | 80 | `src/hassette/utils/type_utils.py` |
143+
| +409 | 534 | 125 | `tests/integration/test_web_api.py` |
144+
| +365 | 365 | 0 | `src/hassette/core/app_registry.py` |
145+
146+
Growth is primarily in tests (healthy) and new core services (`data_sync_service.py`, `app_registry.py`, `type_utils.py`).
147+
148+
---
149+
150+
### Concerning: Large Files Approaching Complexity Limits
151+
152+
**Impact**: Larger files are harder to navigate, review, and modify safely.
153+
154+
**Files exceeding 400 lines:**
155+
156+
| File | Lines | Churn (6mo) | Concern |
157+
| ------------------------------ | ------- | ----------- | --------------------------------------------------------- |
158+
| `api/api.py` | **882** | 15 | All REST/WebSocket methods in one file |
159+
| `bus/bus.py` | **809** | 11 | 6 subscription methods with duplicated predicate assembly |
160+
| `utils/app_utils.py` | **518** | 12 | Mixed concerns: detection, loading, validation |
161+
| `scheduler/scheduler.py` | **505** | 10 | 53% coverage |
162+
| `api/sync.py` | **505** || Auto-generated sync facade |
163+
| `core/scheduler_service.py` | **503** || Job execution coordinator |
164+
| `event_handling/predicates.py` | **498** || 30+ predicate dataclasses |
165+
| `core/bus_service.py` | **494** || Bus service coordination |
166+
| `core/data_sync_service.py` | **460** | new | Data aggregation for frontend |
167+
| `config/config.py` | **459** | 13 | Configuration parsing |
168+
| `resources/base.py` | **451** || Resource base class, 21 dependents |
169+
| `test_utils/harness.py` | **438** | 22 | Test harness |
170+
| `event_handling/conditions.py` | **433** || 17+ condition dataclasses |
171+
| `core/websocket_service.py` | **420** | 10 | WebSocket management |
172+
| `utils/type_utils.py` | **415** | new | Type introspection (70% coverage) |
173+
174+
**Repetitive patterns in bus.py** — the 6 subscription methods (`on_state_change`, `on_attribute_change`, `on_call_service`, `on_component_loaded`, `on_service_registered`, `on_event`) each:
175+
1. Log the subscription
176+
2. Build a `preds: list[Predicate]` with entity/domain/service matching
177+
3. Handle `changed_from`/`changed_to` variants
178+
4. Handle `where` clause
179+
5. Delegate to `self.on()`
180+
181+
This is ~400 lines of highly similar code that could be consolidated with a builder or factory.
182+
183+
---
184+
185+
### Concerning: Cold Spots Still In Active Use
186+
187+
**Impact**: Code written months ago that hasn't been reviewed or tested against current patterns.
188+
189+
**Files not touched since Oct 2025 or earlier (still actively imported):**
190+
191+
| Last touched | File | Used by |
192+
| ------------ | -------------------------- | ----------------------------- |
193+
| 2025-09-04 | `const/sensor.py` | State models |
194+
| 2025-10-07 | `models/services.py` | API, state manager |
195+
| 2025-10-16 | `utils/request_utils.py` | API module (67% coverage) |
196+
| 2025-10-20 | `const/colors.py` | Light state model |
197+
| 2025-10-27 | `events/hass/raw.py` | Event system |
198+
| 2025-10-31 | `api/__init__.py` | Public API |
199+
| 2025-10-31 | `events/hass/__init__.py` | Event exports |
200+
| 2025-10-31 | `resources/__init__.py` | Resource exports |
201+
| 2025-10-31 | `scheduler/__init__.py` | Scheduler exports |
202+
| 2025-11-02 | `models/states/simple.py` | State registry |
203+
| 2025-11-02 | `utils/glob_utils.py` | Bus predicates (70% coverage) |
204+
| 2025-11-02 | `utils/service_utils.py` | Core services |
205+
| 2025-11-07 | `utils/exception_utils.py` | 8+ modules |
206+
207+
Most state model files (`models/states/*.py`) are also cold but are simple Pydantic models that rarely need changes — these are low risk.
208+
209+
---
210+
211+
### Positive: Clean Dependency Structure
212+
213+
**No circular imports found.** The codebase uses `TYPE_CHECKING` guards strategically to prevent runtime cycles.
214+
215+
**Dependency layering is sound:**
216+
217+
```
218+
Infrastructure (const, types, exceptions, resources/base) ← used by everything
219+
220+
Utilities (utils/*, conversion/*) ← used by ~15 modules
221+
222+
Domain (events, event_handling, models) ← well-isolated
223+
224+
User APIs (app, bus, scheduler, api, state_manager) ← moderate fan-out
225+
226+
Core Orchestration (core/*) ← high fan-out (expected)
227+
228+
Web Layer (web/*) ← isolated, only reads from core
229+
```
230+
231+
**Top fan-in modules** (most depended upon — changes here cascade widely):
232+
233+
| Module | Dependents |
234+
| ---------------- | ---------- |
235+
| `types` | 25+ |
236+
| `exceptions` | 22+ |
237+
| `resources/base` | 21+ |
238+
| `events` | 16+ |
239+
| `const` | 12+ |
240+
241+
**God module**: `core/core.py` imports 15+ modules, but this is expected and acceptable for the main orchestrator. It delegates to child services rather than implementing logic directly.
242+
243+
---
244+
245+
### Worth Noting
246+
247+
1. **Only 3 TODOs in the entire codebase**, all well-documented:
248+
- Fixture scope limitation (test isolation)
249+
- App reload optimization (restart granularity)
250+
- Unmaintained `coloredlogs` dependency (broken on Python >3.13)
251+
252+
2. **Minimal hardcoded values** — retry backoff (1s initial, 32s max, 5 attempts) in `websocket_service.py` and a 1s timeout in test harness. Otherwise config-driven.
253+
254+
3. **Strong type coverage** — all functions have type hints, extensive use of `@dataclass(frozen=True)` for immutability, Protocol-based duck typing.
255+
256+
4. **Well-isolated web layer** — imports only within `web.*` and from core dependencies. Changes don't cascade.
257+
258+
## Recommended Actions
259+
260+
Ordered by impact (highest first):
261+
262+
| Priority | Finding | Recommended action | Tool |
263+
| -------- | ----------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------- |
264+
| **1** | 67 broad `except Exception` | Audit each instance, narrow to specific types | `/mine.refactor` or dedicated task |
265+
| **2** | Scheduler at 53% coverage | Add test coverage for uncovered paths | tdd-guide agent |
266+
| **3** | `app_utils.py` — 70% coverage, 12 changes | Add tests, then consider splitting (discovery, loading, validation) | tdd-guide, then `/mine.refactor` |
267+
| **4** | `type_utils.py` — 70% coverage, +415 lines net growth | Stabilize with tests before it grows further | tdd-guide agent |
268+
| **5** | xdist test isolation | Switch to `--dist loadscope` in CI and docs; fix the 1 throttle failure | tdd-guide agent |
269+
| **6** | `bus.py` — 809 lines, repetitive subscription methods | Extract predicate assembly to builder/factory | `/mine.refactor` |
270+
| **7** | `api.py` — 882 lines | Split by concern (state ops, service calls, data retrieval, WS) | `/mine.refactor` |
271+
| **8** | Config hotspot coupling | Consider extracting shared models to reduce bidirectional coupling | `/mine.adrs` |
272+
273+
## Appendix: Raw Data
274+
275+
### Commit Activity
276+
277+
- Total commits: 202
278+
- Last 3 months: 44
279+
- Last 6 months: 202
280+
- Contributors: 1 primary (Jessica Smith), 2 minor
281+
282+
### Source File Count
283+
284+
- `src/hassette/`: 153 Python files, ~20,800 lines
285+
- `tests/`: 76 Python files
286+
- Largest test file: `tests/integration/test_web_ui.py` (48 KB, +1,158 net lines in 3 months)

0 commit comments

Comments
 (0)