Open History to see the last commit
1import * as Effect from "effect/Effect";
2import * as Schema from "effect/Schema";
3import type * as Stream from "effect/Stream";
4import type {
5 PullRequestStackMembership,
6 PullRequestAction,
7 PullRequestStackHead,
8 PullRequestActor,
9 PullRequestReferenceCandidate,
10 PullRequestReferenceSigil,
11 PullRequestBaseComparison,
12 PullRequestCapabilities,
13 PullRequestChecksState,
14 PullRequestCheck,
15 PullRequestChecks,
16 PullRequestComment,
17 PullRequestFileViewed,
18 PullRequestCommit,
19 PullRequestInvolvement,
20 PullRequestLabel,
21 PullRequestListFilters,
22 PullRequestListState,
23 PullRequestMergeCapabilities,
24 PullRequestMergeMethod,
25 PullRequestMergeQueueRemoval,
26 PullRequestMergeability,
27 PullRequestOmittedFileStat,
28 PullRequestReaction,
29 PullRequestReactionContent,
30 PullRequestReviewCommentDraft,
31 PullRequestReviewDecision,
32 PullRequestOverviewThread,
33 PullRequestReviewThread,
34 PullRequestThreadCommentsResult,
35 PullRequestReviewVerdict,
36 PullRequestReviewerCandidateList,
37 PullRequestReviewerKind,
38 PullRequestLabelCandidateList,
39 PullRequestState,
40 PullRequestPreview,
41 PullRequestUpdateMethod,
42 PullRequestViewerPermissions,
43 SourceControlProviderKind,
44} from "@t3tools/contracts";
45import { SourceControlProviderKind as SourceControlProviderKindSchema } from "@t3tools/contracts";
46
47export class PullRequestProviderError extends Schema.TaggedError<PullRequestProviderError>()(
48 "PullRequestProviderError",
49 {
50 provider: SourceControlProviderKindSchema,
51 operation: Schema.String,
52 reason: Schema.Literals([
53 "missing-tool",
54 "unauthenticated",
55 "forbidden",
56 "rate-limited",
57 "not-found",
58 "failed",
59 ]),
60 detail: Schema.String,
61 retryAt: Schema.optional(Schema.Number),
62 /**
63 * Why the host would not return a repository and what to change, in the reader's terms.
64 * Shown beside that repository in the list; `detail` is about the failed call instead.
65 */
66 accessHint: Schema.optional(Schema.String),
67 cause: Schema.optional(Schema.Defect()),
68 },
69) {
70 override get message(): string {
71 return `${this.provider} failed in ${this.operation}: ${this.detail}`;
72 }
73}
74
75export interface ProviderChangeRequestWatchFingerprint {
76 readonly status: string;
77 readonly remarks: string;
78}
79
80export interface PullRequestProviderFailure {
81 readonly reason: PullRequestProviderError["reason"];
82 readonly retryAt?: number | undefined;
83 readonly accessHint?: string | undefined;
84}
85
86export interface ProviderChangeRequest {
87 readonly stack?: PullRequestStackMembership;
88 readonly number: number;
89 readonly title: string;
90 readonly url: string;
91 readonly author: PullRequestActor | null;
92 readonly headBranch: string;
93 readonly headRepositoryNameWithOwner?: string | null;
94 readonly baseBranch: string;
95 readonly state: PullRequestState;
96 readonly isDraft: boolean;
97 readonly mergeability: PullRequestMergeability;
98 readonly additions: number;
99 readonly deletions: number;
100 readonly createdAt: string;
101 readonly closedAt?: string | null;
102 readonly mergedAt?: string | null;
103 readonly updatedAt: string;
104 readonly reviewRequestLogins: ReadonlyArray<string>;
105 readonly labels: ReadonlyArray<PullRequestLabel>;
106 readonly reviewDecision?: PullRequestReviewDecision | null | undefined;
107 readonly checksState?: PullRequestChecksState | null | undefined;
108}
109
110export interface ProviderChangeRequestSummary {
111 readonly number: number;
112 readonly title: string;
113 readonly url: string;
114 readonly headBranch: string;
115 readonly baseBranch: string;
116 readonly state: PullRequestState;
117 readonly isDraft?: boolean;
118 readonly closedAt?: string | null;
119 readonly mergedAt?: string | null;
120 readonly updatedAt: string;
121 readonly author?: PullRequestActor | null | undefined;
122 readonly additions?: number | undefined;
123 readonly deletions?: number | undefined;
124 readonly changedFiles?: number | undefined;
125 readonly reviewDecision?: PullRequestReviewDecision | null | undefined;
126 readonly checksState?: PullRequestChecksState | null | undefined;
127 readonly mergeability?: PullRequestMergeability | undefined;
128 /** Null means no stack; absent means not requested. */
129 readonly stack?: PullRequestStackMembership | null | undefined;
130 readonly headSha?: string | undefined;
131 /** Absent means the read did not report auto-merge state, not that it is off. */
132 readonly autoMergeEnabled?: boolean | undefined;
133 readonly autoMergeMethod?: PullRequestMergeMethod | undefined;
134}
135
136export interface ProviderChangeRequestStackLayer {
137 readonly title?: string;
138 readonly isDraft?: boolean;
139 readonly headSha?: string;
140 readonly number: number;
141 readonly headBranch: string;
142 readonly state: PullRequestState;
143}
144
145export interface ProviderChangeRequestStack {
146 readonly id: string;
147 readonly number: number;
148 readonly url: string;
149 readonly base: string;
150 readonly layers: ReadonlyArray<ProviderChangeRequestStackLayer>;
151}
152
153export interface ProviderChangeRequestPage {
154 readonly items: ReadonlyArray<ProviderChangeRequest>;
155 readonly truncated: boolean;
156 /** Offset-paged hosts must include malformed raw rows in their advance count. */
157 readonly cursorAdvance?: number;
158 /**
159 * Where a host that can only page from the top carries on: an opaque position past the last
160 * row delivered, handed back as the cursor's `resumeAfter`. Unlike a count of rows it survives
161 * rows above it closing between reads.
162 */
163 readonly resumeAfter?: string;
164 readonly continues: boolean;
165}
166
167export interface ProviderListCursor {
168 /** Inclusive boundary: equal-time rows can span pages and must not be skipped. */
169 readonly updatedBefore: string;
170 readonly delivered: number;
171 /**
172 * Rows already handed over at `updatedBefore`. Timestamp-paged hosts can exclude these before
173 * applying their limit so a full page of tied updates does not repeat the same prefix.
174 */
175 readonly seenAt?: ReadonlyArray<number> | undefined;
176 readonly resumeAfter?: string | undefined;
177}
178
179export interface ProviderBatchedChangeRequest extends ProviderChangeRequest {
180 readonly repository: string;
181}
182
183export interface ProviderBatchedChangeRequestPage {
184 readonly items: ReadonlyArray<ProviderBatchedChangeRequest>;
185 readonly truncated: boolean;
186 /**
187 * Repositories asked about that the page cannot vouch for: its silence about them may be an
188 * omission rather than an empty answer, so the caller reads each on its own.
189 */
190 readonly unresolved?: ReadonlyArray<string>;
191}
192
193export interface ProviderChangeRequestStat {
194 readonly repository: string;
195 readonly number: number;
196 readonly additions: number;
197 readonly deletions: number;
198}
199
200export interface ProviderChangeRequestDetail extends ProviderChangeRequest {
201 readonly maintainerCanModify?: boolean;
202 readonly headSha?: string | null;
203 readonly body: string;
204 readonly changedFiles: number;
205 readonly mergedAt: string | null;
206 readonly closedAt: string | null;
207 readonly reviewers: ReadonlyArray<PullRequestActor>;
208 readonly checks: ReadonlyArray<PullRequestCheck>;
209 readonly mergeCapabilities: PullRequestMergeCapabilities;
210 readonly viewerPermissions: PullRequestViewerPermissions;
211 readonly baseComparison?: PullRequestBaseComparison;
212 readonly behindBy?: number;
213 readonly autoMergeEnabled?: boolean;
214 readonly autoMergeMethod?: PullRequestMergeMethod;
215 readonly workflowApprovalsRequired?: number;
216 readonly mergeQueueRemoval?: PullRequestMergeQueueRemoval;
217}
218
219export interface ProviderChangeRequestActivity {
220 readonly author?: PullRequestActor | null;
221 readonly reviewers?: ReadonlyArray<PullRequestActor>;
222 readonly comments: ReadonlyArray<PullRequestComment>;
223 readonly commentCount: number;
224 readonly commentsTruncated: boolean;
225 readonly reviewThreadsTruncated?: boolean;
226 readonly reviewThreads: ReadonlyArray<PullRequestReviewThread>;
227 readonly overviewThreads?: ReadonlyArray<PullRequestOverviewThread>;
228 readonly commits: ReadonlyArray<PullRequestCommit>;
229 readonly reactions?: ReadonlyArray<PullRequestReaction>;
230}
231
232export interface ProviderDiffSlice {
233 readonly patch: string;
234 /** Content was omitted within this slice. */
235 readonly truncated: boolean;
236 readonly nextCursor: string | null;
237 readonly omittedFileStats?: ReadonlyArray<PullRequestOmittedFileStat>;
238}
239
240export interface ProviderDiffFileContents {
241 readonly oldContents: string;
242 readonly newContents: string;
243}
244
245export interface ProviderDiffFilePaths {
246 readonly files: ReadonlyArray<{ readonly oldPath: string; readonly newPath: string }>;
247 readonly nextCursor: string | null;
248}
249
250export interface ProviderFilesViewed {
251 readonly files: ReadonlyArray<PullRequestFileViewed>;
252 /** More files exist; missing entries are unknown, not "unviewed". */
253 readonly truncated: boolean;
254}
255
256export interface ProviderFileStats {
257 readonly files: ReadonlyArray<{
258 readonly path: string;
259 readonly additions: number;
260 readonly deletions: number;
261 }>;
262 /** The host has more files than were read. */
263 readonly truncated: boolean;
264}
265
266/**
267 * Opaque strings, compared only against one another, where the empty string is the answer for a
268 * file the change request deletes rather than a gap. A path is absent only where the read could
269 * not say, so a provider converts its host's own absences on the way here.
270 */
271export interface ProviderFileRevisions {
272 readonly revisions: ReadonlyMap<string, string>;
273 /** All revisions were read and can be reused for other paths. */
274 readonly complete?: boolean;
275}
276
277export interface ProviderFilterOptionPage<A> {
278 readonly items: ReadonlyArray<A>;
279 readonly truncated: boolean;
280}
281
282export interface ProviderRepositoryRef {
283 readonly cwd: string;
284 readonly repository: string;
285 readonly host: string;
286}
287
288export interface PullRequestProviderApi {
289 readonly withVerifiedCredential?: <A, E, R>(
290 /** `repository` lets a host pick the credential that can reach it. */
291 input: { readonly cwd: string; readonly host: string; readonly repository?: string },
292 use: (identity: {
293 readonly accountId: string;
294 readonly viewer: string;
295 readonly credentialFingerprint: string;
296 }) => Effect.Effect<A, E, R>,
297 ) => Effect.Effect<A, E | PullRequestProviderError, R>;
298 readonly getRoutingIdentity?: (input: {
299 readonly cwd: string;
300 readonly host: string;
301 }) => Effect.Effect<
302 { readonly accountId: string; readonly viewer: string },
303 PullRequestProviderError
304 >;
305 readonly kind: SourceControlProviderKind;
306 readonly capabilities: PullRequestCapabilities;
307 /**
308 * What a reader is told when this host cannot be read on the server: `missing` for no tool or
309 * credential, `unauthenticated` for one the host refused. Absent means a generic sentence.
310 */
311 readonly unavailableRequirement?: {
312 readonly missing: string;
313 readonly unauthenticated: string;
314 };
315 readonly mergeMessageRewrite?: (message: string) => string;
316 /** Use matching checkout keys instead of `owner/name`. */
317 readonly repositoryKey?: (input: { readonly canonicalKey: string }) => string;
318 /**
319 * Names the repository in every request and authenticates by host, so a change request on the
320 * definition's `publicHost` can be read from a project with no checkout of its repository.
321 */
322 readonly readsPublicHostWithoutCheckout?: boolean;
323 /** Concurrent summary reads share requests, so callers may keep many in flight. */
324 readonly batchesReads?: boolean;
325 /**
326 * Absent means every host detected as this kind is read. A host it refuses is listed as one that
327 * cannot be browsed here, and nothing is sent for it.
328 */
329 readonly servesHost?: (host: string) => boolean;
330
331 readonly getViewer: (input: {
332 readonly cwd: string;
333 readonly host?: string;
334 }) => Effect.Effect<string, PullRequestProviderError>;
335
336 readonly listChangeRequests: (
337 input: ProviderRepositoryRef & {
338 readonly state: PullRequestListState;
339 readonly involvement: PullRequestInvolvement;
340 readonly viewer: string;
341 readonly limit: number;
342 /** Hosts may ignore unsupported search; the caller filters returned rows. */
343 readonly query?: string | undefined;
344 readonly cursor?: ProviderListCursor | undefined;
345 /** The caller applies any filters the host ignores. */
346 readonly filters?: PullRequestListFilters | undefined;
347 },
348 ) => Effect.Effect<ProviderChangeRequestPage, PullRequestProviderError>;
349
350 /** Newest `limit` rows total across repositories, under one cursor boundary. */
351 readonly listChangeRequestsAcross?: (input: {
352 readonly cwd: string;
353 readonly host: string;
354 readonly repositories: ReadonlyArray<string>;
355 readonly state: PullRequestListState;
356 readonly involvement: PullRequestInvolvement;
357 readonly viewer: string;
358 readonly limit: number;
359 readonly query?: string | undefined;
360 readonly cursor?: ProviderListCursor | undefined;
361 readonly filters?: PullRequestListFilters | undefined;
362 }) => Effect.Effect<ProviderBatchedChangeRequestPage, PullRequestProviderError>;
363
364 readonly listChangeRequestStats?: (input: {
365 readonly cwd: string;
366 readonly host: string;
367 readonly changeRequests: ReadonlyArray<{
368 readonly repository: string;
369 readonly number: number;
370 }>;
371 }) => Effect.Effect<ReadonlyArray<ProviderChangeRequestStat>, PullRequestProviderError>;
372
373 readonly getChangeRequestChecks?: (
374 input: ProviderRepositoryRef & { readonly number: number },
375 ) => Effect.Effect<Pick<PullRequestChecks, "state" | "checks">, PullRequestProviderError>;
376
377 readonly getChangeRequest: (
378 input: ProviderRepositoryRef & { readonly number: number },
379 ) => Effect.Effect<ProviderChangeRequestDetail, PullRequestProviderError>;
380
381 /**
382 * For a provider that holds change request reads of its own: called when the service drops its
383 * reads, so a refresh or a finished turn reaches the host. No scope forgets everything; a scope
384 * without a number forgets the repository's change requests.
385 */
386 readonly forgetChangeRequestReads?: (scope?: {
387 readonly repository: string;
388 readonly number?: number;
389 }) => Effect.Effect<void>;
390
391 readonly getChangeRequestPreview?: (
392 input: ProviderRepositoryRef & { readonly number: number },
393 ) => Effect.Effect<
394 Omit<PullRequestPreview, "projectId" | "repository">,
395 PullRequestProviderError
396 >;
397
398 readonly getChangeRequestSummary?: (
399 input: ProviderRepositoryRef & { readonly number: number },
400 ) => Effect.Effect<ProviderChangeRequestSummary, PullRequestProviderError>;
401
402 /** Null means unknown; an absent method requires full watch reads. */
403 readonly getChangeRequestWatchFingerprint?: (
404 input: ProviderRepositoryRef & { readonly number: number },
405 ) => Effect.Effect<ProviderChangeRequestWatchFingerprint | null, PullRequestProviderError>;
406
407 /**
408 * Emits whenever the host says the change request may have changed, and once on connecting.
409 * Ends where the host or account offers no such feed; readers then rely on polling alone.
410 */
411 readonly changeRequestEvents?: (
412 input: ProviderRepositoryRef & { readonly number: number },
413 ) => Stream.Stream<void>;
414
415 readonly getChangeRequestStack?: (
416 input: ProviderRepositoryRef & { readonly includeDetails?: boolean; readonly number: number },
417 ) => Effect.Effect<ProviderChangeRequestStack | null, PullRequestProviderError>;
418
419 readonly getChangeRequestActivity: (
420 input: ProviderRepositoryRef & { readonly number: number },
421 ) => Effect.Effect<ProviderChangeRequestActivity, PullRequestProviderError>;
422
423 readonly getReviewThreadComments?: (
424 input: ProviderRepositoryRef & {
425 readonly number: number;
426 readonly threadId: string;
427 readonly cursor: string;
428 },
429 ) => Effect.Effect<PullRequestThreadCommentsResult, PullRequestProviderError>;
430
431 /** Read fresh before writes; client permissions may be stale or bypassed. */
432 readonly getViewerPermissions: (
433 input: ProviderRepositoryRef & {
434 readonly number: number;
435 readonly includeUpdateBranch?: boolean;
436 },
437 ) => Effect.Effect<PullRequestViewerPermissions, PullRequestProviderError>;
438
439 readonly getDiff: (
440 input: ProviderRepositoryRef & {
441 readonly number: number;
442 readonly cursor?: string | undefined;
443 readonly commit?: string | undefined;
444 },
445 ) => Effect.Effect<ProviderDiffSlice, PullRequestProviderError>;
446
447 /** Exact changed path pairs, including renames, without fetching patch contents. */
448 readonly getDiffFilePaths?: (
449 input: ProviderRepositoryRef & {
450 readonly number: number;
451 readonly cursor?: string | undefined;
452 readonly commit?: string | undefined;
453 },
454 ) => Effect.Effect<ProviderDiffFilePaths, PullRequestProviderError>;
455
456 readonly getDiffFileContents?: (
457 input: ProviderRepositoryRef & {
458 readonly number: number;
459 readonly commit?: string | undefined;
460 readonly changeType: "change" | "rename-pure" | "rename-changed" | "new" | "deleted";
461 readonly oldPath: string;
462 readonly newPath: string;
463 },
464 ) => Effect.Effect<ProviderDiffFileContents, PullRequestProviderError>;
465
466 readonly getFilesViewed?: (
467 input: ProviderRepositoryRef & { readonly number: number },
468 ) => Effect.Effect<ProviderFilesViewed, PullRequestProviderError>;
469
470 /** Line counts for every changed file. Optional: a host without one shows no breakdown. */
471 readonly getFileStats?: (
472 input: ProviderRepositoryRef & { readonly number: number },
473 ) => Effect.Effect<ProviderFileStats, PullRequestProviderError>;
474
475 /**
476 * A host with no bulk form is still owed one round trip for the batch rather than one per
477 * file, since the point of gathering presses is that the host is asked once.
478 */
479 readonly setFilesViewed?: (
480 input: ProviderRepositoryRef & {
481 readonly number: number;
482 readonly files: ReadonlyArray<{ readonly path: string; readonly viewed: boolean }>;
483 },
484 ) => Effect.Effect<void, PullRequestProviderError>;
485
486 /** Required for `environment` viewed marks to invalidate them after changes. */
487 readonly getFileRevisions?: (
488 input: ProviderRepositoryRef & {
489 readonly number: number;
490 readonly paths: ReadonlyArray<string>;
491 },
492 ) => Effect.Effect<ProviderFileRevisions, PullRequestProviderError>;
493
494 /** Returns the change request as the host's reply left it, or void when the reply lacks it. */
495 readonly runAction: (
496 input: ProviderRepositoryRef & {
497 readonly number: number;
498 readonly action: PullRequestAction;
499 readonly stackNumber?: number;
500 readonly expectedStackHeads?: ReadonlyArray<PullRequestStackHead>;
501 readonly removeAgentCreditsOnMerge?: boolean;
502 readonly mergeMethod?: PullRequestMergeMethod;
503 readonly updateMethod?: PullRequestUpdateMethod;
504 },
505 ) => Effect.Effect<ProviderChangeRequestSummary | void, PullRequestProviderError>;
506
507 /** Requires edit capability and at least one field. */
508 readonly updateChangeRequest?: (
509 input: ProviderRepositoryRef & {
510 readonly maintainerCanModify?: boolean | undefined;
511 readonly number: number;
512 readonly title?: string | undefined;
513 readonly body?: string | undefined;
514 },
515 ) => Effect.Effect<void, PullRequestProviderError>;
516
517 readonly comment: (
518 input: ProviderRepositoryRef & { readonly number: number; readonly body: string },
519 ) => Effect.Effect<void, PullRequestProviderError>;
520
521 readonly updateComment?: (
522 input: ProviderRepositoryRef & {
523 readonly number: number;
524 readonly commentId: string;
525 readonly kind: "issue-comment" | "review-comment";
526 readonly body: string;
527 },
528 ) => Effect.Effect<void, PullRequestProviderError>;
529
530 readonly submitReview: (
531 input: ProviderRepositoryRef & {
532 readonly number: number;
533 readonly verdict: PullRequestReviewVerdict;
534 readonly body: string;
535 readonly comments: ReadonlyArray<PullRequestReviewCommentDraft>;
536 },
537 ) => Effect.Effect<void, PullRequestProviderError>;
538
539 /** Exclude the author using host-specific account identities. */
540 readonly listReviewerCandidates: (
541 input: ProviderRepositoryRef & {
542 readonly number: number;
543 /** Asks the host to search past its first page of candidates. */
544 readonly query?: string | undefined;
545 },
546 ) => Effect.Effect<PullRequestReviewerCandidateList, PullRequestProviderError>;
547
548 readonly setReviewerRequest: (
549 input: ProviderRepositoryRef & {
550 readonly number: number;
551 readonly reviewers: ReadonlyArray<{
552 readonly id: string;
553 readonly kind: PullRequestReviewerKind;
554 }>;
555 readonly requested: boolean;
556 },
557 ) => Effect.Effect<void, PullRequestProviderError>;
558
559 readonly listLabelCandidates?: (
560 input: ProviderRepositoryRef & { readonly number: number },
561 ) => Effect.Effect<PullRequestLabelCandidateList, PullRequestProviderError>;
562
563 /**
564 * The labels a repository offers, for narrowing a listing by. `query` narrows by name as far as
565 * the host can; the caller still matches the answer against it.
566 */
567 readonly listRepositoryLabels?: (
568 input: ProviderRepositoryRef & { readonly query?: string | undefined },
569 ) => Effect.Effect<ProviderFilterOptionPage<PullRequestLabel>, PullRequestProviderError>;
570
571 /**
572 * People who could have opened a change request in the repository, for narrowing a listing by
573 * author. A host that can only search its whole user base answers from that.
574 */
575 readonly searchRepositoryAuthors?: (
576 input: ProviderRepositoryRef & { readonly query?: string | undefined },
577 ) => Effect.Effect<ProviderFilterOptionPage<PullRequestActor>, PullRequestProviderError>;
578
579 /**
580 * Issues and change requests a composer can reference, recently updated first. A numeric
581 * `query` also finds that number exactly; other text matches titles as far as the host can.
582 */
583 readonly searchRepositoryReferences?: (
584 input: ProviderRepositoryRef & {
585 readonly query?: string | undefined;
586 readonly sigil?: PullRequestReferenceSigil | undefined;
587 },
588 ) => Effect.Effect<
589 ProviderFilterOptionPage<PullRequestReferenceCandidate>,
590 PullRequestProviderError
591 >;
592
593 readonly setLabels?: (
594 input: ProviderRepositoryRef & {
595 readonly number: number;
596 readonly labels: ReadonlyArray<string>;
597 readonly applied: boolean;
598 },
599 ) => Effect.Effect<void, PullRequestProviderError>;
600
601 readonly replyToThread: (
602 input: ProviderRepositoryRef & {
603 readonly number: number;
604 readonly threadId: string;
605 readonly body: string;
606 },
607 ) => Effect.Effect<void, PullRequestProviderError>;
608
609 /** `subjectId` is the conversation's remark ID; absent targets the change request description. */
610 readonly setReaction: (
611 input: ProviderRepositoryRef & {
612 readonly number: number;
613 readonly subjectId?: string | undefined;
614 readonly content: PullRequestReactionContent;
615 readonly reacted: boolean;
616 },
617 ) => Effect.Effect<void, PullRequestProviderError>;
618
619 readonly setThreadResolution: (
620 input: ProviderRepositoryRef & {
621 readonly number: number;
622 readonly threadId: string;
623 readonly resolved: boolean;
624 },
625 ) => Effect.Effect<void, PullRequestProviderError>;
626}