Co-Study4Grid / docs /architecture /code-quality-analysis.md
github-actions[bot]
Deploy 7688ef1
13d4e44
|
Raw
History Blame Contribute Delete
67.2 kB
# Code Quality & Maintainability Analysis
**Date:** 2026-04-11
**Last updated:** 2026-06-18
**Scope:** Full repository diagnostic — backend, frontend, repo structure, security, testing
> **Continuous reporting (new 2026-04-20):**
> - `python scripts/code_quality_report.py` — metrics dump (JSON + Markdown)
> - `python scripts/check_code_quality.py` — CI gate (non-zero exit on regression)
> - Unit-tested in `scripts/test_code_quality_report.py` (13 tests, part of the pytest suite)
> - Runs in CI: `.github/workflows/code-quality.yml` (CircleCI removed — QW23)
> (jobs `gate` / `code-quality`). Also uploads a report artifact.
> - See [CONTRIBUTING.md](../../CONTRIBUTING.md#code-quality-checks)
> for local usage and thresholds.
---
## Executive Summary
| Dimension | Grade | Status | Notes |
|-----------|-------|--------|-------|
| **TypeScript correctness** | **A** | — | Zero compiler errors, zero lint warnings, strict mode |
| **Test suite** | **A+** | **Improved** | 560+ frontend tests, 53 backend test files, 13 quality-reporter tests |
| **Documentation** | **A** | — | CLAUDE.md refreshed, `CONTRIBUTING.md` added, 17 docs/ |
| **Frontend architecture** | **B+** | **Improved** | Oversized components split into focused subcomponents |
| **Backend architecture** | **B+** | **Fixed** | Split into 5 focused modules (was one 3,151-line monolith) |
| **Security posture** | **B+** | **Fixed** | Path traversal patched; CORS now honours `CORS_ALLOWED_ORIGINS` env var |
| **Type safety (Python)** | **C** | **Improving** | Core utilities typed; route handlers still missing return annotations |
| **Error handling (Python)** | **A-** | **Fixed** | `logger.exception()` replaces all `traceback.print_exc()` in backend sources |
| **Continuous quality reporting** | **A** | **New** | Gate script + unit tests + CI job + ruff (F/E9 rules) |
---
## 1. What's Working Well
### Frontend Tooling
- `tsc --noEmit` passes with **zero errors** under `strict: true`
- ESLint passes with **zero warnings**
- All **560 unit tests pass** across 35 test files (Vitest + React Testing Library)
- No `any` types in source code, no `@ts-ignore`
- Well-structured custom hooks: `useSettings`, `useAnalysis`, `useDiagrams`, `useSldOverlay`, `useSession`, `usePanZoom`
### Documentation
- CLAUDE.md (250+ lines) provides comprehensive project context
- 17 architecture/design docs in `docs/`
- Consistent copyright headers (MPL-2.0)
### Git Hygiene
- Conventional commit prefixes (`feat:`, `fix:`, `debug:`, `test:`)
- PR-based workflow with descriptive merge commits
- Dual CI: CircleCI + GitHub Actions running pytest, Vitest, and ESLint
### Backend Test Coverage
- 39 test files in `expert_backend/tests/`
- Covers sanitization, overload filtering, combined actions, PST tap actions, regression scenarios
---
## 2. Critical Issues — All Resolved
### 2.1 ~~Security: Path Traversal Vulnerabilities~~ ✅ FIXED
Added `_validate_path_within()` helper to `main.py` that resolves paths and ensures they don't escape the allowed directory. Applied to `save_session` and `load_session` endpoints. PDF copy source restricted to `Overflow_Graph/` directory.
### 2.2 ~~Security: Wildcard CORS~~ ✅ FIXED
CORS origins now configurable via `CORS_ALLOWED_ORIGINS` environment variable (comma-separated). `allow_credentials` disabled when using wildcard. Removed dead `GZipMiddleware` import.
### 2.3 ~~Backend God Object: `recommender_service.py` (3,151 lines)~~ ✅ FIXED
Split into focused mixin modules:
| File | Lines | Responsibility |
|------|-------|----------------|
| `recommender_service.py` | **410** | Core orchestrator, config, network lifecycle |
| `diagram_mixin.py` | **833** | NAD/SLD generation, flow deltas, overload detection |
| `analysis_mixin.py` | **1,030** | Contingency analysis, action enrichment, MW computation |
| `simulation_mixin.py` | **984** | Manual action simulation, superposition |
| `sanitize.py` | **40** | JSON serialization utility |
All imports re-exported for backward compatibility. `RecommenderService` inherits from `DiagramMixin`, `AnalysisMixin`, `SimulationMixin`.
### 2.4 ~~Silent Exception Swallowing~~ ✅ FIXED
Replaced **15 bare `except Exception: pass`** patterns with `except Exception as e: logger.debug("Suppressed exception: %s", e)`.
### 2.5 ~~Debug Prints Instead of Logging~~ ✅ FIXED
Replaced **80+ bare `print()` calls** with Python `logging` module calls at appropriate levels (`logger.info`, `logger.warning`, `logger.debug`). Added `import logging` and `logger = logging.getLogger(__name__)` to all service modules.
---
## 3. High-Priority Issues
### 3.1 ~~Frontend: Oversized Components~~ ✅ FIXED
Split the four worst offenders into focused subcomponents:
| Original File | Before | After | Extracted Components |
|------|-------|-------|-------|
| `ActionFeed.tsx` | **1,406** | **796** | `ActionCard.tsx` (370), `ActionSearchDropdown.tsx` (486) |
| `VisualizationPanel.tsx` | **1,285** | **554** | `SldOverlay.tsx` (705), `MemoizedSvgContainer.tsx` (48) |
| `CombinedActionsModal.tsx` | **777** | **397** | `ComputedPairsTable.tsx` (145), `ExplorePairsTab.tsx` (380) |
| `useDiagrams.ts` | **767** | **693** | `useSldOverlay.ts` (107) |
| `App.tsx` | **758** | **758** | Orchestration hub — acceptable at boundary |
**ActionFeed.tsx** (−43%): `renderActionList` (280 lines of per-card rendering) extracted to `ActionCard`. The search dropdown with its score table, type filters, and search results (350+ lines) extracted to `ActionSearchDropdown`.
**VisualizationPanel.tsx** (−57%): The 680-line inline `SldOverlay` component (SLD delta coloring, highlight clones, pan/zoom) moved to its own file. `MemoizedSvgContainer` (SVG DOM injection wrapper) also extracted.
**CombinedActionsModal.tsx** (−49%): Computed pairs table and explore pairs tab (with selection chips, filter buttons, grouped table, and comparison card) each extracted to dedicated components.
**useDiagrams.ts** (−10%): SLD overlay state management (`fetchSldVariant`, `handleVlDoubleClick`, `handleOverlaySldTabChange`, `handleOverlayClose`) extracted to `useSldOverlay` hook.
**Test coverage for all extracted components:** 7 new test files added (106 tests), covering every extracted component and hook:
| Test File | Tests | Coverage |
|-----------|-------|---------|
| `ActionCard.test.tsx` | 25 | Severity badges, VIEWING state, star/reject, LS/RC/PST re-simulation, MW/tap inputs |
| `ActionSearchDropdown.test.tsx` | 14 | Type filters, search input, scored actions table, manual ID, loading/error states |
| `ComputedPairsTable.test.tsx` | 13 | Pair rendering, simulate/re-simulate, empty state, islanding, color coding |
| `ExplorePairsTab.test.tsx` | 21 | Selection chips, filter buttons, estimate/simulate workflow, comparison card, errors |
| `MemoizedSvgContainer.test.tsx` | 6 | String + SVGSVGElement injection, tab-specific IDs, display prop, perf logging |
| `SldOverlay.test.tsx` | 14 | Header, close/tab callbacks, mode indicator, loading/error, conditional tabs |
| `useSldOverlay.test.ts` | 13 | Hook init, vlOverlay state, fetchSldVariant success/error, interaction logging |
### 3.2 ~~Missing React Error Boundary~~ ✅ FIXED
Added `ErrorBoundary` class component at `frontend/src/components/ErrorBoundary.tsx` and wired it into `main.tsx` wrapping `<App />` inside `<StrictMode>`. The boundary:
- Catches render/lifecycle errors via `getDerivedStateFromError` + `componentDidCatch`
- Logs the error and React component stack to `console.error`
- Renders a styled fallback UI (`role="alert"`) with the error name/message, a collapsible `<details>` block containing the full stack trace, and two recovery actions:
- **Try again** — resets boundary state (re-mounts the child tree)
- **Reload page** — calls `window.location.reload()`
- Accepts an optional `fallback` prop to override the default UI
Covered by 7 new unit tests in `ErrorBoundary.test.tsx` (happy path, default fallback, custom fallback, console logging, reset, reload, details block). Total frontend test count is now 578 (was 571).
### 3.3 Python Type Hint Coverage — **Partially Fixed**
- `sanitize.py:sanitize_for_json` — typed as `Any → Any` (2026-04-20).
- FastAPI route handlers still return implicit `dict` — the return annotations
are a good candidate for the next pass (would improve the auto-generated OpenAPI).
**Recommendation:** Add return type annotations to all FastAPI route handlers (also enables better auto-generated OpenAPI docs).
### 3.4 Weak TypeScript Typing Patterns
65 occurrences of `Record<string, unknown>` and `as unknown as` double-casts across the frontend, indicating the API response types aren't fully modeled:
- `ActionFeed.tsx`: 7 occurrences
- `api.ts`, `useDiagrams.ts`, `useSession.ts`: scattered throughout
These bypass TypeScript's type checking at integration boundaries.
**Recommendation:** Define typed API response interfaces in `types.ts` to replace `Record<string, unknown>` casts.
### 3.5 Accessibility Gaps
- No `aria-label` on icon/emoji buttons (ActionFeed, Header, OverloadPanel)
- No `aria-selected`/`aria-controls` on tab buttons (SettingsModal)
- No focus trapping in modals
- No semantic list markup (`<ul>`/`<li>`) for action lists
- No screen reader announcements for async operations
**Recommendation:** Audit against WCAG 2.1 AA; add aria attributes, semantic HTML, and focus management to modals.
---
## 4. Medium-Priority Issues
### 4.1 Standalone HTML Maintenance Burden
`standalone_interface.html` is **7,178 lines** — a complete duplicate of the React frontend in a single file. Every UI change must be manually mirrored. This is a significant ongoing maintenance cost and drift risk.
**Recommendation:** Consider deprecating or auto-generating from the React build.
### 4.2 Inconsistent API Response Formats
- Some endpoints: `{"status": "success", ...}`
- Others: direct data `{"branches": [...]}`
- Streaming endpoints: custom NDJSON error format
- Error responses: `HTTPException(detail=str(e))` leaks internal stack details
**Recommendation:** Standardize on a response envelope format and error schema across all endpoints.
### 4.3 ~~Hardcoded Configuration~~ Partially Fixed
CORS origins now configurable via `CORS_ALLOWED_ORIGINS` env var
(implemented at `main.py:174-184`; `.env.example` added 2026-04-20 as
a template). Remaining:
| Value | Location | Should Be |
|-------|----------|-----------|
| Port 8000, host 0.0.0.0 | `main.py` | Environment variable |
| `"Overflow_Graph"` directory | 5 locations in main.py | Config constant |
| Worsening thresholds | `recommender_service.py` | Config parameter |
**Recommendation:** Parameterize remaining values via environment variables or a central config object.
### 4.4 ~~Unused Dependencies~~ ✅ FIXED
`framer-motion` and `lucide-react` were removed from
`frontend/package.json` on 2026-04-20 — neither was imported in any
source file. The stale `GZipMiddleware` import in `main.py` was also
removed in the same pass.
### 4.5 ~~Incomplete Function~~ ✅ FIXED
`network_service.py:get_load_voltage_levels_bulk()` fell through to the
next function without a loop body. Implemented on 2026-04-20 to populate
`result` from `loads.loc[lid]['voltage_level_id']` — matching the sister
`get_generator_types_bulk()` pattern.
### 4.6 Code Duplication in Backend
- Voltage-level resolution logic repeated 4 times across `network_service.py`
- Action detail computation (`_compute_load_shedding_details`, `_compute_curtailment_details`, `_compute_pst_details`) share near-identical structure
- Try/except blocks for element voltage-level access repeated 5+ times
**Recommendation:** Extract shared patterns into helper functions.
### 4.7 ~~Frontend: Swallowed Errors in Async Code~~ ✅ FIXED
- `SettingsModal.tsx` — empty `.catch(() => { })` replaced with
`.catch(err => console.error(...))` on the two config-file-path
handlers (2026-04-20).
- `useSettings.ts:174,220` — already logs a warning via `console.warn`;
the 2026-04-13 audit predated that change.
- `lines.pop()!` in NDJSON stream readers is safe: `String.split('\n')`
always returns at least one element, so the non-null assertion does
not mask a bug. No change needed.
---
## 5. Low-Priority / Housekeeping
| Item | Status |
|------|--------|
| Empty doc file `docs/proposals/rendering-lod-strategies.md` | ✅ Filled in (9.2 KB of consolidated LoD history) |
| Stale lockfile `.~lock.Cases_Defauts_10032026.odt#` | ✅ No longer present |
| Redundant CI (CircleCI + GitHub Actions) | Both kept for belt-and-braces; the new `code-quality` job runs in each |
| No `.env.example` | ✅ Added 2026-04-20 (CORS_ALLOWED_ORIGINS, PYPOWSYBL_FAST_MODE, LOG_LEVEL) |
| No Dockerfile | Still open |
| CLAUDE.md drift (missing root `test_*.py`) | ✅ Refreshed 2026-04-20 — now documents `scripts/` and the code-quality gate |
| Missing `.editorconfig` | ✅ Added 2026-04-20 |
| Missing `CONTRIBUTING.md` | ✅ Added 2026-04-20 (setup, tests, quality gate, commit conventions) |
---
## 6. Metrics Summary
Numbers below are produced by `scripts/code_quality_report.py` against
the current HEAD. Re-generate via
`python scripts/code_quality_report.py --markdown reports/code-quality.md`.
```
Backend (expert_backend/)
Source files (non-test): 9 (added simulation_helpers.py)
Total lines: 5,491
Largest module: analysis_mixin.py (1,116 lines)
Top 3 longest functions:
run_analysis 169 lines
update_config 166 lines
simulate_manual_action 146 lines (was 599 — split via helpers)
compute_superposition → 108 lines (was 285)
print() calls in source: 0 (was 80+)
traceback.print_exc() calls: 0 (was distributed across mixins)
Silent except: pass: 0 (was 80+)
Test files: 41 (+ test_simulation_helpers.py with 66 tests)
Frontend (frontend/src/)
Source files (non-test): 37
Total lines: 14,872
Largest component: utils/svgUtils.ts (1,807 lines — util, exempt)
Test files: 43
Tests passing: ≥ 578 (unchanged — still 7 new ErrorBoundary tests)
TypeScript errors: 0
Lint warnings: 0
`any` types in source: 0
`@ts-ignore` directives: 0
`as unknown as` casts: 15
`Record<string, unknown>`: 37 occurrences
Repository
Standalone HTML (legacy): ~7,100 lines (frozen snapshot only)
Standalone HTML (built): auto-generated from frontend/src/
Documentation files: 17 in docs/
CI pipelines: 3 jobs on each (test-backend, test-frontend,
code-quality) across CircleCI + GH Actions
```
## 6b. Continuous-Quality Tooling (new 2026-04-20)
Three shipped surfaces keep the metrics above honest over time:
1. **`scripts/code_quality_report.py`** — AST-walks the backend and
globs the frontend source tree to emit a JSON + Markdown report.
Uses `ast.parse` for backend counts so strings / docstrings with
`print(` inside them are not flagged (exactly the case of the
tkinter subprocess script in `main.py`).
2. **`scripts/check_code_quality.py`** — the PR gate. Encodes the
current reductions as hard ceilings:
| Metric | Ceiling |
|--------|---------|
| `print(` calls in backend sources | 0 |
| `traceback.print_exc()` calls in backend | 0 |
| `except Exception: pass` (silent) | 0 |
| Backend module LoC | 1,200 |
| Frontend component LoC (non-App, non-util) | 1,500 |
| Frontend util LoC (`frontend/src/utils/*`) | 2,000 |
| `any` in frontend source | 0 |
| `@ts-ignore` in frontend source | 0 |
Ceilings are intentionally tight — lowering them is how we lock in
future improvements; raising them requires a documented reason.
3. **Ruff** — light pyflakes + syntax ruleset (`E9`, `F`) configured
in `pyproject.toml`. Catches undefined names (found a latent
`RecommenderService` self-reference in `simulation_mixin.py` on
the first run — now fixed) and unused imports. Stylistic rules
(`W`, `B`, `UP`) are deliberately off today; they can be opted in
per-file as we clean up.
**CI wiring:**
- `.github/workflows/code-quality.yml` — runs `ruff`, the gate, the
reporter unit tests, and uploads `reports/` as an artifact.
- `.circleci/config.yml` — mirrors the above under a `code-quality`
job added to the `test-workflow` workflow.
- `pytest.ini` — includes `scripts/test_code_quality_report.py` so
the gate logic has its own unit-test coverage (13 tests).
---
## 7. Prioritized Recommendations
### ~~Immediate (High Risk)~~ ✅ ALL DONE
1. ~~**Fix path traversal**~~ ✅ `_validate_path_within()` added to session save/load; PDF copy restricted
2. ~~**Refactor `recommender_service.py`**~~ ✅ Split into 5 modules via mixin architecture
3. ~~**Replace silent exceptions**~~ ✅ 80+ print→logger, 15 bare except→logged
4. ~~**Add a React Error Boundary**~~ ✅ `ErrorBoundary` wraps `<App />` in `main.tsx` with styled fallback + recovery buttons
### Short-Term (High Value)
5. ~~**Split `ActionFeed.tsx`** into subcomponents~~ ✅ Extracted `ActionCard`, `ActionSearchDropdown`; also split `VisualizationPanel` (→ `SldOverlay`, `MemoizedSvgContainer`), `CombinedActionsModal` (→ `ComputedPairsTable`, `ExplorePairsTab`), and `useDiagrams` (→ `useSldOverlay`)
6. **Add return type annotations** to all FastAPI route handlers
7. **Define typed API response interfaces** in `types.ts` to eliminate `Record<string, unknown>` casts (37 remaining; down from 65)
8. ~~**Remove unused deps**~~ ✅ Completed 2026-04-20: `framer-motion` and `lucide-react` dropped; `GZipMiddleware` import cleaned.
### Medium-Term (Improvement)
9. ~~**Parameterize hardcoded values**~~ Partially done: CORS origins configurable; port/host still hardcoded; `.env.example` template added 2026-04-20
10. **Standardize API response formats** across all endpoints
11. **Add accessibility attributes** (aria-labels, semantic HTML, focus trapping)
12. ~~**Consolidate CI** to a single pipeline~~ Decision reversed: both pipelines kept intentionally (cross-platform coverage). Each runs the new `code-quality` job.
### Long-Term (Strategic)
13. ~~**Deprecate or auto-generate `standalone_interface.html`**~~ ✅ Shipped as the 0.6.0 release (see `build:standalone` and the `dist-standalone` bundle). Legacy file frozen as `standalone_interface_legacy.html`.
14. **Add Python dependency lockfile** (Poetry or uv) for reproducible builds
15. **Add accessibility testing** (jest-axe or similar)
---
## 8. Bugs Found & Fixed During This Audit
Beyond the planned quality improvements, the audit uncovered and fixed several production bugs:
### 8.1 Second Contingency Crash (`content=None`) ✅ FIXED
Auto-generated `disco_` actions had no `content` field. The library's rule validator crashed on `content.get("set_bus", {})` during the second contingency analysis. Also fixed: `simulate_manual_action` merge logic used truthiness check (`if content:`) instead of `is not None`, preventing empty dicts from replacing stale `None`.
### 8.2 N-1 Auto-Zoom Lost on Second Contingency ✅ FIXED
`clearContingencyState()` reset `activeTab` to `'n'`, causing the auto-zoom guard (`branchChanged && activeTab === 'n'`) to skip zoom. Fixed in both `standalone_interface.html` and `App.tsx` — tab reset now only in `resetAllState` (full network reload).
### 8.3 `min_renewable_curtailment_actions` Not Saved ✅ FIXED
The standalone's user-config persist effect was missing `min_renewable_curtailment_actions` from the POST body. Also added the field to `config.default.json`. Added 17 regression tests verifying config save parity.
### 8.4 `PYPOWSYBL_FAST_MODE` Silently Overridden in Tests ✅ FIXED
The `conftest.py` `reset_config` fixture unconditionally re-applied `PYPOWSYBL_FAST_MODE=True`, overriding test modules' intentional `False` setting. This caused AC load flow to produce wrong reactive power values for PST combined actions (~110% instead of ~94%).
### 8.5 `RecommenderService._build_action_entry_from_topology` NameError ✅ FIXED
`simulation_mixin.py` called `RecommenderService._build_action_entry_from_topology(...)` without importing `RecommenderService`. The best-effort topology-reconstruction branch would have raised `NameError` at runtime if ever taken. Surfaced by the new ruff `F821` rule on 2026-04-20; fixed by switching to `self._build_action_entry_from_topology(...)` (the staticmethod lives on `SimulationMixin`, which `self` already is).
### 8.6 `get_load_voltage_levels_bulk` Incomplete ✅ FIXED
`network_service.py:get_load_voltage_levels_bulk()` fell through to the next function definition with an unclosed loop body. Implemented 2026-04-20 by mirroring the pattern of `get_generator_types_bulk()`.
---
## 9. Delta — 2026-04-20
One-pass sweep to land continuous reporting and close remaining
housekeeping issues. Scope:
- **Added**: `scripts/code_quality_report.py` (AST-based metrics
dump, JSON + Markdown output) and `scripts/check_code_quality.py`
(CI gate with per-metric thresholds).
- **Added**: `scripts/test_code_quality_report.py` — 7 unit tests
covering the AST smell-walker (print in string literals not
flagged, logged except not silent, etc.). Collected by
`pytest.ini`.
- **Added**: ruff configuration in `pyproject.toml` (F, E9) +
per-file ignores for tests/scripts; optional `[quality]` extra
that pulls `ruff` + `radon`.
- **Added**: `.github/workflows/code-quality.yml` +
`code-quality` CircleCI job — runs ruff, the gate, the reporter
unit tests, and uploads `reports/` as a CI artifact.
- **Added**: `CONTRIBUTING.md`, `.editorconfig`, `.env.example`
all referenced from `CLAUDE.md`.
- **Fixed bugs surfaced by the new tooling**:
- `RecommenderService._build_action_entry_from_topology(...)`
self-reference (F821) in `simulation_mixin.py`.
- `get_load_voltage_levels_bulk()` empty-body bug.
- Redundant shadowed imports (`io`, `threading`, `time`, and
`run_analysis_step2`) in `analysis_mixin.py` / `diagram_mixin.py`.
- 9 auto-fixed `F541` (f-strings without placeholders).
- Stray `.catch(() => {})` in `SettingsModal.tsx` replaced with
`console.error` logging.
- `traceback.print_exc()` in `analysis_mixin.py` converted to
`logger.exception(...)`; stray `except Exception: pass` swapped
for a logged suppression.
- **Removed**: unused frontend dependencies (`framer-motion`,
`lucide-react`) and the unused `GZipMiddleware` import.
- **Refreshed**: root `CLAUDE.md` — removed references to
root-level `test_*.py` scripts that no longer exist, added the
`code-quality` workflow to the Notes section, and pointed at
`CONTRIBUTING.md`.
Expected CI behaviour after the sweep: the new `code-quality` job is
green at current HEAD, and any regression (new `print(`, new module
crossing the ceiling, new `any` in frontend source, &c.) fails the
job with a prescriptive message.
---
## 10. Delta — 2026-04-20 (follow-up: function decomposition)
Second pass targeted at the two longest functions flagged in §6 by the
continuous reporter:
| Function | Before | After | Lever |
|----------|--------|-------|-------|
| `simulate_manual_action` | 599 | **146** | 12 private helpers + 11 helpers in new `simulation_helpers.py` |
| `compute_superposition` | 285 | **108** | 5 private helpers (pair-simulate / identify-with-PST-fallback / care-mask / metrics / diagnostics) |
The new stateless module `expert_backend/services/simulation_helpers.py`
(403 LoC) owns the pieces that don't need `self`:
`canonicalize_action_id`, `compute_reduction_setpoint`,
`parse_pst_tap_id` + `clamp_tap`, `classify_action_content`,
`is_pst_action` + `pst_fallback_line_idxs`, `build_care_mask`,
`resolve_lines_overloaded`, `compute_action_metrics`,
`extract_action_topology`, `serialize_action_result`,
`normalise_non_convergence`, `build_combined_description`, and
`compute_combined_rho`.
Coverage: 66 focused unit tests in
`expert_backend/tests/test_simulation_helpers.py` — one test class per
helper, exercising edge cases that previously lived as implicit
branches inside the god-method (islanding, PST-tap-only topologies,
heuristic power-reduction fallback, pre-existing overload exclusion,
combined-pair description building, sign-preserving rho superposition).
Test surface: the 130 existing simulation + superposition tests
continue to pass (same mocks, same call order). The extraction is
behaviour-preserving — helpers were pulled from the method body
verbatim wherever possible.
Next candidates (see §6 metrics): `run_analysis` (169 lines),
`update_config` (166 lines), `_enrich_actions` (125 lines).
---
## 11. Delta — 2026-04-20 (follow-up: frontend svgUtils decomposition)
Third pass targeted at the largest frontend file flagged in §6 — the
1807-line `frontend/src/utils/svgUtils.ts` omnibus. Split into
focused modules under `frontend/src/utils/svg/`:
| Module | Lines | Responsibility |
|--------|-------|----------------|
| `idMap.ts` | 29 | Cached DOM-id map for an SVG container |
| `svgBoost.ts` | 122 | Dynamic font/node scaling for large grids; `processSvg` |
| `metadataIndex.ts` | 40 | Build the `MetadataIndex` from pypowsybl metadata |
| `highlights.ts` | 422 | Overloaded / action-target / contingency halos; line & VL resolution |
| `deltaVisuals.ts` | 137 | Delta flow coloring + text replacement |
| `actionPinData.ts` | 344 | Pin descriptors, severity palette, anchor resolution — pure / no DOM |
| `actionPinRender.ts` | 537 | DOM injection for pins, highlights, click semantics, scale math |
| `fitRect.ts` | 125 | Padded viewBox computation for action overview + focused zoom |
`svgUtils.ts` itself is now a **60-line barrel** that re-exports every
symbol — no caller had to change. Largest frontend file is now
`App.tsx` at 1370 lines (the state orchestration hub, which is exempt
from the component size ceiling by design).
### New helpers promoted to the public API
Five pieces that used to be inline / closure-scoped are now exported
as standalone pure functions — making them unit-testable and reusable:
- `formatPinLabel(details)` — percentage / DIV / ISL / em-dash
- `formatPinTitle(idLabel, details)` — hover tooltip
- `fanOutColocatedPins(pins)` — circular distribution for colocated pins
- `curveMidpoint(p1, p2)` — quadratic Bezier midpoint + control point
- `computePinScale(baseR, pxPerSvgUnit, viewBoxMax)` — pin scale math
### Coverage
Five new co-located test files (61 new tests) covering each module in
isolation:
| Test file | Tests |
|-----------|-------|
| `idMap.test.ts` | 5 |
| `svgBoost.test.ts` | 7 |
| `metadataIndex.test.ts` | 6 |
| `actionPinData.test.ts` | 19 |
| `actionPinRender.test.ts` | 12 |
| `fitRect.test.ts` | 12 |
The pre-existing `svgUtils.test.ts` (144 tests) continues to pass
unchanged — the refactor is behaviour-preserving. Full frontend
suite: **1000 tests, all green**.
---
## 12. Delta — 2026-04-21 (follow-up: analysis_mixin decomposition)
Fourth pass targeted at the 1,116-line `analysis_mixin.py` — the
largest backend file. Split into four focused modules under a new
`expert_backend/services/analysis/` package:
| Module | Lines | Responsibility |
|--------|-------|----------------|
| `pdf_watcher.py` | 43 | Glob + mtime search for overflow PDFs |
| `action_enrichment.py` | 389 | Load-shedding / curtailment / PST details, renewable detection, rho scaling, topology extraction, non-convergence normalisation — pure |
| `mw_start_scoring.py` | 341 | MW-at-start dispatcher + per-type math (load shedding, curtailment, line disco, PST, open coupling) — pure |
| `analysis_runner.py` | 193 | Legacy AC→DC fallback worker + PDF-polling generator + `derive_analysis_message` |
`analysis_mixin.py` shrank **1,116 → 509 lines**. The three public
entry points (`run_analysis_step1`, `run_analysis_step2`, `run_analysis`)
are now thin orchestrators plus `_enrich_actions` /
`_compute_mw_start_for_scores` iterators that delegate to the
stateless helpers.
### Dependency injection instead of monkey patching
Two call sites rely on module-level library references that tests
traditionally `@patch`:
- `expert_backend.services.analysis_mixin.get_virtual_line_flow`
(used by `mw_start_open_coupling`)
- `expert_backend.services.analysis_mixin.run_analysis` (used by
the legacy worker)
Both helpers now accept the callable as an optional keyword argument.
The mixin reads the module-level name at call time and threads it in,
so existing `@patch('expert_backend.services.analysis_mixin.*')`
tests keep working without modification.
### Coverage
- **68 new unit tests** in `expert_backend/tests/test_analysis_helpers.py`
covering every new helper in isolation (pdf_watcher, non-convergence
normalisation, lines-overloaded reconstruction, load-shedding /
curtailment / PST details, MW-start dispatcher, analysis-message
derivation, …).
- **All 410 pre-existing analysis / simulation tests** continue to
pass — baseline diffed against post-refactor and showed zero new
regressions (same 19 pre-existing test-pollution failures on both
sides).
- Quality gate green; ruff (E9/F) clean.
### Top-5 longest functions after the pass
```
update_config 166
simulate_manual_action 146
compute_superposition 112
get_action_variant_sld 97
_compute_deltas 92
```
`run_analysis` (was 169 lines) and `_enrich_actions` (was 125 lines)
both fell out of the top-5. Next candidates: `update_config`,
`get_action_variant_sld`, `_compute_deltas`.
---
## 13. Delta — 2026-04-21 (follow-up: diagram_mixin decomposition)
Fifth pass targeted at `diagram_mixin.py` — the second-largest
backend file after the analysis sweep. Split into seven focused
modules under a new `expert_backend/services/diagram/` package:
| Module | Lines | Responsibility |
|--------|------:|----------------|
| `__init__.py` | 22 | Package docstring + module index |
| `layout_cache.py` | 63 | `(path, mtime)`-keyed ``grid_layout.json`` loader |
| `nad_params.py` | 41 | Default ``NadParameters`` factory (perf-tuned) |
| `nad_render.py` | 89 | ``generate_diagram`` + NaN element stripping |
| `sld_render.py` | 36 | SLD SVG + metadata extraction with fallbacks |
| `overloads.py` | 131 | Overload filtering + per-element current scans |
| `flows.py` | 67 | Branch + asset flow extractors (vectorised) |
| `deltas.py` | 241 | Terminal-aware flow-delta math (pure) |
`diagram_mixin.py` shrank **974 → 469 lines**. Every public method
(`get_network_diagram`, `get_n1_diagram`, `get_action_variant_diagram`,
`get_n_sld`, `get_n1_sld`, `get_action_variant_sld`) is now a short
orchestrator that switches the right variant, calls the stateless
helpers, and stashes results. Five new private helpers isolate the
variant-switching patterns that previously repeated across methods:
- `_require_action(action_id)` — validate + fetch the action entry
- `_lf_status_for_variant(...)` — load-flow status with variant cache
- `_snapshot_n1_state(...)` — N-1 branch + asset flows with variant restore
- `_attach_flow_deltas_vs_base(...)` — populate `flow_deltas` / `asset_deltas` on a diagram
- `_attach_convergence_from_obs(...)` — copy `lf_converged` / `non_convergence` from an observation
- `_diff_switches(...)` — `{switch_id: {from_open, to_open}}` diff between variants
### Coverage
- **39 new unit tests** in `expert_backend/tests/test_diagram_helpers.py`
covering every helper in isolation (layout cache eviction, NaN
stripping, overload filtering with N-state exclusion, terminal-aware
delta math with direction-flip, vectorised equivalent against scalar
reference, asset-delta categorisation, …).
- **1,000+ diagram-side test assertions** across the existing suite
continue to pass. One edge case (`test_sld_highlight.py::
TestChangedSwitchesInSld`) uncovered a subtle ordering dependency
— `changed_switches` must be captured BEFORE attempting flow
extraction so a mock network with missing flows still returns the
switch diff. Fixed in the orchestrator and regression-pinned.
- Full-suite diff against pre-refactor baseline: **identical 19
pre-existing test-pollution failures on both sides, zero new
regressions.**
- Quality gate green, ruff (E9/F) clean.
### Top-5 longest functions after the pass
```
update_config 166
simulate_manual_action 146
compute_superposition 112
compute_action_metrics 87
_augment_superposition_result 81
```
Every diagram-related function fell out of the top-5. Next natural
candidates: `update_config` (orchestrator in
`recommender_service.py`), then the simulation helpers.
---
## 14. Delta — 2026-05-05 (post-0.7.0 review)
Quality review covering the **104 commits across 16 PRs (#105–#131)**
landed on top of 0.6.5, headlined by the 0.7.0 release: interactive
overflow analysis (PRs #116, #122–#127), PyPSA-EUR European-wide grid
(PRs #112, #117), design-token migration (PR #120), tiered notice
system (PR #122), progressive-disclosure ActionCard (PR #121), VL-names
toggle (PR #118), and the overflow-pin layer pipeline (PRs #128–#131).
### 14.1 Quality gate — still green
`python scripts/check_code_quality.py` exits 0 at HEAD. Every
hard ceiling holds:
| Metric | Ceiling | HEAD |
|--------|---------|------|
| `print(` calls in backend sources | 0 | **0** |
| `traceback.print_exc()` | 0 | **0** |
| Silent `except Exception: pass` | 0 | **0** |
| Backend module LoC | 1200 | **1115** (`diagram_mixin.py`) |
| Frontend component LoC | 1500 | **1342** (`VisualizationPanel.tsx`) |
| Frontend util LoC | 2000 | **1807** (`utils/svgUtils.ts` — exempt barrel) |
| `any` in frontend source | 0 | **0** |
| `@ts-ignore` directives | 0 | **0** |
| Hex literals outside `tokens.{css,ts}` | 0 | **0****NEW gate** (PR #120) |
The hex-literal ceiling (`FRONTEND_HEX_LITERAL_MAX = 0`) is the most
significant gate addition since the last review. Every colour in the
React source now flows through `frontend/src/styles/tokens.{css,ts}`.
### 14.2 Metrics summary (HEAD vs 2026-04-21 baseline)
```
Backend (expert_backend/)
Source files (non-test): 24 (was 9 — +5 services + 11 helpers in subpackages)
Total lines: 8,931 (was 5,491 — +63 %)
Largest module: diagram_mixin.py (1,115 lines, ~7 % under ceiling)
Test files: 48 (was 41)
Frontend (frontend/src/)
Source files (non-test): 62 (was 37)
Total lines: 19,180 (was 14,872 — +29 %)
Largest component: App.tsx (1,399 lines, exempt) ; VisualizationPanel
1,342 (active under 1,500 ceiling, ex-1,654 pre-extraction)
Test files: 59 (was 43)
any types / @ts-ignore: 0 / 0
`as unknown as` casts: 18 (was 15) ← +3 regression
`Record<string, unknown>`: 39 (was 37) ← +2 regression
Hex literals (non-token): 0 (was uncounted; PR #120 zeroed it)
```
Backend and frontend each grew ~30–60 % in raw LoC, driven entirely
by net-new feature surface (overflow viewer overlay, design-token
migration, NoticesPanel, DiagramLegend, action-pin pipeline). No
existing module crossed its ceiling; the few that came close
(`VisualizationPanel.tsx`) were proactively decomposed to stay
under the gate.
### 14.3 Top-5 longest functions
```
expert_backend/services/overflow_overlay.py
_build_overlay_block 832 (template f-string — see §14.4)
expert_backend/services/analysis/overflow_geo_transform.py
transform_html 290
expert_backend/services/diagram_mixin.py
get_action_variant_diagram_patch 259
expert_backend/services/simulation_mixin.py
simulate_manual_action 186 (was 146 — +40 from inlined reco_)
expert_backend/services/recommender_service.py
update_config 184 (was 166 — +18; new overflow config)
```
`simulate_manual_action` and `update_config` both grew between
deltas (vs. §6 baseline of 146 / 166). Both still well below the
function-level threshold the gate enforces today (the gate is on
*module* size, not function size), but they're trending up. Worth a
defensive pass before the next release if either crosses 200 lines.
### 14.4 New large module: `services/overflow_overlay.py` (950 LoC)
PR #116 added the iframe overlay injector that grafts the
Co-Study4Grid pin layer + filter chips onto the upstream
`alphaDeesp/core/interactive_html.py` viewer. The module is 950 lines
total but the body is **one 832-line f-string** producing the
`<style>…</style><script>…</script>` block injected before the iframe
`</body>`. AST sees it as a single function (`_build_overlay_block`);
in practice it is template content, not control flow.
**Trade-off accepted:** the alternative would be a separate `.css` +
`.js` asset served alongside the iframe, but the f-string keeps the
overlay self-contained and matches how the upstream HTML is shipped
(everything inlined). Code-quality gate passes (the module is below
the 1,200-line backend ceiling; the function-size threshold isn't
gated). **Action item:** if the overlay grows further, consider
extracting the static CSS / JS to sibling files and `Path.read_text()`
them in. Coverage is provided indirectly by
`utils/svg/overflowOverlayRender.test.ts` and the iframe smoke tests
in `expert_backend/tests/test_overflow_html_dim_logic.py`.
### 14.5 Architecture additions since 2026-04-21
#### Backend
- `services/overflow_overlay.py` — pin / filter overlay injector
for the interactive HTML viewer.
- `services/analysis/overflow_geo_transform.py` — pure lxml
transform repositioning hierarchical-layout SVG to geo coords for
the `/api/regenerate-overflow-graph` toggle.
- New endpoint `/api/regenerate-overflow-graph` and
`/api/voltage-level-substations`.
#### Frontend
- `src/styles/tokens.{css,ts}` — design-token palette (colours,
spacing, typography, radius) consumed everywhere; gate enforces
no hex literals outside these two files.
- New components: `NoticesPanel`, `DiagramLegend`,
`InspectSearchField`, `DetachedPlaceholder`, `ActionCardPopover`,
`ActionTypeFilterChips`, `AppSidebar`, `SidebarSummary`,
`StatusToasts`.
- New hook: `useOverflowIframe.ts` — owns the iframe lifecycle,
layer-toggle state, layout-mode toggle, postMessage bridge, and
pin overlay payload computation; extracted from
`VisualizationPanel.tsx` to bring it back under the 1,500-line
ceiling (1,654 → 1,342).
- New SVG utils: `utils/svg/vlTitles.ts`,
`utils/svg/overflowPinPayload.ts`,
`utils/svg/overflowOverlayRender.ts`, `utils/svg/pinGlyph.{js,ts}`.
### 14.6 Documentation freshness
- `CHANGELOG.md` — current. 0.7.0 entry comprehensive (PR-by-PR
attribution).
- `README.md` — current. Already references 0.7.0, design tokens,
interactive overflow, PyPSA-EUR, ~1000 specs.
- Root `CLAUDE.md`**drift fixed in this pass**: bumped the
changelog reference from 0.6.5 to 0.7.0; added
`overflow_overlay.py` and `overflow_geo_transform.py` to the
backend tree; added `styles/tokens.{css,ts}`,
`components/NoticesPanel`, `DiagramLegend`, `InspectSearchField`,
`DetachedPlaceholder`, `ActionCardPopover`, `useOverflowIframe`,
and the new svg utils to the frontend tree; added
`/api/regenerate-overflow-graph` and
`/api/voltage-level-substations` to the API table.
- `expert_backend/CLAUDE.md` — listed `overflow_overlay.py` in this
pass; the rest was already current (overflow_geo_transform was
already documented in §"Analysis").
- `frontend/CLAUDE.md` — already documents the design-token rule
(§"Code style"). No drift.
### 14.7 Concerns and recommendations
#### Open
1. **Function-level size drift**: `simulate_manual_action` (146 →
186) and `update_config` (166 → 184) both grew. Add a
function-line ceiling (e.g. 250) to the gate to lock in the
April decompositions; this prevents inline-feature bloat from
eroding them. Low effort, high value.
2. **`as unknown as` and `Record<string, unknown>` regressions**:
+3 and +2 occurrences respectively since April. Origin: new
overflow-iframe payload glue (postMessage bridge, layer-toggle
payload). These are at integration boundaries (iframe
↔ React) so they're *defensible*, but the trend is unhealthy.
Define typed message-payload interfaces in `types.ts` for the
`cs4g:overflow-*` postMessage envelope.
3. **`_build_overlay_block` (832 lines)**: see §14.4. Acceptable
today, ratchet trigger for the next release if it grows.
4. **Function return-type annotations on FastAPI handlers** —
carried over from §3.3. The OpenAPI schema would benefit; this
is still the highest-leverage Python typing pass available.
#### Closed since last delta
- ~~Frontend hex-literal sprawl~~ ✅ design-token migration (PR #120)
lands a zero-hex ceiling.
- ~~`VisualizationPanel.tsx` size pressure~~ ✅ extracted
`InspectSearchField`, `DetachedPlaceholder`, and
`useOverflowIframe` (1,654 → 1,342).
- ~~Manual graphviz install required for overflow viewer~~ ✅ PR #126
auto-installs the `dot` binary cross-platform on package install.
- ~~Build extras vanished after PEP 621 migration~~ ✅ PR #127
restored `[quality]` extras under `optional-dependencies`.
### 14.8 Suggested follow-ups for the next pass
1. ~~Add `FUNCTION_LOC_MAX = 250` to `check_code_quality.py`~~ ✅
Landed in this pass — see §15.1.
2. ~~Type the `cs4g:overflow-*` postMessage envelope in `types.ts`~~
✅ Landed in this pass — see §15.2.
3. ~~Add return type annotations to all `@app.{get,post}` handlers
in `main.py`~~ ✅ Landed in this pass — see §15.3.
4. If the overflow overlay grows again, split
`_build_overlay_block` into sibling `overlay.css` / `overlay.js`
files and inline at runtime via `Path.read_text()`. **Deferred
until the overlay needs to grow** — exempt from the new
function-LoC ceiling per §15.1.
---
## 15. Delta — 2026-05-05 (follow-up: §14.8 recommendations)
Three of the four §14.8 follow-ups landed in the same pass that
opened §14. The fourth (overlay split) is deferred per its own
trigger condition.
### 15.1 Function-LoC ceiling — backend gate
`scripts/check_code_quality.py` now enforces
``BACKEND_FUNCTION_MAX = 250``. Implementation details:
- ``code_quality_report.py`` was extended with
``extract_all_functions(path)`` returning every backend function
with its LoC; the gate iterates the full list (was: top-5 only).
- The new ``BackendReport.all_functions`` field is populated for
every scan and a focused unit test
(``test_all_functions_populated``) guards the wiring.
- ``BACKEND_FUNCTION_EXEMPTIONS`` allows three pre-existing
overruns through the gate:
- ``services/overflow_overlay.py::_build_overlay_block`` (832
lines — template f-string, not control flow; see §14.4).
- ``services/analysis/overflow_geo_transform.py::transform_html``
(290 lines — single-pass lxml transform).
- ``services/diagram_mixin.py::get_action_variant_diagram_patch``
(259 lines).
Trend-watch §14.7's list of drifting functions
(``simulate_manual_action`` 186, ``update_config`` 184) now have
a hard ceiling at 250: any growth past that fails the gate.
### 15.2 Typed postMessage envelope (frontend)
`frontend/src/types.ts` gained a discriminated-union envelope for
the iframe ↔ parent postMessage bridge between the React shell and
the interactive overflow viewer:
- ``IframeToParentMessage`` — covers
``cs4g:overlay-ready``, ``cs4g:pin-clicked``,
``cs4g:pin-double-clicked``,
``cs4g:overflow-unsimulated-pin-double-clicked``,
``cs4g:overflow-filter-changed``,
``cs4g:overflow-layer-toggled``,
``cs4g:overflow-select-all-layers``,
``cs4g:overflow-node-double-clicked``.
- ``ParentToIframeMessage`` — covers ``cs4g:pins`` and
``cs4g:filters``.
- ``OverflowIframeScreenRect`` — pin bounding rect in iframe
coordinates.
`useOverflowIframe.ts` was migrated to:
- Validate inbound messages with a single ``isIframeMessage``
guard, then narrow per-branch via ``switch (msg.type)``. Each
case now accesses payload fields with full type safety (no
``typeof msg.x === 'string'`` runtime guards inline; the
envelope union enforces them at compile time).
- Type the parent → iframe ``postMessage`` call sites against
``ParentToIframeMessage``, so an envelope-shape regression in
``services/overflow_overlay.py`` would surface as a TypeScript
error here.
Side benefit: caught one unnecessary ``as unknown as ActionDetail``
in ``utils/svg/overflowPinPayload.ts:490`` (the stub object already
satisfied the required-fields contract). Dropped it in the same
pass; ``as unknown as`` count goes 18 → 17.
Future: when the overlay grows new message types, add a branch to
``IframeToParentMessage`` first; TypeScript's exhaustiveness check
then forces the consumer to handle it.
### 15.3 FastAPI handler return annotations
Every ``@app.{get,post,put}`` handler in ``expert_backend/main.py``
now carries a return-type annotation. Three categories:
- **JSON-returning handlers (19)** — ``-> dict``. Covers
``user-config``, ``config-file-path``, ``config``, ``branches``,
``voltage-levels``, ``nominal-voltages``,
``voltage-level-substations``, ``pick-path``, ``save-session``,
``list-sessions``, ``load-session``, ``restore-analysis-context``,
``run-analysis-step1``, ``regenerate-overflow-graph``,
``element-voltage-levels``, ``actions``,
``simulate-manual-action``, ``compute-superposition``,
``update_config``.
- **Per-endpoint-gzip handlers (10)**``-> Response``. Covers
every diagram / SLD endpoint plus ``actions``: they all wrap
``_maybe_gzip_json`` / ``_maybe_gzip_svg_text`` which return
``Response``.
- **NDJSON streaming handlers (3)**``-> StreamingResponse``.
Covers ``run-analysis``, ``run-analysis-step2``,
``simulate-and-variant-diagram``.
The pre-existing ``serve_overflow_artifact`` already had the
correct ``-> Response`` annotation.
OpenAPI consequence: ``GET /openapi.json`` now reflects the
return shapes for every handler. Useful for downstream
client-generation tooling.
### 15.4 Verification
- ``ruff check expert_backend`` — clean.
- ``python scripts/check_code_quality.py`` — green (modules,
functions, hex literals, smells all under ceiling).
- ``python -m pytest scripts/test_code_quality_report.py`` —
8 / 8 pass (one new test guards the function-list wiring).
- ``npx tsc --noEmit`` — no errors.
- ``npx vitest run`` — 1,260 / 1,260 pass.
- ``python -m pytest expert_backend/tests`` — 674 / 674 pass when
the documented test-pollution failures are isolated (same 19
pre-existing failures as the §13 baseline; no new regressions).
---
## 16. Delta — 2026-06-17 (0.8.0)
The 0.8.0 cycle broadened the feature surface (redispatch action
type, GST-estimable injection pairs, interactive SLD topology edit,
light/dark theme, Game Mode, the HuggingFace Docker deployment)
while keeping every ceiling green. The structural changes that
matter for this audit:
### 16.1 Diagram-mixin decomposition — landed
The §14.8 / §15.1 watch item on the diagram patch pipeline is
**resolved by extraction**, not by an exemption. ``diagram_mixin.py``
shed two helper packages:
- ``services/diagram/action_patch.py`` (new) — the entire
``/api/action-variant-diagram-patch`` pipeline (~510 LoC): the
280-line ``get_action_variant_diagram_patch`` orchestrator, its
three patch helpers (``compute_vl_topology_diff``,
``extract_vl_subtrees_with_edges``,
``get_disconnected_branches_from_snapshot``) and three private
helpers. ``_compute_vl_topology_diff`` /
``_get_disconnected_branches_from_snapshot`` stay re-exported as
static methods on ``DiagramMixin`` so ``test_diagram_patch_helpers.py``
passes unchanged.
- ``services/diagram/obs_prewarm.py`` (new) — the post-contingency
observation pre-warm helper (``build_prewarmed_obs``) that drives
``_cached_obs_n1`` so ``run_analysis_step1`` can skip the redundant
load flow.
Result: ``diagram_mixin.py`` **1220 → 769 lines** (-451, 37 %), a
431-line buffer below the ``BACKEND_MODULE_MAX`` 1200 ceiling. The
``get_action_variant_diagram_patch`` ``BACKEND_FUNCTION_EXEMPTIONS``
entry from §15.1 moves with the function to the new module. New
coverage: ``test_obs_prewarm_for_step1.py`` (9 tests),
``test_action_patch_module.py`` (16 tests).
### 16.2 New frontend module — ``src/game/``
Game Mode (a timed, scored session wrapper) lands as a **self-
contained ``frontend/src/game/`` package** rather than as additions
to ``App.tsx``. It is **additive and inert unless ``?game=1``**: the
App integration is three ``gameBridge.isGameMode()``-guarded touch
points, so the orchestration hub stays at its existing altitude and
the analysis engine is untouched. The decoupling singleton
(``gameBridge``) mirrors the established ``interactionLogger`` pattern,
keeping the dependency direction one-way (game → App via a registered
loader, never App → game).
### 16.3 Surface growth held to the established patterns
The new capabilities reuse existing extension seams instead of
widening hot files:
- **Redispatch action type** threads through the same per-type
dispatchers the other action families use
(``mw_start_scoring.classify_action_type`` +
``mw_start_redispatch``, ``action_enrichment.compute_redispatch_details``,
``simulation_helpers.compute_redispatch_setpoint``) — no new
god-function, each helper stays well under the 250-line ceiling.
- **Light / dark theme** is a **token swap, not per-component
overrides**: it re-points the ``styles/tokens.{css,ts}`` custom
properties, so the ``FRONTEND_HEX_LITERAL_MAX = 0`` rule (§14) keeps
holding — no theme-specific hex literals leaked into components.
- **Recommender action-type restriction** is plumbed end-to-end
(``useSettings`` → ``SettingsModal`` → ``POST /api/config`` →
``config.ALLOWED_ACTION_TYPES``) through the existing settings /
save / reload / replay paths.
### 16.4 CI / supply-chain note
From 0.8.0 the backend test installs tracked the latest published
``expert_op4grid_recommender`` on every run — which meant an upstream
release could turn an unrelated PR red or ship a regressed image on a
zero-change rebuild. **From 0.9.0 (QW8) the PR/deploy installs pin an
exact version** via ``recommender-pin.txt`` (consumed by
``.github/workflows/test.yml`` and the ``Dockerfile``); a weekly
``canary.yml`` floats to the latest release so upgrades are a deliberate
bump of that file. CircleCI was removed in the same release (QW23 —
GitHub Actions is a strict superset). The new HuggingFace
Docker deployment adds an **optional** same-origin SPA mount in
``main.py`` (``COSTUDY4GRID_FRONTEND_DIST``, mounted last, inert when
absent — local dev / the test suite are unaffected) and Git LFS
tracking for ``*.zip`` / ``*.png`` / ``*.jpg`` binaries (the Space git
endpoint rejects non-LFS binaries; the France 225/400 kV
``network.xiidm`` now ships compressed).
### 16.5 Verdict
No ceiling regressed; the one outstanding backend-module watch item
(the diagram patch pipeline) was retired by extraction. The feature
breadth of 0.8.0 was absorbed through existing decomposition,
token, and bridge patterns rather than by growing the hot files.
---
## 17. Delta — 2026-06-18 (metrics revision)
A pass over the gate **itself** rather than the code it measures —
closing measurement blind spots, adding detectors, ratcheting
already-measured-but-ungated smells, and tightening ceilings that had
accumulated headroom. The live numbers remain auto-generated by
`scripts/code_quality_report.py`; the before/after figures in the
historical deltas above are point-in-time snapshots from when each
decomposition landed and are intentionally **not** rewritten.
### 17.1 Coverage gap closed — `recommenders/` was never scanned
The backend scan rooted at `main.py` + `services/` only. Widening it
to all of `expert_backend/` (minus the `tests/` suite, the ad-hoc
`test_backend.py`, and the setup-time `install_graphviz.py`) revealed
that the entire **`expert_backend/recommenders/`** package — the
pluggable model registry behind `/api/models` (8 files, ~1,200 LoC) —
had **zero** gate coverage. It is clean on the smell axes
(`print`/`traceback`/silent-except all 0) but had been invisible to
the gate; its largest function (`_run_analysis_step2_with_model`,
**226** lines) is now the binding non-exempt backend function.
### 17.2 New detectors + scoping fix
- **`as any`** assertion casts now count toward the `any` gate (was a
blind spot — only annotation positions were caught). Currently 0.
- **`@ts-expect-error` / `@ts-nocheck`** join `@ts-ignore` as gated
type-suppressions (all three silence the checker). The 4 legitimate
uses live in `src/test/setup.ts`, which is now correctly excluded —
the frontend scan skips the `src/test/` infra dir, not just
`*.test.*` files, so test scaffolding no longer counts as product
source.
- **`# noqa` / `# type: ignore`** in backend source are now reported.
### 17.3 Ratchets — freeze, don't big-bang
Three smells exist today in small, often-legitimate counts. Rather
than force an immediate cleanup (or ignore them), the gate freezes
them at the current level — lowering is welcome, raising is a
regression:
| Smell | Frozen at | Notes |
|-------|----------:|-------|
| `as unknown as` casts | **19** | DOM/SVG boundary casts, mostly in `actionPinRender` / `highlights` / `ActionFeed` |
| `Record<string, unknown>` | **46** | JSON-ish payload typing; prefer named interfaces going forward |
| backend `# noqa` / `# type: ignore` | **3** | all justified inline (`F401` re-exports, one logged `BLE001`) |
### 17.4 Ceilings tightened; `App.tsx` bounded not exempt
Headroom had accumulated, so ceilings were ratcheted toward the
current maxima (the doc's standing "lowering is welcome" policy):
| Ceiling | Was | Now | Current max |
|---------|----:|----:|------------:|
| Backend module | 1200 | **1150** | 1100 (`simulation_mixin.py`) |
| Backend function | 250 | **240** | 226 (`_run_analysis_step2_with_model`) |
| Frontend component | 1500 | **1450** | 1407 (`VisualizationPanel.tsx`) |
| `frontend/src/utils/**` | 2000 | **1000** | 592 (`actionPinRender.ts`) |
| `App.tsx` hub | *exempt* | **2100** | 1982 |
`App.tsx` traded its blanket size exemption for a generous *bounded*
ceiling — "the orchestration hub" is a reason for a higher bar, not an
infinite one. The dead `diagram_mixin::get_action_variant_diagram_patch`
function-size exemption (retired in §16.1, now 9 lines) was removed
from the allowlist.
### 17.5 Watch items
The two tightest margins are deliberate locks, not comfortable
headroom: `simulation_mixin.py` (1100 / 1150) and
`_run_analysis_step2_with_model` (226 / 240). The next substantive
edit to either should come with an extraction. Reporter unit tests
grew from 7 to **13** (new detector + scoping coverage).
---
## 18. Delta — 2026-06-18 (new measurement dimensions)
Adds axes beyond line counts. Two are dependency-free and **gated**;
two are introduced as **non-blocking foundations** because a hard gate
today would be either noisy (mypy) or set blind (coverage).
### 18.1 Cyclomatic complexity + nesting depth — gated, AST-native
Per-function McCabe complexity and max block-nesting are computed from
the AST the reporter already parses — **no external dependency** (the
`radon` quality-extra is now optional, kept only for ad-hoc `radon cc`).
Both are reported (top-5 each) and gated:
| Metric | Ceiling | Current non-exempt max | Exemption |
|--------|--------:|-----------------------:|-----------|
| Cyclomatic complexity | **38** | 35 (`recommender_service.update_config`) | `overflow_geo_transform.transform_html` (49 — lxml transform, already size-exempt) |
| Nesting depth | **8** | 7 (`analysis_mixin._narrow_context_to_selected_overloads`) | `sanitize.sanitize_for_json` (9 — recursion shape) |
Ratchets: lower over time; new offenders are not welcome.
### 18.2 Logical (code) lines — reported
Non-blank, non-comment counts now sit alongside raw LoC — backend
**8,919 / 11,596**, frontend **18,619 / 25,238**. The gated ceilings
stay on raw LoC; code-lines are context (a dense 600-line module reads
differently from one that is half blanks and docstrings). The counter
is a deliberately simple heuristic, not a tokenizer.
### 18.3 mypy — advisory (non-blocking)
`pyproject [tool.mypy]` (`ignore_missing_imports`, scoped to
`expert_backend` minus `tests/` + the setup scripts) plus a
**non-blocking** step in both CI pipelines. Baseline **~69 errors**,
dominated by `"<Mixin>" has no attribute "_…"` — mypy cannot model the
deliberate mixin composition (`DiagramMixin` / `AnalysisMixin` /
`SimulationMixin` all operate on the composed `RecommenderService`
`self`; the backend guide explicitly says "treat the mixins as one
class"). A hard gate would be noise. The path to gating is typing the
mixin surface via `Protocol`s; until then mypy is **visibility**, not a
gate.
### 18.4 Coverage — report-only
`pytest-cov` (backend, in the `test` extra) and `@vitest/coverage-v8`
(frontend dev-dep) are wired into both pipelines as **non-blocking**
reporting + artifacts (`npm run test:coverage` / `pytest --cov`
locally). There is **no `--cov-fail-under` floor yet**: the backend
suite cannot be measured offline (several tests need the real
`expert_op4grid_recommender`), so a floor must be read off the first
green CI run and then ratcheted — setting one blind would break CI on
day one.
### 18.5 Verdict
The gate now measures four axes **dependency-free** — size, smells,
complexity/nesting, weak-typing — each gated or ratcheted. mypy and
coverage are staged foundations, deliberately non-blocking until they
can gate without noise (mypy: type the mixin surface) or guesswork
(coverage: a measured baseline). Reporter unit tests **13 → 16**.
---
## 19. Revision — 2026-06-18 (acting on the review)
A measured code-quality revision (full scorecard below) and the
improvement targets it produced — implemented bar one. The two
"staged foundations" from §18 were the headline opportunities, and
both got promoted from advisory to gating here.
### 19.1 Scorecard (measured)
| Dimension | Grade | Evidence |
|-----------|-------|----------|
| Backend smell hygiene | A+ | print/traceback/silent all **0**; 3 suppressions, all justified |
| Frontend strictness surface | A | **0** `any`/`as any`, **0** ts-suppressions, **0** hex |
| Quality tooling / gate | A | size + smells + complexity/nesting + weak-typing, all dependency-free |
| Module / function size | B+ | App.tsx 1982/2100, simulation_mixin 1100/1150, overflow_overlay 1055 ride ceilings |
| Cyclomatic complexity | B+ | hotspots: transform_html CC49, update_config CC35, _narrow_context CC30/nest7 |
| Test coverage | B | frontend **73.7% stmt / 76.3% line** (now gated); backend unmeasured |
| Frontend weak typing | B→B+ | `as unknown as` 19 → **12**; `Record<string,unknown>` **46** |
| Backend type safety | C+→B | mypy **69 → 0, now gating**; return annotations, missing **112 → 86** |
### 19.2 Targets and outcomes
| # | Target | Outcome |
|---|--------|---------|
| 1 | Type the mixin surface | `services/_recommender_state.py` — a `TYPE_CHECKING`-only base the three mixins inherit (`object` at runtime, no MRO/behaviour change). mypy **69 → 0**, flipped to **gating** (pinned `mypy==1.19.*`). |
| 2 | Fix the 3 real type bugs | `run_analysis_step2` `list[str]` defaults → `\| None` (×2); `_cached_obs_n1_elements` Optional mismatch resolved by the shared declaration. |
| 3 | Return-annotation coverage | 26 void fns annotated `-> None` (libcst codemod, mypy-verified); reporter metric + **missing-return ratchet at 86** (was 112). 80 value-returning + 6 generators remain the ratchet-down target. |
| 5 | Coverage floor | **Frontend now gates** on `vite.config.ts` thresholds (70/65/70/73, below the 73.7/70/73.8/76.3 baseline). **Backend** stays report-only — unmeasurable offline; floor to be read off the first green CI run. |
| 6 | Frontend weak typing | 7 over-cautious SVG-DOM `as unknown as` casts simplified to plain downcasts (type-only, tsc+eslint clean); **weak-cast ratchet 19 → 12**. |
| 4 | Decompose ceiling-riders | **Deferred** (explicitly out of scope this round). simulation_mixin / App.tsx / VisualizationPanel / the CC hotspots remain the size-and-complexity targets. |
### 19.3 Net effect on the gate
Two axes graduated from advisory to **gating**: mypy (0 errors,
enforced by the shared-state base) and frontend coverage (a measured
floor). New ratchets/metrics: backend missing-return annotations (86),
plus the tightened `as unknown as` ratchet (12). Every change was
validated locally — mypy clean, tsc + eslint clean, the offline backend
suite byte-identical (no runtime touched), and `npm run test:coverage`
green against the new floor.
### 19.4 What's next (unchanged from the review)
The remaining type-debt is now *visible and bounded*: 80 value-returning
backend functions to annotate (mypy will verify), the 46
`Record<string,unknown>` to model, target **#4** (decompose the
ceiling-riders), and the backend coverage floor once CI yields a number.
---
## 20. Delta — 2026-06-18 (backend coverage floor)
The first green CI run after §19 produced the number §18.4 was waiting
for: the backend no-graphviz job reported **78%** (`TOTAL 4771 1063
78%`, 849 passed). So the backend coverage floor is now **set and
gating** — `pyproject.toml [tool.coverage.report] fail_under = 72` (6
points below the baseline, same margin philosophy as the frontend
floor), enforced wherever `pytest --cov` runs (both CI pipelines).
Both coverage halves now gate (frontend §19, backend here); the only
remaining "advisory → gating" promotions from the §18 staging are
complete. Ratchet the floors up as coverage climbs.
---
## 21. Delta — 2026-06-18 (return annotations, ratchet-down)
Continuing §19's #3, the missing-return ratchet dropped **86 → 60**
26 functions annotated with **concrete, mypy-verified** types:
- A conservative AST codemod annotated the *shape-clear* ones (where
every value-return agrees): 6 `tuple`, 4 generators → `Iterator[Any]`,
2 `dict`, plus `| None` where there's an explicit bare return.
- A second hand-built pass added the unambiguous literal returns:
`network_service` list/dict getters, `deltas._signed``float`,
`_load_layout_coords``dict`, the `is_renewable` predicates → `bool`.
An attempt with the `infer-types` tool was **reverted**: it annotated
75 in one shot but introduced 37 mypy errors — wrong `-> None` on
value-returning methods, over-broad unions that broke unpacking — and
even its "passing" annotations couldn't be trusted (untyped deps let
wrong types through as `Any`). The conservative route is slower but
every annotation is correct.
**Why it stopped at 60, not 0:** the residual 60 are `return helper(…)`
/ `return self._x` delegations to **untyped** pypowsybl / recommender
helpers, so their honest type is `Any`. Padding them `-> Any` would push
the metric to 100% while mypy verifies nothing — gaming it. The
principled path to lowering the ratchet further is typing the **helpers**
bottom-up (in `services/analysis/`, `services/diagram/`,
`simulation_helpers.py`), after which concrete types flow up to these
delegators and mypy can actually check them. Offline suite byte-identical
(624 passed); no runtime touched.
---
## 22. Tracked for next release — target #4 (decompose ceiling-riders)
Target **#4** from the §19 review (decompose the files riding their
size/complexity ceilings) was deliberately deferred — it is structural
work, not a gate tweak, and should be scheduled deliberately rather than
done under deadline when a feature PR trips the gate.
It is now **traced as a proposal** with the full investigation captured:
[`docs/proposals/decompose-ceiling-riders.md`](../proposals/decompose-ceiling-riders.md).
That doc lists every ceiling-rider with its current margin, a concrete
extraction candidate per target, a priority order (tightest margin
first), and acceptance criteria.
Headline margins at hand-off (ceiling − current):
- **`simulation_mixin.py` 1110/1150 (+40)** — tightest module; it grew
this session (the annotation imports), so it is the first to act on.
Extract the superposition slice.
- **`VisualizationPanel.tsx` 1407/1450 (+43)** — tightest frontend file.
- **`_run_analysis_step2_with_model` 226/240 (+14)** — tightest function.
- **`update_config` CC 35 / `_narrow_context…` nest 7** — tightest
complexity (+3 CC / +1 nest).
- `overflow_overlay.py` (1055) — move the 924-line template f-string to
external assets; high leverage and retires a gate exemption.
- `App.tsx` (1982/2100) — already scoped as Option 3 in
`frontend/CLAUDE.md`.
Also surfaced in `CHANGELOG.md` `[Unreleased]` so it is bound to the
next release.
---
## 23. Delta — 2026-07-09 (D9: docs as a checked artifact)
The review's recurring "docs drift" finding — the `CLAUDE.md` inventory
trees reference files that were renamed away, and their `file.py:NNN`
line anchors rot on the next edit (all seven had drifted by hundreds of
lines) — is now a **gate**, not a periodic manual audit.
`scripts/check_docs_tree.py` scans the four inventory docs (root +
`expert_backend/` + `expert_backend/tests/` + `frontend/`) and fails on:
1. **Referenced-path existence** — every backtick-quoted, directory-
qualified `path/like/this.ext` must resolve to a real file under any
sensible base dir (the docs write paths relative to their own
subtree). Exemptions: generated/runtime artifacts
(`dist-standalone/standalone.html`, `Overflow_Graph/…`) and paths
referenced *as removed* (the line — or a ±1-line window, for wrapped
prose — carries "removed / former / renamed / superseded / …").
2. **No stale line-number anchors**`file.py:NNN` is forbidden; the
convention is a **symbol anchor** (name the function/class, which
survives edits). The seven pre-existing anchors were converted (e.g.
`network_service.py:352` → "the module-level `NetworkService()`
singleton in `network_service.py`").
Wired into `.github/workflows/code-quality.yml` as its own gate step,
with unit coverage in `scripts/test_check_docs_tree.py` (fixture-driven
classification + a self-guard asserting the real repo stays clean, so
drift fails the pytest suite too, not only the CI step). A `--warn-only`
flag supports a roll-in period; the tree was brought fully green before
the gate was made blocking. This closes the last "inventory layer rots"
finding from the 2026-07 review (D9).