diff --git a/conductor/archive/codebase_curation_20260507/CURATION_PROTOCOL.md b/conductor/archive/codebase_curation_20260507/CURATION_PROTOCOL.md index 1955951a..fc54d2f7 100644 --- a/conductor/archive/codebase_curation_20260507/CURATION_PROTOCOL.md +++ b/conductor/archive/codebase_curation_20260507/CURATION_PROTOCOL.md @@ -11,7 +11,6 @@ For every `.py` file identified for curation: 3. **Redundancy Identification:** Cross-reference the file against `CULLING_CANDIDATES_PHASE5.md`. 4. **Proposed Change Log:** Before editing, document the specific lines/symbols to be removed or refactored and the technical justification (e.g., "Superseded by theme_2.py"). 5. **Surgical Edit:** Use the `replace` tool for targeted deletions. Avoid bulk file overwrites. -6. **Style Verification:** Immediately run `scripts/ai_style_formatter.py` on the modified file to ensure indentation and formatting standards are maintained. ## 2. Regression Guardrails diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index 9693f1e9..07f4551a 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -29,8 +29,6 @@ ## Configuration & Tooling -- **ai_style_formatter.py:** Custom Python formatter specifically designed to enforce 1-space indentation and ultra-compact whitespace to minimize token consumption. - - **src/paths.py:** Centralized module for path resolution. Supports project-specific conductor directory overrides via project TOML (`[conductor].dir`), enabling isolated track management per project. If not specified, conductor paths default to `./conductor` relative to each project's TOML file. All paths are resolved to absolute objects. Provides **Path Resolution Metadata**, exposing the source of each resolved path (default, environment variable, or configuration file) for high-fidelity GUI display. Supports **Runtime Re-Resolution** via `reset_resolved()`, allowing path changes to be applied immediately without an application restart. Path configuration (logs, scripts) can also be configured via `config.toml` or environment variables, eliminating hardcoded filesystem dependencies. - **src/presets.py:** Implements `PresetManager` for high-performance CRUD operations on system prompt presets stored in TOML format (`presets.toml`, `project_presets.toml`). Supports dynamic path resolution, scope-based inheritance, and foundational base prompt customization. diff --git a/scripts/transform_render_methods.py b/scripts/transform_render_methods.py new file mode 100644 index 00000000..dcb8ee7f --- /dev/null +++ b/scripts/transform_render_methods.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +""" +AST-based transformer to extract App._render_xxx methods into module-level functions. + +Transformation: + class App: + def _render_xxx(self, ...) -> ReturnType: + self.foo = bar + self.baz.qux() + + Becomes: + + def render_xxx(app: App, ...) -> ReturnType: + app.foo = bar + app.baz.qux() + + class App: + def _render_xxx(self, ...) -> ReturnType: + render_xxx(self, ...) +""" + +import ast +import sys +import argparse +from pathlib import Path +from typing import Optional + + +class RenderMethodTransformer(ast.NodeTransformer): + def __init__(self, class_name: str = "App", method_prefix: str = "_render"): + self.class_name = class_name + self.method_prefix = method_prefix + self.transformed_methods: list[str] = [] + self.in_target_class = False + self.current_method: Optional[str] = None + + def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef: + if node.name == self.class_name: + self.in_target_class = True + self.current_method = None + node = self.generic_visit(node) + self.in_target_class = False + return node + return node + + def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef: + if not self.in_target_class: + return node + + if not node.name.startswith(self.method_prefix): + return node + + if node.name == "__init__" or node.name.startswith("__"): + return node + + self.current_method = node.name + new_name = node.name[1:] # Remove underscore prefix: _render_xxx -> render_xxx + + self.transformed_methods.append(node.name) + + new_func = ast.FunctionDef( + name=new_name, + args=ast.arguments( + posonlyargs=[], + args=[ast.arg(arg="app", annotation=ast.Name(id=self.class_name, ctx=ast.Load()))] + node.args.args, + kwonlyargs=node.args.kwonlyargs, + kw_defaults=node.args.kw_defaults, + defaults=node.args.defaults, + ), + body=[self._transform_body(body_item) for body_item in node.body], + decorator_list=node.decorator_list, + returns=node.returns, + type_comment=node.type_comment, + ) + + delegation_method = ast.FunctionDef( + name=node.name, + args=node.args, + body=[ast.Expr(value=ast.Call( + func=ast.Name(id=new_name, ctx=ast.Load()), + args=[ast.Name(id="self", ctx=ast.Load())] + [self._arg_to_expr(arg) for arg in node.args.args], + keywords=[ast.keyword(arg=arg.arg, value=ast.Name(id=arg.arg, ctx=ast.Load())) for arg in node.args.kwonlyargs] + ))], + decorator_list=node.decorator_list, + returns=node.returns, + type_comment=node.type_comment, + ) + + return ast.Module(body=[new_func, delegation_method], type_ignores=[]) + + def _arg_to_expr(self, arg: ast.arg) -> ast.expr: + return ast.Name(id=arg.arg, ctx=ast.Load()) + + def _transform_body(self, node: ast.AST) -> ast.AST: + return ast.NodeTransformer.generic_visit(self, node) + + def visit_Name(self, node: ast.Name) -> ast.Name: + if isinstance(node.ctx, ast.Load) and node.id == "self": + return ast.Name(id="app", ctx=node.ctx) + return node + + def visit_Attribute(self, node: ast.Attribute) -> ast.Attribute: + if isinstance(node.value, ast.Name) and node.value.id == "self": + new_value = ast.Name(id="app", ctx=node.value.ctx) + return ast.Attribute(value=new_value, attr=node.attr, ctx=node.ctx) + return self.generic_visit(node) + + +class AppRenderExtractor(ast.NodeVisitor): + def __init__(self, class_name: str = "App", method_prefix: str = "_render"): + self.class_name = class_name + self.method_prefix = method_prefix + self.in_target_class = False + self.methods: dict[str, dict] = {} + + def visit_ClassDef(self, node: ast.ClassDef) -> None: + if node.name == self.class_name: + self.in_target_class = True + self.generic_visit(node) + self.in_target_class = False + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + if not self.in_target_class: + return + + if not node.name.startswith(self.method_prefix): + return + + if node.name == "__init__" or node.name.startswith("__"): + return + + args = [arg.arg for arg in node.args.args] + self.methods[node.name] = { + "line": node.lineno, + "end_line": node.end_lineno, + "args": args, + "has_return": any(isinstance(n, ast.Return) for n in ast.walk(node)), + } + + +def extract_render_methods(source_path: str, class_name: str = "App", method_prefix: str = "_render") -> dict: + with open(source_path, "r", encoding="utf-8") as f: + source = f.read() + + tree = ast.parse(source) + extractor = AppRenderExtractor(class_name, method_prefix) + extractor.visit(tree) + + return extractor.methods + + +def transform_file(source_path: str, output_path: Optional[str] = None, class_name: str = "App", method_prefix: str = "_render") -> list[str]: + with open(source_path, "r", encoding="utf-8") as f: + source = f.read() + + tree = ast.parse(source) + + transformer = RenderMethodTransformer(class_name, method_prefix) + new_tree = transformer.visit(tree) + + result = ast.unparse(new_tree) + + if output_path: + with open(output_path, "w", encoding="utf-8", newline="") as f: + f.write(result) + else: + print(result) + + return transformer.transformed_methods + + +def main(): + parser = argparse.ArgumentParser(description="Transform App._render_xxx methods to module-level functions") + parser.add_argument("source", help="Source Python file to transform") + parser.add_argument("-o", "--output", help="Output file (default: stdout)") + parser.add_argument("-c", "--cls", default="App", help="Class name to transform (default: App)") + parser.add_argument("-p", "--prefix", default="_render", help="Method prefix to extract (default: _render)") + parser.add_argument("--extract-only", action="store_true", help="Only extract method info, don't transform") + + args = parser.parse_args() + + if args.extract_only: + methods = extract_render_methods(args.source, args.cls, args.prefix) + print(f"Found {len(methods)} methods:") + for name, info in sorted(methods.items(), key=lambda x: x[1]["line"]): + print(f" {name}: lines {info['line']}-{info['end_line']}, args={info['args']}") + else: + transformed = transform_file(args.source, args.output, args.cls, args.prefix) + print(f"Transformed {len(transformed)} methods: {transformed}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/test_hot_reload_integration.py b/tests/test_hot_reload_integration.py index 92032f48..2da78119 100644 --- a/tests/test_hot_reload_integration.py +++ b/tests/test_hot_reload_integration.py @@ -4,165 +4,165 @@ from src.hot_reloader import HotReloader, HotModule def test_hot_module_dataclass_fields(): - hm = HotModule(name='test.module', file_path='/path/to/module.py', state_keys=['key1', 'key2'], delegation_targets=['func1', 'func2']) - assert hm.name == 'test.module' - assert hm.file_path == '/path/to/module.py' - assert hm.state_keys == ['key1', 'key2'] - assert hm.delegation_targets == ['func1', 'func2'] + hm = HotModule(name='test.module', file_path='/path/to/module.py', state_keys=['key1', 'key2'], delegation_targets=['func1', 'func2']) + assert hm.name == 'test.module' + assert hm.file_path == '/path/to/module.py' + assert hm.state_keys == ['key1', 'key2'] + assert hm.delegation_targets == ['func1', 'func2'] def test_hot_reloader_register_and_get(): - HotReloader.HOT_MODULES.clear() - hm = HotModule(name='test.module', file_path='/path/to/module.py') - HotReloader.register(hm) - assert 'test.module' in HotReloader.HOT_MODULES - assert HotReloader.HOT_MODULES['test.module'] is hm + HotReloader.HOT_MODULES.clear() + hm = HotModule(name='test.module', file_path='/path/to/module.py') + HotReloader.register(hm) + assert 'test.module' in HotReloader.HOT_MODULES + assert HotReloader.HOT_MODULES['test.module'] is hm def test_hot_reloader_register_duplicate_raises(): - HotReloader.HOT_MODULES.clear() - hm = HotModule(name='test.module', file_path='/path/to/module.py') - HotReloader.register(hm) - with pytest.raises(ValueError, match="already registered"): - HotReloader.register(hm) + HotReloader.HOT_MODULES.clear() + hm = HotModule(name='test.module', file_path='/path/to/module.py') + HotReloader.register(hm) + with pytest.raises(ValueError, match="already registered"): + HotReloader.register(hm) def test_hot_reloader_is_error_state(): - HotReloader.HOT_MODULES.clear() - assert HotReloader.is_error_state is False - HotReloader.last_error = "Test error" - HotReloader.is_error_state = True - assert HotReloader.is_error_state is True + HotReloader.HOT_MODULES.clear() + assert HotReloader.is_error_state is False + HotReloader.last_error = "Test error" + HotReloader.is_error_state = True + assert HotReloader.is_error_state is True def test_reload_unknown_module_returns_false(): - HotReloader.HOT_MODULES.clear() - mock_app = MagicMock() - result = HotReloader.reload('unknown.module', mock_app) - assert result is False - assert HotReloader.last_error == "Module unknown.module not registered" - assert HotReloader.is_error_state is True + HotReloader.HOT_MODULES.clear() + mock_app = MagicMock() + result = HotReloader.reload('unknown.module', mock_app) + assert result is False + assert HotReloader.last_error == "Module unknown.module not registered" + assert HotReloader.is_error_state is True def test_reload_success_clears_error_state(): - HotReloader.HOT_MODULES.clear() - HotReloader.last_error = "Previous error" - HotReloader.is_error_state = True - hm = HotModule(name='test.module', file_path='/path/to/module.py', state_keys=['active_discussion']) - HotReloader.register(hm) - mock_app = MagicMock() - with patch('importlib.reload') as mock_reload, \ + HotReloader.HOT_MODULES.clear() + HotReloader.last_error = "Previous error" + HotReloader.is_error_state = True + hm = HotModule(name='test.module', file_path='/path/to/module.py', state_keys=['active_discussion']) + HotReloader.register(hm) + mock_app = MagicMock() + with patch('importlib.reload') as mock_reload, \ patch('importlib.import_module') as mock_import: - mock_import.side_effect = Exception("Module does not exist") - result = HotReloader.reload('test.module', mock_app) - assert result is False - assert HotReloader.last_error is not None - HotReloader.HOT_MODULES.clear() + mock_import.side_effect = Exception("Module does not exist") + result = HotReloader.reload('test.module', mock_app) + assert result is False + assert HotReloader.last_error is not None + HotReloader.HOT_MODULES.clear() def test_reload_captures_and_restores_state_on_failure(): - HotReloader.HOT_MODULES.clear() - hm = HotModule(name='test.module', file_path='/path/to/module.py', state_keys=['active_discussion']) - HotReloader.register(hm) - mock_app = MagicMock() - mock_app.active_discussion = 'main' - with patch('importlib.reload', side_effect=Exception("Reload failed")): - result = HotReloader.reload('test.module', mock_app) - assert result is False - assert HotReloader.is_error_state is True + HotReloader.HOT_MODULES.clear() + hm = HotModule(name='test.module', file_path='/path/to/module.py', state_keys=['active_discussion']) + HotReloader.register(hm) + mock_app = MagicMock() + mock_app.active_discussion = 'main' + with patch('importlib.reload', side_effect=Exception("Reload failed")): + result = HotReloader.reload('test.module', mock_app) + assert result is False + assert HotReloader.is_error_state is True def test_reload_all_success(): - HotReloader.HOT_MODULES.clear() - hm1 = HotModule(name='module1', file_path='/path/to/module1.py', state_keys=[]) - hm2 = HotModule(name='module2', file_path='/path/to/module2.py', state_keys=[]) - HotReloader.register(hm1) - HotReloader.register(hm2) - mock_app = MagicMock() - with patch('importlib.reload') as mock_reload, \ + HotReloader.HOT_MODULES.clear() + hm1 = HotModule(name='module1', file_path='/path/to/module1.py', state_keys=[]) + hm2 = HotModule(name='module2', file_path='/path/to/module2.py', state_keys=[]) + HotReloader.register(hm1) + HotReloader.register(hm2) + mock_app = MagicMock() + with patch('importlib.reload') as mock_reload, \ patch('importlib.import_module') as mock_import: - mock_reload.return_value = None - mock_import.return_value = MagicMock() - result = HotReloader.reload_all(mock_app) - assert result is True + mock_reload.return_value = None + mock_import.return_value = MagicMock() + result = HotReloader.reload_all(mock_app) + assert result is True def test_reload_all_partial_failure(): - HotReloader.HOT_MODULES.clear() - hm1 = HotModule(name='module1', file_path='/path/to/module1.py', state_keys=[]) - HotReloader.register(hm1) - mock_app = MagicMock() - with patch('importlib.reload', side_effect=Exception("Fail")): - result = HotReloader.reload_all(mock_app) - assert result is False + HotReloader.HOT_MODULES.clear() + hm1 = HotModule(name='module1', file_path='/path/to/module1.py', state_keys=[]) + HotReloader.register(hm1) + mock_app = MagicMock() + with patch('importlib.reload', side_effect=Exception("Fail")): + result = HotReloader.reload_all(mock_app) + assert result is False class TestHotReloadTriggerIntegration: - def test_trigger_hot_reload_calls_reload_all(self): - HotReloader.HOT_MODULES.clear() - hm = HotModule(name='src.gui_2', file_path='/path/gui_2.py', state_keys=['active_discussion'], delegation_targets=[]) - HotReloader.register(hm) - mock_app = MagicMock() - mock_app._hot_reload_error = None - with patch.object(HotReloader, 'reload_all', return_value=True) as mock_reload_all: - from src.gui_2 import App - with patch.object(App, '_trigger_hot_reload', App._trigger_hot_reload.__wrapped__ if hasattr(App._trigger_hot_reload, '__wrapped__') else None): - pass - HotReloader.HOT_MODULES.clear() + def test_trigger_hot_reload_calls_reload_all(self): + HotReloader.HOT_MODULES.clear() + hm = HotModule(name='src.gui_2', file_path='/path/gui_2.py', state_keys=['active_discussion'], delegation_targets=[]) + HotReloader.register(hm) + mock_app = MagicMock() + mock_app._hot_reload_error = None + with patch.object(HotReloader, 'reload_all', return_value=True) as mock_reload_all: + from src.gui_2 import App + with patch.object(App, '_trigger_hot_reload', App._trigger_hot_reload.__wrapped__ if hasattr(App._trigger_hot_reload, '__wrapped__') else None): + pass + HotReloader.HOT_MODULES.clear() - def test_hot_reload_error_state_tracked_in_app(self): - from src.gui_2 import App - with patch('src.gui_2.app_controller.AppController'): - app = App.__new__(App) - app._hot_reload_error = None - assert app._hot_reload_error is None + def test_hot_reload_error_state_tracked_in_app(self): + from src.gui_2 import App + with patch('src.gui_2.app_controller.AppController'): + app = App.__new__(App) + app._hot_reload_error = None + assert app._hot_reload_error is None - def test_keyboard_shortcut_check_in_gui_func(self): - from src.gui_2 import App - mock_imgui = MagicMock() - mock_io = MagicMock() - mock_io.key_ctrl = True - mock_io.key_alt = True - mock_io.keys_down = {ord('R'): True} - mock_imgui.get_io.return_value = mock_io - mock_app = MagicMock() - mock_app._trigger_hot_reload = MagicMock(return_value=True) - mock_app._render_custom_title_bar = MagicMock() - mock_app._render_shader_live_editor = MagicMock() - mock_app._render_history_window = MagicMock() - mock_app.perf_profiling_enabled = False - mock_app.is_viewing_prior_session = False - mock_app._render_main_interface = MagicMock() - mock_app._handle_history_logic = MagicMock() - mock_app.ai_status = 'idle' - mock_app.ui_crt_filter = False - with patch('src.gui_2.imgui', mock_imgui), \ + def test_keyboard_shortcut_check_in_gui_func(self): + from src.gui_2 import App + mock_imgui = MagicMock() + mock_io = MagicMock() + mock_io.key_ctrl = True + mock_io.key_alt = True + mock_io.keys_down = {ord('R'): True} + mock_imgui.get_io.return_value = mock_io + mock_app = MagicMock() + mock_app._trigger_hot_reload = MagicMock(return_value=True) + mock_app._render_custom_title_bar = MagicMock() + mock_app._render_shader_live_editor = MagicMock() + mock_app._render_history_window = MagicMock() + mock_app.perf_profiling_enabled = False + mock_app.is_viewing_prior_session = False + mock_app._render_main_interface = MagicMock() + mock_app._handle_history_logic = MagicMock() + mock_app.ai_status = 'idle' + mock_app.ui_crt_filter = False + with patch('src.gui_2.imgui', mock_imgui), \ patch('src.gui_2.theme') as mock_theme, \ patch('src.gui_2.bg_shader') as mock_bg: - mock_bg.get_bg.return_value.enabled = False - mock_theme.is_nerv_active.return_value = False - App._gui_func(mock_app) - mock_app._trigger_hot_reload.assert_called_once() + mock_bg.get_bg.return_value.enabled = False + mock_theme.is_nerv_active.return_value = False + App._gui_func(mock_app) + mock_app._trigger_hot_reload.assert_called_once() - def test_mma_global_controls_renders_reload_button(self): - from src.gui_2 import App - mock_imgui = MagicMock() - mock_imgui.checkbox.return_value = (False, False) - mock_imgui.ImVec4 = MagicMock(side_effect=lambda r, g, b, a: (float(r), float(g), float(b), float(a))) - mock_imgui.ImVec2 = MagicMock(side_effect=lambda x, y: (float(x), float(y))) - mock_app = MagicMock() - mock_app.mma_step_mode = False - mock_app.mma_status = 'idle' - mock_app.controller = None - mock_app.active_tier = None - mock_app._pending_mma_spawns = [] - mock_app._pending_mma_approvals = [] - mock_app._pending_ask_dialog = False - mock_app._trigger_hot_reload = MagicMock(return_value=True) - mock_app._hot_reload_error = None - mock_app.controller = MagicMock() - mock_app.controller.engine = None - with patch('src.gui_2.imgui', mock_imgui), \ + def test_mma_global_controls_renders_reload_button(self): + from src.gui_2 import App + mock_imgui = MagicMock() + mock_imgui.checkbox.return_value = (False, False) + mock_imgui.ImVec4 = MagicMock(side_effect=lambda r, g, b, a: (float(r), float(g), float(b), float(a))) + mock_imgui.ImVec2 = MagicMock(side_effect=lambda x, y: (float(x), float(y))) + mock_app = MagicMock() + mock_app.mma_step_mode = False + mock_app.mma_status = 'idle' + mock_app.controller = None + mock_app.active_tier = None + mock_app._pending_mma_spawns = [] + mock_app._pending_mma_approvals = [] + mock_app._pending_ask_dialog = False + mock_app._trigger_hot_reload = MagicMock(return_value=True) + mock_app._hot_reload_error = None + mock_app.controller = MagicMock() + mock_app.controller.engine = None + with patch('src.gui_2.imgui', mock_imgui), \ patch('src.gui_2.C_VAL', (1, 0.5, 0, 1)): - App._render_mma_global_controls(mock_app) - mock_imgui.button.assert_any_call("Reload GUI") \ No newline at end of file + App._render_mma_global_controls(mock_app) + mock_imgui.button.assert_any_call("Reload GUI") \ No newline at end of file