From 07ddcf068b8ee447c03bf8694edb39df1174f8a4 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 14 May 2026 22:06:44 -0400 Subject: [PATCH] docs(spec): Add hot reloader design spec --- .../specs/2026-05-14-hot-reloader-design.md | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-14-hot-reloader-design.md diff --git a/docs/superpowers/specs/2026-05-14-hot-reloader-design.md b/docs/superpowers/specs/2026-05-14-hot-reloader-design.md new file mode 100644 index 0000000..e9b38fb --- /dev/null +++ b/docs/superpowers/specs/2026-05-14-hot-reloader-design.md @@ -0,0 +1,192 @@ +# Hot Reloader Design Spec + +**Date:** 2026-05-14 +**Author:** Tier 2 Tech Lead +**Status:** Draft + +## Overview + +Implement a selective, state-preserving hot-reload system for the Manual Slop `./src` Python codebase. This follows the "data in stable memory, code in reloadable modules" pattern pioneered by Casey's Handmade Hero hot-reload in C. + +## Goals + +- Enable hot-reloading of `src/gui_2.py` and `src/app_controller.py` initially +- Preserve App instance state across reloads (scroll position, form inputs, open windows) +- Extensible architecture — future hot modules (e.g., `ai_client.py`) can be added via registry +- Manual trigger only (Ctrl+Alt+R keyboard shortcut + GUI button) +- Silent fallback on failure with visual error tint + +## Architecture + +### Delegation Pattern + +Code is separated into **delegators** (thin wrappers in stable modules) and **delegation targets** (actual logic in reloadable modules). + +```python +# src/gui_2.py (reloadable) +def render_main_interface(app: App) -> None: + if app.perf_profiling_enabled: + app.perf_monitor.start_component("_render_main_interface") + app._render_window_if_open("Project Settings", app._render_project_settings_hub) + # ... all render logic here + +# src/app_controller.py (stable) — App class stays stable +class App: + def _render_main_interface(self) -> None: + import src.gui_2 as gui2 + gui2.render_main_interface(self) +``` + +### HotModule Registry Entry + +```python +@dataclass +class HotModule: + name: str # "src.gui_2" + file_path: str # Absolute path to .py + state_keys: list[str] # App attrs to preserve during reload + delegation_targets: list[str] # Method names on App that delegate here +``` + +### HotReloader Core API + +```python +class HotReloader: + HOT_MODULES: dict[str, HotModule] # Module registry + + @classmethod + def register(cls, module: HotModule) -> None: ... + + @classmethod + def reload(cls, module_name: str, app: App) -> bool: + """Reload a single module. Returns True on success, False on failure.""" + + @classmethod + def reload_all(cls, app: App) -> bool: + """Reload all registered modules in dependency order.""" + + @classmethod + def capture_state(cls, app: App, state_keys: list[str]) -> dict: ... + + @classmethod + def restore_state(cls, app: App, state: dict) -> None: ... +``` + +### State Capture/Restore + +Before reload, `HotReloader.capture_state()` serializes App attributes listed in `state_keys`. After reload (success or failure), `restore_state()` writes them back. + +State is captured as a dict of `{attr_name: copy.deepcopy(value)}`. Restoration uses `setattr` with re-imported module reference. + +### Error Handling Flow + +``` +Reload triggered + ↓ +capture_state(app) + ↓ +attempt importlib.reload(module) + ↓ +on Exception: + restore_state(app) # revert to pre-reload state + cls.last_error = traceback + cls.is_error_state = True + tint GUI red + return False + ↓ +on success: + clear last_error + clear error tint + return True +``` + +### Trigger Mechanism + +- `Ctrl+Alt+R` keyboard shortcut captured in main input loop +- GUI button in MMA Dashboard: "Hot Reload" with icon +- Both call `HotReloader.reload_all(self)` on the App instance + +### Visual Error Tint + +When `HotReloader.is_error_state` is True: +- If NERV theme active: overlay with NERV red (rgba 255, 72, 64, alpha) +- Else: overlay with red tint +- Tint cleared on next successful reload + +## Module Registry (Initial) + +```python +HOT_MODULES = { + "src.gui_2": HotModule( + name="src.gui_2", + file_path=str(Path(__file__).parent / "gui_2.py"), + state_keys=[ + "_active_discussion", "_disc_entries", "_disc_roles", + "show_windows", "ui_discussion_split_h", "active_tickets", + # ... more keys TBD during implementation + ], + delegation_targets=[ + "_render_main_interface", "_render_discussion_hub", + "_render_discussion_panel", "_render_discussion_selector", + # ... more targets TBD during implementation + ], + ), +} +``` + +## Delegation Refactoring Phases + +### Phase 1: GUI Methods +Extract render methods from `App` in `app_controller.py` into delegation targets in `gui_2.py`. + +### Phase 2: State Keys Inventory +Catalog all `App` instance attributes that need preservation. + +### Phase 3: HotReloader Implementation +Implement `src/hot_reloader.py` with capture/restore, registry, and error handling. + +### Phase 4: Trigger Integration +Add Ctrl+Alt+R handler and GUI button. + +### Phase 5: Visual Tint +Implement error tint overlay. + +## Extensibility: Adding Future Hot Modules + +```python +# Add ai_client as hot module: +HotReloader.register(HotModule( + name="src.ai_client", + file_path=str(Path(__file__).parent / "ai_client.py"), + state_keys=["_pending_requests", "_api_key"], + delegation_targets=["_send_request", "_stream_response"], +)) + +# Or via decorator on the delegation target: +@hot_module(state_keys=["_pending_requests"]) +def send_request(app: App) -> None: + ... +``` + +## Files Affected + +| File | Change | +|------|--------| +| `src/hot_reloader.py` | New — HotReloader class | +| `src/gui_2.py` | Refactor render methods to module-level functions | +| `src/app_controller.py` | Refactor App methods to delegation wrappers | +| `src/imgui_scopes.py` | Unchanged (still used by refactored code) | + +## Success Criteria + +1. Pressing Ctrl+Alt+R reloads `src.gui_2` without losing App state +2. GUI button triggers same reload +3. Failed reload shows error tint, preserves last-good state +4. New hot modules can be added via `HotReloader.register()` without modifying core +5. No performance impact when not reloading (< 1ms overhead per frame) + +## Out of Scope + +- Automatic file watching (manual trigger only per design) +- Hot-reloading of C extensions or native code +- Cross-platform reload support (Windows focus for now) \ No newline at end of file