# p0147f: Ticket Provider Family Unification

- [Architecture] Per-provider `ITicketFieldMapper<TRaw>` (generic in the raw payload type), one implementation per platform — rejected a single polymorphic mapper because Jira's nested-fields/ADF, ADO's reference-name dictionary, GitLab's flat JSON, and GitHub's Octokit `Issue` diverge enough that a unifying type would have been a tagged-union god-class. Composition over polymorphism.
- [Architecture] No `TicketProviderBase` — explicit decision. Shared "authed HTTP + send/deserialise/throw" lives in `TicketProviderHttpClient` and is composed in (Jira + GitLab), not inherited. A base class would have recreated the god-class problem at the inheritance level instead of the class level.
- [Architecture] `ITicketStatusTransitioner` already exists in `AgentSmith.Contracts/Services` from earlier phases, so the YAML's conditional new interface (`ITicketTransitioner`) was not introduced. The per-provider transitioner classes already align; no rework needed in this phase.
- [Implementation] Mappers are stateless and constructed in `TicketProviderFactory.Create*()` alongside the provider instance — not registered as DI singletons. Reason: providers themselves are not DI singletons (the factory builds them per `TrackerConnection` config), so mapping the mappers through DI would just add indirection. The YAML's `AddTicketProviders()` extension is therefore not introduced; the mapper construction lives next to the provider construction it serves.
- [Implementation] Per-provider helpers extracted beyond the YAML's mapper-only ask, because every provider would otherwise still exceed the 120-line hard limit:
  - `GitHubIssueLister` (per-label fan-out + dedupe; Octokit's `RepositoryIssueRequest` ANDs labels).
  - `GitLabIssueLister` (same fan-out shape for GitLab v4 REST).
  - `AzureDevOpsConnectionCache` (TTL + transport-failure eviction; lifted verbatim from the inline static cache).
  - `AzureDevOpsWorkItemLister` (WIQL build/run/hydrate + transport-failure recovery).
  - `JiraIssueSearcher` (POST /search with optional project clause).
  - `JiraTransitioner` (GET /transitions -> match by name -> POST /transitions).
- [Implementation] `JiraAdfRenderer` is the outbound counterpart to the existing `JiraAdfParser`: it emits the doc -> paragraph -> text ADF envelope a Jira Cloud REST v3 endpoint requires for comment bodies. Anonymous-object output, no DTO — `System.Text.Json` serialises it straight.
- [Implementation] Shared `TicketImageAttachmentDownloader` replaces the identical `refs -> downloaded-bytes` foreach previously inlined in all four providers. Single static helper invoked with a `Func<AttachmentRef, CancellationToken, Task<byte[]?>>` delegate so each provider passes its own attachment loader's `DownloadAsync` method group.
- [TradeOff] No mapper unit tests for GitHub — Octokit's `Issue` type is sealed with internal setters, so fabricating instances for unit tests would require either reflection hacks or recording-handler integration tests. The provider's existing integration tests cover the mapping behaviour transitively. Mapper unit tests added for GitLab (5), ADO (3), and Jira (4): all use plain JSON or `Dictionary<string,object>` shapes.
- [TradeOff] `RecordingHandler` in `JiraTicketProviderListByLifecycleTests` now snapshots the request body inside `SendAsync` rather than reading it from `LastRequest.Content` post-call. Required because `TicketProviderHttpClient.SendForJsonAsync` wraps the request in `using`, which disposes the content. Cleaner test fixture; identical assertion strength.
- [Implementation] Final line counts (`<= 120` hard limit per coding principles): GitHub 219 -> 107, GitLab 278 -> 108, Azure DevOps 340 -> 111, Jira 394 -> 100. All four under the limit; Jira (the tightest constraint, was 3.3x over) now sits at 17% headroom.
