| @@ -0,0 +1,268 @@ |
| 1 | +# Version Control Phase 2: Source Control Provider Foundation |
| 2 | + |
| 3 | +## Goal |
| 4 | + |
| 5 | +Introduce a pluggable source-control provider layer and rewrite GitHub support as an Effect-native provider. This phase should preserve the existing GitHub Commit + PR flow while making GitLab and Azure DevOps additive drivers rather than branches inside GitHub-oriented code. |
| 6 | + |
| 7 | +The existing `GitHubCli` service and GitHub-specific `GitManager` paths are behavior references. The new provider layer should use detailed tagged errors, schema decode boundaries, `effect/unstable/process`, capability flags, and provider-neutral change-request types. |
| 8 | + |
| 9 | +## Scope |
| 10 | + |
| 11 | +- Add provider-domain contracts in `packages/contracts/src/sourceControl.ts`. |
| 12 | +- Add provider URL/reference parsing helpers in `packages/shared/src/sourceControl/*`. |
| 13 | +- Add server services under `apps/server/src/sourceControl`: |
| 14 | + - `Services/SourceControlProvider.ts` |
| 15 | + - `Services/SourceControlProviderRegistry.ts` |
| 16 | + - `Services/SourceControlProcess.ts` |
| 17 | + - `Layers/GitHubSourceControlProvider.ts` |
| 18 | + - `errors.ts` |
| 19 | +- Migrate PR creation, PR lookup, default-branch lookup, clone URL lookup, and PR checkout through the provider layer. |
| 20 | +- Update active consumers to the provider APIs directly; do not add backwards-compatible `GitHubCli` export shims. |
| 21 | +- Keep GitHub as the only production provider at the end of this phase, but make GitLab and Azure implementation paths obvious and bounded. |
| 22 | + |
| 23 | +## Non-Goals |
| 24 | + |
| 25 | +- No GitLab implementation in this phase, except fixtures/contracts that prove the abstraction can represent merge requests. |
| 26 | +- No Azure DevOps implementation in this phase, except URL/reference parser test cases if cheap. |
| 27 | +- No in-app review UI yet. |
| 28 | +- No hard dependency on one CLI forever. The first GitHub driver may use `gh`, but the interface should support REST/GraphQL implementations later. |
| 29 | + |
| 30 | +## Provider Model |
| 31 | + |
| 32 | +Use provider-neutral names: |
| 33 | + |
| 34 | +- `SourceControlProvider`: hosted repository and change-request mechanics. |
| 35 | +- `ChangeRequest`: GitHub pull request, GitLab merge request, Azure pull request. |
| 36 | +- `ChangeRequestThread`: review or discussion thread. |
| 37 | +- `ChangeRequestComment`: top-level or inline comment. |
| 38 | +- `ProviderRepository`: owner/project/repo identity plus clone URLs. |
| 39 | + |
| 40 | +Core provider operations: |
| 41 | + |
| 42 | +- `detectRemote(remoteUrl)` |
| 43 | +- `checkAuth(cwd)` |
| 44 | +- `getRepository(cwd | remoteUrl)` |
| 45 | +- `getDefaultTargetRef(repository)` |
| 46 | +- `listChangeRequests(repository, filters)` |
| 47 | +- `getChangeRequest(repository, reference)` |
| 48 | +- `createChangeRequest(repository, input)` |
| 49 | +- `checkoutChangeRequest(cwd, changeRequest, options)` |
| 50 | +- `getCloneUrls(repository)` |
| 51 | + |
| 52 | +Review-facing operations should be designed now, even if unimplemented: |
| 53 | + |
| 54 | +- `listReviewThreads(changeRequest)` |
| 55 | +- `createReviewComment(changeRequest, input)` |
| 56 | +- `replyToReviewThread(thread, input)` |
| 57 | +- `resolveReviewThread(thread)` |
| 58 | +- `submitReview(changeRequest, input)` |
| 59 | + |
| 60 | +Each operation should be guarded by capabilities: |
| 61 | + |
| 62 | +```ts |
| 63 | +export interface SourceControlProviderCapabilities { |
| 64 | + readonly kind: "github" | "gitlab" | "azure-devops" | "unknown"; |
| 65 | + readonly supportsCreateChangeRequest: boolean; |
| 66 | + readonly supportsCheckoutChangeRequest: boolean; |
| 67 | + readonly supportsReviewThreads: boolean; |
| 68 | + readonly supportsInlineComments: boolean; |
| 69 | + readonly supportsDraftChangeRequests: boolean; |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +## Provider Registry |
| 74 | + |
| 75 | +Add a registry that resolves a provider from repository remotes and explicit user input. |
| 76 | + |
| 77 | +Rules: |
| 78 | + |
| 79 | +- Detection should be pure where possible and testable without spawning CLIs. |
| 80 | +- Remote URL parsing belongs in `packages/shared`, not server-only provider layers. |
| 81 | +- Unknown providers should return explicit unsupported-operation errors, not silently fall back to GitHub. |
| 82 | +- Provider selection should be stable per operation and logged with enough context to debug bad remote detection. |
| 83 | + |
| 84 | +The registry should support multiple provider implementations at runtime, not a single dispatcher file with inline provider branches. |
| 85 | + |
| 86 | +## Rate Limits and Provider Caching |
| 87 | + |
| 88 | +Design the provider layer around a strict freshness budget. Provider API and CLI calls must not be part of frequent background polling unless the operation is explicitly marked safe and cached. |
| 89 | + |
| 90 | +Default behavior: |
| 91 | + |
| 92 | +- Pure URL/remote parsing is always live because it is local. |
| 93 | +- Provider detection from local remotes is live-local. |
| 94 | +- Authentication checks are cached. |
| 95 | +- Repository metadata is cached. |
| 96 | +- Default branch metadata is cached. |
| 97 | +- Change-request lists are cached and refreshed on explicit user actions or coarse intervals. |
| 98 | +- Full review threads, comments, file diffs, and timeline data are fetched only when the user opens the relevant review surface or explicitly refreshes it. |
| 99 | +- Create/update operations invalidate affected cache keys immediately after success. |
| 100 | + |
| 101 | +The provider API should make freshness explicit: |
| 102 | + |
| 103 | +```ts |
| 104 | +export interface SourceControlFreshness { |
| 105 | + readonly source: "live-local" | "cached-provider" | "live-provider"; |
| 106 | + readonly observedAt: string; |
| 107 | + readonly expiresAt?: string; |
| 108 | + readonly stale?: boolean; |
| 109 | +} |
| 110 | + |
| 111 | +export type ProviderRefreshPolicy = |
| 112 | + | "cache-first" |
| 113 | + | "stale-while-revalidate" |
| 114 | + | "force-refresh" |
| 115 | + | "local-only"; |
| 116 | +``` |
| 117 | + |
| 118 | +Every read operation that can touch a provider should accept a refresh policy. Background UI reads should default to `cache-first` or `stale-while-revalidate`; direct user actions like pressing refresh can use `force-refresh`. |
| 119 | + |
| 120 | +Use Effect `Cache` for provider data: |
| 121 | + |
| 122 | +- auth status: key by provider kind, hostname, workspace identity, and account if known; TTL around minutes, not seconds |
| 123 | +- repository metadata/default branch: key by provider repository stable ID or normalized remote URL; TTL around tens of minutes |
| 124 | +- change-request summary lists: key by provider repository, state/filter, source ref, target ref; short TTL with stale-while-revalidate |
| 125 | +- individual change-request summaries: key by provider repository and provider CR ID; short TTL, invalidated after create/update/comment operations |
| 126 | +- review threads/comments/diffs: key by provider CR ID and head SHA/version when available; fetch on demand for T3 Review |
| 127 | + |
| 128 | +Provider drivers should surface rate-limit signals when available: |
| 129 | + |
| 130 | +- remaining quota |
| 131 | +- reset time |
| 132 | +- retry-after duration |
| 133 | +- whether the limit is primary, secondary/abuse, or unknown |
| 134 | + |
| 135 | +Rate-limit errors should be typed, retryable when the provider gives a reset/retry time, and visible enough for the UI to avoid repeatedly retrying a blocked operation. |
| 136 | + |
| 137 | +Avoid rate-limit footguns: |
| 138 | + |
| 139 | +- no provider calls from render loops or fast status polling |
| 140 | +- no listing all PRs/MRs across all repos to infer one branch state |
| 141 | +- no silent GitHub fallback for unknown providers |
| 142 | +- no unbounded cache cardinality for branch names or free-form search queries |
| 143 | +- no per-thread duplicate provider refresh when multiple views observe the same repository |
| 144 | + |
| 145 | +## GitHub Provider Rewrite |
| 146 | + |
| 147 | +Rewrite GitHub support as `GitHubSourceControlProvider`. |
| 148 | + |
| 149 | +Carry forward behavior from: |
| 150 | + |
| 151 | +- `apps/server/src/git/Layers/GitHubCli.ts` |
| 152 | +- `apps/server/src/git/Layers/GitHubCli.test.ts` |
| 153 | +- `apps/server/src/git/githubPullRequests.ts` |
| 154 | +- GitHub-specific `GitManager` PR paths |
| 155 | + |
| 156 | +Implementation requirements: |
| 157 | + |
| 158 | +- Use `SourceControlProcess` built on `effect/unstable/process`, not `runProcess`. |
| 159 | +- Decode `gh api` and `gh pr --json` responses with Effect Schema. |
| 160 | +- Use typed errors for auth failure, missing CLI, command failure, output decode failure, unsupported reference, and provider mismatch. |
| 161 | +- Keep stdout/stderr bounded. |
| 162 | +- Avoid global mutable auth caches unless they are Effect `Cache` values with explicit keys, TTLs, and invalidation behavior. |
| 163 | +- Parse provider rate-limit headers or CLI/API error payloads when available and map them to typed rate-limit errors. |
| 164 | +- Keep GitHub nouns inside the GitHub driver; convert to `ChangeRequest` at the provider boundary. |
| 165 | + |
| 166 | +## GitManager Cutover |
| 167 | + |
| 168 | +Refactor `GitManager` so it coordinates three independent services: |
| 169 | + |
| 170 | +- `VcsDriver` for local repository mechanics. |
| 171 | +- `SourceControlProviderRegistry` for hosted provider selection. |
| 172 | +- `TextGeneration` for message/body generation. |
| 173 | + |
| 174 | +`GitManager` should stop depending directly on GitHub services. User-visible step labels should be provider-neutral unless the selected provider is known and the label is intentionally provider-specific. |
| 175 | + |
| 176 | +The Commit + PR flow should become: |
| 177 | + |
| 178 | +1. Resolve VCS repository and local status. |
| 179 | +2. Resolve source-control provider from remotes. |
| 180 | +3. Generate commit content through the existing text generation service. |
| 181 | +4. Create local change through `VcsDriver`. |
| 182 | +5. Push through `VcsDriver` or a narrow provider push helper only if the VCS requires provider-specific target syntax. |
| 183 | +6. Generate change-request title/body. |
| 184 | +7. Create the change request through `SourceControlProvider`. |
| 185 | + |
| 186 | +## Cutover Policy |
| 187 | + |
| 188 | +This phase should aggressively remove old GitHub-specific internals. |
| 189 | + |
| 190 | +Rules: |
| 191 | + |
| 192 | +- Move each active consumer directly to `SourceControlProviderRegistry` or a concrete provider test layer. |
| 193 | +- Delete migrated `GitHubCli` methods, tests, and GitHub-specific helper exports in the same PR that moves their final consumer. |
| 194 | +- Do not add compatibility export shims from `apps/server/src/git` to `apps/server/src/sourceControl`. |
| 195 | +- Transitional modules are allowed only for a bounded `GitManager` slice that cannot move safely with the rest of the provider cutover. |
| 196 | +- Every transitional module must have an owner comment, a removal checklist, and no public exports consumed by new code. |
| 197 | +- Provider-neutral web parsing should replace GitHub-only parsing directly; do not keep parallel parser stacks unless a route still requires both during a single PR. |
| 198 | + |
| 199 | +## GitLab and Azure Readiness |
| 200 | + |
| 201 | +Use the triaged references as implementation inputs, not merge targets: |
| 202 | + |
| 203 | +- GitLab PR #592 is useful for `glab mr` command mapping and JSON normalization. |
| 204 | +- Azure issue #1138 defines a good first Azure slice: remote/URL detection and change-request thread setup for same-repo URLs. |
| 205 | + |
| 206 | +The abstraction should let Phase 3 add: |
| 207 | + |
| 208 | +- `GitLabSourceControlProvider` using `glab`. |
| 209 | +- `AzureDevOpsSourceControlProvider` using `az repos pr` or REST APIs. |
| 210 | + |
| 211 | +No provider should need to edit GitHub code to join the registry. |
| 212 | + |
| 213 | +## T3 Review Design Constraint |
| 214 | + |
| 215 | +Do not optimize only for creation/checkout. The provider layer must be able to support a future in-app review surface. |
| 216 | + |
| 217 | +That means contracts should include stable IDs and enough metadata for: |
| 218 | + |
| 219 | +- file-level diffs |
| 220 | +- inline review threads |
| 221 | +- resolved/unresolved state |
| 222 | +- top-level discussion comments |
| 223 | +- pending review submission |
| 224 | +- provider URL back-links |
| 225 | + |
| 226 | +Provider-specific fields can live in a metadata bag, but core review behavior should not require the UI to know whether the backing service is GitHub, GitLab, or Azure DevOps. |
| 227 | + |
| 228 | +## Tests |
| 229 | + |
| 230 | +Add tests at three levels: |
| 231 | + |
| 232 | +- Pure parser tests for GitHub, GitLab, and Azure remote URLs and change-request references. |
| 233 | +- Provider unit tests with fake `SourceControlProcess` output and schema decode failures. |
| 234 | +- Integration-style GitHub CLI tests only where they can run hermetically or be skipped without hiding unit coverage. |
| 235 | + |
| 236 | +Required cases: |
| 237 | + |
| 238 | +- GitHub PR URL, number, and branch-ish references. |
| 239 | +- GitLab MR URL/reference parsing. |
| 240 | +- Azure DevOps PR URL parsing for same-repo URLs. |
| 241 | +- unknown provider returns unsupported-operation errors. |
| 242 | +- missing CLI and auth failures produce distinct typed errors. |
| 243 | +- invalid CLI JSON fails at decode boundary with useful context. |
| 244 | + |
| 245 | +## Migration Steps |
| 246 | + |
| 247 | +1. Add `sourceControl` contracts and provider-neutral schemas. |
| 248 | +2. Add shared remote/reference parser helpers and tests. |
| 249 | +3. Add `SourceControlProcess` and provider errors. |
| 250 | +4. Add provider registry with GitHub-only registration. |
| 251 | +5. Implement `GitHubSourceControlProvider` from scratch against the new process layer. |
| 252 | +6. Cut GitHub PR operations in `GitManager` over to the provider registry. |
| 253 | +7. Replace web PR-reference parsing with provider-neutral parser output while keeping current GitHub UX. |
| 254 | +8. Add provider cache metrics and tests for cache hit, stale refresh, invalidation, and rate-limit error mapping. |
| 255 | +9. Delete the migrated `GitHubCli` implementation, tests, and GitHub-specific helper exports unless an explicit transitional checklist remains. |
| 256 | + |
| 257 | +## Acceptance Criteria |
| 258 | + |
| 259 | +- Existing GitHub Commit + PR and PR checkout flows still work. |
| 260 | +- `GitManager` no longer imports or depends on `GitHubCli`. |
| 261 | +- Active consumers use source-control provider APIs directly; any remaining transitional module has a written removal checklist and no compatibility export shim. |
| 262 | +- Source-control contracts can represent GitHub PRs, GitLab MRs, and Azure DevOps PRs. |
| 263 | +- Unknown/unsupported providers fail explicitly and visibly. |
| 264 | +- GitHub command execution does not depend on `processRunner.ts`. |
| 265 | +- Background provider reads are cached/coalesced and do not consume provider API quota on every status refresh. |
| 266 | +- Rate-limit responses become typed errors with retry/reset metadata where available. |
| 267 | +- The provider API includes the review operations needed by future T3 Review work, even if they are capability-gated. |
| 268 | +- `bun fmt`, `bun lint`, and `bun typecheck` pass. |