# 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 `` inside ``. 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 `
` 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` 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` 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 (`
    `/`
  • `) 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`: 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 `` 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` 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`: 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 `` block injected before the iframe ``. 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` 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` | **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 `"" 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` **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` 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).