1import * as Effect from "effect/Effect";
2import * as Schema from "effect/Schema";
3import type {
4 PullRequestStackMembership,
5 PullRequestAction,
6 PullRequestStackHead,
7 PullRequestActor,
8 PullRequestBaseComparison,
9 PullRequestCapabilities,
10 PullRequestChecksState,
11 PullRequestCheck,
12 PullRequestChecks,
13 PullRequestComment,
14 PullRequestFileViewed,
15 PullRequestCommit,
16 PullRequestInvolvement,
17 PullRequestLabel,
18 PullRequestListFilters,
19 PullRequestListState,
20 PullRequestMergeCapabilities,
21 PullRequestMergeMethod,
22 PullRequestMergeability,
23 PullRequestOmittedFileStat,
24 PullRequestReaction,
25 PullRequestReactionContent,
26 PullRequestReviewCommentDraft,
27 PullRequestReviewDecision,
28 PullRequestReviewThread,
29 PullRequestThreadCommentsResult,
30 PullRequestReviewVerdict,
31 PullRequestReviewerCandidateList,
32 PullRequestReviewerKind,
33 PullRequestLabelCandidateList,
34 PullRequestState,
35 PullRequestPreview,
36 PullRequestUpdateMethod,
37 PullRequestViewerPermissions,
38 SourceControlProviderKind,
39} from "@t3tools/contracts";
40import { SourceControlProviderKind as SourceControlProviderKindSchema } from "@t3tools/contracts";
41
42/**
43 * The one failure shape every provider reports, so the service can decide what a failure means
44 * without knowing which CLI or API produced it.
45 *
46 * `reason` is the part the service acts on: a missing or unauthenticated tool disables the
47 * provider for the whole workspace, a rate limit pauses its host, and anything else is specific
48 * to the request.
49 */
50export class PullRequestProviderError extends Schema.TaggedError<PullRequestProviderError>()(
51 "PullRequestProviderError",
52 {
53 provider: SourceControlProviderKindSchema,
54 operation: Schema.String,
55 reason: Schema.Literals([
56 "missing-tool",
57 "unauthenticated",
58 "rate-limited",
59 "not-found",
60 "failed",
61 ]),
62 detail: Schema.String,
63 retryAt: Schema.optional(Schema.Number),
64 cause: Schema.optional(Schema.Defect()),
65 },
66) {
67 override get message(): string {
68 return `${this.provider} failed in ${this.operation}: ${this.detail}`;
69 }
70}
71
72/**
73 * `status` moves with the state, mergeability, head, or checks, which the detail read reports;
74 * `remarks` moves with comments and reviews, which only the activity read reports.
75 */
76export interface ProviderChangeRequestWatchFingerprint {
77 readonly status: string;
78 readonly remarks: string;
79}
80
81export interface PullRequestProviderFailure {
82 readonly reason: PullRequestProviderError["reason"];
83 readonly retryAt?: number | undefined;
84}
85
86/** A change request as the provider sees it, before the service attaches project context. */
87export interface ProviderChangeRequest {
88 readonly stack?: PullRequestStackMembership;
89 readonly number: number;
90 readonly title: string;
91 readonly url: string;
92 readonly author: PullRequestActor | null;
93 readonly headBranch: string;
94 readonly headRepositoryNameWithOwner?: string | null;
95 readonly baseBranch: string;
96 readonly state: PullRequestState;
97 readonly isDraft: boolean;
98 readonly mergeability: PullRequestMergeability;
99 readonly additions: number;
100 readonly deletions: number;
101 readonly createdAt: string;
102 readonly closedAt?: string | null;
103 readonly mergedAt?: string | null;
104 readonly updatedAt: string;
105 /** Accounts with a review requested. Team-level requests are excluded by each provider. */
106 readonly reviewRequestLogins: ReadonlyArray<string>;
107 readonly labels: ReadonlyArray<PullRequestLabel>;
108 /** Absent from a host that does not summarise its reviews, which is every host but GitHub. */
109 readonly reviewDecision?: PullRequestReviewDecision | null | undefined;
110 /** Absent from a host that reports no check rollup on its listings. */
111 readonly checksState?: PullRequestChecksState | null | undefined;
112}
113
114/** The fields needed to keep a linked thread's pull request status live. */
115export interface ProviderChangeRequestSummary {
116 readonly number: number;
117 readonly title: string;
118 readonly url: string;
119 readonly headBranch: string;
120 readonly baseBranch: string;
121 readonly state: PullRequestState;
122 /** Present when the host says an open pull request is still a draft. */
123 readonly isDraft?: boolean;
124 readonly closedAt?: string | null;
125 readonly mergedAt?: string | null;
126 readonly updatedAt: string;
127 /** Overview fields, present where the host's single read returns them at no extra cost. */
128 readonly author?: PullRequestActor | null | undefined;
129 readonly additions?: number | undefined;
130 readonly deletions?: number | undefined;
131 readonly changedFiles?: number | undefined;
132 readonly reviewDecision?: PullRequestReviewDecision | null | undefined;
133 readonly checksState?: PullRequestChecksState | null | undefined;
134 readonly mergeability?: PullRequestMergeability | undefined;
135 /**
136 * The host-native stack the pull request sits in, from the same read. Null when the host says
137 * it is in none; absent when the read did not ask.
138 */
139 readonly stack?: PullRequestStackMembership | null | undefined;
140}
141
142/** One layer of a host-native stack, bottom to top order is the array's. */
143export interface ProviderChangeRequestStackLayer {
144 readonly title?: string;
145 readonly isDraft?: boolean;
146 readonly headSha?: string;
147 readonly number: number;
148 readonly headBranch: string;
149 readonly state: PullRequestState;
150}
151
152/**
153 * A host-native stack: an ordered set of change requests the host itself merges and retargets as
154 * a unit. Only GitHub offers one today; the neutral shape lets the sync reactor and the UI stay
155 * ignorant of which host said so.
156 */
157export interface ProviderChangeRequestStack {
158 readonly id: string;
159 readonly number: number;
160 readonly url: string;
161 readonly base: string;
162 readonly layers: ReadonlyArray<ProviderChangeRequestStackLayer>;
163}
164
165export interface ProviderChangeRequestPage {
166 readonly items: ReadonlyArray<ProviderChangeRequest>;
167 /** True when the host has more rows than the page size asked for. */
168 readonly truncated: boolean;
169 /**
170 * Optional count-based cursor advance. Most hosts advance by the rows delivered after local
171 * de-duplication; an offset-paged host may need to count malformed raw rows it consumed too.
172 */
173 readonly cursorAdvance?: number;
174 /**
175 * This page can be carried on from, so the service may hand the caller a cursor for it. False
176 * where the host answered in an order a cursor means nothing in, which leaves a larger `limit`
177 * as the only way to the rest — what every listing did before there were cursors.
178 */
179 readonly continues: boolean;
180}
181
182/**
183 * Where a repository's next slice starts, as the provider that has to ask for it needs it. Built
184 * by the service out of the slice it just handed over, so the boundary that decides whether a row
185 * arrives twice or not at all is decided in one place rather than in four.
186 */
187export interface ProviderListCursor {
188 /**
189 * The instant of the oldest row already handed over, checked against a timestamp's shape before
190 * it gets here because it goes into a host's own filter. Asked for inclusively: several rows
191 * share one instant often enough — a bot that touches eight change requests writes one timestamp
192 * on all eight — and asking for strictly older would lose whichever of them the slice ended
193 * before. The service drops the ones it has already sent.
194 */
195 readonly updatedBefore: string;
196 /**
197 * How many provider rows this repository has consumed so far, for a host that carries on by
198 * counting rather than by date. Usually this is the number handed over; malformed raw rows may
199 * count too when the provider reports a `cursorAdvance`.
200 */
201 readonly delivered: number;
202}
203
204/** One repository's row inside an answer that spans several of them. */
205export interface ProviderBatchedChangeRequest extends ProviderChangeRequest {
206 /** Provider-native identity, exactly as it was asked for, so the caller can file the row. */
207 readonly repository: string;
208}
209
210/**
211 * One slice of a host read across several repositories at once, newest update first across all
212 * of them. There is no per-repository page here because the host was asked one question: the
213 * caller splits the rows by `repository` and works out where each of them carries on from the
214 * oldest row in the slice, which every repository the slice covers is now read up to.
215 */
216export interface ProviderBatchedChangeRequestPage {
217 readonly items: ReadonlyArray<ProviderBatchedChangeRequest>;
218 /** True when the host has more rows than the slice asked for, for any of the repositories. */
219 readonly truncated: boolean;
220}
221
222/** The line counts for one change request, which a listing may leave for a second read. */
223export interface ProviderChangeRequestStat {
224 readonly repository: string;
225 readonly number: number;
226 readonly additions: number;
227 readonly deletions: number;
228}
229
230export interface ProviderChangeRequestDetail extends ProviderChangeRequest {
231 /** The head commit, where the host's detail read reports it. */
232 readonly headSha?: string | null;
233 readonly body: string;
234 readonly changedFiles: number;
235 readonly mergedAt: string | null;
236 readonly closedAt: string | null;
237 readonly reviewers: ReadonlyArray<PullRequestActor>;
238 readonly checks: ReadonlyArray<PullRequestCheck>;
239 readonly mergeCapabilities: PullRequestMergeCapabilities;
240 readonly viewerPermissions: PullRequestViewerPermissions;
241 /** Absent from a host that cannot compare the branch with its base, which is most of them. */
242 readonly baseComparison?: PullRequestBaseComparison;
243 readonly behindBy?: number;
244 /** Absent from a host that does not report whether it is armed to merge this on its own. */
245 readonly autoMergeEnabled?: boolean;
246 /** The strategy stored with an armed auto-merge, where the host reports it. */
247 readonly autoMergeMethod?: PullRequestMergeMethod;
248 /** Workflow runs on this head commit that still need a maintainer's approval. */
249 readonly workflowApprovalsRequired?: number;
250}
251
252/** The conversation-shaped half of a detail, loaded after the core can already render. */
253export interface ProviderChangeRequestActivity {
254 /** An optional richer actor, e.g. after GitHub's GraphQL read supplies an avatar. */
255 readonly author?: PullRequestActor | null;
256 /** Optional because most hosts already report their reviewer list in the core detail. */
257 readonly reviewers?: ReadonlyArray<PullRequestActor>;
258 readonly comments: ReadonlyArray<PullRequestComment>;
259 /**
260 * The host's own count of the conversation, which a bounded read can fall short of. A host
261 * that reports no count of its own answers with what it handed over, which is the same number
262 * once the read went to the end.
263 */
264 readonly commentCount: number;
265 readonly commentsTruncated: boolean;
266 readonly reviewThreadsTruncated?: boolean;
267 readonly reviewThreads: ReadonlyArray<PullRequestReviewThread>;
268 readonly commits: ReadonlyArray<PullRequestCommit>;
269 /** The change request's own reactions, from a host that has them. */
270 readonly reactions?: ReadonlyArray<PullRequestReaction>;
271}
272
273export interface ProviderDiffSlice {
274 readonly patch: string;
275 /** Something in this slice could not be shown, as opposed to there being more slices. */
276 readonly truncated: boolean;
277 readonly nextCursor: string | null;
278 /** The host's own counts for the files whose hunks it withheld from this slice. */
279 readonly omittedFileStats?: ReadonlyArray<PullRequestOmittedFileStat>;
280}
281
282export interface ProviderDiffFileContents {
283 readonly oldContents: string;
284 readonly newContents: string;
285}
286
287export interface ProviderFilesViewed {
288 readonly files: ReadonlyArray<PullRequestFileViewed>;
289 /** The host has more files than were read, so the ones missing here are not "unviewed". */
290 readonly truncated: boolean;
291}
292
293/**
294 * What version each of the asked-for files is at, on the change request's head. Opaque strings,
295 * compared only against one another, where the empty string is the answer for a file the change
296 * request deletes rather than a gap. A path is absent only where the read could not say, so a
297 * provider converts its host's own absences on the way here.
298 *
299 * The whole path a version travels, and the three senses of null along it, are in
300 * `docs/internals/pull-request-file-revisions.md`.
301 */
302export interface ProviderFileRevisions {
303 readonly revisions: ReadonlyMap<string, string>;
304 /**
305 * Whether these are every file the change request carries rather than only the paths asked
306 * about. A host with no per-file version reads the whole change to answer for one file, and
307 * saying so is what keeps the next tick, naming a path nothing asked about before, from making
308 * it read the whole change again. False for a read cut short, which cannot speak past the cut.
309 */
310 readonly complete?: boolean;
311}
312
313export interface ProviderRepositoryRef {
314 readonly cwd: string;
315 /** Provider-native repository identity, e.g. `owner/repo` or `group/subgroup/project`. */
316 readonly repository: string;
317 /**
318 * The host it lives on, which `repository` deliberately leaves out — the same `owner/repo`
319 * exists on github.com and on a GitHub Enterprise install, and only the caller knows which
320 * one a project's remote points at.
321 */
322 readonly host: string;
323}
324
325/**
326 * One host's change requests. Implementations own their own tool and JSON shapes and hand back
327 * the neutral types above; anything a host cannot do is declared in `capabilities` rather than
328 * failing at call time.
329 */
330export interface PullRequestProviderApi {
331 readonly withVerifiedCredential?: <A, E, R>(
332 input: { readonly cwd: string; readonly host: string },
333 use: (identity: {
334 readonly accountId: string;
335 readonly viewer: string;
336 readonly credentialFingerprint: string;
337 }) => Effect.Effect<A, E, R>,
338 ) => Effect.Effect<A, E | PullRequestProviderError, R>;
339 readonly getRoutingIdentity?: (input: {
340 readonly cwd: string;
341 readonly host: string;
342 }) => Effect.Effect<
343 { readonly accountId: string; readonly viewer: string },
344 PullRequestProviderError
345 >;
346 readonly kind: SourceControlProviderKind;
347 readonly capabilities: PullRequestCapabilities;
348 /**
349 * Rewrites the message a merge will use, for a host that lets the merge carry custom text.
350 * Absent means the host always writes its own message, so nothing is read to decide on one.
351 * Today's only rewrite strips agent credits (`mergeMessage.removeAgentCredits`) when the
352 * project asks for it.
353 */
354 readonly mergeMessageRewrite?: (message: string) => string;
355 /**
356 * The key a checkout's repository is known by on this host, where `owner/name` is not enough.
357 * Absent means `owner/name` on the host identifies it, and any checkout on the host can serve a
358 * change request in another repository there.
359 *
360 * A host that resolves it (Azure DevOps: `dev.azure.com/org/project/_git/repo`) derives its
361 * organization from the checkout, so only a checkout whose key matches the reference serves
362 * it. The key also names the repository in list cursors and on routing.
363 */
364 readonly repositoryKey?: (input: {
365 /** The checkout's canonical key, as its remote names it. */
366 readonly canonicalKey: string;
367 }) => string;
368
369 /** The signed-in account, which is what involvement filtering compares against. */
370 readonly getViewer: (input: {
371 readonly cwd: string;
372 readonly host?: string;
373 }) => Effect.Effect<string, PullRequestProviderError>;
374
375 readonly listChangeRequests: (
376 input: ProviderRepositoryRef & {
377 readonly state: PullRequestListState;
378 readonly involvement: PullRequestInvolvement;
379 readonly viewer: string;
380 readonly limit: number;
381 /**
382 * Free text to narrow the listing by, as the host understands it. A host with no text
383 * filter of its own ignores it and answers with the page it would have answered with
384 * anyway — the caller narrows what it gets, so an unfiltered page is a wider answer
385 * rather than a wrong one.
386 */
387 readonly query?: string | undefined;
388 /**
389 * Where to carry on from, rather than reading this repository from its newest row. Absent
390 * asks for the first slice, which is every listing that has not been continued.
391 */
392 readonly cursor?: ProviderListCursor | undefined;
393 /**
394 * Further narrowings, which a host applies as far as it can and ignores the rest of —
395 * an unnarrowed page is a wider answer rather than a wrong one, and the caller narrows
396 * what it gets for the fields a row carries.
397 */
398 readonly filters?: PullRequestListFilters | undefined;
399 },
400 ) => Effect.Effect<ProviderChangeRequestPage, PullRequestProviderError>;
401
402 /**
403 * The same listing for a whole host in one request, for a host that has a search across
404 * repositories. Optional: three of the four hosts here have no such API, and breaking the port
405 * for them to spare GitHub a fan-out would be paying for the fix with everyone else's clarity.
406 * The caller falls back to `listChangeRequests` per repository where this is absent, and where
407 * it fails.
408 *
409 * `limit` is the whole slice rather than a size per repository, because that is the shape of
410 * the answer: the newest `limit` rows across every repository named, which is exactly the rows
411 * a page ordered by update shows.
412 *
413 * `cursor` is one boundary for all of them, so a caller with repositories standing at different
414 * boundaries asks in groups rather than in one call.
415 */
416 readonly listChangeRequestsAcross?: (input: {
417 /** Any checkout on the host, which is what the tool is run in. */
418 readonly cwd: string;
419 readonly host: string;
420 readonly repositories: ReadonlyArray<string>;
421 readonly state: PullRequestListState;
422 readonly involvement: PullRequestInvolvement;
423 readonly viewer: string;
424 readonly limit: number;
425 readonly query?: string | undefined;
426 readonly cursor?: ProviderListCursor | undefined;
427 readonly filters?: PullRequestListFilters | undefined;
428 }) => Effect.Effect<ProviderBatchedChangeRequestPage, PullRequestProviderError>;
429
430 /**
431 * The line counts for rows a listing has already handed over. Only implemented by a provider
432 * whose listing leaves them out — for everyone else the numbers arrived with the row, and the
433 * caller has nothing to ask for.
434 */
435 readonly listChangeRequestStats?: (input: {
436 readonly cwd: string;
437 readonly host: string;
438 readonly changeRequests: ReadonlyArray<{
439 readonly repository: string;
440 readonly number: number;
441 }>;
442 }) => Effect.Effect<ReadonlyArray<ProviderChangeRequestStat>, PullRequestProviderError>;
443
444 readonly getChangeRequestChecks?: (
445 input: ProviderRepositoryRef & { readonly number: number },
446 ) => Effect.Effect<PullRequestChecks, PullRequestProviderError>;
447
448 readonly getChangeRequest: (
449 input: ProviderRepositoryRef & { readonly number: number },
450 ) => Effect.Effect<ProviderChangeRequestDetail, PullRequestProviderError>;
451
452 /** Hosts without a narrow read use their existing detail response for hover cards. */
453 readonly getChangeRequestPreview?: (
454 input: ProviderRepositoryRef & { readonly number: number },
455 ) => Effect.Effect<
456 Omit<PullRequestPreview, "projectId" | "repository">,
457 PullRequestProviderError
458 >;
459
460 /**
461 * The cheap live fields used by linked threads. Optional because a provider without a narrow
462 * endpoint can fall back to its full detail read at the service boundary.
463 */
464 readonly getChangeRequestSummary?: (
465 input: ProviderRepositoryRef & { readonly number: number },
466 ) => Effect.Effect<ProviderChangeRequestSummary, PullRequestProviderError>;
467
468 /**
469 * A cheap fingerprint of what a pull request watch reports, so it reads the change request in
470 * full only when this moves. Null when the host gave no answer for it. Optional: a host
471 * without one has its watched change requests read in full on every pass.
472 */
473 readonly getChangeRequestWatchFingerprint?: (
474 input: ProviderRepositoryRef & { readonly number: number },
475 ) => Effect.Effect<ProviderChangeRequestWatchFingerprint | null, PullRequestProviderError>;
476
477 /**
478 * The host-native stack a change request belongs to, or null when it is not stacked. Optional
479 * because most hosts have no such object; the service derives chains from base branches there.
480 */
481 readonly getChangeRequestStack?: (
482 input: ProviderRepositoryRef & { readonly includeDetails?: boolean; readonly number: number },
483 ) => Effect.Effect<ProviderChangeRequestStack | null, PullRequestProviderError>;
484
485 /** Comments, line threads, and commits, kept off the critical path for the core detail. */
486 readonly getChangeRequestActivity: (
487 input: ProviderRepositoryRef & { readonly number: number },
488 ) => Effect.Effect<ProviderChangeRequestActivity, PullRequestProviderError>;
489
490 /** One explicit page after a reader asks to continue an unfinished review thread. */
491 readonly getReviewThreadComments?: (
492 input: ProviderRepositoryRef & {
493 readonly number: number;
494 readonly threadId: string;
495 readonly cursor: string;
496 },
497 ) => Effect.Effect<PullRequestThreadCommentsResult, PullRequestProviderError>;
498
499 /**
500 * The same answer `getChangeRequest` carries, on its own. Asked before anything is written, so
501 * a request that reached the server without going past the page is refused by what the host
502 * says rather than by what the client claimed — and asked freshly, because access granted or
503 * taken away since the page loaded is exactly the case this guards.
504 *
505 * Implementations read the cheapest thing that answers it, which for a host with nothing to say
506 * is no request at all.
507 */
508 readonly getViewerPermissions: (
509 input: ProviderRepositoryRef & {
510 readonly number: number;
511 /** Skip branch comparison when checking permission for an unrelated operation. */
512 readonly includeUpdateBranch?: boolean;
513 },
514 ) => Effect.Effect<PullRequestViewerPermissions, PullRequestProviderError>;
515
516 /**
517 * One slice of the patch. Only called when `capabilities.diff` is true. A provider that can
518 * serve the whole diff at once answers with `nextCursor: null` and is done; one that pages
519 * hands back whatever it needs to find the next slice.
520 */
521 readonly getDiff: (
522 input: ProviderRepositoryRef & {
523 readonly number: number;
524 readonly cursor?: string | undefined;
525 /** One commit's own changes, rather than everything the change request carries. */
526 readonly commit?: string | undefined;
527 },
528 ) => Effect.Effect<ProviderDiffSlice, PullRequestProviderError>;
529
530 /**
531 * Full files at the exact revisions the host used for its patch. Optional where the provider
532 * exposes no diff at all; the service refuses expansion there just as it refuses the patch.
533 */
534 readonly getDiffFileContents?: (
535 input: ProviderRepositoryRef & {
536 readonly number: number;
537 readonly commit?: string | undefined;
538 readonly changeType: "change" | "rename-pure" | "rename-changed" | "new" | "deleted";
539 readonly oldPath: string;
540 readonly newPath: string;
541 },
542 ) => Effect.Effect<ProviderDiffFileContents, PullRequestProviderError>;
543
544 /**
545 * Which files the reader has already cleared. Only called when `capabilities.viewedFiles` is
546 * `"host"`, and read apart from the patch: this moves with every press rather than every push.
547 */
548 readonly getFilesViewed?: (
549 input: ProviderRepositoryRef & { readonly number: number },
550 ) => Effect.Effect<ProviderFilesViewed, PullRequestProviderError>;
551
552 /**
553 * Clears files, or puts them back. Only called when `capabilities.viewedFiles` is `"host"`.
554 * A host with no bulk form is still owed one round trip for the batch rather than one per file,
555 * since the point of gathering presses is that the host is asked once.
556 */
557 readonly setFilesViewed?: (
558 input: ProviderRepositoryRef & {
559 readonly number: number;
560 readonly files: ReadonlyArray<{ readonly path: string; readonly viewed: boolean }>;
561 },
562 ) => Effect.Effect<void, PullRequestProviderError>;
563
564 /**
565 * What version the head has of each of these files. Required of a host whose
566 * `capabilities.viewedFiles` is `"environment"`, and unused by one that keeps the marks itself:
567 * the marks live here, but only the host can say whether what a reader cleared last week is
568 * still what is in front of them. Asked for the marked paths alone, so the cost follows how
569 * much of the change request has been read rather than how large it is.
570 */
571 readonly getFileRevisions?: (
572 input: ProviderRepositoryRef & {
573 readonly number: number;
574 readonly paths: ReadonlyArray<string>;
575 },
576 ) => Effect.Effect<ProviderFileRevisions, PullRequestProviderError>;
577
578 readonly runAction: (
579 input: ProviderRepositoryRef & {
580 readonly number: number;
581 readonly action: PullRequestAction;
582 readonly stackNumber?: number;
583 readonly expectedStackHeads?: ReadonlyArray<PullRequestStackHead>;
584 /** Apply `mergeMessageRewrite` to the merge message; never sent to a host without one. */
585 readonly removeAgentCreditsOnMerge?: boolean;
586 /** Meaningful for `merge` and `enable-auto-merge`; absent takes the host's own default. */
587 readonly mergeMethod?: PullRequestMergeMethod;
588 /** Only meaningful for `update-branch`; absent takes the host's own default. */
589 readonly updateMethod?: PullRequestUpdateMethod;
590 },
591 ) => Effect.Effect<void, PullRequestProviderError>;
592
593 /**
594 * Rewrites the change request's own words. Only called when `capabilities.edit.changeRequest`
595 * is true, and never with both fields absent — the caller refuses that before it gets here,
596 * because a host asked to change nothing answers differently on each of them.
597 */
598 readonly updateChangeRequest?: (
599 input: ProviderRepositoryRef & {
600 readonly number: number;
601 readonly title?: string | undefined;
602 readonly body?: string | undefined;
603 },
604 ) => Effect.Effect<void, PullRequestProviderError>;
605
606 readonly comment: (
607 input: ProviderRepositoryRef & { readonly number: number; readonly body: string },
608 ) => Effect.Effect<void, PullRequestProviderError>;
609
610 /**
611 * Rewrites a remark somebody already posted. Only called when `capabilities.edit.comment` is
612 * true, with an id exactly as the conversation carried it.
613 *
614 * Whether this remark is the reader's to rewrite is the host's own answer: no read here can
615 * settle it, since access can be taken away between the conversation being read and the
616 * rewrite being sent, and a host refuses a stranger's remark with a sentence saying so.
617 */
618 readonly updateComment?: (
619 input: ProviderRepositoryRef & {
620 readonly number: number;
621 readonly commentId: string;
622 readonly kind: "issue-comment" | "review-comment";
623 readonly body: string;
624 },
625 ) => Effect.Effect<void, PullRequestProviderError>;
626
627 /**
628 * Sends a whole review at once. Only called for a verdict the host declared in
629 * `capabilities.review.verdicts`, and with line comments only where it declared
630 * `inlineComment`.
631 */
632 readonly submitReview: (
633 input: ProviderRepositoryRef & {
634 readonly number: number;
635 readonly verdict: PullRequestReviewVerdict;
636 readonly body: string;
637 readonly comments: ReadonlyArray<PullRequestReviewCommentDraft>;
638 },
639 ) => Effect.Effect<void, PullRequestProviderError>;
640
641 /**
642 * The people this viewer may ask for a review, with whoever has already been asked marked as
643 * such. Only called when `capabilities.reviewers.listCandidates` is true.
644 *
645 * The author is left out by each provider rather than by the caller, because only the provider
646 * knows how the host spells the same person in a candidate list and on a pull request.
647 */
648 readonly listReviewerCandidates: (
649 input: ProviderRepositoryRef & { readonly number: number },
650 ) => Effect.Effect<PullRequestReviewerCandidateList, PullRequestProviderError>;
651
652 /**
653 * Asks for a review, or takes the request back. Only called when
654 * `capabilities.reviewers.request` is true.
655 *
656 * One call for both directions, because that is what every host does with them: GitHub posts and
657 * deletes the same collection, and GitLab and Bitbucket write the whole reviewer set either way.
658 * Asking again somebody who has already reviewed is a request like any other — which is how a
659 * re-request is made.
660 */
661 readonly setReviewerRequest: (
662 input: ProviderRepositoryRef & {
663 readonly number: number;
664 readonly reviewers: ReadonlyArray<{
665 readonly id: string;
666 readonly kind: PullRequestReviewerKind;
667 }>;
668 readonly requested: boolean;
669 },
670 ) => Effect.Effect<void, PullRequestProviderError>;
671
672 /**
673 * The repository's labels, with the ones already on the change request marked. Present with
674 * `setLabels` only where `capabilities.labels` is true; the service refuses both without it.
675 */
676 readonly listLabelCandidates?: (
677 input: ProviderRepositoryRef & { readonly number: number },
678 ) => Effect.Effect<PullRequestLabelCandidateList, PullRequestProviderError>;
679
680 /** Puts labels on the change request, or takes them off. One call for both directions. */
681 readonly setLabels?: (
682 input: ProviderRepositoryRef & {
683 readonly number: number;
684 readonly labels: ReadonlyArray<string>;
685 readonly applied: boolean;
686 },
687 ) => Effect.Effect<void, PullRequestProviderError>;
688
689 /** Only called when `capabilities.review.reply` is true. */
690 readonly replyToThread: (
691 input: ProviderRepositoryRef & {
692 readonly number: number;
693 readonly threadId: string;
694 readonly body: string;
695 },
696 ) => Effect.Effect<void, PullRequestProviderError>;
697
698 /**
699 * Adds a reaction, or takes it back. Only called when `capabilities.reactions` is true.
700 *
701 * `subjectId` is a remark's id as the conversation carried it; absent means the change request
702 * itself, whose reactions sit on its description. Whatever a host needs to address either of
703 * them is worked out here, because the id a conversation travels with is the one the reader has.
704 */
705 readonly setReaction: (
706 input: ProviderRepositoryRef & {
707 readonly number: number;
708 readonly subjectId?: string | undefined;
709 readonly content: PullRequestReactionContent;
710 readonly reacted: boolean;
711 },
712 ) => Effect.Effect<void, PullRequestProviderError>;
713
714 /** Only called when `capabilities.review.resolve` is true. */
715 readonly setThreadResolution: (
716 input: ProviderRepositoryRef & {
717 readonly number: number;
718 readonly threadId: string;
719 readonly resolved: boolean;
720 },
721 ) => Effect.Effect<void, PullRequestProviderError>;
722}