| @@ -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. |