ADR-0067 — IssueFetcherPort: GitHub Issue Fetching Boundary¶
Status: Proposed
Date: 2026-05-19
Enforced by: (none) — structural subtype check planned for tests/test_ports.py in follow-up
Context¶
Domain code (phases, background loops) needs to fetch GitHub issues. The concrete IssueFetcher class carries significant infrastructure: gh CLI subprocess calls, rate-limit back-off with jitter, two internal caches (PR cache, collaborator cache), and IncompleteIssueFetchError retry logic. Domain code does not need any of that; it needs two operations — single-issue lookup and label-scoped batch fetch.
Without a formal port, phases imported IssueFetcher directly, making them depend on the full infrastructure stack and harder to test (every test that touches a phase had to mock the entire IssueFetcher surface).
Decision¶
Define IssueFetcherPort as a @runtime_checkable Protocol in src/ports.py with exactly two methods:
fetch_issue_by_number(issue_number)— returnsGitHubIssue | Nonefetch_issues_by_labels(labels, limit, exclude_labels, require_complete)— returnslist[GitHubIssue]
IssueFetcher satisfies this port via structural subtyping. Tests pass AsyncMock(spec=IssueFetcherPort). The concrete class retains all infrastructure methods (PR cache, collaborator cache, etc.) that stay off the port.
Consequences¶
- Phases and loops depend only on the two-method surface they actually use.
- Tests no longer need to mock the full
IssueFetchersurface. - Infrastructure methods on the concrete class can evolve without touching the port.
Alternatives considered¶
- Pass
IssueFetcherdirectly. Simpler but couples domain code to the infrastructure layer and makes tests heavier. - One method per call site. More granular ports make injection boilerplate grow; two methods cover all domain call sites.
Related¶
src/ports.py:IssueFetcherPort— the port definitionsrc/issue_fetcher.py:IssueFetcher— the concrete adapter- ADR-0044 — four-layer architecture