docs(spec): Add hot reloader design spec
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user