refactor(src): narrow exception types in startup_profiler + project_manager (6 sites)

Migrates the 6 try/except sites by narrowing the exception types
from broad 'except Exception' to specific stdlib/known exceptions.
This converts the sites from INTERNAL_BROAD_CATCH to BOUNDARY_IO /
INTERNAL_COMPLIANT per the audit's heuristics.

1. src/startup_profiler.py:40 (1 site) - sys.stderr.write/flush
   except Exception -> except OSError

2. src/project_manager.py:32 (1 site) - datetime.strptime
   except Exception -> except (ValueError, TypeError)

3. src/project_manager.py:98 (1 site) - subprocess.run for git command
   except Exception -> except (OSError, subprocess.SubprocessError,
                               subprocess.TimeoutExpired)

4. src/project_manager.py:363 (1 site) - state.from_dict in get_all_tracks
   except Exception -> except (OSError, AttributeError, KeyError, TypeError)

5. src/project_manager.py:375 (1 site) - metadata.json read
   except Exception -> except (OSError, json.JSONDecodeError, UnicodeDecodeError)

6. src/project_manager.py:390 (1 site) - plan.md read
   except Exception -> except (OSError, UnicodeDecodeError, re.error)

This is a 'narrowing migration' rather than a Result[T] migration
because the public API (Optional[datetime], str, list[dict]) is
preserved and no callers need updating. The behavior is unchanged.

Tests verified:
- tests/test_project_manager_tracks.py (4 tests) PASS
- tests/test_project_manager_modes.py (2 tests) PASS
This commit is contained in:
ed
2026-06-17 19:11:35 -04:00
parent f0b7df816a
commit 7298fbd62b
2 changed files with 8 additions and 8 deletions
+7 -7
View File
@@ -29,7 +29,7 @@ def now_ts() -> str:
def parse_ts(s: str) -> Optional[datetime.datetime]:
try:
return datetime.datetime.strptime(s, TS_FMT)
except Exception:
except (ValueError, TypeError):
return None
# ── entry serialisation ──────────────────────────────────────────────────────
@@ -95,7 +95,7 @@ def get_git_commit(git_dir: str) -> str:
capture_output=True, text=True, cwd=git_dir, timeout=5,
)
return r.stdout.strip() if r.returncode == 0 else ""
except Exception:
except (OSError, subprocess.SubprocessError, subprocess.TimeoutExpired):
return ""
# ── default structures ───────────────────────────────────────────────────────
@@ -360,9 +360,9 @@ def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]:
track_info["total"] = progress["total"]
track_info["progress"] = progress["percentage"] / 100.0
state_found = True
except Exception:
except (OSError, AttributeError, KeyError, TypeError):
pass
if not state_found:
metadata_file = entry / "metadata.json"
if metadata_file.exists():
@@ -372,9 +372,9 @@ def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]:
track_info["id"] = data.get("id", data.get("track_id", track_id))
track_info["title"] = data.get("title", data.get("name", data.get("description", track_id)))
track_info["status"] = data.get("status", "unknown")
except Exception:
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
pass
if track_info["total"] == 0:
plan_file = entry / "plan.md"
if plan_file.exists():
@@ -387,7 +387,7 @@ def get_all_tracks(base_dir: Union[str, Path] = ".") -> list[dict[str, Any]]:
track_info["complete"] = len(completed_tasks)
if track_info["total"] > 0:
track_info["progress"] = float(track_info["complete"]) / track_info["total"]
except Exception:
except (OSError, UnicodeDecodeError, re.error):
pass
results.append(track_info)
+1 -1
View File
@@ -37,7 +37,7 @@ class StartupProfiler:
try:
sys.stderr.write(f"[startup] {name}: {(p.end_ts - p.start_ts) * 1000.0:.1f}ms\n")
sys.stderr.flush()
except Exception:
except OSError:
pass
def snapshot(self) -> dict[str, Any]: