Bug: Phase 11 sites 5+6 migration extracted _set_tool_preset_result and
_set_bias_profile_result helpers. The _set_tool_preset_result helper
modifies _active_tool_preset, _tool_approval_modes, _agent_tools without
declaring them as global, which causes the assignments to create LOCAL
variables instead of modifying the module-level globals.
This regression broke tests/test_bias_integration.py::test_set_tool_preset_with_objects:
preset = ToolPreset(name='ObjTest', categories={'General': [Tool(name='read_file', approval='auto')]})
with patch('src.tool_presets.ToolPresetManager.load_all', return_value={'ObjTest': preset}):
ai_client.set_tool_preset('ObjTest')
assert ai_client._agent_tools['read_file'] is True
# Fails: KeyError 'read_file' (the helper created a local _agent_tools,
# not modifying the module global; set_tool_preset legacy then ran
# cache-invalidation but never assigned _agent_tools to the test's view)
Fix: Add 'global _active_tool_preset, _tool_approval_modes, _agent_tools'
declaration to _set_tool_preset_result. The original set_tool_preset had
this declaration at the top; the helper extraction lost it.
Audit: no audit change (the helper still classifies as BOUNDARY_CONVERSION
via Heuristic A 'returns Result' pattern).
Site 5 (BC at L290): _async_search_mcp (nested in _search_mcp) had:
try:
data = json.loads(res_str)
if isinstance(data, list): return data
elif isinstance(data, dict) and 'results' in data: return data['results']
return []
except:
return []
Body: bare 'except:' + return [] = empty default = SS-style violation.
Migrated to Result[T] via new module-level helper _parse_search_response_result:
- Returns Result(data=parsed_list) on success
- Returns Result(data=None, errors=[ErrorInfo]) on JSON parse failure
- Handles the list/dict/no-results branch logic
The helper is module-level (does not use self) and is placed BEFORE
class RAGEngine to avoid breaking the class definition (a def at column 0
inside a class ends the class prematurely).
Legacy _async_search_mcp delegates to the helper; on Result errors,
returns [] (preserving the original behavior).
Audit: rag_engine BC 1 -> 0; migration-target: 0.
Remaining 4 INTERNAL_RETHROW sites are Pattern 1/3 of the styleguide
(known audit limitation).
index_file had 3 try/except sites with similar patterns:
Site 3 (BC at L247): try: mtime = os.path.getmtime(full_path); except Exception: return
Site 4 (BC at L261): try: with open(full_path, ...) as f: content = f.read(); except Exception: return
Site 6 (SS at L255): try: res = self.collection.get(...); ...; except Exception: pass
Body: broad catch + early return/pass = SS-style violation.
New helpers:
- _get_file_mtime_result(full_path) -> Result[float]
Catches OSError only (specific to file stat failures).
- _check_existing_index_result(file_path, mtime) -> Result[bool]
Catches broad Exception (chromadb collection.get failures vary).
Returns data=True if already indexed (skip), data=False if needs re-indexing.
- _read_file_content_result(full_path) -> Result[str]
Catches (OSError, UnicodeDecodeError) (file I/O + encoding failures).
Legacy index_file calls each helper; on Result errors, returns early
(preserving the original behavior of skipping the file on failure).
Audit: rag_engine BC 3 -> 1 (L341 _async_search_mcp remaining).
SS: 1 -> 0.
Site 2 (BC at L224): _chunk_code had a fallback to text chunking on any
failure:
try:
parser = ASTParser('python')
tree = parser.parse(content)
...
return chunks
except Exception:
return self._chunk_text(content)
Body: broad catch + fallback to a different implementation = empty-default
fallback = SS-style violation.
New helper _chunk_code_result(content, file_path) -> Result[List[str]]:
- Returns Result(data=chunks) on AST parse success
- Returns Result(data=None, errors=[ErrorInfo]) on parse failure
Legacy _chunk_code calls helper; on Result errors, falls back to
_chunk_text (preserving original behavior). The catch logic is in the
legacy, not the helper, so the caller decides the fallback strategy.
Audit: rag_engine BC 4 -> 3.
Site 1 (BC at L33) was:
except Exception as e:
sys.stderr.write(f'FAILED to import sentence_transformers: {e}')
sys.stderr.flush()
raise e
Per TIER1_REVIEW: catch + log + re-raise is Pattern 2 of the styleguide.
The fix is to narrow the except to specific exception types that
sentence_transformers could raise on import (ImportError, AttributeError).
Refactored to:
except (ImportError, AttributeError) as e:
sys.stderr.write(f'FAILED to import sentence_transformers: {e}')
sys.stderr.flush()
raise
The bare 'raise' re-raises the current exception being handled,
preserving the original type and traceback. (Replaces 'raise e' which
raised a specific value but lost the traceback context.)
Audit: rag_engine BC 5 -> 4. RETHROW +1 (the narrowed except is now
classified as Pattern 3 catch+re-raise; strict mode accepts).
Phase 13: rag_engine migration (9 sites: 1 SS + 5 BC + 3 RETHROW).
rag_engine.py is the smallest baseline file. Single phase since 9 sites
fit comfortably.
Migration rules (per TIER1_REVIEW Phase 9 redo):
- SS sites (1): MIGRATE to Result[T] (no logging, no pass, no empty default)
- BC sites (5): narrow to specific types; if body returns structured error
carrier use Heuristic E match; otherwise migrate to Result[T]
- RETHROW sites (3): classify per Pattern 1/2/3; if Pattern 1 fits add
'from e'; if suspicious catch+bare-raise migrate to Result[T]
rag_engine is a RAG subsystem (vector store). Most sites are likely at
the SDK boundary (chromadb, embedding providers). Pattern matches
should be straightforward.
Phase 12: ai_client rethrow classification (6 sites).
Site 1 (L276 _load_credentials): added 'from e' (Pattern 1)
Sites 2+3 (L878+L879 _default_send nested): added 'from None' (Pattern 1)
Site 4 (L1336 _list_anthropic_models): migrated to Result (the broken
'raise ErrorInfo from exc' runtime bug — same pattern as Phase 10 site 1)
Site 5 (L2078 _send inside _send_gemini_cli): added 'from None' (Pattern 1)
Site 6 (L2759 _dashscope_call): added 'from None' (Pattern 1)
KNOWN LIMITATION: the audit script does not have a heuristic for
'raise X from e' or 'from None' (Pattern 1 compliant). The 5 Pattern 1
sites remain classified as INTERNAL_RETHROW ('suspicious but not
violation') in the audit. Strict mode (Phase 14 gate) accepts this.
Adding a Pattern 1 heuristic requires Tier 1 approval per the
conventions ('Never modify audit heuristics without explicit Tier 1
approval'). Documented in the end-of-track report.
Audit state (after Phase 12):
mcp_client: 0 migration-target (Phase 3-8 complete)
ai_client: 7 -> 6 migration-target (5 RETHROW + 0 SS + 0 BC + 0 UNCLEAR)
BC: 0 (Phase 10)
SS: 0 (Phase 11)
RETHROW: 7 -> 6 (one site migrated to Result in Phase 12)
UNCLEAR: 0
COMPLIANT: 33 -> 34 (+1)
rag_engine: 9 migration-target (Phase 13)
Tests: 109 pass (was 97; +12 Phase 12 site/invariant tests).
Site 4 (L1337) had:
try: anthropic = _require_warmed('anthropic'); ... client.models.list() ...
except Exception as exc:
raise _classify_anthropic_error(exc) from exc
BUG: _classify_anthropic_error returns ErrorInfo (a dataclass), NOT
an Exception. 'raise ErrorInfo from exc' would fail at runtime.
Migration per Phase 9 redo precedent: convert to Result[T]. This is
the same fix pattern applied to _list_gemini_models in Phase 10.
New helper _list_anthropic_models_result() -> Result[list[str]]:
- Returns Result(data=sorted_models) on success
- Returns Result(data=[], errors=[_classify_anthropic_error(...)])
on SDK/credentials failure
Legacy _list_anthropic_models returns result.data (preserves signature).
Audit: ai_client RETHROW 5 -> 5 (no change; site 4 was previously
counted as INTERNAL_RETHROW, now classified as INTERNAL_COMPLIANT
since the try/except is gone — the helper has the Result-returning
exception body which matches Heuristic A).
Actually let me verify with audit_summary...
Per styleguide §7.6 Pattern 1: 'catch + convert + raise as different type'
requires 'raise X from e' to preserve the original exception in the
traceback.
Sites updated:
Site 1 (L277 _load_credentials):
except FileNotFoundError as e:
raise FileNotFoundError(f'...') from e
Sites 2+3 (L878+L879 _default_send, nested in run_with_tool_loop):
if not res.ok:
raise res.errors[0].original from None
raise RuntimeError(...) from None
The exceptions come from a Result, not a local except; 'from None'
suppresses the implicit context.
Site 5 (L2061 _send inside _send_gemini_cli):
raise cast(Exception, send_result.errors[0].original) from None
Site 6 (L2742 _dashscope_call):
raise classify_dashscope_error(_dashscope_exception_from_response(resp)) from None
KNOWN LIMITATION: the audit script does not have a heuristic for
'raise X from e' / 'from None' (Pattern 1). The sites remain
INTERNAL_RETHROW in the audit. INTERNAL_RETHROW is 'suspicious but
not violation' (strict mode accepts). Adding a heuristic requires
Tier 1 approval per the conventions.
Audit: ai_client RETHROW 6 -> 5 (site 4 migrated separately; these
4 sites stay as INTERNAL_RETHROW by audit classification but follow
Pattern 1 by styleguide).
Phase 12: ai_client rethrow classification (6 sites).
3 legitimate re-raise patterns from styleguide:
1. Catch + convert + raise as different type (with rom e):
try: json.loads(raw)
except json.JSONDecodeError as e: raise ValueError(f'Invalid JSON: {e}') from e
2. Catch + log + re-raise:
try: do_something()
except Exception as e: logger.exception('failed; will propagate'); raise
3. Catch + cleanup + re-raise (or use try/finally for pure cleanup).
SUSPICIOUS pattern (NOT compliant):
try: do_something()
except Exception: raise
This catches an exception, does nothing with it, and re-raises. The
try/except is dead code; remove it or use Result-based propagation.
Per MUST-DOT-DO #4: 'raise a custom exception class for runtime failures' is forbidden.
Migration rules per Phase 12 plan:
- If site fits Pattern 1/2/3: leave as-is (audit should classify as COMPLIANT)
- If site is SUSPICIOUS (catch + bare raise): MIGRATE to Result[T]
- Do NOT classify as 'suspicious' (= sliming)
- Per-site: test (if migrated), commit
Both classify functions had:
try:
sdk = _require_warmed('xxx')
if isinstance(exc, sdk.SomeException): return ErrorInfo(...)
...
except (ImportError, AttributeError):
pass
# body-string matching fallback
...
Body: bare 'except: pass' = SS violation (silent recovery).
Migration per TIER1_REVIEW directive (per-site decision):
- Initial attempt: _try_warm_sdk(name) -> Any sentinel (None on failure)
- Audit flagged the sentinel helper as UNCLEAR (Heuristic B requires class
method with self.attr assignment; module-level sentinel doesn't match)
- Per Phase 9 redo precedent: migrate to Result instead of adding heuristic
Final approach: _try_warm_sdk_result(name) -> Result[Any]
Returns Result(data=module) on success,
Result(data=None, errors=[ErrorInfo]) on ImportError/AttributeError.
Classify callers check result.ok and use result.data on success.
Audit: ai_client SS 2 -> 0; UNCLEAR 1 -> 0 (after Result migration).
COMPLIANT 32 -> 33.
Site 11 at module level had:
if os.environ.get('SLOP_TOOL_PRESET'):
try:
set_tool_preset(os.environ['SLOP_TOOL_PRESET'])
except Exception:
pass
Body: bare 'except Exception: pass' = SS violation.
Migration: call the _set_tool_preset_result helper from Phase 11 site 5.
The helper returns Result[None]; on error it captures the structured
ErrorInfo. The top-level loader ignores the Result (env-var preset is
optional, errors are not fatal at module load time).
Audit: ai_client SS 3 -> 2.
Both sites 9 (gemini) and 10 (gemini_cli) in get_token_stats had:
try: _ensure_gemini_client()
if _gemini_client:
resp = _gemini_client.models.count_tokens(model=_model, contents=md_content)
total_tokens = cast(int, resp.total_tokens)
except Exception: pass
Body: pass = SS violation.
New helper _count_gemini_tokens_for_stats_result(md_content) -> Result[int]:
- Returns Result(data=token_count) on success
- Returns Result(data=0, errors=[ErrorInfo]) on SDK failure or warmup failure
- Caller treats 0 as 'token count unavailable' and falls back to
character-based estimation
Legacy get_token_stats now uses:
if p in ('gemini', 'gemini_cli'):
total_tokens = _count_gemini_tokens_for_stats_result(md_content).data
(combined both branches into one since the logic was identical)
Audit: ai_client SS 5 -> 3. COMPLIANT 31 -> 32.
Both functions had:
try: ToolPresetManager().load_all() ...
except (OSError, ValueError, AttributeError) as e:
sys.stderr.write(f'[ERROR] Failed to set {preset_name}: {e}')
sys.stderr.flush()
sys.stderr.write is logging = NOT a drain = SS violation per MUST-NOT-DO #6.
New helpers:
- _set_tool_preset_result(preset_name: Optional[str]) -> Result[None]
Empty/None preset short-circuits to Result(data=None).
On failure: Result(data=None, errors=[ErrorInfo]).
- _set_bias_profile_result(profile_name: Optional[str]) -> Result[None]
Same pattern.
Legacy wrappers set the global state (or skip on empty preset) and
delegate to the _result helper. Cache invalidation runs regardless.
Audit: ai_client SS 9 -> 7. COMPLIANT 27 -> 29.
Sites L432 (cleanup) and L450 (reset_session) had:
try: _gemini_client.caches.delete(name=_gemini_cache.name)
except Exception: pass
This is bare 'except: pass' = INTERNAL_SILENT_SWALLOW violation (logging is NOT
a drain; 'pass' is the worst form of silent recovery).
Migration: use existing _delete_gemini_cache_result() helper (added Phase 10).
The helper returns Result[None]; on SDK error logs a warning to comms.
The caller ignores the Result (cleanup is best-effort).
Audit: ai_client SS 11 -> 9.
Phase 11: ai_client silent-swallow (11 sites; was 9, +2 from Phase 9 narrowing set_tool_preset/set_bias_profile).
CRITICAL ANTI-SLIMING RULES (MUST follow):
1. NO narrowing + logging: 'except (NarrowType): logging.error(...)' is a VIOLATION
2. NO empty defaults: 'except (NarrowType): args = {}' is a VIOLATION (sliming)
3. NO pass: 'except: pass' is a VIOLATION (silent)
4. NO traceback.print_exc alone: similar to logging, data is lost
5. logging.error / logger.exception / sys.stderr.write alone: NOT a drain
Per MUST-NOT-DO #6: 'DO NOT catch except Exception and silently swallow.'
Per MUST-NOT-DO #7: 'DO NOT catch except Exception in non-*_result code without conversion to ErrorInfo.'
Per TIER1_REVIEW 2026-06-20 (Phase 9 redo): 'empty default is NOT a drain — the caller must observe the errors.'
Canonical pattern for SS sites:
def _feature_result(...) -> Result[T]:
try:
return Result(data=compute())
except (NarrowType) as e:
return Result(data=<zero>, errors=[ErrorInfo(kind=INTERNAL, message=str(e), source=..., original=e)])
Legacy wrapper preserves original signature; surface errors via Result where possible.
Some sites may not have a clear 'caller' (e.g., _extract_gemini_thoughts is called inline); for these, the _result helper captures the structured error and the legacy function returns the empty data default (preserving current behavior).
All 3 run_tier4_* functions had the same pattern:
try: ... AI call ...
except Exception as e: return '[XXX FAILED] {e}' (or None)
Per TIER1_REVIEW: empty-default return = MIGRATE to Result[T].
New helpers:
- _run_tier4_analysis_result(stderr: str) -> Result[str]
Returns Result(data=analysis) on success, Result(data='', errors=[ErrorInfo])
on SDK failure. Empty stderr short-circuits to Result(data='').
- _run_tier4_patch_callback_result(stderr: str, base_dir: str) -> Result[Optional[str]]
Returns Result(data=patch) on valid diff, Result(data=None) when no
valid diff, Result(data=None, errors=[ErrorInfo]) on SDK failure.
- _run_tier4_patch_generation_result(error: str, file_context: str) -> Result[str]
Returns Result(data=patch) on success, Result(data='', errors=[ErrorInfo])
on SDK failure. Empty error short-circuits to Result(data='').
Legacy wrappers delegate to _result helpers and return result.data,
preserving original signatures (str for sites 7,9; Optional[str] for site 8).
Existing tier4 tests pass (13/13 in test_tier4_patch_generation +
test_tier4_interceptor).
Audit: ai_client BC 3 -> 0. All 9 Phase 10 BC sites migrated.