192 lines
5.8 KiB
Markdown
192 lines
5.8 KiB
Markdown
# 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) |