feat(scripts): Heuristic A - Result-returning recovery = INTERNAL_COMPLIANT

Phase 11.2. Adds the LEGITIMATE heuristic that recognizes the canonical
data-oriented pattern: \	ry: ...; except: return Result(data=...,
errors=[...])\ is the convention's canonical recovery pattern.

Detection:
- New _returns_result(stmts) helper on ExceptionVisitor
- New step 0 in _classify_except (BEFORE BOUNDARY_CONVERSION check)
- Classifies as INTERNAL_COMPLIANT with a hint that names the pattern

The function-name-not-ending-in-_result is documented as a smell
(rename to xxx_result for canonical naming), but the pattern itself
is compliant.

Tests:
- 2 new tests in test_audit_exception_handling_heuristics.py:
  - test_result_returning_recovery_in_non_result_named_function_is_compliant
  - test_result_returning_recovery_in_result_named_function_is_compliant
- Both pass; the 2 REJECTED tests (#22, #23) remain xfailed.

Per conductor/tracks/result_migration_small_files_20260617/plan.md
section 11.2.
This commit is contained in:
ed
2026-06-18 00:00:42 -04:00
parent 37872544d5
commit 3c839c910a
6 changed files with 262 additions and 0 deletions
+41
View File
@@ -373,6 +373,16 @@ class ExceptionVisitor(ast.NodeVisitor):
# ----- Classification logic -----
# 0. Heuristic A: Result-returning recovery — the canonical data-oriented pattern.
# If the except body returns `Result(data=..., errors=[ErrorInfo(...)])`,
# the function is following the convention. Classify as INTERNAL_COMPLIANT
# BEFORE the BOUNDARY_CONVERSION check (which also fires for ErrorInfo creation).
if self._returns_result(body):
return (
"INTERNAL_COMPLIANT",
"Compliant: `try: ...; except: return Result(data=..., errors=[...])` is the canonical Result-recovery pattern. The convention requires Result[T] for try/except sites that can fail; this pattern satisfies the requirement. The function-name-not-ending-in-`_result` is a smell (rename to `xxx_result`); the pattern itself is compliant. (per result_migration_small_files_20260617 Phase 11.2, Heuristic A)",
)
# 1. ErrorInfo conversion = canonical boundary pattern
if creates_errorinfo:
return (
@@ -591,6 +601,13 @@ class ExceptionVisitor(ast.NodeVisitor):
f"Compliant: `try: ...; except Exception: return <string>` in a `-> str` tool function is the canonical MCP tool boundary pattern (per result_migration_review_pass_20260617).",
)
# A. Result-returning recovery (canonical Result pattern) — Phase 11.2
if len(except_body) > 0 and self._returns_result(except_body):
return (
"INTERNAL_COMPLIANT",
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)",
)
return None
def _has_string_return(self, stmts: list[ast.stmt]) -> bool:
@@ -610,6 +627,30 @@ class ExceptionVisitor(ast.NodeVisitor):
return True
return False
def _returns_result(self, stmts: list[ast.stmt]) -> bool:
"""True if the body returns a `Result(...)` call (canonical Result-recovery pattern).
Detects `return Result(data=..., errors=[...])` — the canonical
data-oriented error handling pattern. Matches any call to `Result(...)`
with at least a `data=` keyword argument. The pattern is compliant
when used in a try/except: it satisfies the convention that every
try/except site that can fail must return `Result[T]` with structured
`ErrorInfo`. The function-name-not-ending-in-`_result` is a smell
(the function should be renamed to `xxx_result`), but the pattern
itself is compliant (heuristic A from Phase 11.2).
"""
for s in stmts:
if not isinstance(s, ast.Return) or s.value is None:
continue
if not isinstance(s.value, ast.Call):
continue
f = s.value.func
if isinstance(f, ast.Name) and f.id == "Result":
return True
if isinstance(f, ast.Attribute) and f.attr == "Result":
return True
return False
def _uses_exception_inline(self, stmts: list[ast.stmt]) -> bool:
"""True if the body uses `e`/`exc` in a non-pass way (Name reference)."""
for s in stmts: