|
| 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