Merge branch 'tier2/result_migration_gui_2_20260619'

This commit is contained in:
ed
2026-06-20 07:23:02 -04:00
20 changed files with 7970 additions and 472 deletions
+92
View File
@@ -222,6 +222,23 @@ PROGRAMMER_ERROR_EXCEPTIONS: frozenset[str] = frozenset({
"NotImplementedError",
})
# Lazy-loader method names: the canonical naming convention for proxy
# classes that defer a heavy import until first attribute access or call
# (e.g. _LazyModule._resolve, _load, _get, _try_load). The audit
# recognizes these as the canonical context for the sentinel-fallback
# pattern (Phase 12.1 result_migration_gui_2_20260619): when the import
# or attribute access fails, the except body falls back to a documented
# sentinel class instance with an `available: bool = False` flag (or
# similar) so the UI can detect the stub and offer an alternative
# path. This is the canonical graceful-degradation pattern per
# error_handling.md:625-690 (Re-Raise Patterns).
LAZY_LOADER_METHOD_NAMES: frozenset[str] = frozenset({
"_resolve",
"_load",
"_get",
"_try_load",
})
# Categories that are considered violations
VIOLATION_CATEGORIES: frozenset[str] = frozenset({
"INTERNAL_SILENT_SWALLOW",
@@ -743,6 +760,35 @@ class ExceptionVisitor(ast.NodeVisitor):
f"Compliant: `try: ...; except ({', '.join(sorted(exc_set))}): return Result(data=..., errors=[...])` is the canonical Result-recovery pattern. The function-name-not-ending-in-`_result` is a smell (rename to `xxx_result`); the pattern itself is the data-oriented convention. (per result_migration_small_files_20260617 Phase 11.2)",
)
# B. Lazy-loading sentinel fallback — Phase 12.1 (result_migration_gui_2_20260619)
# Per error_handling.md:625-690 (Re-Raise Patterns) and the lazy-loading
# pattern guidance, when a module is loaded lazily (e.g. numpy, tkinter
# at first attribute access) and the import or attribute access fails,
# falling back to a documented sentinel class instance with an
# `available: bool = False` flag is the canonical graceful-degradation
# pattern. The sentinel is NOT a silent swallow: the UI can detect the
# stub via the `available` flag and offer an alternative code path
# (e.g. ImGui file dialog when tkinter.filedialog is unavailable).
# This is analogous to the nil-sentinel dataclass (Pattern 1 in
# error_handling.md). The function-name heuristic (`_resolve`/`_load`/
# `_get`/`_try_load`) is the standard lazy-loader naming convention.
# The except body must NOT re-raise; the recovery is via assignment
# to `self.<attr>` (directly or via a nested try/except).
except_body_re_raises = any(
isinstance(s, ast.Raise) and s.exc is None
for s in ast.walk(ast.Module(body=except_body, type_ignores=[]))
)
if (
self._current_func_name() in LAZY_LOADER_METHOD_NAMES
and not except_body_re_raises
and exc_set & {"AttributeError", "ImportError", "ModuleNotFoundError"}
and self._has_self_attr_assign(except_body)
):
return (
"INTERNAL_COMPLIANT",
f"Compliant: lazy-loading sentinel fallback. `try: ...; except ({', '.join(sorted(exc_set))}): self.<attr> = <sentinel>()` in `{self._current_func_name()}` is the canonical graceful-degradation pattern. The sentinel class exposes an `available: bool = False` flag (or similar) so the UI can detect the stub and offer an alternative path. Per error_handling.md:625-690 and Phase 12.1 result_migration_gui_2_20260619.",
)
return None
def _has_string_return(self, stmts: list[ast.stmt]) -> bool:
@@ -921,6 +967,37 @@ class ExceptionVisitor(ast.NodeVisitor):
has_return_none_after = True
return has_for_range_with_try and has_return_none_after
def _has_self_attr_assign(self, stmts: list[ast.stmt]) -> bool:
"""True if any statement (recursively) assigns to a `self.<attr>` attribute.
Used by the lazy-loading sentinel fallback heuristic (Phase 12.1) to
detect the canonical graceful-degradation pattern: the except body
falls back to a sentinel class instance via `self._cached = _Stub()`
either directly OR via a nested try/except (e.g., an outer try that
catches AttributeError and a nested try that ultimately falls back
to the stub). The recursive walk handles both cases:
- Direct: `try: getattr(...); except AttributeError: self._cached = _Stub()`
- Nested: `try: getattr(...); except AttributeError: try: importlib...; except: self._cached = _Stub()`
Per the styleguide (error_handling.md:625-690), this is the canonical
graceful-degradation pattern for lazy-loading modules that may not
be present on every Python install. The sentinel's `available: bool = False`
flag (or similar) lets the UI detect the stub and offer an alternative
path (e.g., ImGui file dialog when tkinter.filedialog is unavailable).
"""
for s in stmts:
for node in ast.walk(s):
if isinstance(node, ast.Assign):
for target in node.targets:
if (
isinstance(target, ast.Attribute)
and isinstance(target.value, ast.Name)
and target.value.id == "self"
):
return True
return False
def _has_imgui_end_call(self, stmts: list[ast.stmt]) -> bool:
"""True if any statement is a call to an imgui.end_* function."""
for s in stmts:
@@ -1028,6 +1105,21 @@ class ExceptionVisitor(ast.NodeVisitor):
f"Compliant: `raise {exc_short}` inside `if <var> is None:` is the canonical validation/precondition-check pattern (per result_migration_review_pass_20260617).",
)
# Heuristic added by result_migration_gui_2_20260619 (Phase 11):
# Bare `raise AttributeError(...)` or `raise NameError(...)` in a dunder
# method (__getattr__/__getattribute__/__setattr__/__delattr__) is the
# canonical Python dunder-method programmer-error pattern. Per the
# styleguide "Re-Raise Patterns" (error_handling.md lines 625-690), bare
# raises are reserved for programmer errors / impossible states /
# canonical dunder method behaviors. The Python data-model contract for
# these dunders explicitly raises AttributeError when an attribute does
# not exist or is not settable.
if exc_short in {"AttributeError", "NameError"} and self._current_func_name() in {"__getattr__", "__getattribute__", "__setattr__", "__delattr__"}:
return (
"INTERNAL_PROGRAMMER_RAISE",
f"Compliant: `raise {exc_short}` in `{self._current_func_name()}` is the canonical dunder-method programmer-error pattern (per styleguide 'Re-Raise Patterns' and Phase 11 result_migration_gui_2_20260619).",
)
return (
"INTERNAL_RETHROW",
f"Review: `raise {exc_name}` in internal code. Confirm this is a programmer error (assertion) and not a runtime failure (which should be a Result).",
@@ -0,0 +1,6 @@
with open('src/app_controller.py', 'rb') as f:
data = f.read()
needle = b' at_data = mma_sec.get'
idx = data.find(needle)
chunk = data[idx:idx+800]
print(repr(chunk.decode('utf-8', errors='replace')))
@@ -0,0 +1,13 @@
import sys
sys.path.insert(0, 'scripts')
from audit_exception_handling import audit_file
from pathlib import Path
r = audit_file(Path('src/app_controller.py'))
silent = [f for f in r.findings if f.category == 'INTERNAL_SILENT_SWALLOW']
broad = [f for f in r.findings if f.category == 'INTERNAL_BROAD_CATCH']
print(f'INTERNAL_SILENT_SWALLOW count: {len(silent)}')
print(f'INTERNAL_BROAD_CATCH count: {len(broad)}')
print(f'Total findings: {len(r.findings)}')
for s in silent:
print(f' L{s.line}: {s.snippet[:80].strip()}')
@@ -0,0 +1,11 @@
import sys, json, subprocess
result = subprocess.run(['uv', 'run', 'python', 'scripts/audit_exception_handling.py', '--json'],
capture_output=True, text=True)
data = json.loads(result.stdout)
for f in data['files']:
fn = f.get('filename', '')
if fn.endswith('api_hooks.py') or fn.endswith('app_controller.py'):
bfapi = [x for x in f.get('findings', []) if x.get('category') == 'BOUNDARY_FASTAPI']
print(fn + ': ' + str(len(bfapi)) + ' BOUNDARY_FASTAPI sites')
for x in bfapi[:5]:
print(' L' + str(x['line']) + ': ' + x['snippet'][:60])
@@ -0,0 +1,9 @@
import sys
sys.path.insert(0, 'scripts')
from audit_exception_handling import audit_file
from pathlib import Path
r = audit_file(Path('src/app_controller.py'))
for f in r.findings:
if f.line in (242, 256, 5064, 5093):
print(f'L{f.line}: category={f.category}')
print(f' snippet: {f.snippet[:120].strip()}')
@@ -0,0 +1,7 @@
with open('tests/test_audit_heuristics.py', 'r', encoding='utf-8') as f:
src = f.read()
lines = src.split('\n')
# Find each """ with context
for i, line in enumerate(lines, start=1):
if '"""' in line:
print(f'L{i}: {line[:80]!r}')
@@ -0,0 +1,23 @@
import sys
sys.path.insert(0, '.')
from src.app_controller import AppController
from src.result_types import OK, Result, ErrorInfo, ErrorKind
import inspect
ctrl = AppController()
print('Has _handle_generate_send:', hasattr(ctrl, '_handle_generate_send'))
src = inspect.getsource(ctrl._handle_generate_send)
print('Has Result[None] annotation:', 'Result[None]' in src)
print('Has return OK:', 'return OK' in src)
print('Has event_queue.put:', 'event_queue.put' in src)
print('Has ai_status sending:', "ai_status = \"sending...\"" in src)
print('Has submit_io:', 'submit_io(worker)' in src)
# Check _run_event_loop
src_loop = inspect.getsource(ctrl._run_event_loop)
print('_run_event_loop has _process_event_queue:', '_process_event_queue()' in src_loop)
print('_run_event_loop position of _process_event_queue:')
for i, line in enumerate(src_loop.split('\n')):
if '_process_event_queue' in line:
print(f' Line {i}: {line!r}')
@@ -0,0 +1,20 @@
import json
import sys
import json
audit_path = sys.argv[1] if len(sys.argv) > 1 else 'C:/tmp/audit_after_l7208.json'
with open(audit_path, 'rb') as f:
raw = f.read()
if raw.startswith(b'\xff\xfe'):
raw = raw.decode('utf-16-le')[1:]
else:
raw = raw.decode('utf-8')
data = json.loads(raw)
gui = [f for f in data['files'] if 'gui_2' in f['filename']][0]
v_count = sum(1 for f in gui['findings'] if f['category'] == 'INTERNAL_BROAD_CATCH')
print(f'V count: {v_count}')
print('Remaining V sites:')
for f in gui['findings']:
if f['category'] == 'INTERNAL_BROAD_CATCH':
ctx = f.get('context', '')[:80]
line = f['line']
print(f' L{line}: [{f["category"]}] {ctx}')
@@ -0,0 +1,15 @@
import json
with open('tests/artifacts/PHASE1_AUDIT.json', 'r', encoding='utf-8') as f:
data = json.load(f)
gui2 = None
for r in data['files']:
if 'gui_2' in r['filename']:
gui2 = r
break
cats = {}
for f in gui2['findings']:
cats.setdefault(f['category'], []).append((f['line'], f['context'], f['kind']))
for cat in sorted(cats):
print(f'\n{cat} ({len(cats[cat])}):')
for line, ctx, kind in sorted(cats[cat]):
print(f' L{line:>4} {kind:<10} {ctx}')
@@ -0,0 +1,16 @@
import json
with open('C:/tmp/audit_pre.json', encoding='utf-16-le') as f:
raw = f.read()
# Strip BOM if present
if raw.startswith('\ufeff'):
raw = raw[1:]
data = json.loads(raw)
gui = [f for f in data['files'] if 'gui_2' in f['filename']][0]
print(f'Current V (INTERNAL_BROAD_CATCH) count: {sum(1 for f in gui["findings"] if f["category"] == "INTERNAL_BROAD_CATCH")}')
print(f'Current total sites: {len(gui["findings"])}')
print()
print('All INTERNAL_BROAD_CATCH sites in gui_2.py:')
for f in gui['findings']:
if f['category'] == 'INTERNAL_BROAD_CATCH':
ctx = f.get('context', '')[:120]
print(f' L{f["line"]}: [{f["category"]}] {ctx}')
@@ -0,0 +1,11 @@
import json, subprocess
r = subprocess.run(['uv', 'run', 'python', 'scripts/audit_exception_handling.py', '--src', 'src', '--json'], capture_output=True, text=True)
data = json.loads(r.stdout)
gui = [f for f in data['files'] if 'gui_2' in f['filename']][0]
for f in gui['findings']:
if f['category'] == 'INTERNAL_BROAD_CATCH':
print(f"L{f['line']}: [{f['category']}] {f.get('context', '')}")
print()
for f in gui['findings']:
if f['category'] == 'INTERNAL_SILENT_SWALLOW':
print(f"L{f['line']}: [{f['category']}] {f.get('context', '')}")