@gitkraken/core-gitlens 0.5.108 → 0.5.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/CHANGELOG.md +49 -1
  2. package/dist/git/models/issue.d.ts +70 -0
  3. package/dist/git/models/issue.d.ts.map +1 -1
  4. package/dist/git/models/issue.js +17 -0
  5. package/dist/git/models/issue.js.map +1 -1
  6. package/dist/git/models/pullRequest.d.ts +81 -0
  7. package/dist/git/models/pullRequest.d.ts.map +1 -1
  8. package/dist/git/models/pullRequest.js +15 -0
  9. package/dist/git/models/pullRequest.js.map +1 -1
  10. package/dist/git/utils/issue.utils.d.ts +41 -1
  11. package/dist/git/utils/issue.utils.d.ts.map +1 -1
  12. package/dist/git/utils/issue.utils.js +80 -0
  13. package/dist/git/utils/issue.utils.js.map +1 -1
  14. package/dist/git/utils/pullRequest.utils.d.ts +11 -1
  15. package/dist/git/utils/pullRequest.utils.d.ts.map +1 -1
  16. package/dist/git/utils/pullRequest.utils.js +40 -0
  17. package/dist/git/utils/pullRequest.utils.js.map +1 -1
  18. package/dist/plus/git-github/api/github.d.ts +70 -10
  19. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  20. package/dist/plus/git-github/api/github.js +284 -31
  21. package/dist/plus/git-github/api/github.js.map +1 -1
  22. package/dist/plus/git-github/api/issueSearchQuery.d.ts +49 -5
  23. package/dist/plus/git-github/api/issueSearchQuery.d.ts.map +1 -1
  24. package/dist/plus/git-github/api/issueSearchQuery.js +64 -6
  25. package/dist/plus/git-github/api/issueSearchQuery.js.map +1 -1
  26. package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts +27 -1
  27. package/dist/plus/git-github/api/pullRequestSearchQuery.d.ts.map +1 -1
  28. package/dist/plus/git-github/api/pullRequestSearchQuery.js +73 -6
  29. package/dist/plus/git-github/api/pullRequestSearchQuery.js.map +1 -1
  30. package/dist/plus/git-github/models.d.ts +17 -5
  31. package/dist/plus/git-github/models.d.ts.map +1 -1
  32. package/dist/plus/git-github/models.js +14 -1
  33. package/dist/plus/git-github/models.js.map +1 -1
  34. package/dist/plus/integrations/collectionMetadata.d.ts +18 -0
  35. package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
  36. package/dist/plus/integrations/collectionMetadata.js +24 -0
  37. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  38. package/dist/plus/integrations/integrationService.d.ts +31 -10
  39. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  40. package/dist/plus/integrations/integrationService.js +9 -1
  41. package/dist/plus/integrations/integrationService.js.map +1 -1
  42. package/dist/plus/integrations/manager.d.ts +85 -31
  43. package/dist/plus/integrations/manager.d.ts.map +1 -1
  44. package/dist/plus/integrations/models/gitHostIntegration.d.ts +18 -31
  45. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  46. package/dist/plus/integrations/models/gitHostIntegration.js +29 -1
  47. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  48. package/dist/plus/integrations/models/integration.d.ts +1 -1
  49. package/dist/plus/integrations/models/integration.d.ts.map +1 -1
  50. package/dist/plus/integrations/models/integration.js.map +1 -1
  51. package/dist/plus/integrations/models/issueReads.d.ts +63 -1
  52. package/dist/plus/integrations/models/issueReads.d.ts.map +1 -1
  53. package/dist/plus/integrations/models/issuesIntegration.d.ts +7 -21
  54. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
  55. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  56. package/dist/plus/integrations/providerFilters.d.ts +2 -2
  57. package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
  58. package/dist/plus/integrations/providers/accounts.d.ts +6 -0
  59. package/dist/plus/integrations/providers/accounts.d.ts.map +1 -0
  60. package/dist/plus/integrations/providers/accounts.js +30 -0
  61. package/dist/plus/integrations/providers/accounts.js.map +1 -0
  62. package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -1
  63. package/dist/plus/integrations/providers/azureDevOps.js +1 -0
  64. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  65. package/dist/plus/integrations/providers/github.d.ts +9 -0
  66. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  67. package/dist/plus/integrations/providers/github.js +115 -135
  68. package/dist/plus/integrations/providers/github.js.map +1 -1
  69. package/dist/plus/integrations/providers/gitlab.d.ts +9 -0
  70. package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
  71. package/dist/plus/integrations/providers/gitlab.js +24 -4
  72. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  73. package/dist/plus/integrations/providers/issueSorts.d.ts +36 -0
  74. package/dist/plus/integrations/providers/issueSorts.d.ts.map +1 -0
  75. package/dist/plus/integrations/providers/issueSorts.js +76 -0
  76. package/dist/plus/integrations/providers/issueSorts.js.map +1 -0
  77. package/dist/plus/integrations/providers/jira.d.ts +3 -9
  78. package/dist/plus/integrations/providers/jira.d.ts.map +1 -1
  79. package/dist/plus/integrations/providers/jira.js +5 -1
  80. package/dist/plus/integrations/providers/jira.js.map +1 -1
  81. package/dist/plus/integrations/providers/linear.d.ts +24 -9
  82. package/dist/plus/integrations/providers/linear.d.ts.map +1 -1
  83. package/dist/plus/integrations/providers/linear.js +63 -5
  84. package/dist/plus/integrations/providers/linear.js.map +1 -1
  85. package/dist/plus/integrations/providers/models.d.ts +76 -38
  86. package/dist/plus/integrations/providers/models.d.ts.map +1 -1
  87. package/dist/plus/integrations/providers/models.js +91 -105
  88. package/dist/plus/integrations/providers/models.js.map +1 -1
  89. package/dist/plus/integrations/providers/providersApi.d.ts +18 -3
  90. package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
  91. package/dist/plus/integrations/providers/providersApi.js +24 -7
  92. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  93. package/dist/plus/integrations/providers/pullRequestReviews.d.ts +50 -0
  94. package/dist/plus/integrations/providers/pullRequestReviews.d.ts.map +1 -0
  95. package/dist/plus/integrations/providers/pullRequestReviews.js +99 -0
  96. package/dist/plus/integrations/providers/pullRequestReviews.js.map +1 -0
  97. package/dist/plus/integrations/providers/trello.d.ts +3 -9
  98. package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
  99. package/dist/plus/integrations/providers/trello.js +1 -0
  100. package/dist/plus/integrations/providers/trello.js.map +1 -1
  101. package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
  102. package/dist/plus/integrations/providers/utils/providerPaging.js +5 -1
  103. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  104. package/dist/plus/integrations/reads/broaden.d.ts +6 -0
  105. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
  106. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  107. package/dist/plus/integrations/reads/counts.d.ts +63 -1
  108. package/dist/plus/integrations/reads/counts.d.ts.map +1 -1
  109. package/dist/plus/integrations/reads/counts.js +146 -2
  110. package/dist/plus/integrations/reads/counts.js.map +1 -1
  111. package/dist/plus/integrations/reads/drains.d.ts +7 -1
  112. package/dist/plus/integrations/reads/drains.d.ts.map +1 -1
  113. package/dist/plus/integrations/reads/drains.js +13 -2
  114. package/dist/plus/integrations/reads/drains.js.map +1 -1
  115. package/dist/plus/integrations/reads/filters.d.ts +59 -9
  116. package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
  117. package/dist/plus/integrations/reads/filters.js +39 -0
  118. package/dist/plus/integrations/reads/filters.js.map +1 -1
  119. package/dist/plus/integrations/reads/issueTracker.d.ts +18 -1
  120. package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
  121. package/dist/plus/integrations/reads/issueTracker.js +40 -3
  122. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  123. package/dist/plus/integrations/reads/issues.d.ts +27 -3
  124. package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
  125. package/dist/plus/integrations/reads/issues.js +223 -123
  126. package/dist/plus/integrations/reads/issues.js.map +1 -1
  127. package/dist/plus/integrations/reads/ordering.d.ts +101 -0
  128. package/dist/plus/integrations/reads/ordering.d.ts.map +1 -0
  129. package/dist/plus/integrations/reads/ordering.js +48 -0
  130. package/dist/plus/integrations/reads/ordering.js.map +1 -0
  131. package/dist/plus/integrations/reads/searchIssues.d.ts +7 -2
  132. package/dist/plus/integrations/reads/searchIssues.d.ts.map +1 -1
  133. package/dist/plus/integrations/reads/searchIssues.js +9 -3
  134. package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
  135. package/dist/plus/integrations/reads/searchPullRequests.d.ts +4 -3
  136. package/dist/plus/integrations/reads/searchPullRequests.d.ts.map +1 -1
  137. package/dist/plus/integrations/reads/searchPullRequests.js +6 -4
  138. package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
  139. package/dist/plus/integrations/reads/sweeps.js +1 -1
  140. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  141. package/dist/plus/integrations/reads/warnings.d.ts +35 -3
  142. package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
  143. package/dist/plus/integrations/reads/warnings.js +58 -4
  144. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  145. package/dist/plus/integrations/results.d.ts +15 -0
  146. package/dist/plus/integrations/results.d.ts.map +1 -1
  147. package/dist/plus/integrations/results.js.map +1 -1
  148. package/docs/integrations.md +76 -10
  149. package/docs/kepler-read-api-parity.md +9 -0
  150. package/package.json +2 -2
  151. package/src/git/models/issue.ts +73 -0
  152. package/src/git/models/pullRequest.ts +84 -0
  153. package/src/git/utils/issue.utils.ts +91 -1
  154. package/src/git/utils/pullRequest.utils.ts +50 -0
  155. package/src/plus/git-github/api/github.ts +356 -36
  156. package/src/plus/git-github/api/issueSearchQuery.ts +67 -7
  157. package/src/plus/git-github/api/pullRequestSearchQuery.ts +81 -7
  158. package/src/plus/git-github/models.ts +29 -5
  159. package/src/plus/integrations/collectionMetadata.ts +24 -0
  160. package/src/plus/integrations/integrationService.ts +40 -12
  161. package/src/plus/integrations/manager.ts +89 -35
  162. package/src/plus/integrations/models/gitHostIntegration.ts +47 -31
  163. package/src/plus/integrations/models/integration.ts +1 -0
  164. package/src/plus/integrations/models/issueReads.ts +65 -1
  165. package/src/plus/integrations/models/issuesIntegration.ts +7 -6
  166. package/src/plus/integrations/providerFilters.ts +8 -1
  167. package/src/plus/integrations/providers/accounts.ts +34 -0
  168. package/src/plus/integrations/providers/azureDevOps.ts +1 -0
  169. package/src/plus/integrations/providers/github.ts +134 -144
  170. package/src/plus/integrations/providers/gitlab.ts +29 -10
  171. package/src/plus/integrations/providers/issueSorts.ts +89 -0
  172. package/src/plus/integrations/providers/jira.ts +9 -3
  173. package/src/plus/integrations/providers/linear.ts +77 -8
  174. package/src/plus/integrations/providers/models.ts +185 -128
  175. package/src/plus/integrations/providers/providersApi.ts +51 -13
  176. package/src/plus/integrations/providers/pullRequestReviews.ts +122 -0
  177. package/src/plus/integrations/providers/trello.ts +4 -3
  178. package/src/plus/integrations/providers/utils/providerPaging.ts +6 -1
  179. package/src/plus/integrations/reads/broaden.ts +6 -0
  180. package/src/plus/integrations/reads/counts.ts +281 -3
  181. package/src/plus/integrations/reads/drains.ts +12 -2
  182. package/src/plus/integrations/reads/filters.ts +92 -8
  183. package/src/plus/integrations/reads/issueTracker.ts +78 -4
  184. package/src/plus/integrations/reads/issues.ts +386 -194
  185. package/src/plus/integrations/reads/ordering.ts +124 -0
  186. package/src/plus/integrations/reads/searchIssues.ts +15 -3
  187. package/src/plus/integrations/reads/searchPullRequests.ts +12 -4
  188. package/src/plus/integrations/reads/sweeps.ts +1 -1
  189. package/src/plus/integrations/reads/warnings.ts +84 -2
  190. package/src/plus/integrations/results.ts +15 -0
@@ -1,4 +1,4 @@
1
- import type { IssueSearchCriteria, IssueSearchRelationship } from '../../../git/models/issue.js';
1
+ import type { IssueSearchCriteria, IssueSearchRelationship, IssueSorting } from '../../../git/models/issue.js';
2
2
 
3
3
  /**
4
4
  * Translation of the provider-neutral issue-search criteria into GitHub search qualifiers, and the sanitizing
@@ -70,6 +70,56 @@ export const gitHubIssueSearchRelationships: Record<IssueSearchRelationship, { q
70
70
  unassigned: { qualifier: 'no:assignee', alias: 'unassigned' },
71
71
  };
72
72
 
73
+ /**
74
+ * How each orderable key becomes a GitHub `sort:` qualifier.
75
+ *
76
+ * A literal table rather than a derived transform, for the same reason {@link gitHubIssueSearchRelationships} is
77
+ * one: being a `Record` over the union means adding a sort field without deciding its qualifier FAILS THE BUILD,
78
+ * instead of quietly emitting a search with no ordering constraint at all — which is the one failure mode the
79
+ * ordering contract exists to prevent, and the one a caller cannot detect from the result.
80
+ *
81
+ * `updated:desc` maps to the bare `sort:updated`, not `sort:updated-desc`. The two are the same query to GitHub,
82
+ * but the bare form is what this read has always emitted, so keeping it makes the default byte-identical to
83
+ * today's query — and the exact-query tests and recorded fixtures stay valid. Do not "normalize" it.
84
+ *
85
+ * `reactions` maps to the per-emoji `sort:reactions-+1`, NOT the bare `sort:reactions`. GitHub's bare form orders
86
+ * by the total across every reaction type, but the only reaction count on an `IssueShape` is `thumbsUpCount`,
87
+ * which this client fills from `reactions(content: THUMBS_UP)`. Ordering by the total would rank the page by a
88
+ * number the caller cannot see — and would disagree with `getIssueComparator`, which re-sorts a merged page from
89
+ * `thumbsUpCount`, so the same request would order one way with a single relationship and another way with two.
90
+ * `@gitkraken/provider-apis` maps it the same way for the same reason.
91
+ *
92
+ * Absent keys are absent on purpose: `closed` is NOT here even though `IssueShape.closedDate` exists, because
93
+ * GitHub's search cannot order by close date and a client-side sort over a page cut off at the result ceiling is
94
+ * exactly the lie the contract avoids. `sorts` in `githubIssueSearchCapabilities` is the same set, and a parity
95
+ * test pins the two together.
96
+ */
97
+ export const gitHubIssueSortQualifiers: Partial<Record<IssueSorting, string>> = {
98
+ 'created:asc': 'sort:created-asc',
99
+ 'created:desc': 'sort:created-desc',
100
+ 'updated:asc': 'sort:updated-asc',
101
+ 'updated:desc': 'sort:updated',
102
+ 'comments:asc': 'sort:comments-asc',
103
+ 'comments:desc': 'sort:comments-desc',
104
+ 'reactions:asc': 'sort:reactions-+1-asc',
105
+ 'reactions:desc': 'sort:reactions-+1-desc',
106
+ };
107
+
108
+ /**
109
+ * The `sort:` qualifier for a key, or `undefined` when there is no key or GitHub can't express it.
110
+ *
111
+ * Applies no default of its own: the two callers differ on whether they HAVE one — the filtered search always
112
+ * orders, and `searchMyIssues` has never requested an order at all — so the default belongs at the call site that
113
+ * has one rather than behind a guard at the one that doesn't.
114
+ *
115
+ * `undefined` for an inexpressible key rather than a fallback to something orderable: every caller validated
116
+ * against the declared capability first, so reaching that case means the capability table has outrun this one, and
117
+ * a silently unordered search would change which rows land inside the result ceiling.
118
+ */
119
+ export function toGitHubIssueSortQualifier(sort: IssueSorting | undefined): string | undefined {
120
+ return sort != null ? gitHubIssueSortQualifiers[sort] : undefined;
121
+ }
122
+
73
123
  /**
74
124
  * The scope half of an issue search query: `org:` plus one `repo:` per repository.
75
125
  *
@@ -106,11 +156,18 @@ export function toGitHubIssueSearchScopeQualifiers(
106
156
  * becomes its own aliased search, since GitHub AND-s qualifiers and relationships are OR-ed) and excluding the
107
157
  * repository/org scope (which {@link toGitHubIssueSearchScopeQualifiers} owns).
108
158
  *
109
- * `sort:updated` is always emitted: the read's contract is most-recently-updated-first, so a consumer's "show the
110
- * N most recent" policy at the result ceiling is correct. Free-form values go through the sanitizers above, and
111
- * one emptied by sanitizing is dropped rather than emitted as an empty qualifier.
159
+ * A `sort:` qualifier is ALWAYS emitted. Never none: the provider serves at most a bounded window of matches, and
160
+ * without an explicit order GitHub answers in relevance order, so which rows land inside that window would shift
161
+ * with GitHub's ranking even when nothing changed upstream. A key GitHub can't express is refused before this by
162
+ * the facade, so it cannot arrive here.
163
+ *
164
+ * `sort` is passed in rather than read off `criteria` so that the qualifier this emits and the key the caller
165
+ * records elsewhere cannot disagree — see `effectiveIssueSort`.
166
+ *
167
+ * Free-form values go through the sanitizers above, and one emptied by sanitizing is dropped rather than emitted
168
+ * as an empty qualifier.
112
169
  */
113
- export function toGitHubIssueSearchQualifiers(criteria: IssueSearchCriteria | undefined): string[] {
170
+ export function toGitHubIssueSearchQualifiers(criteria: IssueSearchCriteria | undefined, sort: IssueSorting): string[] {
114
171
  const qualifiers = ['type:issue'];
115
172
 
116
173
  switch (criteria?.state) {
@@ -158,8 +215,11 @@ export function toGitHubIssueSearchQualifiers(criteria: IssueSearchCriteria | un
158
215
  }
159
216
  }
160
217
 
161
- // Contract, not an option: see `GitHubApi.searchIssuesPage`.
162
- qualifiers.push('sort:updated');
218
+ // Last, where it has always been: the exact-query tests pin the whole qualifier string, not a set.
219
+ const sortQualifier = toGitHubIssueSortQualifier(sort);
220
+ if (sortQualifier != null) {
221
+ qualifiers.push(sortQualifier);
222
+ }
163
223
 
164
224
  return qualifiers;
165
225
  }
@@ -1,6 +1,10 @@
1
- import type { PullRequestSearchCriteria, PullRequestStateFilter } from '../../../git/models/pullRequest.js';
2
- import { PullRequestFilter } from '../../../git/models/pullRequest.js';
3
- import { sanitizeGitHubSearchText } from './issueSearchQuery.js';
1
+ import type {
2
+ PullRequestSearchCriteria,
3
+ PullRequestSorting,
4
+ PullRequestStateFilter,
5
+ } from '../../../git/models/pullRequest.js';
6
+ import { defaultPullRequestSort, PullRequestFilter } from '../../../git/models/pullRequest.js';
7
+ import { sanitizeGitHubQualifierValue, sanitizeGitHubSearchText } from './issueSearchQuery.js';
4
8
 
5
9
  export type GitHubPullRequestSearchFacet = {
6
10
  /** Stable GraphQL alias and composite-cursor key. */
@@ -8,10 +12,20 @@ export type GitHubPullRequestSearchFacet = {
8
12
  qualifiers: string[];
9
13
  };
10
14
 
11
- const relationshipQualifier: Record<PullRequestFilter, string> = {
15
+ /**
16
+ * The GitHub search qualifier each relationship translates to. Exhaustive over `PullRequestFilter` so a new
17
+ * member can't be silently unmapped.
18
+ *
19
+ * Exported because the account-wide read builds its own facets from the same vocabulary (see
20
+ * `GitHubIntegrationBase.getProviderMyPullRequestsForUser`). Kept as ONE declaration rather than a copy per
21
+ * caller: the exhaustive `Record` makes a missing key a compile error, but nothing would catch two copies
22
+ * disagreeing on a qualifier STRING, and a wrong qualifier is a read that quietly answers a different question.
23
+ */
24
+ export const gitHubPullRequestRelationshipQualifiers: Record<PullRequestFilter, string> = {
12
25
  [PullRequestFilter.Author]: 'author:@me',
13
26
  [PullRequestFilter.Assignee]: 'assignee:@me',
14
27
  [PullRequestFilter.ReviewRequested]: 'review-requested:@me',
28
+ [PullRequestFilter.Reviewed]: 'reviewed-by:@me',
15
29
  [PullRequestFilter.Mention]: 'mentions:@me',
16
30
  };
17
31
 
@@ -19,6 +33,7 @@ const relationshipAlias: Record<PullRequestFilter, string> = {
19
33
  [PullRequestFilter.Author]: 'author',
20
34
  [PullRequestFilter.Assignee]: 'assignee',
21
35
  [PullRequestFilter.ReviewRequested]: 'reviewRequested',
36
+ [PullRequestFilter.Reviewed]: 'reviewed',
22
37
  [PullRequestFilter.Mention]: 'mention',
23
38
  };
24
39
 
@@ -29,6 +44,30 @@ const stateAlias: Record<PullRequestStateFilter, string> = {
29
44
  all: 'All',
30
45
  };
31
46
 
47
+ /**
48
+ * How each orderable key becomes a GitHub `sort:` qualifier.
49
+ *
50
+ * A literal `Record` over the union rather than a derived transform, for the same reason the relationship tables
51
+ * are: adding a sort field without deciding its qualifier fails the build instead of quietly emitting a search
52
+ * with no ordering constraint — the one failure mode the ordering contract exists to prevent, and the one a
53
+ * caller cannot detect from the result.
54
+ *
55
+ * `updated:desc` maps to the bare `sort:updated`, not `sort:updated-desc`. The two are the same query to GitHub,
56
+ * but the bare form is what this read has always emitted, so keeping it makes the default byte-identical to
57
+ * today's query — matching `toGitHubIssueSortQualifier`. Do not "normalize" it.
58
+ */
59
+ export const gitHubPullRequestSortQualifiers: Partial<Record<PullRequestSorting, string>> = {
60
+ 'created:asc': 'sort:created-asc',
61
+ 'created:desc': 'sort:created-desc',
62
+ 'updated:asc': 'sort:updated-asc',
63
+ 'updated:desc': 'sort:updated',
64
+ };
65
+
66
+ /** The `sort:` qualifier for a key, or `undefined` when there is no key or GitHub can't express it. */
67
+ export function toGitHubPullRequestSortQualifier(sort: PullRequestSorting | undefined): string | undefined {
68
+ return sort != null ? gitHubPullRequestSortQualifiers[sort] : undefined;
69
+ }
70
+
32
71
  function stateQualifiers(state: PullRequestStateFilter): string[] {
33
72
  switch (state) {
34
73
  case 'closed':
@@ -62,18 +101,53 @@ export function toGitHubPullRequestSearchFacets(
62
101
  : ['open'];
63
102
  const states: PullRequestStateFilter[] = requestedStates.includes('all') ? ['all'] : requestedStates;
64
103
  const text = criteria?.text != null ? sanitizeGitHubSearchText(criteria.text) : '';
104
+ // Ordering is part of the contract, not an option: without an explicit `sort:` GitHub answers in relevance
105
+ // order, so which rows land inside the result ceiling would shift with its ranking even when nothing changed
106
+ // upstream. A key GitHub can't express is REFUSED here, not approximated with a fallback order — a silent
107
+ // `sort:updated` substitute would emit one order in the query while the merged-page comparator applied another
108
+ // (or none for an unknown field), which is exactly the "the N most recent are what you get" promise the ceiling
109
+ // contract cannot keep under a guessed order. Unreachable through the facade — every key is validated against
110
+ // `githubPullRequestSearchCapabilities.sorts` first — so this guards the direct callers of this exported builder
111
+ // and of `GitHubApi.searchPullRequestsPage`.
112
+ //
113
+ // Deliberately STRICTER than the issue path, which drops an inexpressible qualifier and searches on
114
+ // (`toGitHubIssueSearchQualifiers` pushes `toGitHubIssueSortQualifier`'s result only when it is non-null). The
115
+ // two reads differ in what that costs: an issue search is served by whichever facets the provider ordered, while
116
+ // this one hands its merged page to `getPullRequestComparator`, so a dropped qualifier here means the query and
117
+ // the comparator disagree about the order the ceiling was applied under. Refusing is the only answer that
118
+ // cannot lie about which rows were reachable.
119
+ const sort = criteria?.sort ?? defaultPullRequestSort;
120
+ const sortQualifier = toGitHubPullRequestSortQualifier(sort);
121
+ if (sortQualifier == null) {
122
+ throw new Error(`GitHub cannot order a pull request search by '${sort}'`);
123
+ }
124
+
125
+ // Facet-independent, unlike the relationship/state qualifiers below — build once and share.
126
+ const dateQualifiers: string[] = [];
127
+ const pushDate = (value: string | undefined, toQualifier: (sanitized: string) => string): void => {
128
+ if (value == null) return;
129
+
130
+ const sanitized = sanitizeGitHubQualifierValue(value);
131
+ if (sanitized.length > 0) {
132
+ dateQualifiers.push(toQualifier(sanitized));
133
+ }
134
+ };
135
+ pushDate(criteria?.updatedAfter, v => `updated:>=${v}`);
136
+ pushDate(criteria?.createdAfter, v => `created:>=${v}`);
65
137
 
66
138
  return relationships.flatMap(relationship =>
67
139
  states.map(state => ({
68
140
  alias: `${relationship != null ? relationshipAlias[relationship] : 'scope'}${stateAlias[state]}`,
69
141
  qualifiers: [
70
142
  'is:pr',
71
- ...(relationship != null ? [relationshipQualifier[relationship]] : []),
143
+ ...(relationship != null ? [gitHubPullRequestRelationshipQualifiers[relationship]] : []),
72
144
  ...stateQualifiers(state),
145
+ // `!= null`, not truthy: `draft:false` narrows to ready-for-review PRs just as `draft:true` narrows to drafts.
146
+ ...(criteria?.draft != null ? [`draft:${criteria.draft}`] : []),
73
147
  ...(criteria?.includeArchived === true ? [] : ['archived:false']),
74
148
  ...(text.length > 0 ? [text] : []),
75
- // Contract, not an option: a capped result is the N most recently updated only under this order.
76
- 'sort:updated',
149
+ ...dateQualifiers,
150
+ sortQualifier,
77
151
  ],
78
152
  })),
79
153
  );
@@ -194,6 +194,13 @@ export type GitHubPullRequestMergeableState = 'MERGEABLE' | 'CONFLICTING' | 'UNK
194
194
  export type GitHubPullRequestStatusCheckRollupState = 'SUCCESS' | 'FAILURE' | 'PENDING' | 'EXPECTED' | 'ERROR';
195
195
  export type GitHubPullRequestReviewState = 'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'DISMISSED' | 'PENDING';
196
196
 
197
+ type GitHubPullRequestReview = {
198
+ id: string;
199
+ author: GitHubMember | null;
200
+ state: GitHubPullRequestReviewState;
201
+ commit?: { oid: string } | null;
202
+ };
203
+
197
204
  export interface GitHubPullRequest extends GitHubPullRequestLite {
198
205
  additions: number;
199
206
  assignees: {
@@ -204,11 +211,14 @@ export interface GitHubPullRequest extends GitHubPullRequestLite {
204
211
  mergeable: GitHubPullRequestMergeableState;
205
212
  reviewDecision: GitHubPullRequestReviewDecision;
206
213
  latestReviews: {
207
- nodes: {
208
- author: GitHubMember | null;
209
- state: GitHubPullRequestReviewState;
210
- }[];
214
+ nodes: GitHubPullRequestReview[];
211
215
  };
216
+ /**
217
+ * The current user's own latest review. `latestReviews` is capped, so on a heavily-reviewed pull request
218
+ * the viewer's own review can fall outside that window — which is exactly the row a "needs my review"
219
+ * surface is asking about.
220
+ */
221
+ viewerLatestReview: GitHubPullRequestReview | null;
212
222
  reviewRequests: {
213
223
  nodes: {
214
224
  asCodeOwner: boolean;
@@ -401,6 +411,19 @@ export function fromGitHubPullRequestStatusCheckRollupState(
401
411
  }
402
412
 
403
413
  export function fromGitHubPullRequest(pr: GitHubPullRequest, provider: Provider): PullRequest {
414
+ // `latestReviews` is capped, so keep the viewer's own review even when it falls outside that window, deduped
415
+ // by review id since the two selections overlap. Unsubmitted drafts are dropped from the union rather than
416
+ // from the viewer's side alone: `PENDING` is in GitHub's review-state enum on both selections, and the field
417
+ // this feeds is documented as reviews already SUBMITTED — a draft there tells a "needs my review" consumer
418
+ // the review is done, and a reviewer with a submitted review plus a draft would get two conflicting rows.
419
+ const viewerLatestReview = pr.viewerLatestReview;
420
+ const latestReviews = [
421
+ ...pr.latestReviews.nodes,
422
+ ...(viewerLatestReview != null && !pr.latestReviews.nodes.some(r => r.id === viewerLatestReview.id)
423
+ ? [viewerLatestReview]
424
+ : []),
425
+ ].filter(review => review.state !== 'PENDING');
426
+
404
427
  return new PullRequest(
405
428
  provider,
406
429
  fromGitHubMemberOrGhost(pr.author),
@@ -467,9 +490,10 @@ export function fromGitHubPullRequest(pr: GitHubPullRequest, provider: Provider)
467
490
  : undefined,
468
491
  )
469
492
  .filter(<T>(r?: T): r is T => Boolean(r)),
470
- pr.latestReviews.nodes.map(r => ({
493
+ latestReviews.map(r => ({
471
494
  reviewer: fromGitHubMemberOrGhost(r.author),
472
495
  state: fromGitHubPullRequestReviewState(r.state),
496
+ commitOid: r.commit?.oid,
473
497
  })),
474
498
  pr.assignees.nodes.map(r => ({
475
499
  id: r.login,
@@ -4,12 +4,36 @@ import type {
4
4
  CollectionScope,
5
5
  CollectionScopeFailure,
6
6
  } from '@gitkraken/provider-apis';
7
+ // Matched by its `code` discriminator rather than by `instanceof`: the SDK ships one bundle per entry point, so
8
+ // the class reached through the root is not the same object as the one reached through `/providers`.
9
+ import { isInvalidRequestError, isUnsupportedSortError } from '@gitkraken/provider-apis';
7
10
  import { AuthenticationError, RequestNotFoundError, RequestRateLimitError } from '../../git/errors.js';
8
11
  import type { IntegrationIds } from './constants.js';
9
12
  import { isRateLimitResponse } from './errors.js';
10
13
  import type { ProviderWarning, ProviderWarningOmission } from './results.js';
11
14
  import { appendDedupedWarning } from './results.js';
12
15
 
16
+ /**
17
+ * Re-throws an error that is a fact about the CALL rather than about one scope of a fan-out.
18
+ *
19
+ * Every provider fan-out catches per-scope rejections and records them as {@link CollectionScopeFailure}s, which is
20
+ * right for auth, rate limits and a missing project: those really did happen to one scope, and the siblings that
21
+ * succeeded must survive. A caller-contract error is the opposite — it is identical for every scope and was decided
22
+ * before any request went out — so degrading it would hand back N indistinguishable failures and an empty `partial`
23
+ * page, describing an invalid call as an incomplete read.
24
+ *
25
+ * Today that means the SDK's `UnsupportedSortError` and `InvalidRequestError` — the two codes it lists in its own
26
+ * `CALLER_CONTRACT_ERROR_CODES`, both documented as "never degraded into a `CollectionScopeFailure` or an
27
+ * omission". The second covers arguments rejected before the first request goes out: a project key list longer
28
+ * than one Jira search can carry (or an empty one), a non-integer `pageSize`, an unknown current-user filter, or a
29
+ * Jira Server read with no `baseUrl` to address. Called from every fan-out rather than spelled out in each, so a
30
+ * fan-out added later inherits the rule instead of silently omitting it — which is exactly how Azure's
31
+ * account-wide drain came to degrade it while its three siblings did not.
32
+ */
33
+ export function throwIfCallerContractError(ex: unknown): void {
34
+ if (isUnsupportedSortError(ex) || isInvalidRequestError(ex)) throw ex;
35
+ }
36
+
13
37
  /**
14
38
  * Maps a caught GitLens request error to the SDK collection failure vocabulary used inside provider fan-outs.
15
39
  */
@@ -67,19 +67,24 @@ import { createApiClients } from './providers/apiClients.js';
67
67
  import type { GitHubApi } from './providers/github/github.js';
68
68
  import type {
69
69
  IssueFilter,
70
- IssueSearchCapabilities,
70
+ IssueSorting,
71
71
  ProviderOrganization,
72
72
  ProviderReposInput,
73
73
  ProviderRepositoryShape,
74
74
  PullRequestFilter,
75
- PullRequestSearchCapabilities,
76
75
  } from './providers/models.js';
77
76
  import { providersMetadata } from './providers/models.js';
78
77
  import type { ProvidersApi } from './providers/providersApi.js';
79
78
  import { broadenIssues } from './reads/broaden.js';
80
79
  import type { RepositoryResolutionContext } from './reads/context.js';
81
- import type { IssueCountResult, IssueCountScope } from './reads/counts.js';
82
- import { countIssues } from './reads/counts.js';
80
+ import type {
81
+ IssueCountResult,
82
+ IssueCountScope,
83
+ PullRequestCountResult,
84
+ PullRequestCountScope,
85
+ } from './reads/counts.js';
86
+ import { countIssues, countPullRequests } from './reads/counts.js';
87
+ import type { SupportedFilters } from './reads/filters.js';
83
88
  import { getSupportedFilters } from './reads/filters.js';
84
89
  import { listOrgs, listProjects, listRepos } from './reads/hierarchy.js';
85
90
  import { listIssuesPage } from './reads/issues.js';
@@ -537,14 +542,7 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
537
542
  * stays a method here even though it needs no instance state; see {@link getSupportedFilters} for the
538
543
  * capability table itself and why a consumer should intersect against it before reading.
539
544
  */
540
- getSupportedFilters(providerId: IntegrationIds): {
541
- pullRequests: PullRequestFilter[];
542
- pullRequestsAccountWide: PullRequestFilter[];
543
- pullRequestSearch: PullRequestSearchCapabilities;
544
- issues: IssueFilter[];
545
- issuesAccountWide: IssueFilter[];
546
- issueSearch: IssueSearchCapabilities;
547
- } {
545
+ getSupportedFilters(providerId: IntegrationIds): SupportedFilters {
548
546
  return getSupportedFilters(providerId);
549
547
  }
550
548
 
@@ -1092,6 +1090,13 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
1092
1090
  filters?: IssueFilter[];
1093
1091
  /** Broadens the read to every assignee. Contradicts `filters`; passing both is refused. */
1094
1092
  includeAllAssignees?: boolean;
1093
+ /**
1094
+ * How to order the page, as `field:direction`. Omitted orders most-recently-updated-first wherever the
1095
+ * provider can express it. Validated against `getSupportedFilters().issueSorts` on the repo-scoped path and
1096
+ * `.issueSortsAccountWide` on the account-wide one, and refused rather than downgraded; a key no normalized
1097
+ * issue carries is additionally refused for a page spanning several repositories/projects, which is a merge.
1098
+ */
1099
+ sort?: IssueSorting;
1095
1100
  page?: number;
1096
1101
  cursor?: string;
1097
1102
  itemsPerPage?: number;
@@ -1147,6 +1152,24 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
1147
1152
  return countIssues(this, options);
1148
1153
  }
1149
1154
 
1155
+ /**
1156
+ * How many pull requests match each scope, without fetching any — the PR twin of {@link countIssues}. See
1157
+ * {@link IntegrationManager.countPullRequests} for the cost model, the per-scope isolation rules, and why
1158
+ * `count: undefined` must not be rendered as zero.
1159
+ */
1160
+ async countPullRequests(options: {
1161
+ providerId: IntegrationIds;
1162
+ scopes: readonly PullRequestCountScope[];
1163
+ connectionId?: string;
1164
+ /**
1165
+ * Explicit self-managed host domain. Used only when the requested connection has no configured domain;
1166
+ * it must come from the trusted authentication configuration, not repository or remote data.
1167
+ */
1168
+ domain?: string;
1169
+ }): Promise<ProviderResult<PullRequestCountResult>> {
1170
+ return countPullRequests(this, options);
1171
+ }
1172
+
1150
1173
  /**
1151
1174
  * Reads the user's issues from an issue-tracker provider (Jira/Linear/Trello), whose issues live under
1152
1175
  * resource → project (not repos), so they can't go through {@link listIssuesPage} (git-host, repo-scoped).
@@ -1174,6 +1197,11 @@ export class IntegrationService implements Disposable, RepositoryResolutionConte
1174
1197
  project?: string;
1175
1198
  filters?: IssueFilter[];
1176
1199
  includeAllAssignees?: boolean;
1200
+ /**
1201
+ * How to order the issues, as `field:direction`. Validated against `getSupportedFilters().issueSorts`, which
1202
+ * is where a tracker reports; a key no normalized issue carries is refused for a multi-project page.
1203
+ */
1204
+ sort?: IssueSorting;
1177
1205
  forceSync?: boolean;
1178
1206
  page?: number;
1179
1207
  cursor?: string;
@@ -8,13 +8,14 @@ import type { Event } from '../../utils/event.js';
8
8
  import type { ConfiguredIntegrationsChangeEvent } from './authentication/configuredIntegrationService.js';
9
9
  import type { ConfiguredIntegrationDescriptor } from './authentication/models.js';
10
10
  import type { IntegrationIds } from './constants.js';
11
+ import type { IssueFilter, IssueSorting, PullRequestFilter } from './providerFilters.js';
11
12
  import type {
12
- IssueFilter,
13
- IssueSearchCapabilities,
14
- PullRequestFilter,
15
- PullRequestSearchCapabilities,
16
- } from './providerFilters.js';
17
- import type { IssueCountResult, IssueCountScope } from './reads/counts.js';
13
+ IssueCountResult,
14
+ IssueCountScope,
15
+ PullRequestCountResult,
16
+ PullRequestCountScope,
17
+ } from './reads/counts.js';
18
+ import type { SupportedFilters } from './reads/filters.js';
18
19
  import type {
19
20
  ConnectionStateChangeEvent,
20
21
  ProviderBroadenResult,
@@ -160,6 +161,22 @@ type PullRequestSweepCommonOptions = {
160
161
  * This extends the provider-native result; it is not a narrowing filter.
161
162
  */
162
163
  includeReviewRequested?: boolean;
164
+ /**
165
+ * Account-wide only: fetch the full PR projection (review decision, review requests, latest reviews)
166
+ * instead of the default summary/lite one. Opt-in because it enlarges every account-wide page; callers
167
+ * that classify by review state (e.g. a "needs my review" surface) set it, others keep the lean read.
168
+ *
169
+ * GitHub/GHE only today — no other provider's account-wide read has a projection switch, so they return
170
+ * their native shape regardless. A cross-provider sweep therefore gets reviews from GitHub targets and
171
+ * whatever the others natively carry; it is a breadth request, not a guarantee, so it does not refuse
172
+ * (unlike `filters`, where dropping a member would change the requested set).
173
+ *
174
+ * Projection only: it cannot change which pull requests come back. Every GitHub account-wide read goes through
175
+ * the same search whether or not it is set, so the Launchpad ignored-repository and organization qualifiers
176
+ * (and the per-facet result ceiling) apply the same either way. What it costs is the reduced page size, and a
177
+ * document whose weight scales with the number of relationship × state facets in flight.
178
+ */
179
+ includeReviews?: boolean;
163
180
  forceSync?: boolean;
164
181
  maxPages?: number;
165
182
  /**
@@ -281,24 +298,7 @@ export interface IntegrationManager {
281
298
  * This is a capability table, not a recommendation: a consumer matching another tool's behavior may pass fewer
282
299
  * filters than are listed, or none where the underlying read is already scoped.
283
300
  */
284
- getSupportedFilters(providerId: IntegrationIds): {
285
- pullRequests: PullRequestFilter[];
286
- /** Optional for structural compatibility; missing means no account-wide narrowing filters are supported. */
287
- pullRequestsAccountWide?: PullRequestFilter[];
288
- /**
289
- * Criteria and scopes {@link searchPullRequestsPage} can express. Always present; an empty relationship list
290
- * means the provider exposes no filtered pull-request search.
291
- */
292
- pullRequestSearch: PullRequestSearchCapabilities;
293
- issues: IssueFilter[];
294
- issuesAccountWide: IssueFilter[];
295
- /**
296
- * What {@link searchIssuesPage} (and {@link countIssues}, over the same criteria) can express for this
297
- * provider. Always present: a provider with no filtered issue search reports an empty `relationships` and
298
- * all-false flags, which is the signal to hide that surface rather than to hide individual chips.
299
- */
300
- issueSearch: IssueSearchCapabilities;
301
- };
301
+ getSupportedFilters(providerId: IntegrationIds): SupportedFilters;
302
302
  /** Forces an authoritative cloud connection refresh. Rejects if the backend connection list cannot be read. */
303
303
  refreshConnections(): Promise<void>;
304
304
  /** Rejects unless `connectionId` is a configured cloud connection for `id`. */
@@ -376,10 +376,17 @@ export interface IntegrationManager {
376
376
  * GitHub's mismatched `involves:@me`. Omit relationships only with a repository/organization scope to search
377
377
  * every PR there.
378
378
  *
379
- * Results are always ordered most-recently-updated-first. If the provider's result ceiling is reached, the
380
- * request still succeeds and carries a warning omission with `totalCount`, `limit`, and `recovery: 'none'`.
381
- * `totalCount` is the largest provider-reported pre-ceiling facet count, not the reachable or returned row
382
- * count; this mirrors the per-search ceiling's own unit.
379
+ * **Ordering is `criteria.sort`**, most-recently-updated-first when omitted. It is validated like every other
380
+ * criterion a key not in `getSupportedFilters().pullRequestSearch.sorts` refuses the whole read rather than
381
+ * falling back, because at a bounded result window another order reaches another subset. The page is a union of
382
+ * the provider's relationship × state facets, so it is re-ordered as a whole rather than served as concatenated
383
+ * per-facet runs. Changing the sort invalidates a threaded cursor (it is part of the cursor's fingerprint,
384
+ * alongside the text and the scope); drop the cursor when you change the order.
385
+ *
386
+ * If the provider's result ceiling is reached, the request still succeeds and carries a warning omission with
387
+ * `totalCount`, `limit`, `sort`, and `recovery: 'none'`. `totalCount` is the largest provider-reported
388
+ * pre-ceiling facet count, not the reachable or returned row count; this mirrors the per-search ceiling's own
389
+ * unit.
383
390
  *
384
391
  * Check `getSupportedFilters().pullRequestSearch` before calling. A provider that reports no relationships
385
392
  * refuses the read rather than returning a page that never honored the criteria or scope.
@@ -427,6 +434,21 @@ export interface IntegrationManager {
427
434
  filters?: IssueFilter[];
428
435
  /** Broadens to every assignee. On account-wide reads it contradicts `filters`, so passing both is refused. */
429
436
  includeAllAssignees?: boolean;
437
+ /**
438
+ * How to order the page, as `field:direction` (e.g. `updated:desc`, `created:asc`). Omitted orders
439
+ * most-recently-updated-first wherever the provider can express it — this facade's default, not the
440
+ * provider's, which differ from each other.
441
+ *
442
+ * Validated against `getSupportedFilters().issueSorts` when `repos` is supplied and
443
+ * `.issueSortsAccountWide` when it isn't, and refused (warning + `fetchFailed`) rather than downgraded: at
444
+ * the provider's reachable window another order is another subset, so a silently different one is
445
+ * indistinguishable from the one asked for. Keep it fixed across a pagination.
446
+ *
447
+ * With SEVERAL repositories/projects the page is a merge of one query each, so only a key a normalized
448
+ * issue carries can be honored — `priority`, `dueDate` and `resolved` are refused there even where the
449
+ * provider supports them on a single scope. The order applies within a page; across pages it is per scope.
450
+ */
451
+ sort?: IssueSorting;
430
452
  /**
431
453
  * Requested 1-based page. Without a `cursor` this may cost O(page) upstream requests on cursor-backed
432
454
  * reads such as repo-scoped GitHub/GHE; aggregate single-page account-wide reads remain O(1).
@@ -454,16 +476,21 @@ export interface IntegrationManager {
454
476
  * - **Scope is mandatory.** Pass `repos`, `org`, or a user relationship (`authored`/`assigned`/`mentioned`).
455
477
  * `any-assignee`/`unassigned` do NOT scope anything — either alone matches every such issue on the host —
456
478
  * so a call carrying only those is refused (warning + `fetchFailed`).
457
- * - **Ordering is always most-recently-updated-first**, not an option. A "show the N most recent" policy at
458
- * the provider's result ceiling is only correct under a guaranteed order.
479
+ * - **Ordering is `criteria.sort`**, most-recently-updated-first when omitted. It is validated like every other
480
+ * criterion (against `getSupportedFilters().issueSearch.sorts`) and refused rather than downgraded, and SOME
481
+ * order is always requested — a "show the N most recent" policy at the provider's result ceiling is only
482
+ * correct under a guaranteed one. Keep it fixed for the life of a pagination: a cursor is bound to the order
483
+ * that produced it, and threading it under a different key is REFUSED (warning + `fetchFailed`) rather than
484
+ * resumed into a differently-ordered set. To change the order, drop the cursor and read from the first page.
459
485
  * - **At the result ceiling the read SUCCEEDS.** It reports an omission carrying `totalCount` (how many
460
- * matched) and `limit` (how many are reachable) with `recovery: 'none'`, so a consumer can say "19.240
461
- * matched, showing the 1.000 most recent" and know not to offer a "load more". It never falls back to a
462
- * per-repository recovery walk.
486
+ * matched), `limit` (how many are reachable) and `sort` (the order that window was selected under) with
487
+ * `recovery: 'none'`, so a consumer can say "19.240 matched, showing the 1.000 most recent" naming the
488
+ * order, since which 1.000 are reachable depends on it — and know not to offer a "load more". It never falls
489
+ * back to a per-repository recovery walk.
463
490
  *
464
491
  * Check `getSupportedFilters().issueSearch` first: a provider with no filtered issue search reports empty
465
- * relationships (and this read refuses), and a criterion it can't express refuses the whole read rather than
466
- * serving a wider result than asked for.
492
+ * relationships (and this read refuses), and a criterion or sort key it can't express refuses the whole read
493
+ * rather than serving a wider or differently-ordered result than asked for.
467
494
  */
468
495
  searchIssuesPage(options: {
469
496
  providerId: IntegrationIds;
@@ -519,6 +546,23 @@ export interface IntegrationManager {
519
546
  /** Self-managed host domain fallback; see {@link ProviderSweepTarget.domain}. */
520
547
  domain?: string;
521
548
  }): Promise<ProviderResult<IssueCountResult>>;
549
+ /**
550
+ * How many pull requests match each scope, fetching none of them — the PR twin of {@link countIssues}, behind a
551
+ * "this will fetch ~N pull requests" preview and a live count next to an unapplied filter. Same cost model,
552
+ * per-scope isolation, `key`-echo, and `count: undefined` ≠ zero rule as the issue count; GitHub/GHE only.
553
+ *
554
+ * The one PR-specific difference: a scope's `states` are counted as independent searches, so the reported count
555
+ * is the LARGEST of them (the same total {@link searchPullRequestsPage} surfaces), not their sum. Several states
556
+ * in one scope are therefore fine; only several relationships are refused — one relationship per scope, see
557
+ * {@link PullRequestCountScope}.
558
+ */
559
+ countPullRequests(options: {
560
+ providerId: IntegrationIds;
561
+ scopes: readonly PullRequestCountScope[];
562
+ connectionId?: string;
563
+ /** Self-managed host domain fallback; see {@link ProviderSweepTarget.domain}. */
564
+ domain?: string;
565
+ }): Promise<ProviderResult<PullRequestCountResult>>;
522
566
  /** Issue trackers are cloud-only, so this read takes no `domain`. */
523
567
  listIssueTrackerIssuesPage(options: {
524
568
  providerId: IntegrationIds;
@@ -526,6 +570,16 @@ export interface IntegrationManager {
526
570
  project?: string;
527
571
  filters?: IssueFilter[];
528
572
  includeAllAssignees?: boolean;
573
+ /**
574
+ * How to order the issues, as `field:direction`. Omitted orders most-recently-updated-first where the
575
+ * tracker can express it; validated against `getSupportedFilters().issueSorts` and refused rather than
576
+ * downgraded. A page spanning several projects is a merge, so a key no normalized issue carries
577
+ * (`priority`, `dueDate`, `resolved`) is refused there.
578
+ *
579
+ * Safe to change between pages, unlike the git-host reads: this cursor windows PROJECTS and drains each in
580
+ * full, so which projects a round covers doesn't depend on how their issues are ordered.
581
+ */
582
+ sort?: IssueSorting;
529
583
  forceSync?: boolean;
530
584
  page?: number;
531
585
  /**