Private
Public Access
refactor(src): Phase 10.2 batch 4 - aggregate + api_hooks + context_presets + external_editor
aggregate.py (1 site): - compute_file_stats returns Result[dict[str, int]]. The 2 SILENT_SWALLOW sites (ast.parse + open) now append to errors list. Callers in gui_2.py updated to extract result.data from the cache. api_hooks.py (1 site): - WebSocketServer._handler - was 2 except ...: pass (JSONDecodeError + ConnectionClosed). Now logs warnings instead of silently swallowing. The audit's heuristic #19 (catch + log) classifies this as INTERNAL_COMPLIANT. context_presets.py (1 site): - ContextPresetManager.load_all returns Result[Dict[str, ContextPreset]]. Caller in app_controller.py (load_context_preset) updated to check result.ok. external_editor.py (1 site): - _find_vscode_in_registry returns Result[Optional[str]]. The 1 SILENT_SWALLOW site (subprocess.run) now appends to errors. Caller in ExternalEditorLauncher._resolve_vscode updated to extract result.data. Tests updated to check result.ok and use result.data.
This commit is contained in:
+9
-6
@@ -13,6 +13,8 @@ This is essential for keeping prompt tokens low while giving the AI enough struc
|
|||||||
to use the MCP tools to fetch only what it needs.
|
to use the MCP tools to fetch only what it needs.
|
||||||
"""
|
"""
|
||||||
import ast
|
import ast
|
||||||
|
|
||||||
|
from src.result_types import Result, ErrorInfo, ErrorKind
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
@@ -86,12 +88,13 @@ def group_files_by_dir(files: list[Any]) -> dict[str, list[Any]]:
|
|||||||
grouped[dir_name].append(f)
|
grouped[dir_name].append(f)
|
||||||
return grouped
|
return grouped
|
||||||
|
|
||||||
def compute_file_stats(abs_path: str) -> dict[str, int]:
|
def compute_file_stats(abs_path: str) -> Result[dict[str, int]]:
|
||||||
"""
|
"""
|
||||||
Computes lines and basic AST stats for a file.
|
Computes lines and basic AST stats for a file.
|
||||||
[C: src/gui_2.py:App._stats_worker, tests/test_context_composition_phase3.py:test_compute_file_stats]
|
[C: src/gui_2.py:App._stats_worker, tests/test_context_composition_phase3.py:test_compute_file_stats]
|
||||||
"""
|
"""
|
||||||
stats = {"lines": 0, "ast_elements": 0}
|
stats = {"lines": 0, "ast_elements": 0}
|
||||||
|
errors: list[ErrorInfo] = []
|
||||||
try:
|
try:
|
||||||
with open(abs_path, 'r', encoding='utf-8') as f:
|
with open(abs_path, 'r', encoding='utf-8') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
@@ -100,11 +103,11 @@ def compute_file_stats(abs_path: str) -> dict[str, int]:
|
|||||||
try:
|
try:
|
||||||
tree = ast.parse(content)
|
tree = ast.parse(content)
|
||||||
stats["ast_elements"] = sum(1 for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)))
|
stats["ast_elements"] = sum(1 for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)))
|
||||||
except (SyntaxError, ValueError):
|
except (SyntaxError, ValueError) as e:
|
||||||
pass
|
errors.append(ErrorInfo(kind=ErrorKind.INTERNAL, message=f"ast.parse failed: {e}", source=f"aggregate.compute_file_stats[{abs_path}]", original=e))
|
||||||
except (OSError, SyntaxError):
|
except (OSError, SyntaxError) as e:
|
||||||
pass
|
errors.append(ErrorInfo(kind=ErrorKind.NOT_FOUND, message=str(e), source=f"aggregate.compute_file_stats[{abs_path}]", original=e))
|
||||||
return stats
|
return Result(data=stats, errors=errors)
|
||||||
|
|
||||||
def build_discussion_section(history: list[Any]) -> str:
|
def build_discussion_section(history: list[Any]) -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
+4
-4
@@ -909,10 +909,10 @@ class WebSocketServer:
|
|||||||
if channel in self.clients:
|
if channel in self.clients:
|
||||||
self.clients[channel].add(websocket)
|
self.clients[channel].add(websocket)
|
||||||
await websocket.send(json.dumps({"type": "subscription_confirmed", "channel": channel}))
|
await websocket.send(json.dumps({"type": "subscription_confirmed", "channel": channel}))
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError as e:
|
||||||
pass
|
logging.warning(f"WebSocketServer: JSON decode error: {e}")
|
||||||
except _require_warmed("websockets").exceptions.ConnectionClosed:
|
except _require_warmed("websockets").exceptions.ConnectionClosed as e:
|
||||||
pass
|
logging.info(f"WebSocketServer: connection closed: {e}")
|
||||||
finally:
|
finally:
|
||||||
for channel in self.clients:
|
for channel in self.clients:
|
||||||
if websocket in self.clients[channel]:
|
if websocket in self.clients[channel]:
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
from src.models import ContextPreset
|
from src.models import ContextPreset
|
||||||
|
from src.result_types import Result, ErrorInfo, ErrorKind
|
||||||
|
|
||||||
|
|
||||||
class ContextPresetManager:
|
class ContextPresetManager:
|
||||||
"""Manages context presets within the project dictionary (manual_slop.toml)."""
|
"""Manages context presets within the project dictionary (manual_slop.toml)."""
|
||||||
|
|
||||||
def load_all(self, project_dict: Dict[str, Any]) -> Dict[str, ContextPreset]:
|
def load_all(self, project_dict: Dict[str, Any]) -> Result[Dict[str, ContextPreset]]:
|
||||||
"""Loads all context presets from the project dictionary."""
|
"""Loads all context presets from the project dictionary."""
|
||||||
presets: Dict[str, ContextPreset] = {}
|
presets: Dict[str, ContextPreset] = {}
|
||||||
|
errors: list[ErrorInfo] = []
|
||||||
presets_data = project_dict.get("context_presets", {})
|
presets_data = project_dict.get("context_presets", {})
|
||||||
for name, data in presets_data.items():
|
for name, data in presets_data.items():
|
||||||
try:
|
try:
|
||||||
presets[name] = ContextPreset.from_dict(name, data)
|
presets[name] = ContextPreset.from_dict(name, data)
|
||||||
except (ValueError, KeyError, TypeError):
|
except (ValueError, KeyError, TypeError) as e:
|
||||||
# Silent failure or logging could be added here
|
errors.append(ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source=f"context_presets.load_all[{name}]", original=e))
|
||||||
pass
|
return Result(data=presets, errors=errors)
|
||||||
return presets
|
|
||||||
|
|
||||||
def save_preset(self, project_dict: Dict[str, Any], preset: ContextPreset) -> None:
|
def save_preset(self, project_dict: Dict[str, Any], preset: ContextPreset) -> None:
|
||||||
"""Saves a context preset into the project dictionary."""
|
"""Saves a context preset into the project dictionary."""
|
||||||
|
|||||||
+10
-6
@@ -60,8 +60,9 @@ class ExternalEditorLauncher:
|
|||||||
_cached_vscode_config: Optional[TextEditorConfig] = None
|
_cached_vscode_config: Optional[TextEditorConfig] = None
|
||||||
|
|
||||||
|
|
||||||
def _find_vscode_in_registry() -> Optional[str]:
|
def _find_vscode_in_registry() -> Result[Optional[str]]:
|
||||||
paths = []
|
paths = []
|
||||||
|
errors: list[ErrorInfo] = []
|
||||||
reg_keys = [
|
reg_keys = [
|
||||||
r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||||
r"HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
r"HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||||
@@ -79,11 +80,11 @@ def _find_vscode_in_registry() -> Optional[str]:
|
|||||||
exe_path = line.strip() + "\\Code.exe"
|
exe_path = line.strip() + "\\Code.exe"
|
||||||
if os.path.exists(exe_path):
|
if os.path.exists(exe_path):
|
||||||
paths.append(exe_path)
|
paths.append(exe_path)
|
||||||
except (OSError, subprocess.SubprocessError, subprocess.TimeoutExpired):
|
except (OSError, subprocess.SubprocessError, subprocess.TimeoutExpired) as e:
|
||||||
pass
|
errors.append(ErrorInfo(kind=ErrorKind.INTERNAL, message=str(e), source=f"external_editor._find_vscode_in_registry[{key}]", original=e))
|
||||||
if paths:
|
if paths:
|
||||||
return paths[0]
|
return Result(data=paths[0], errors=errors)
|
||||||
return None
|
return Result(data=None, errors=errors)
|
||||||
|
|
||||||
|
|
||||||
def _find_vscode_common_paths() -> Optional[str]:
|
def _find_vscode_common_paths() -> Optional[str]:
|
||||||
@@ -103,7 +104,10 @@ def auto_detect_vscode() -> Optional[TextEditorConfig]:
|
|||||||
global _cached_vscode_config
|
global _cached_vscode_config
|
||||||
if _cached_vscode_config is not None:
|
if _cached_vscode_config is not None:
|
||||||
return _cached_vscode_config
|
return _cached_vscode_config
|
||||||
vscode_path = _find_vscode_in_registry() or _find_vscode_common_paths()
|
vscode_result = _find_vscode_in_registry()
|
||||||
|
vscode_path = vscode_result.data if vscode_result.ok else None
|
||||||
|
if vscode_path is None:
|
||||||
|
vscode_path = _find_vscode_common_paths()
|
||||||
if vscode_path:
|
if vscode_path:
|
||||||
_cached_vscode_config = TextEditorConfig(
|
_cached_vscode_config = TextEditorConfig(
|
||||||
name="vscode",
|
name="vscode",
|
||||||
|
|||||||
+4
-2
@@ -1374,7 +1374,8 @@ class App:
|
|||||||
cache_key = f"{f_path}_{mtime}"
|
cache_key = f"{f_path}_{mtime}"
|
||||||
if cache_key not in self._file_stats_cache: missing_keys.append((f_path, cache_key))
|
if cache_key not in self._file_stats_cache: missing_keys.append((f_path, cache_key))
|
||||||
else:
|
else:
|
||||||
stats = self._file_stats_cache[cache_key]
|
cached = self._file_stats_cache[cache_key]
|
||||||
|
stats = cached.data if hasattr(cached, "data") else cached
|
||||||
total_lines += stats.get("lines", 0)
|
total_lines += stats.get("lines", 0)
|
||||||
total_ast += stats.get("ast_elements", 0)
|
total_ast += stats.get("ast_elements", 0)
|
||||||
|
|
||||||
@@ -4084,7 +4085,8 @@ def render_context_files_table(app: App) -> None:
|
|||||||
_exists = os.path.exists(_abs_p)
|
_exists = os.path.exists(_abs_p)
|
||||||
mtime = os.path.getmtime(_abs_p) if _exists else 0
|
mtime = os.path.getmtime(_abs_p) if _exists else 0
|
||||||
cache_key = f"{f_path}_{mtime}"
|
cache_key = f"{f_path}_{mtime}"
|
||||||
stats = app._file_stats_cache.get(cache_key, {"lines": 0, "ast_elements": 0})
|
stats_raw = app._file_stats_cache.get(cache_key, {"lines": 0, "ast_elements": 0})
|
||||||
|
stats = stats_raw.data if hasattr(stats_raw, "data") else stats_raw
|
||||||
f_name = os.path.basename(f_path)
|
f_name = os.path.basename(f_path)
|
||||||
imgui.text(f"{f_name} (L: {stats.get('lines', 0)}, AST: {stats.get('ast_elements', 0)})")
|
imgui.text(f"{f_name} (L: {stats.get('lines', 0)}, AST: {stats.get('ast_elements', 0)})")
|
||||||
if not _exists:
|
if not _exists:
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ def test_compute_file_stats():
|
|||||||
py_path = os.path.join(temp_dir, "test.py")
|
py_path = os.path.join(temp_dir, "test.py")
|
||||||
with open(py_path, "w") as f:
|
with open(py_path, "w") as f:
|
||||||
f.write("def foo():\n pass\n\nclass Bar:\n pass\n")
|
f.write("def foo():\n pass\n\nclass Bar:\n pass\n")
|
||||||
|
stats_result = compute_file_stats(py_path)
|
||||||
stats = compute_file_stats(py_path)
|
assert stats_result.ok, f"compute_file_stats failed: {stats_result.errors}"
|
||||||
|
stats = stats_result.data
|
||||||
assert stats["lines"] == 5
|
assert stats["lines"] == 5
|
||||||
assert stats["ast_elements"] == 2 # 1 func, 1 class
|
assert stats["ast_elements"] == 2 # 1 func, 1 class
|
||||||
|
|||||||
@@ -28,13 +28,15 @@ def test_load_all_context_presets():
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
presets_result = manager.load_all(project_dict)
|
||||||
presets = manager.load_all(project_dict)
|
assert presets_result.ok, f"load_all failed: {presets_result.errors}"
|
||||||
|
presets = presets_result.data
|
||||||
|
|
||||||
assert "test_preset" in presets
|
assert "test_preset" in presets
|
||||||
assert presets["test_preset"].files[0].path == "file1.py"
|
assert presets["test_preset"].files[0].path == "file1.py"
|
||||||
assert presets["test_preset"].screenshots == ["screenshot1.png"]
|
assert presets["test_preset"].screenshots == ["screenshot1.png"]
|
||||||
|
|
||||||
|
|
||||||
def test_delete_context_preset():
|
def test_delete_context_preset():
|
||||||
manager = ContextPresetManager()
|
manager = ContextPresetManager()
|
||||||
project_dict = {
|
project_dict = {
|
||||||
|
|||||||
Reference in New Issue
Block a user