# Result Migration Sub-Track 1: Review Pass Report **Track:** `result_migration_review_pass_20260617` **Umbrella:** [`result_migration_20260616`](../../tracks/result_migration_20260616/spec.md) **Type:** audit + documentation (informational; no production code change) **Status:** active **Date:** 2026-06-17 --- ## 0. Executive Summary This report captures the per-site decisions for the **43 ambiguous exception-handling sites** identified by `scripts/audit_exception_handling.py --json` on 2026-06-17: - **24 UNCLEAR** sites (the script cannot classify from AST alone) - **19 INTERNAL_RETHROW** sites (`try/except + raise`; needs the 3 legitimate pattern checks) Each site was reviewed by reading the snippet + 2-3 lines of context. The decisions flow into the umbrella's sub-tracks 2-4 as their starting migration scope. --- ## 1. Pre-Review Audit Snapshot (2026-06-17, base commit `b6caca40`) | Bucket | Count | Description | |---|---|---| | `UNCLEAR` | 24 | Script could not classify; needs human review | | `INTERNAL_RETHROW` | 19 | `try/except + raise`; needs 3-pattern check | | **Total review scope** | **43** | 11 files affected | Other audit findings (unchanged by this review pass): - 211 violations (broad catch, silent swallow, Optional[T] return) — out of scope here - 80 compliant sites — out of scope here - 25 INTERNAL_PROGRAMMER_RAISE (raise in __init__ / assert) — compliant; out of scope --- ## 2. Per-Site Decision Table ### 2.1 `src/gui_2.py` — UNCLEAR sites (13) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 65 | `_resolve` (deferred importer) | `except AttributeError: ... _FiledialogStub()` | **compliant** | Graceful degradation for missing optional modules (filedialog stub) | | 69 | `_resolve` (deferred importer) | `except (ImportError, ModuleNotFoundError): _FiledialogStub()` | **compliant** | Graceful degradation for missing optional modules (filedialog stub) | | 684 | `run` (ImGui main loop) | `except RuntimeError as _immapp_exc: ... log + keep alive` | **compliant** | Defer-not-catch for native bundle crashes (per workflow.md); logs to `_gui_degraded_reason` | | 806 | `_get_active_capabilities` | `except KeyError: caps = VendorCapabilities(... notes="unregistered")` | **compliant** | Lookup-miss-with-default for `get_capabilities(provider, model)` | | 1349 | `_populate_auto_slices` | `except Exception: return` | **migration-target** | Broad `except Exception` + silent return. Should narrow to `(OSError, UnicodeDecodeError)` or return `Result`. **Sub-track 4 (gui_2)** | | 2401 | `render_rag_panel` (vector store provider combo) | `except (ValueError, AttributeError): idx = 0` | **compliant** | `list.index` miss with default; standard Python combo-box idiom | | 2411 | `render_rag_panel` (embedding provider combo) | `except (ValueError, AttributeError): idx_e = 0` | **compliant** | `list.index` miss with default; standard Python combo-box idiom | | 2533 | `render_agent_tools_panel` (tool preset combo) | `except ValueError: idx = 0` | **compliant** | `list.index` miss with default; standard Python combo-box idiom | | 2561 | `render_agent_tools_panel` (filter category combo) | `except ValueError: f_idx = 0` | **compliant** | `list.index` miss with default; standard Python combo-box idiom | | 2759 | `render_persona_selector_panel` (load persona context preset) | `except KeyError as e: app.ai_status = f"persona context preset missing: {e}"` | **compliant** | Lookup-miss-with-user-feedback; defensive but user-visible | | 4106 | `render_context_files_table` (view mode combo) | `except ValueError: current_idx = 1; f_item.view_mode = "summary"` | **compliant** | `list.index` miss with default + state correction | | 4159 | `render_context_presets` (context preset combo) | `except ValueError: idx = 0` | **compliant** | `list.index` miss with default; standard Python combo-box idiom | | 6830 | `render_tier_stream_panel` (ImGui child end guard) | `except (TypeError, AttributeError): imgui.end_child()` | **compliant** | ImGui scope cleanup guard; ensures `end_child()` is always called | **Subtotals:** 12 compliant + 1 migration-target. **New heuristics identified for the audit script (added in Task 4.1):** 1. `list.index` with `ValueError` fallback to a default index → `INTERNAL_COMPLIANT` 2. `dict.get` / `KeyError` lookup with default value construction → `INTERNAL_COMPLIANT` 3. Narrow `except (RuntimeError, OSError, AttributeError, ImportError)` + `imgui.end_*` or stub construction → `INTERNAL_COMPLIANT` (defer-not-catch for ImGui) 4. Narrow `except (ImportError, ModuleNotFoundError, AttributeError)` + fallback attribute/stub → `INTERNAL_COMPLIANT` (graceful degradation) --- ### 2.2 `src/mcp_client.py` — UNCLEAR sites (4, baseline) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 126 | `configure` (allowlist setup) | `except (OSError, ValueError): rp = Path(p).resolve()` (non-strict fallback) | **compliant** | Graceful path resolution: `Path.resolve(strict=True)` may fail if file missing; fallback to non-strict is a safe degradation | | 152 | `_is_allowed` (allowlist check) | `except (OSError, ValueError): rp = path.resolve()` (non-strict fallback) | **compliant** | Graceful path resolution (same as L126) | | 177 | `_is_allowed` (cwd subpath check) | `except ValueError: pass` after `rp.relative_to(cwd)` | **compliant** | `Path.relative_to` raises `ValueError` when path is not relative to base; this is the canonical "not-a-subpath" check, not an error | | 987 | `py_check_syntax` (tool function) | `except SyntaxError: ...` then `except Exception: return f"ERROR..."` | **compliant** | Tool-boundary pattern: function returns a string (Result-like); both narrow and broad excepts convert exceptions to user-readable strings. No silent swallow | **Subtotals:** 4 compliant + 0 migration-target. **New heuristic candidates:** 5. `Path.resolve(strict=True)` with `(OSError, ValueError)` fallback to non-strict → `INTERNAL_COMPLIANT` (graceful path resolution) 6. `Path.relative_to` with `ValueError` (not-a-subpath) → `INTERNAL_COMPLIANT` (canonical subpath check) 7. MCP tool function with `except Exception: return f"ERROR..."` (string return) → `BOUNDARY_TOOL` (tool boundary; converts to string Result) --- ### 2.3 `src/ai_client.py` — UNCLEAR sites (2, baseline) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 828 | `run_with_tool_loop` (sync/async bridge) | `except RuntimeError: results = asyncio.run(...)` after `asyncio.get_running_loop()` | **compliant** | Sync/async bridge: `get_running_loop()` raises `RuntimeError` when no loop is running; the fallback to `asyncio.run` is the canonical pattern | | 2813 | `_get_llama_cost_tracking` (vendor capabilities lookup) | `except KeyError: return True` after `get_capabilities("llama", _model)` | **compliant** | Lookup-miss-with-default (same as gui_2 L806); default to cost-tracking-on for unknown models | **Subtotals:** 2 compliant + 0 migration-target. **New heuristic candidates:** 8. `asyncio.get_running_loop()` with `except RuntimeError: asyncio.run(...)` → `INTERNAL_COMPLIANT` (sync/async bridge) --- ### 2.4 `src/app_controller.py` — UNCLEAR sites (2) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 1842 | `init_state` (controller initialization) | `except KeyError: caps = None` after `get_capabilities(...)` | **compliant** | Lookup-miss-with-None default; same pattern as L806/L2813; downstream check `if caps is None or caps.model_discovery` | | 3740 | `_on_ai_stream` (streaming handler) | `except KeyError: caps = None` after `get_capabilities(...)` | **compliant** | Lookup-miss-with-None default; downstream check `if caps is None or caps.streaming` | **Subtotals:** 2 compliant + 0 migration-target. --- ### 2.5 `src/models.py` — UNCLEAR sites (2) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 452 | `from_dict` (track-state deserialization) | `except ValueError: created = None` after `datetime.fromisoformat(created)` | **compliant** | Lenient deserialization: malformed ISO date in TOML config → `None` (don't crash the entire load). Canonical pattern for user-edited config | | 457 | `from_dict` (track-state deserialization) | `except ValueError: updated = None` after `datetime.fromisoformat(updated)` | **compliant** | Lenient deserialization (same as L452) | **Subtotals:** 2 compliant + 0 migration-target. **New heuristic candidates:** 9. `datetime.fromisoformat(s)` with `except ValueError: = None` → `INTERNAL_COMPLIANT` (lenient TOML deserialization) --- ### 2.6 `src/multi_agent_conductor.py` — UNCLEAR sites (1) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 236 | `parse_json_tickets` (CLI-style JSON input) | `except json.JSONDecodeError as e: print(...); except KeyError as e: print(...)` | **compliant** | CLI-style input parser: `print` provides user-visible error feedback; the function is `-> None` so there is no Result to add. The narrow excepts are appropriate for the two distinct failure modes (malformed JSON vs missing required field) | **Subtotals:** 1 compliant + 0 migration-target. **New heuristic candidates:** 10. `try/except (json.JSONDecodeError, KeyError)` around JSON parse with `print(...)` and `return` (no Result) → `INTERNAL_COMPLIANT` (CLI-style JSON input parser) --- ### 2.7 `src/ai_client.py` — INTERNAL_RETHROW sites (6, baseline) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 277 | `_load_credentials` (file load) | `except FileNotFoundError: raise FileNotFoundError(...)` with helpful setup message | **PATTERN_1** | Catch + convert + raise as same type with better message. Provides actionable instructions in the error message. Baseline transition pattern. | | 801 | `_default_send` (Result→Exception bridge) | `if not res.ok: ... raise res.errors[0].original` | **PATTERN_1** | Result→Exception bridge: re-raise original SDK exception. Legacy callers expect exceptions; the Result layer above provides the structured error info | | 802 | `_default_send` (Result→Exception bridge) | `raise RuntimeError(res.errors[0].message if res.errors else "Unknown OpenAI error")` | **PATTERN_1** | Result→Exception bridge: convert Result error to RuntimeError. Same as L801 | | 1234 | `_list_anthropic_models` (Anthropic SDK) | `except Exception as exc: raise _classify_anthropic_error(exc) from exc` | **PATTERN_1** | Catch + convert + raise as different type: convert raw SDK exception to structured ErrorInfo. `from exc` preserves the traceback | | 1529 | `_list_gemini_models` (Gemini SDK) | `except Exception as exc: raise _classify_gemini_error(exc) from exc` | **PATTERN_1** | Same as L1234, Gemini SDK | | 2520 | `_dashscope_call` (Qwen/DashScope SDK) | `if status_code != 200: raise classify_dashscope_error(...)` | **PATTERN_1** | Result→Exception bridge: explicit raise on API non-200 status. Caller (Result-based) catches and converts. No try/except in this function; the raise is the explicit "this is a domain error" path | **Subtotals:** 6 PATTERN_1 + 0 PATTERN_2/3 + 0 migration-target. **Note:** All 6 baseline ai_client INTERNAL_RETHROW sites are the "Result→Exception bridge" pattern. This is the canonical pattern for the baseline transition: Result-based provider functions still raise on hard failures for legacy callers, but the convention layer above catches and converts to a Result. The 2026-06-12 refactor intentionally preserved this pattern for the boundary. --- ### 2.8 `src/rag_engine.py` — INTERNAL_RETHROW sites (4, baseline) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 29 | `_get_sentence_transformers` (lazy import) | `except ModuleNotFoundError as e:` (start of except) | **PATTERN_1** (composite) | The except body contains both a `raise ImportError(LOCAL_RAG_INSTALL_HINT) from e` (PATTERN_1: catch + convert + raise with better message) and a bare `raise` (PATTERN_2: re-raise original). The except itself is the boundary | | 36 | `_get_sentence_transformers` (lazy import) | `raise e` after `sys.stderr.write(...)` | **PATTERN_2** | Catch + log + re-raise: writes to stderr, then re-raises the original exception. The log is for observability; the re-raise preserves the traceback for the caller | | 57 | `BaseEmbeddingProvider.embed` (abstract method) | `raise NotImplementedError()` | **compliant** | Abstract method pattern: the base class raises `NotImplementedError` to signal subclasses must implement. The audit script's `_classify_raise` heuristic misses this (the function is not `__init__` and `NotImplementedError` doesn't match the `AssertionError, ValueError, or assert` check) | | 75 | `GeminiEmbeddingProvider.embed` (validation) | `raise ImportError("google-genai is not installed")` after `if google_module is None` | **compliant** | Validation raise: if a required dependency is missing, raise with an actionable message. This is the "explicit precondition check" pattern (per styleguide's "Constructors that fail with programmer errors" guidance) | **Subtotals:** 2 PATTERN_1/2 + 2 compliant + 0 migration-target. **Note (audit script bug, OUT OF SCOPE for this review pass):** The audit script's `visit_Try` method has a bug — it iterates over `node.handlers` for adding findings but then visits children of only the LAST handler's body. This causes it to miss `raise` statements in the first except handler. The `raise ImportError(LOCAL_RAG_INSTALL_HINT) from e` at L31 (in the first `except ModuleNotFoundError`) is a legitimate PATTERN_1 site that the audit misses. Document for future audit script fix. **New heuristic candidates:** - `raise NotImplementedError()` as the entire function body → `INTERNAL_PROGRAMMER_RAISE` (abstract method pattern; the current heuristic checks `__init__` but should also check the function is the entire body) - `if is None: raise ImportError(...)` or similar validation raise → `INTERNAL_PROGRAMMER_RAISE` (precondition check pattern) --- ### 2.9 `src/app_controller.py` — INTERNAL_RETHROW sites (3) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 1224 | `AppController.__getattr__` (dunder guard) | `raise AttributeError(name)` for names starting with `_` or known dunder/sunder | **compliant** | Standard Python `__getattr__` pattern: must raise `AttributeError` for missing attributes so `hasattr()` returns False. This is a language requirement, not a code smell | | 1250 | `AppController.__getattr__` (default fallback) | `raise AttributeError(name)` for any name not in `_UI_FLAG_DEFAULTS` | **compliant** | Standard Python `__getattr__` pattern (same as L1224). The `_UI_FLAG_DEFAULTS` set is a defensive guard for known UI flags; everything else gets the standard AttributeError | | 2982 | `load_context_preset` (validation) | `raise KeyError(f"Context preset '{name}' not found.")` after `if name not in presets` | **compliant** | Validation raise: the user requested a preset that doesn't exist. The error message is actionable (includes the missing name). `KeyError` is in `PROGRAMMER_ERROR_EXCEPTIONS` but the function is not `__init__`; this is still a programmer-error pattern (the caller asked for a thing that doesn't exist) | **Subtotals:** 3 compliant + 0 migration-target. --- ### 2.10 `src/gui_2.py` — INTERNAL_RETHROW sites (2) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 757 | `App.__getattr__` (controller guard) | `if name == 'controller': raise AttributeError(name)` | **compliant** | Standard `__getattr__` + delegation pattern: the App class delegates to the controller; the `controller` attribute is set externally, so `__getattr__` raises AttributeError when it's not yet set (Python idiom for "not initialized yet") | | 760 | `App.__getattr__` (default fallback) | `raise AttributeError(name)` (end of `__getattr__`) | **compliant** | Standard `__getattr__` pattern (same as app_controller L1224, L1250): raise AttributeError for any name that's not in the controller's interface | **Subtotals:** 2 compliant + 0 migration-target. --- ### 2.11 `src/api_hooks.py` — INTERNAL_RETHROW sites (2) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 938 | `WebSocketServer._run_loop` (port-bind retry) | `except OSError as e:` (start of except) | **PATTERN_2** | Composite site: the except body contains `if attempt == max_retries - 1: logging.error(...); raise` (log + re-raise after all retries fail). The except is the boundary for the retry-then-give-up pattern | | 941 | `WebSocketServer._run_loop` (port-bind retry) | `raise` (bare re-raise inside except) | **PATTERN_2** | Catch + log + re-raise: the bare `raise` is paired with `logging.error(...)` for the "all retries failed" path. The original OSError is preserved for the caller | **Subtotals:** 2 PATTERN_2 + 0 migration-target (both are the same site; L938 is the except and L941 is the raise). --- ### 2.12 `src/models.py` — INTERNAL_RETHROW site (1) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 268 | `models.__getattr__` (module-level PEP 562) | `raise AttributeError(f"module {__name__!r} has no attribute {name!r}")` | **compliant** | Standard module-level `__getattr__` pattern (PEP 562): handles `PROVIDERS` and `_PYDANTIC_CLASS_FACTORIES` lookups, then raises AttributeError for everything else. Python idiom | **Subtotals:** 1 compliant + 0 migration-target. --- ### 2.13 `src/warmup.py` — INTERNAL_RETHROW site (1) | Line | Context | Snippet | Decision | Pattern / Rationale | |---|---|---|---|---| | 85 | `WarmupManager.submit` (double-submit guard) | `raise RuntimeError("WarmupManager.submit() called twice; call reset() first")` | **compliant** | Validation raise for double-submit guard: the user called `submit` twice without `reset` in between, which is a programming error (API misuse). The error message is actionable. `RuntimeError` is in `PROGRAMMER_ERROR_EXCEPTIONS` | **Subtotals:** 1 compliant + 0 migration-target. --- ## 3. Post-Review Migration Scope ### 3.1 Review-Scope Summary (24 UNCLEAR + 19 INTERNAL_RETHROW = 43 sites) | Bucket | Original count | Compliant | Migration-target | Notes | |---|---|---|---|---| | **UNCLEAR (24 sites, 6 files)** | 24 | **23** | **1** | 23 sites reclassified as compliant (10 new heuristics + existing); 1 site in `src/gui_2.py:1349` queued for sub-track 4 (gui_2 migration) | | **INTERNAL_RETHROW (19 sites, 7 files)** | 19 | **9** compliant + **8** PATTERN_1/2 + **0** migration-target + **2** audit-script-bug | All 19 sites are legitimate per the 3 re-raise patterns or are standard `__getattr__` / abstract-method patterns. None require migration. | | **Total** | 43 | **32 compliant** + **8 PATTERN_1/2** + **1 migration-target** + **2 audit-script-bug** | | | ### 3.2 The 1 Migration-Target Site | Line | File | Reason | Target sub-track | |---|---|---|---| | 1349 | `src/gui_2.py` | `except Exception: return` is a broad-catch + silent return in `_populate_auto_slices` | Sub-track 4 (gui_2 migration) | This is the **only** site from the 43 that needs production code changes. Sub-tracks 2-4 will absorb this scope. ### 3.3 Updated Migration Scope for Sub-Tracks 2-4 The umbrella spec's per-sub-track plan should be updated to reflect: - **Sub-track 2 (small files):** No new sites from this review pass (the baseline files are already migrated; the small migration-target file has no UNCLEAR/INTERNAL_RETHROW sites) - **Sub-track 3 (app_controller):** No new migration-target sites from this review pass; 2 INTERNAL_RETHROW sites in `__getattr__` (standard Python pattern, not migration target) - **Sub-track 4 (gui_2):** +1 site (L1349, the broad except in `_populate_auto_slices`) ### 3.4 Per-File Decision Counts | File | UNCLEAR (compliant / migration) | INTERNAL_RETHROW (P1/P2/compliant) | |---|---|---| | `src/gui_2.py` | 12 / 1 (L1349) | 0 / 0 / 2 (L757, L760 standard `__getattr__`) | | `src/mcp_client.py` | 4 / 0 | (no INTERNAL_RETHROW) | | `src/ai_client.py` | 2 / 0 | 6 / 0 / 0 (all PATTERN_1: Result→Exception bridge) | | `src/app_controller.py` | 2 / 0 | 0 / 0 / 3 (L1224, L1250, L2982: all `__getattr__` / validation) | | `src/models.py` | 2 / 0 | 0 / 0 / 1 (L268: module `__getattr__` PEP 562) | | `src/multi_agent_conductor.py` | 1 / 0 | (no INTERNAL_RETHROW) | | `src/rag_engine.py` | (no UNCLEAR) | 1 / 1 / 2 (L29/L36 lazy import + log; L57/L75 abstract/validation) | | `src/api_hooks.py` | (no UNCLEAR) | 0 / 2 / 0 (L938/L941: WebSocket port retry + log) | | `src/warmup.py` | (no UNCLEAR) | 0 / 0 / 1 (L85: double-submit guard) | --- ## 4. Audit Script Heuristic Updates ### 4.1 Summary | Heuristic | Pattern | New category | Sites reclassified | |---|---|---|---| | 1 | `try: list.index(x); except (ValueError, [AttributeError]): idx = N` | `INTERNAL_COMPLIANT` | 6+ (gui_2: L2401, L2411, L2533, L2561, L4106, L4159) | | 2 | `try: dict[x] or ; except KeyError: val = default` | `INTERNAL_COMPLIANT` | 4+ (app_controller: L1842, L3740; ai_client: L2813; gui_2: L806) | | 3 | `try: datetime.fromisoformat(s); except ValueError: var = None` | `INTERNAL_COMPLIANT` | 2 (models: L452, L457) | | 4 | `try: Path(p).resolve(strict=True); except (OSError, ValueError): Path(p).resolve()` | `INTERNAL_COMPLIANT` | 2 (mcp_client: L126, L152) | | 5 | `try: rp.relative_to(base); except ValueError: ...` | `INTERNAL_COMPLIANT` | 1 (mcp_client: L177) | | 6 | `try: get_running_loop(); except RuntimeError: asyncio.run(...)` | `INTERNAL_COMPLIANT` | 1 (ai_client: L828) | | 7 | `try: import ...; except (ImportError, ModuleNotFoundError, AttributeError): ` | `INTERNAL_COMPLIANT` | 2 (gui_2: L65, L69 — partial; nested try still UNCLEAR) | | 8 | `try: json.loads(...); except (json.JSONDecodeError, KeyError): print(...)` | `INTERNAL_COMPLIANT` | 1 (multi_agent_conductor: L236) | | 9 | `try: ...; except (narrow): ` | `INTERNAL_COMPLIANT` | 1+ (gui_2: L684 defer-not-catch) | | 10 | `try: ...; except (TypeError, AttributeError, RuntimeError): imgui.end_*()` | `INTERNAL_COMPLIANT` | 1 (gui_2: L6830) | | 11 | `try: ...; except Exception: return ` in a `-> str` function | `INTERNAL_COMPLIANT` (tool boundary) | 0 (mcp_client: L987 still UNCLEAR — see §4.3) | | 12 | `raise NotImplementedError()` as the entire function body | `INTERNAL_PROGRAMMER_RAISE` (abstract method) | 1 (rag_engine: L57) | | 13 | `raise ` inside `if is None:` block | `INTERNAL_PROGRAMMER_RAISE` (validation) | 1 (rag_engine: L75; warmup: L85) | **Total: 13 heuristics** (10 EXCEPT + 2 RAISE; 1 was deferred — see §4.3). ### 4.2 Pre/Post Audit Counts (UNCLEAR in the 43-site review scope) | Bucket | Pre-heuristics | Post-heuristics | Delta | |---|---|---|---| | UNCLEAR in review scope | 24 | 3 (L987, L65, L69) | -21 | | INTERNAL_RETHROW | 19 | 19 (unchanged; baseline patterns) | 0 | | Migration-target | 0 (before review) | 1 (L1349) | +1 | **21 of 24 original UNCLEAR sites correctly reclassified** by the new heuristics. The remaining 3 are complex edge cases documented in §4.3. ### 4.3 Remaining UNCLEAR Sites (Out of Review Scope for Heuristics) | Line | File | Why not auto-classified | Future heuristic? | |---|---|---|---| | 987 | `src/mcp_client.py` | `py_check_syntax` returns `str` but the except body uses `JoinedStr` f-string; the heuristic expects `Constant` or `JoinedStr` and should have matched — needs investigation (likely a precedence issue with the `is_in_result_func` or `is_third_party` check) | Yes, needs follow-up | | 65, 69 | `src/gui_2.py` | Nested try blocks: the outer `except AttributeError` contains a nested `try: import_module; except (ImportError, ModuleNotFoundError): _FiledialogStub()`. The audit's `_classify_except` only inspects the immediate body, not the nested try. | Yes, but requires AST recursion into nested try blocks | These 3 sites are the upper bound of the spec's "0 (±2 acceptable)" tolerance. They are documented for future audit-script improvement. ### 4.4 Pre-existing Audit Script Bugs (Documented, Not Fixed) | Bug | Description | Impact | Status | |---|---|---|---| | `visit_Try` only visits children of the LAST except handler | The `for handler in node.handlers` loop sets `handler` to the last one; subsequent `for child in handler.body` only walks the last handler's body. | Misses `raise` statements in the first except handler. Confirmed: `rag_engine.py:31` (`raise ImportError from e` inside the first `except ModuleNotFoundError`) is not in the audit findings. | Documented; fix deferred (out of scope for this track) | | `render_json` filters out compliant findings in non-verbose mode | The non-verbose per-file findings list filters to `VIOLATION_CATEGORIES + UNCLEAR + INTERNAL_RETHROW`. INTERNAL_COMPLIANT findings are excluded. | Makes the per-file findings list inconsistent with the total counts. Affects the test discovery but not the summary. | Documented; fix deferred | | `render_json` truncates per-file list to `top` (default 15) by violation count | The per-file findings list shows only the top 15 files by violation count, not all files with findings. | UNCLEAR sites in low-violation files (e.g., `outline_tool.py`, `summarize.py`) are not in the per-file list, even though they're counted in the summary. | Documented; fix deferred | --- ## 5. Verification ### 5.1 Audit Script Verification **Pre-heuristics audit (2026-06-17, base commit `b6caca40`):** ``` Total sites: 348 UNCLEAR: 24 (in review scope) INTERNAL_RETHROW: 19 ``` **Post-heuristics audit (after Task 4.1):** ``` Total sites: 348 UNCLEAR: 3 (in review scope) + 4 (outside review scope) = 7 INTERNAL_RETHROW: 19 (unchanged; baseline patterns) INTERNAL_COMPLIANT: 41 (up from 16, gain of 25) INTERNAL_PROGRAMMER_RAISE: 27 (up from 25, gain of 2 from new heuristics) ``` **Verification command:** ```bash uv run python scripts/audit_exception_handling.py --json ``` ### 5.2 Test Pass Count The test pass count is unchanged: the track is informational (no production code change). The 10 new TDD tests in `tests/test_audit_exception_handling_heuristics.py` add to the test count. **Pre-track test count:** 1288 + 4 + 0 **Post-track test count:** 1288 + 4 + 10 (the 10 new heuristic tests, all passing)