@gitkraken/core-gitlens 0.7.0 → 0.7.2

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 (125) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/dist/git/remotes/azure-devops.js +3 -3
  3. package/dist/git/remotes/azure-devops.js.map +1 -1
  4. package/dist/plus/integrations/authentication/configuredIntegrationService.js +9 -2
  5. package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
  6. package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +1 -1
  7. package/dist/plus/integrations/collectionMetadata.d.ts +48 -3
  8. package/dist/plus/integrations/collectionMetadata.js +138 -8
  9. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  10. package/dist/plus/integrations/errors.d.ts +3 -1
  11. package/dist/plus/integrations/errors.js +7 -3
  12. package/dist/plus/integrations/errors.js.map +1 -1
  13. package/dist/plus/integrations/index.d.ts +3 -3
  14. package/dist/plus/integrations/index.js.map +1 -1
  15. package/dist/plus/integrations/integrationService.d.ts +35 -1
  16. package/dist/plus/integrations/integrationService.js +90 -43
  17. package/dist/plus/integrations/integrationService.js.map +1 -1
  18. package/dist/plus/integrations/lite.js +5 -4
  19. package/dist/plus/integrations/lite.js.map +1 -1
  20. package/dist/plus/integrations/manager.d.ts +68 -23
  21. package/dist/plus/integrations/models/gitHostIntegration.d.ts +31 -15
  22. package/dist/plus/integrations/models/gitHostIntegration.js +71 -22
  23. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  24. package/dist/plus/integrations/models/integration.d.ts +70 -12
  25. package/dist/plus/integrations/models/integration.js +56 -1
  26. package/dist/plus/integrations/models/integration.js.map +1 -1
  27. package/dist/plus/integrations/models/issueReads.d.ts +17 -0
  28. package/dist/plus/integrations/models/issuesIntegration.js +2 -0
  29. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  30. package/dist/plus/integrations/models/pullRequestReads.d.ts +26 -0
  31. package/dist/plus/integrations/providers/azure/azure.d.ts +66 -1
  32. package/dist/plus/integrations/providers/azure/azure.js +192 -12
  33. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  34. package/dist/plus/integrations/providers/azure/search.d.ts +144 -0
  35. package/dist/plus/integrations/providers/azure/search.js +250 -0
  36. package/dist/plus/integrations/providers/azure/search.js.map +1 -0
  37. package/dist/plus/integrations/providers/azureDevOps.d.ts +73 -36
  38. package/dist/plus/integrations/providers/azureDevOps.js +191 -122
  39. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  40. package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +176 -0
  41. package/dist/plus/integrations/providers/azureDevOpsServer.js +560 -0
  42. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -0
  43. package/dist/plus/integrations/providers/bitbucket-server/discovery.d.ts +12 -0
  44. package/dist/plus/integrations/providers/bitbucket-server/discovery.js +128 -0
  45. package/dist/plus/integrations/providers/bitbucket-server/discovery.js.map +1 -0
  46. package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +10 -1
  47. package/dist/plus/integrations/providers/bitbucket-server/models.js +3 -2
  48. package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
  49. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +58 -0
  50. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +485 -0
  51. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -0
  52. package/dist/plus/integrations/providers/bitbucket-server.d.ts +61 -3
  53. package/dist/plus/integrations/providers/bitbucket-server.js +114 -20
  54. package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
  55. package/dist/plus/integrations/providers/bitbucket.d.ts +9 -1
  56. package/dist/plus/integrations/providers/bitbucket.js +13 -2
  57. package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
  58. package/dist/plus/integrations/providers/github.d.ts +6 -6
  59. package/dist/plus/integrations/providers/github.js +35 -34
  60. package/dist/plus/integrations/providers/github.js.map +1 -1
  61. package/dist/plus/integrations/providers/gitlab.d.ts +6 -4
  62. package/dist/plus/integrations/providers/gitlab.js +32 -28
  63. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  64. package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
  65. package/dist/plus/integrations/providers/jira-server.d.ts +9 -0
  66. package/dist/plus/integrations/providers/jira-server.js +15 -7
  67. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  68. package/dist/plus/integrations/providers/jira.d.ts +9 -0
  69. package/dist/plus/integrations/providers/jira.js +14 -2
  70. package/dist/plus/integrations/providers/jira.js.map +1 -1
  71. package/dist/plus/integrations/providers/linear.js +2 -2
  72. package/dist/plus/integrations/providers/models.d.ts +10 -1
  73. package/dist/plus/integrations/providers/models.js +86 -0
  74. package/dist/plus/integrations/providers/models.js.map +1 -1
  75. package/dist/plus/integrations/providers/providersApi.d.ts +10 -0
  76. package/dist/plus/integrations/providers/providersApi.js +18 -3
  77. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  78. package/dist/plus/integrations/providers/utils.d.ts +12 -1
  79. package/dist/plus/integrations/providers/utils.js +36 -6
  80. package/dist/plus/integrations/providers/utils.js.map +1 -1
  81. package/dist/plus/integrations/reads/broaden.js +6 -6
  82. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  83. package/dist/plus/integrations/reads/context.d.ts +1 -1
  84. package/dist/plus/integrations/reads/counts.d.ts +18 -4
  85. package/dist/plus/integrations/reads/counts.js +66 -15
  86. package/dist/plus/integrations/reads/counts.js.map +1 -1
  87. package/dist/plus/integrations/reads/cursors.js +1 -1
  88. package/dist/plus/integrations/reads/drains.d.ts +2 -1
  89. package/dist/plus/integrations/reads/drains.js +1 -1
  90. package/dist/plus/integrations/reads/filters.d.ts +1 -1
  91. package/dist/plus/integrations/reads/filters.js +17 -12
  92. package/dist/plus/integrations/reads/filters.js.map +1 -1
  93. package/dist/plus/integrations/reads/hierarchy.js +2 -2
  94. package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
  95. package/dist/plus/integrations/reads/hierarchy.utils.js +1 -1
  96. package/dist/plus/integrations/reads/issueBatch.js +1 -1
  97. package/dist/plus/integrations/reads/issueTracker.js +2 -2
  98. package/dist/plus/integrations/reads/issues.js +2 -2
  99. package/dist/plus/integrations/reads/paging.js +1 -1
  100. package/dist/plus/integrations/reads/resolveRepository.d.ts +12 -11
  101. package/dist/plus/integrations/reads/resolveRepository.js +110 -19
  102. package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
  103. package/dist/plus/integrations/reads/searchIssues.js +4 -2
  104. package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
  105. package/dist/plus/integrations/reads/searchPullRequests.js +9 -3
  106. package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
  107. package/dist/plus/integrations/reads/sweeps.js +1 -1
  108. package/dist/plus/integrations/reads/trackerIssue.d.ts +9 -5
  109. package/dist/plus/integrations/reads/trackerIssue.js +15 -9
  110. package/dist/plus/integrations/reads/trackerIssue.js.map +1 -1
  111. package/dist/plus/integrations/reads/warnings.d.ts +6 -2
  112. package/dist/plus/integrations/reads/warnings.js +15 -19
  113. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  114. package/dist/plus/integrations/results.d.ts +84 -6
  115. package/dist/plus/integrations/results.js +9 -2
  116. package/dist/plus/integrations/results.js.map +1 -1
  117. package/dist/plus/integrations/utils/domain.utils.d.ts +19 -0
  118. package/dist/plus/integrations/utils/domain.utils.js +36 -1
  119. package/dist/plus/integrations/utils/domain.utils.js.map +1 -1
  120. package/dist/plus/integrations/utils/integration.utils.d.ts +1 -0
  121. package/dist/plus/integrations/utils/integration.utils.js +12 -1
  122. package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
  123. package/docs/integrations.md +263 -74
  124. package/docs/kepler-read-api-parity.md +19 -6
  125. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { chunk } from "../../../utils/array.js";
2
2
  import { mapBounded } from "../../../utils/promise.js";
3
- import { appendDedupedWarning } from "../results.js";
4
3
  import "../constants.js";
4
+ import { appendDedupedWarning, toProviderWarning } from "../results.js";
5
5
  import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
6
6
  import { providersMetadata } from "../providers/models.js";
7
7
  import { gitHostOnlySurfaceWarning, issuesUnsupportedWarning, otherWarning, unsupportedIssueSearchCriteriaWarning, unsupportedPullRequestSearchCriteriaWarning, unusableSearchScopeMessage } from "./warnings.js";
@@ -23,9 +23,20 @@ const issueCountChunkSize = 25;
23
23
  * The same latency/complexity reasoning as {@link issueCountChunkSize}, kept slightly smaller because a pull-request
24
24
  * scope can fan out into one aliased count PER requested state (open/closed/merged), so a chunk of scopes carries a
25
25
  * small multiple of that many `search` aliases.
26
+ *
27
+ * A provider that counts by reading batches nothing, so it is chunked one scope at a time instead: a chunk is then
28
+ * just a scope, and the facade's bounded concurrency is what paces the requests.
26
29
  */
27
30
  const pullRequestCountChunkSize = 15;
28
31
  /**
32
+ * Whether a provider counts pull requests by READING the matches rather than asking for a total — Bitbucket Data
33
+ * Center, which has no count query. Two rules follow from it: it batches nothing, and it deduplicates the rows
34
+ * themselves, so a relationship set is an exact union it can count in one scope rather than a refusal.
35
+ */
36
+ function countsPullRequestsByReading(providerId) {
37
+ return providerId === "bitbucket-server";
38
+ }
39
+ /**
29
40
  * Counts issues for many scopes without fetching any — the probe behind a "this will fetch ~N issues" preview.
30
41
  *
31
42
  * A distinct method rather than a `countOnly` flag on `searchIssuesPage`, because transferring ZERO issues is the
@@ -97,15 +108,13 @@ async function countIssues(ctx, options) {
97
108
  fetchFailed = true;
98
109
  continue;
99
110
  }
100
- for (let i = 0; i < batch.length; i++) {
101
- const count = value[i];
102
- items.push({
103
- key: batch[i].key,
104
- count,
105
- exceedsProviderLimit: count != null && providerLimit != null && count > providerLimit,
106
- providerLimit
111
+ fetchFailed = collectCounts(batch, value, providerLimit, items, (key, error) => {
112
+ const warning = toProviderWarning(options.providerId, domain, options.connectionId, error);
113
+ appendDedupedWarning(warnings, {
114
+ ...warning,
115
+ message: `Issue count scope '${key}': ${warning.message}`
107
116
  });
108
- }
117
+ }) || fetchFailed;
109
118
  }
110
119
  if (items.length === 0 && warnings.length === 0) return refused(unsupportedIssueSearchCriteriaWarning(options.providerId, domain, options.connectionId, { reason: "unsupported-search" }));
111
120
  return {
@@ -114,6 +123,33 @@ async function countIssues(ctx, options) {
114
123
  fetchFailed: fetchFailed || void 0
115
124
  };
116
125
  }
126
+ /**
127
+ * Turns one batch of positional issue counts into keyed results, returning whether any scope was refused.
128
+ *
129
+ * A slot holding an `Error` is a scope the PROVIDER refused for its own reasons (a scope naming a collection it
130
+ * can't search, say): reported and dropped exactly like a scope the facade refuses, so its siblings in the batch
131
+ * still come back. `'exceeds-limit'` is a count the provider can only bound — more than its ceiling matched — which
132
+ * is reported as `exceedsProviderLimit` with no `count` rather than as the ceiling, which would understate it.
133
+ */
134
+ function collectCounts(batch, value, providerLimit, items, onRefused) {
135
+ let refused = false;
136
+ for (let i = 0; i < batch.length; i++) {
137
+ const count = value[i];
138
+ if (count instanceof Error) {
139
+ onRefused(batch[i].key, count);
140
+ refused = true;
141
+ continue;
142
+ }
143
+ const exceeds = count === "exceeds-limit";
144
+ items.push({
145
+ key: batch[i].key,
146
+ count: exceeds ? void 0 : count,
147
+ exceedsProviderLimit: exceeds || count != null && providerLimit != null && count > providerLimit,
148
+ providerLimit
149
+ });
150
+ }
151
+ return refused;
152
+ }
117
153
  /** The first key that appears twice, or `undefined` when every key is unique. */
118
154
  function findDuplicateKey(scopes) {
119
155
  const seen = /* @__PURE__ */ new Set();
@@ -133,7 +169,7 @@ function findDuplicateKey(scopes) {
133
169
  function rejectScope(providerId, domain, connectionId, scope) {
134
170
  const resolved = resolveIssueSearchCriteria(providerId, scope.criteria);
135
171
  if (resolved.rejection != null) return unsupportedIssueSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);
136
- const scoping = resolveIssueSearchScope(scope.repos, scope.org, scope.criteria);
172
+ const scoping = resolveIssueSearchScope(providerId, scope.repos, scope.org, scope.criteria);
137
173
  switch (scoping.rejection?.reason) {
138
174
  case "repo-ids": return otherWarning(providerId, domain, connectionId, `Issue count scope '${scope.key}' cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.`);
139
175
  case "unscoped": return otherWarning(providerId, domain, connectionId, `Issue count scope '${scope.key}' is unscoped; pass \`repos\`, \`org\`, or a relationship to the current user. \`any-assignee\` and \`unassigned\` are not scopes.`);
@@ -147,9 +183,12 @@ function rejectScope(providerId, domain, connectionId, scope) {
147
183
  *
148
184
  * Identical batching, per-scope isolation, and `key`-echo contract as {@link countIssues}; see it for the cost
149
185
  * model and the `count: undefined` ≠ zero rule. The one difference is inherent to pull requests: a scope's criteria
150
- * can name several STATES, which the provider counts as independent searches — the reported `count` is the LARGEST
186
+ * can name several STATES, which GitHub/GHE count as independent searches — the reported `count` is the LARGEST
151
187
  * of them (mirroring `searchPullRequestsPage`'s total), so an unpaged read still can't exceed a single search's
152
- * ceiling undetected.
188
+ * ceiling undetected. Bitbucket Data Center has no ceiling and no count query: it counts the exact union by reading
189
+ * each scope within a budget, and flags a scope that had more as `lowerBound`. Because it deduplicates rows, it is
190
+ * also the one provider that accepts several relationships in one scope. Azure DevOps Server counts the union of the
191
+ * states from the one drain its search pages through.
153
192
  */
154
193
  async function countPullRequests(ctx, options) {
155
194
  const refused = (warning) => ({
@@ -191,7 +230,8 @@ async function countPullRequests(ctx, options) {
191
230
  }
192
231
  countable.push(scope);
193
232
  }
194
- const batches = await mapBounded(chunk(countable, pullRequestCountChunkSize), 6, (batch) => runCaptured(options.providerId, domain, options.connectionId, () => integration.countPullRequestsResult(batch.map((s) => ({
233
+ const chunks = chunk(countable, countsPullRequestsByReading(options.providerId) ? 1 : pullRequestCountChunkSize);
234
+ const batches = await mapBounded(chunks, 6, (batch) => runCaptured(options.providerId, domain, options.connectionId, () => integration.countPullRequestsResult(batch.map((s) => ({
195
235
  repos: s.repos,
196
236
  org: s.org,
197
237
  criteria: s.criteria
@@ -207,10 +247,21 @@ async function countPullRequests(ctx, options) {
207
247
  continue;
208
248
  }
209
249
  for (let i = 0; i < batch.length; i++) {
210
- const count = value[i];
250
+ const slot = value[i];
251
+ if (slot instanceof Error) {
252
+ const warning = toProviderWarning(options.providerId, domain, options.connectionId, slot);
253
+ appendDedupedWarning(warnings, {
254
+ ...warning,
255
+ message: `Pull request count scope '${batch[i].key}': ${warning.message}`
256
+ });
257
+ fetchFailed = true;
258
+ continue;
259
+ }
260
+ const count = slot?.count;
211
261
  items.push({
212
262
  key: batch[i].key,
213
263
  count,
264
+ ...count != null && slot?.lowerBound === true ? { lowerBound: true } : {},
214
265
  exceedsProviderLimit: count != null && providerLimit != null && count > providerLimit,
215
266
  providerLimit
216
267
  });
@@ -243,7 +294,7 @@ function rejectPullRequestScope(providerId, domain, connectionId, scope) {
243
294
  }
244
295
  const resolved = resolvePullRequestSearchCriteria(providerId, scope.criteria);
245
296
  if (resolved.rejection != null) return unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);
246
- if ((scope.criteria?.relationships?.length ?? 0) > 1) return otherWarning(providerId, domain, connectionId, `Pull request count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`);
297
+ if ((scope.criteria?.relationships?.length ?? 0) > 1 && !countsPullRequestsByReading(providerId)) return otherWarning(providerId, domain, connectionId, `Pull request count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`);
247
298
  }
248
299
  //#endregion
249
300
  export { countIssues, countPullRequests };
@@ -1 +1 @@
1
- {"version":3,"file":"counts.js","names":[],"sources":["../../../../../plus/integrations/src/reads/counts.ts"],"sourcesContent":["import type { IssueSearchCriteria } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSearchCriteria } from '@gitlens/git/models/pullRequest.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';\nimport { providersMetadata } from '../providers/models.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tresolveIssueSearchCriteria,\n\tresolveIssueSearchScope,\n\tresolvePullRequestSearchCriteria,\n\tresolvePullRequestSearchScope,\n} from './filters.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\totherWarning,\n\tunsupportedIssueSearchCriteriaWarning,\n\tunsupportedPullRequestSearchCriteriaWarning,\n\tunusableSearchScopeMessage,\n} from './warnings.js';\n\n/**\n * How many scopes go into one upstream request.\n *\n * Measured against the live GitHub API, 30 aliased counts cost a single rate-limit point and ~2s, and 60 cost the\n * same point and roughly twice as long — so the constraint isn't rate limit, it's latency and the provider's\n * (uncontracted, so not to be leaned on) query-complexity limits. 25 keeps a chunk comfortably inside both while\n * still collapsing a realistic filter matrix into one or two requests.\n */\nconst issueCountChunkSize = 25;\n\n/**\n * How many pull-request scopes go into one upstream request.\n *\n * The same latency/complexity reasoning as {@link issueCountChunkSize}, kept slightly smaller because a pull-request\n * scope can fan out into one aliased count PER requested state (open/closed/merged), so a chunk of scopes carries a\n * small multiple of that many `search` aliases.\n */\nconst pullRequestCountChunkSize = 15;\n\n/**\n * One scope to count.\n *\n * `key` is caller-owned and echoed back verbatim, so a batch result needs no positional matching by the consumer.\n * It never reaches the provider query — the aliases in the upstream document are generated — so it can be any\n * string the caller finds meaningful.\n */\nexport interface IssueCountScope {\n\tkey: string;\n\t/** Repositories to count over. Combines with `org`, exactly as in `searchIssuesPage`. */\n\trepos?: ProviderReposInput;\n\torg?: string;\n\t/**\n\t * Same criteria model as `searchIssuesPage`, validated against the same capability table.\n\t *\n\t * `criteria.sort` does not affect the count — a total is invariant under ordering — but it is still VALIDATED,\n\t * and an unsupported key refuses this scope exactly as it would refuse the read. That is the point of the\n\t * count: it previews the constraints the read will apply, so one that accepted what the read refuses would\n\t * promise a fetch that can't happen, which is worse than no preview at all.\n\t */\n\tcriteria?: IssueSearchCriteria;\n}\n\n/** The count for one {@link IssueCountScope}, echoed back under the caller's own `key`. */\nexport interface IssueCountResult {\n\tkey: string;\n\t/**\n\t * Total matches the provider reports. `undefined` when the provider didn't report one for this scope — NEVER\n\t * zero, which is a real answer. Render the difference: an unreported count means \"unknown\", and showing it as\n\t * 0 would tell the user this filter matches nothing.\n\t */\n\tcount?: number;\n\t/**\n\t * True when `count` exceeds the provider's own result ceiling, so a full read CANNOT return everything no\n\t * matter how it is paged. This is the signal to warn before starting an expensive fetch.\n\t */\n\texceedsProviderLimit: boolean;\n\t/** The ceiling itself, when the provider declares one. */\n\tproviderLimit?: number;\n}\n\n/**\n * One scope to count pull requests over — the PR twin of {@link IssueCountScope}.\n *\n * `key` is caller-owned and echoed back verbatim, never reaching the provider query (the aliases are generated), so\n * it can be any string the caller finds meaningful.\n */\nexport interface PullRequestCountScope {\n\tkey: string;\n\t/** Repositories to count over. Combines with `org`, exactly as in `searchPullRequestsPage`. */\n\trepos?: ProviderReposInput;\n\torg?: string;\n\t/**\n\t * Same criteria model as `searchPullRequestsPage`, validated against the same capability table — so a count\n\t * refuses exactly what the read would, and never previews a fetch that can't happen.\n\t */\n\tcriteria?: PullRequestSearchCriteria;\n}\n\n/** The count for one {@link PullRequestCountScope}, echoed back under the caller's own `key`. */\nexport interface PullRequestCountResult {\n\tkey: string;\n\t/**\n\t * Total matches the provider reports. `undefined` when the provider didn't report one for this scope — NEVER\n\t * zero, which is a real answer. For a multi-state scope this is the LARGEST of its per-state counts, the same\n\t * total `searchPullRequestsPage` surfaces, not their sum.\n\t */\n\tcount?: number;\n\t/**\n\t * True when `count` exceeds the provider's own per-search result ceiling, so a full read CANNOT return\n\t * everything no matter how it is paged. This is the signal to warn before starting an expensive fetch.\n\t */\n\texceedsProviderLimit: boolean;\n\t/** The ceiling itself, when the provider declares one. */\n\tproviderLimit?: number;\n}\n\n/**\n * Counts issues for many scopes without fetching any — the probe behind a \"this will fetch ~N issues\" preview.\n *\n * A distinct method rather than a `countOnly` flag on `searchIssuesPage`, because transferring ZERO issues is the\n * entire value: a flag would return a paged result whose `items`, `cursor` and `hasMore` are all meaningless, and\n * every consumer would have to know which fields to ignore. A separate method has a return type that only\n * describes counts.\n *\n * Scopes are batched into as few upstream requests as possible ({@link issueCountChunkSize} each). A chunk that\n * fails warns and drops only its own scopes, leaving the successful chunks' counts intact — so a partial answer is\n * still useful — with `fetchFailed` set. A scope refused for its own reasons (unscoped, inexpressible criteria) is\n * likewise isolated: its siblings are still counted.\n *\n * Cheap is not free: each chunk is a network request, so a caller driving this from UI state is expected to\n * debounce and cache.\n */\nexport async function countIssues(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\tscopes: readonly IssueCountScope[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<IssueCountResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<IssueCountResult> => ({\n\t\titems: [],\n\t\twarnings: [warning],\n\t\tfetchFailed: true,\n\t});\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Issue counts'));\n\t}\n\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.scopes.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.scopes);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY scope, not just the\n\t\t// repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate issue count scope key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Issue counts'));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\t// A provider with no filtered issue search has no count either: refuse ONCE for the provider rather than\n\t// letting every scope repeat the same rejection. `undefined` criteria probes only the search's existence.\n\t// Matches `countPullRequests`. Refusing it here rather than per scope also keeps the loop's warnings from\n\t// being dominated by N copies of one provider-level fact — see the dedupe below, which handles the\n\t// keyless refusals that remain.\n\tconst searchSupport = resolveIssueSearchCriteria(options.providerId, undefined);\n\tif (searchSupport.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tsearchSupport.rejection,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst providerLimit = providersMetadata[options.providerId]?.issueSearchResultLimit;\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\t// Validate every scope first, so a refusal costs no request at all and the countable ones are still batched\n\t// together.\n\tconst countable: IssueCountScope[] = [];\n\tfor (const scope of options.scopes) {\n\t\tconst warning = rejectScope(options.providerId, domain, options.connectionId, scope);\n\t\tif (warning != null) {\n\t\t\t// Deduped rather than pushed. The rejections that name the offending SCOPE embed its own key, and\n\t\t\t// duplicate keys were already refused above, so those can never collapse. But the ones that report a\n\t\t\t// PROVIDER-level fact — a criterion it cannot express, a contradictory relationship pair — name no\n\t\t\t// scope, so several scopes failing the same way produce byte-identical warnings, and one refusal\n\t\t\t// reported N times reads as N different problems.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcountable.push(scope);\n\t}\n\n\t// Chunks are independent requests over their own slice of scopes — nothing in one reads what another produced,\n\t// and `runCaptured` never throws — so they run concurrently, bounded like every other fan-out on the facade.\n\t// Sequentially they would spend exactly the resource this probe is tuned to conserve: latency, ~2s per chunk.\n\t// `mapBounded` returns in input order, so `items` and `warnings` stay in scope order.\n\tconst batches = await mapBounded(chunk(countable, issueCountChunkSize), providerFanOutConcurrency, batch =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.countIssuesResult(\n\t\t\t\t\tbatch.map(s => ({\n\t\t\t\t\t\trepos: s.repos as ProviderRepoInput[] | undefined,\n\t\t\t\t\t\torg: s.org,\n\t\t\t\t\t\tcriteria: s.criteria,\n\t\t\t\t\t})),\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t).then(result => ({ batch: batch, ...result })),\n\t);\n\n\tconst items: IssueCountResult[] = [];\n\tfor (const { batch, value, warning } of batches) {\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// This batch contributes nothing, but the batches around it still do. Drop only these scopes.\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (let i = 0; i < batch.length; i++) {\n\t\t\tconst count = value[i];\n\t\t\titems.push({\n\t\t\t\tkey: batch[i].key,\n\t\t\t\tcount: count,\n\t\t\t\t// Only a reported count can exceed a declared ceiling; unknown-vs-limit is not a comparison.\n\t\t\t\texceedsProviderLimit: count != null && providerLimit != null && count > providerLimit,\n\t\t\t\tproviderLimit: providerLimit,\n\t\t\t});\n\t\t}\n\t}\n\n\t// A provider with no count support returns `undefined` with no error, which lands as an empty `items` and no\n\t// warning. Say so explicitly rather than letting it read as \"every scope matched nothing\".\n\tif (items.length === 0 && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/** The first key that appears twice, or `undefined` when every key is unique. */\nfunction findDuplicateKey(scopes: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const scope of scopes) {\n\t\tif (seen.has(scope.key)) return scope.key;\n\n\t\tseen.add(scope.key);\n\t}\n\treturn undefined;\n}\n\n/**\n * Why one scope can't be counted, as the warning to report — or `undefined` when it can.\n *\n * The scope and criteria rules come from {@link resolveIssueSearchScope} / {@link resolveIssueSearchCriteria},\n * the same validators `searchIssuesPage` uses, so a count always previews the constraints the read would apply.\n * That is not cosmetic: a count computed under different constraints is a WRONG number rather than a missing\n * one, which is worse. Only the wording is local, because these name the offending scope's key.\n */\nfunction rejectScope(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tscope: IssueCountScope,\n): ProviderWarning | undefined {\n\tconst resolved = resolveIssueSearchCriteria(providerId, scope.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn unsupportedIssueSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);\n\t}\n\n\t// AFTER the criteria check, mirroring `searchIssuesPage`: a provider with no filtered issue search is the more\n\t// fundamental refusal, and a count must preview the refusal its read would give.\n\tconst scoping = resolveIssueSearchScope(scope.repos, scope.org, scope.criteria);\n\tswitch (scoping.rejection?.reason) {\n\t\tcase 'repo-ids':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Issue count scope '${scope.key}' cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.`,\n\t\t\t);\n\t\tcase 'unscoped':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Issue count scope '${scope.key}' is unscoped; pass \\`repos\\`, \\`org\\`, or a relationship to the current user. \\`any-assignee\\` and \\`unassigned\\` are not scopes.`,\n\t\t\t);\n\t\t// A count applies exactly the qualifiers its search would, so an unusable scope would make it agree with\n\t\t// the wrong search rather than expose it — see `isUsableSearchScopeName`.\n\t\tcase 'unusable-scope':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\tunusableSearchScopeMessage('scopes', scoping.rejection.scopes, `Issue count scope '${scope.key}'`),\n\t\t\t);\n\t}\n\n\t// Count-only, with no counterpart in the read: a relationship set is an OR across several searches, which one\n\t// count can't express — summing them would double-count anything matching two, and taking the max would\n\t// under-report. Rather than answer with a wrong number, ask the caller to count each relationship as its own\n\t// scope, where the keys make the OR explicit.\n\tif ((scope.criteria?.relationships?.length ?? 0) > 1) {\n\t\treturn otherWarning(\n\t\t\tproviderId,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t`Issue count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`,\n\t\t);\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Counts pull requests for many scopes without fetching any — the PR twin of {@link countIssues}, and the probe\n * behind a \"this will fetch ~N pull requests\" preview.\n *\n * Identical batching, per-scope isolation, and `key`-echo contract as {@link countIssues}; see it for the cost\n * model and the `count: undefined` ≠ zero rule. The one difference is inherent to pull requests: a scope's criteria\n * can name several STATES, which the provider counts as independent searches — the reported `count` is the LARGEST\n * of them (mirroring `searchPullRequestsPage`'s total), so an unpaged read still can't exceed a single search's\n * ceiling undetected.\n */\nexport async function countPullRequests(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\tscopes: readonly PullRequestCountScope[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<PullRequestCountResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<PullRequestCountResult> => ({\n\t\titems: [],\n\t\twarnings: [warning],\n\t\tfetchFailed: true,\n\t});\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Pull request counts'),\n\t\t);\n\t}\n\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.scopes.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.scopes);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY scope, not just the\n\t\t// repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate pull request count scope key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Pull request counts'),\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\t// A provider with no filtered pull-request search has no count either: refuse ONCE for the provider rather than\n\t// letting every scope repeat the same rejection. `undefined` criteria probes only the search's existence.\n\tconst searchSupport = resolvePullRequestSearchCriteria(options.providerId, undefined);\n\tif (searchSupport.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tsearchSupport.rejection,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst providerLimit = providersMetadata[options.providerId]?.pullRequestSearchResultLimit;\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\t// Validate every scope first, so a refusal costs no request at all and the countable ones are still batched\n\t// together.\n\tconst countable: PullRequestCountScope[] = [];\n\tfor (const scope of options.scopes) {\n\t\tconst warning = rejectPullRequestScope(options.providerId, domain, options.connectionId, scope);\n\t\tif (warning != null) {\n\t\t\t// Deduped rather than pushed: see the issue twin. Here the keyless set is wider — besides the criteria\n\t\t\t// rejections, `unsupported-repository-scope` and `unsupported-organization-scope` are scope rejections\n\t\t\t// that route through the criteria warning and name no key, and both are provider-level facts every\n\t\t\t// scope would repeat.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcountable.push(scope);\n\t}\n\n\t// Chunks are independent requests over their own slice of scopes, `runCaptured` never throws, so they run\n\t// concurrently, bounded like every other fan-out on the facade. `mapBounded` returns in input order, so `items`\n\t// and `warnings` stay in scope order.\n\tconst batches = await mapBounded(chunk(countable, pullRequestCountChunkSize), providerFanOutConcurrency, batch =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.countPullRequestsResult(\n\t\t\t\t\tbatch.map(s => ({\n\t\t\t\t\t\trepos: s.repos as ProviderRepoInput[] | undefined,\n\t\t\t\t\t\torg: s.org,\n\t\t\t\t\t\tcriteria: s.criteria,\n\t\t\t\t\t})),\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t).then(result => ({ batch: batch, ...result })),\n\t);\n\n\tconst items: PullRequestCountResult[] = [];\n\tfor (const { batch, value, warning } of batches) {\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// This batch contributes nothing, but the batches around it still do. Drop only these scopes.\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (let i = 0; i < batch.length; i++) {\n\t\t\tconst count = value[i];\n\t\t\titems.push({\n\t\t\t\tkey: batch[i].key,\n\t\t\t\tcount: count,\n\t\t\t\t// Only a reported count can exceed a declared ceiling; unknown-vs-limit is not a comparison.\n\t\t\t\texceedsProviderLimit: count != null && providerLimit != null && count > providerLimit,\n\t\t\t\tproviderLimit: providerLimit,\n\t\t\t});\n\t\t}\n\t}\n\n\t// A provider that reported nothing for any scope and raised no warning: say so explicitly rather than letting it\n\t// read as \"every scope matched nothing\".\n\tif (items.length === 0 && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/**\n * Why one pull-request scope can't be counted, as the warning to report — or `undefined` when it can. The PR twin\n * of {@link rejectScope}, validating against the same {@link resolvePullRequestSearchScope} /\n * {@link resolvePullRequestSearchCriteria} the read uses, so a count always previews the constraints the read\n * would apply.\n */\nfunction rejectPullRequestScope(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tscope: PullRequestCountScope,\n): ProviderWarning | undefined {\n\tconst scoping = resolvePullRequestSearchScope(providerId, scope.repos, scope.org, scope.criteria);\n\tswitch (scoping.rejection?.reason) {\n\t\tcase 'repo-ids':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Pull request count scope '${scope.key}' cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.`,\n\t\t\t);\n\t\tcase 'unscoped':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Pull request count scope '${scope.key}' is unscoped; pass \\`repos\\`, \\`org\\`, or at least one current-user relationship.`,\n\t\t\t);\n\t\tcase 'unsupported-repository-scope':\n\t\tcase 'unsupported-organization-scope':\n\t\t\treturn unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, {\n\t\t\t\treason: 'unsupported-criteria',\n\t\t\t\tcriteria: [\n\t\t\t\t\tscoping.rejection.reason === 'unsupported-repository-scope'\n\t\t\t\t\t\t? 'repositoryScope'\n\t\t\t\t\t\t: 'organizationScope',\n\t\t\t\t],\n\t\t\t});\n\t\t// See the issue twin, and `isUsableSearchScopeName` for the rule.\n\t\tcase 'unusable-scope':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\tunusableSearchScopeMessage(\n\t\t\t\t\t'scopes',\n\t\t\t\t\tscoping.rejection.scopes,\n\t\t\t\t\t`Pull request count scope '${scope.key}'`,\n\t\t\t\t),\n\t\t\t);\n\t}\n\n\tconst resolved = resolvePullRequestSearchCriteria(providerId, scope.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);\n\t}\n\n\t// Count-only, with no counterpart in the read: a relationship set is an OR across several searches, which one\n\t// count can't express — summing would double-count overlaps and max would under-report. Ask the caller to count\n\t// each relationship as its own scope, where the keys make the OR explicit. (States are disjoint, so a scope may\n\t// still name several — the provider counts them as the max, not a refusal.)\n\tif ((scope.criteria?.relationships?.length ?? 0) > 1) {\n\t\treturn otherWarning(\n\t\t\tproviderId,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t`Pull request count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`,\n\t\t);\n\t}\n\n\treturn undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"counts.js","names":[],"sources":["../../../../../plus/integrations/src/reads/counts.ts"],"sourcesContent":["import type { IssueSearchCriteria } from '@gitlens/git/models/issue.js';\nimport type { PullRequestSearchCriteria } from '@gitlens/git/models/pullRequest.js';\nimport { chunk } from '@gitlens/utils/array.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { GitSelfManagedHostIntegrationId, providerFanOutConcurrency } from '../constants.js';\nimport type { ProviderSearchCount } from '../models/integration.js';\nimport type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';\nimport { providersMetadata } from '../providers/models.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, toProviderWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tresolveIssueSearchCriteria,\n\tresolveIssueSearchScope,\n\tresolvePullRequestSearchCriteria,\n\tresolvePullRequestSearchScope,\n} from './filters.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\totherWarning,\n\tunsupportedIssueSearchCriteriaWarning,\n\tunsupportedPullRequestSearchCriteriaWarning,\n\tunusableSearchScopeMessage,\n} from './warnings.js';\n\n/**\n * How many scopes go into one upstream request.\n *\n * Measured against the live GitHub API, 30 aliased counts cost a single rate-limit point and ~2s, and 60 cost the\n * same point and roughly twice as long — so the constraint isn't rate limit, it's latency and the provider's\n * (uncontracted, so not to be leaned on) query-complexity limits. 25 keeps a chunk comfortably inside both while\n * still collapsing a realistic filter matrix into one or two requests.\n */\nconst issueCountChunkSize = 25;\n\n/**\n * How many pull-request scopes go into one upstream request.\n *\n * The same latency/complexity reasoning as {@link issueCountChunkSize}, kept slightly smaller because a pull-request\n * scope can fan out into one aliased count PER requested state (open/closed/merged), so a chunk of scopes carries a\n * small multiple of that many `search` aliases.\n *\n * A provider that counts by reading batches nothing, so it is chunked one scope at a time instead: a chunk is then\n * just a scope, and the facade's bounded concurrency is what paces the requests.\n */\nconst pullRequestCountChunkSize = 15;\n\n/**\n * Whether a provider counts pull requests by READING the matches rather than asking for a total — Bitbucket Data\n * Center, which has no count query. Two rules follow from it: it batches nothing, and it deduplicates the rows\n * themselves, so a relationship set is an exact union it can count in one scope rather than a refusal.\n */\nfunction countsPullRequestsByReading(providerId: IntegrationIds): boolean {\n\treturn providerId === GitSelfManagedHostIntegrationId.BitbucketServer;\n}\n\n/**\n * One scope to count.\n *\n * `key` is caller-owned and echoed back verbatim, so a batch result needs no positional matching by the consumer.\n * It never reaches the provider query — the aliases in the upstream document are generated — so it can be any\n * string the caller finds meaningful.\n */\nexport interface IssueCountScope {\n\tkey: string;\n\t/** Repositories to count over. Combines with `org`, exactly as in `searchIssuesPage`. */\n\trepos?: ProviderReposInput;\n\torg?: string;\n\t/**\n\t * Same criteria model as `searchIssuesPage`, validated against the same capability table.\n\t *\n\t * `criteria.sort` does not affect the count — a total is invariant under ordering — but it is still VALIDATED,\n\t * and an unsupported key refuses this scope exactly as it would refuse the read. That is the point of the\n\t * count: it previews the constraints the read will apply, so one that accepted what the read refuses would\n\t * promise a fetch that can't happen, which is worse than no preview at all.\n\t */\n\tcriteria?: IssueSearchCriteria;\n}\n\n/** The count for one {@link IssueCountScope}, echoed back under the caller's own `key`. */\nexport interface IssueCountResult {\n\tkey: string;\n\t/**\n\t * Total matches the provider reports. `undefined` when the provider didn't report one for this scope — NEVER\n\t * zero, which is a real answer. Render the difference: an unreported count means \"unknown\", and showing it as\n\t * 0 would tell the user this filter matches nothing.\n\t */\n\tcount?: number;\n\t/**\n\t * True when `count` exceeds the provider's own result ceiling, so a full read CANNOT return everything no\n\t * matter how it is paged. This is the signal to warn before starting an expensive fetch.\n\t */\n\texceedsProviderLimit: boolean;\n\t/** The ceiling itself, when the provider declares one. */\n\tproviderLimit?: number;\n}\n\n/**\n * One scope to count pull requests over — the PR twin of {@link IssueCountScope}.\n *\n * `key` is caller-owned and echoed back verbatim, never reaching the provider query (the aliases are generated), so\n * it can be any string the caller finds meaningful.\n */\nexport interface PullRequestCountScope {\n\tkey: string;\n\t/** Repositories to count over. Combines with `org`, exactly as in `searchPullRequestsPage`. */\n\trepos?: ProviderReposInput;\n\torg?: string;\n\t/**\n\t * Same criteria model as `searchPullRequestsPage`, validated against the same capability table — so a count\n\t * refuses exactly what the read would, and never previews a fetch that can't happen.\n\t */\n\tcriteria?: PullRequestSearchCriteria;\n}\n\n/** The count for one {@link PullRequestCountScope}, echoed back under the caller's own `key`. */\nexport interface PullRequestCountResult {\n\tkey: string;\n\t/**\n\t * Total matches the provider reports. `undefined` when the provider didn't report one for this scope — NEVER\n\t * zero, which is a real answer.\n\t *\n\t * For a multi-state scope, GitHub/GHE report the LARGEST of their per-state counts (the same total\n\t * `searchPullRequestsPage` surfaces, since each state is its own capped search), not their sum. Bitbucket Data\n\t * Center has no ceiling to stay under, so it reports the exact union — the number of rows the search returns —\n\t * and so does Azure DevOps Server, which reads every state in one drain.\n\t */\n\tcount?: number;\n\t/**\n\t * True when `count` is a FLOOR rather than the total: the provider has no count query (Bitbucket Data Center),\n\t * so it counts by reading within a budget, and this scope had more than the budget read. Render it as \"N+\", and\n\t * treat the scope as at least that expensive. Absent means `count` is exact — the provider's own total, or for a\n\t * provider that counts by reading, every match it read.\n\t */\n\tlowerBound?: boolean;\n\t/**\n\t * True when `count` exceeds the provider's own per-search result ceiling, so a full read CANNOT return\n\t * everything no matter how it is paged. This is the signal to warn before starting an expensive fetch.\n\t */\n\texceedsProviderLimit: boolean;\n\t/** The ceiling itself, when the provider declares one. */\n\tproviderLimit?: number;\n}\n\n/**\n * Counts issues for many scopes without fetching any — the probe behind a \"this will fetch ~N issues\" preview.\n *\n * A distinct method rather than a `countOnly` flag on `searchIssuesPage`, because transferring ZERO issues is the\n * entire value: a flag would return a paged result whose `items`, `cursor` and `hasMore` are all meaningless, and\n * every consumer would have to know which fields to ignore. A separate method has a return type that only\n * describes counts.\n *\n * Scopes are batched into as few upstream requests as possible ({@link issueCountChunkSize} each). A chunk that\n * fails warns and drops only its own scopes, leaving the successful chunks' counts intact — so a partial answer is\n * still useful — with `fetchFailed` set. A scope refused for its own reasons (unscoped, inexpressible criteria) is\n * likewise isolated: its siblings are still counted.\n *\n * Cheap is not free: each chunk is a network request, so a caller driving this from UI state is expected to\n * debounce and cache.\n */\nexport async function countIssues(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\tscopes: readonly IssueCountScope[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<IssueCountResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<IssueCountResult> => ({\n\t\titems: [],\n\t\twarnings: [warning],\n\t\tfetchFailed: true,\n\t});\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Issue counts'));\n\t}\n\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.scopes.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.scopes);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY scope, not just the\n\t\t// repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate issue count scope key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Issue counts'));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\t// A provider with no filtered issue search has no count either: refuse ONCE for the provider rather than\n\t// letting every scope repeat the same rejection. `undefined` criteria probes only the search's existence.\n\t// Matches `countPullRequests`. Refusing it here rather than per scope also keeps the loop's warnings from\n\t// being dominated by N copies of one provider-level fact — see the dedupe below, which handles the\n\t// keyless refusals that remain.\n\tconst searchSupport = resolveIssueSearchCriteria(options.providerId, undefined);\n\tif (searchSupport.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tsearchSupport.rejection,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst providerLimit = providersMetadata[options.providerId]?.issueSearchResultLimit;\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\t// Validate every scope first, so a refusal costs no request at all and the countable ones are still batched\n\t// together.\n\tconst countable: IssueCountScope[] = [];\n\tfor (const scope of options.scopes) {\n\t\tconst warning = rejectScope(options.providerId, domain, options.connectionId, scope);\n\t\tif (warning != null) {\n\t\t\t// Deduped rather than pushed. The rejections that name the offending SCOPE embed its own key, and\n\t\t\t// duplicate keys were already refused above, so those can never collapse. But the ones that report a\n\t\t\t// PROVIDER-level fact — a criterion it cannot express, a contradictory relationship pair — name no\n\t\t\t// scope, so several scopes failing the same way produce byte-identical warnings, and one refusal\n\t\t\t// reported N times reads as N different problems.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcountable.push(scope);\n\t}\n\n\t// Chunks are independent requests over their own slice of scopes — nothing in one reads what another produced,\n\t// and `runCaptured` never throws — so they run concurrently, bounded like every other fan-out on the facade.\n\t// Sequentially they would spend exactly the resource this probe is tuned to conserve: latency, ~2s per chunk.\n\t// `mapBounded` returns in input order, so `items` and `warnings` stay in scope order.\n\tconst batches = await mapBounded(chunk(countable, issueCountChunkSize), providerFanOutConcurrency, batch =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.countIssuesResult(\n\t\t\t\t\tbatch.map(s => ({\n\t\t\t\t\t\trepos: s.repos as ProviderRepoInput[] | undefined,\n\t\t\t\t\t\torg: s.org,\n\t\t\t\t\t\tcriteria: s.criteria,\n\t\t\t\t\t})),\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t).then(result => ({ batch: batch, ...result })),\n\t);\n\n\tconst items: IssueCountResult[] = [];\n\tfor (const { batch, value, warning } of batches) {\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// This batch contributes nothing, but the batches around it still do. Drop only these scopes.\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tfetchFailed =\n\t\t\tcollectCounts(batch, value, providerLimit, items, (key, error) => {\n\t\t\t\tconst warning = toProviderWarning(options.providerId, domain, options.connectionId, error);\n\t\t\t\tappendDedupedWarning(warnings, {\n\t\t\t\t\t...warning,\n\t\t\t\t\tmessage: `Issue count scope '${key}': ${warning.message}`,\n\t\t\t\t});\n\t\t\t}) || fetchFailed;\n\t}\n\n\t// A provider with no count support returns `undefined` with no error, which lands as an empty `items` and no\n\t// warning. Say so explicitly rather than letting it read as \"every scope matched nothing\".\n\tif (items.length === 0 && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedIssueSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/**\n * Turns one batch of positional issue counts into keyed results, returning whether any scope was refused.\n *\n * A slot holding an `Error` is a scope the PROVIDER refused for its own reasons (a scope naming a collection it\n * can't search, say): reported and dropped exactly like a scope the facade refuses, so its siblings in the batch\n * still come back. `'exceeds-limit'` is a count the provider can only bound — more than its ceiling matched — which\n * is reported as `exceedsProviderLimit` with no `count` rather than as the ceiling, which would understate it.\n */\nfunction collectCounts<T extends { key: string }>(\n\tbatch: readonly T[],\n\tvalue: readonly ProviderSearchCount[],\n\tproviderLimit: number | undefined,\n\titems: { key: string; count?: number; exceedsProviderLimit: boolean; providerLimit?: number }[],\n\tonRefused: (key: string, error: Error) => void,\n): boolean {\n\tlet refused = false;\n\tfor (let i = 0; i < batch.length; i++) {\n\t\tconst count = value[i];\n\t\tif (count instanceof Error) {\n\t\t\tonRefused(batch[i].key, count);\n\t\t\trefused = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst exceeds = count === 'exceeds-limit';\n\t\titems.push({\n\t\t\tkey: batch[i].key,\n\t\t\tcount: exceeds ? undefined : count,\n\t\t\t// Only a reported count can exceed a declared ceiling; unknown-vs-limit is not a comparison.\n\t\t\texceedsProviderLimit: exceeds || (count != null && providerLimit != null && count > providerLimit),\n\t\t\tproviderLimit: providerLimit,\n\t\t});\n\t}\n\treturn refused;\n}\n\n/** The first key that appears twice, or `undefined` when every key is unique. */\nfunction findDuplicateKey(scopes: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const scope of scopes) {\n\t\tif (seen.has(scope.key)) return scope.key;\n\n\t\tseen.add(scope.key);\n\t}\n\treturn undefined;\n}\n\n/**\n * Why one scope can't be counted, as the warning to report — or `undefined` when it can.\n *\n * The scope and criteria rules come from {@link resolveIssueSearchScope} / {@link resolveIssueSearchCriteria},\n * the same validators `searchIssuesPage` uses, so a count always previews the constraints the read would apply.\n * That is not cosmetic: a count computed under different constraints is a WRONG number rather than a missing\n * one, which is worse. Only the wording is local, because these name the offending scope's key.\n */\nfunction rejectScope(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tscope: IssueCountScope,\n): ProviderWarning | undefined {\n\tconst resolved = resolveIssueSearchCriteria(providerId, scope.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn unsupportedIssueSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);\n\t}\n\n\t// AFTER the criteria check, mirroring `searchIssuesPage`: a provider with no filtered issue search is the more\n\t// fundamental refusal, and a count must preview the refusal its read would give.\n\tconst scoping = resolveIssueSearchScope(providerId, scope.repos, scope.org, scope.criteria);\n\tswitch (scoping.rejection?.reason) {\n\t\tcase 'repo-ids':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Issue count scope '${scope.key}' cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.`,\n\t\t\t);\n\t\tcase 'unscoped':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Issue count scope '${scope.key}' is unscoped; pass \\`repos\\`, \\`org\\`, or a relationship to the current user. \\`any-assignee\\` and \\`unassigned\\` are not scopes.`,\n\t\t\t);\n\t\t// A count applies exactly the qualifiers its search would, so an unusable scope would make it agree with\n\t\t// the wrong search rather than expose it — see `isUsableSearchScopeName`.\n\t\tcase 'unusable-scope':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\tunusableSearchScopeMessage('scopes', scoping.rejection.scopes, `Issue count scope '${scope.key}'`),\n\t\t\t);\n\t}\n\n\t// Count-only, with no counterpart in the read: a relationship set is an OR across several searches, which one\n\t// count can't express — summing them would double-count anything matching two, and taking the max would\n\t// under-report. Rather than answer with a wrong number, ask the caller to count each relationship as its own\n\t// scope, where the keys make the OR explicit.\n\tif ((scope.criteria?.relationships?.length ?? 0) > 1) {\n\t\treturn otherWarning(\n\t\t\tproviderId,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t`Issue count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`,\n\t\t);\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Counts pull requests for many scopes without fetching any — the PR twin of {@link countIssues}, and the probe\n * behind a \"this will fetch ~N pull requests\" preview.\n *\n * Identical batching, per-scope isolation, and `key`-echo contract as {@link countIssues}; see it for the cost\n * model and the `count: undefined` ≠ zero rule. The one difference is inherent to pull requests: a scope's criteria\n * can name several STATES, which GitHub/GHE count as independent searches — the reported `count` is the LARGEST\n * of them (mirroring `searchPullRequestsPage`'s total), so an unpaged read still can't exceed a single search's\n * ceiling undetected. Bitbucket Data Center has no ceiling and no count query: it counts the exact union by reading\n * each scope within a budget, and flags a scope that had more as `lowerBound`. Because it deduplicates rows, it is\n * also the one provider that accepts several relationships in one scope. Azure DevOps Server counts the union of the\n * states from the one drain its search pages through.\n */\nexport async function countPullRequests(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\tscopes: readonly PullRequestCountScope[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t},\n): Promise<ProviderResult<PullRequestCountResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<PullRequestCountResult> => ({\n\t\titems: [],\n\t\twarnings: [warning],\n\t\tfetchFailed: true,\n\t});\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Pull request counts'),\n\t\t);\n\t}\n\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.scopes.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.scopes);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY scope, not just the\n\t\t// repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate pull request count scope key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn { items: [], warnings: early.warnings, fetchFailed: early.fetchFailed || undefined };\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(\n\t\t\tgitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'Pull request counts'),\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\n\t// A provider with no filtered pull-request search has no count either: refuse ONCE for the provider rather than\n\t// letting every scope repeat the same rejection. `undefined` criteria probes only the search's existence.\n\tconst searchSupport = resolvePullRequestSearchCriteria(options.providerId, undefined);\n\tif (searchSupport.rejection != null) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\tsearchSupport.rejection,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst providerLimit = providersMetadata[options.providerId]?.pullRequestSearchResultLimit;\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\t// Validate every scope first, so a refusal costs no request at all and the countable ones are still batched\n\t// together.\n\tconst countable: PullRequestCountScope[] = [];\n\tfor (const scope of options.scopes) {\n\t\tconst warning = rejectPullRequestScope(options.providerId, domain, options.connectionId, scope);\n\t\tif (warning != null) {\n\t\t\t// Deduped rather than pushed: see the issue twin. Here the keyless set is wider — besides the criteria\n\t\t\t// rejections, `unsupported-repository-scope` and `unsupported-organization-scope` are scope rejections\n\t\t\t// that route through the criteria warning and name no key, and both are provider-level facts every\n\t\t\t// scope would repeat.\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tcountable.push(scope);\n\t}\n\n\t// Chunks are independent requests over their own slice of scopes, `runCaptured` never throws, so they run\n\t// concurrently, bounded like every other fan-out on the facade. `mapBounded` returns in input order, so `items`\n\t// and `warnings` stay in scope order.\n\tconst chunks = chunk(countable, countsPullRequestsByReading(options.providerId) ? 1 : pullRequestCountChunkSize);\n\tconst batches = await mapBounded(chunks, providerFanOutConcurrency, batch =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\tintegration.countPullRequestsResult(\n\t\t\t\t\tbatch.map(s => ({\n\t\t\t\t\t\trepos: s.repos as ProviderRepoInput[] | undefined,\n\t\t\t\t\t\torg: s.org,\n\t\t\t\t\t\tcriteria: s.criteria,\n\t\t\t\t\t})),\n\t\t\t\t\tundefined,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t).then(result => ({ batch: batch, ...result })),\n\t);\n\n\tconst items: PullRequestCountResult[] = [];\n\tfor (const { batch, value, warning } of batches) {\n\t\tif (warning != null) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tif (value == null) {\n\t\t\t// This batch contributes nothing, but the batches around it still do. Drop only these scopes.\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tfor (let i = 0; i < batch.length; i++) {\n\t\t\tconst slot = value[i];\n\t\t\t// A scope the provider refused for its own reasons: reported and dropped like one the facade refuses.\n\t\t\tif (slot instanceof Error) {\n\t\t\t\tconst warning = toProviderWarning(options.providerId, domain, options.connectionId, slot);\n\t\t\t\tappendDedupedWarning(warnings, {\n\t\t\t\t\t...warning,\n\t\t\t\t\tmessage: `Pull request count scope '${batch[i].key}': ${warning.message}`,\n\t\t\t\t});\n\t\t\t\tfetchFailed = true;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst count = slot?.count;\n\t\t\titems.push({\n\t\t\t\tkey: batch[i].key,\n\t\t\t\tcount: count,\n\t\t\t\t...(count != null && slot?.lowerBound === true ? { lowerBound: true } : {}),\n\t\t\t\t// Only a reported count can exceed a declared ceiling; unknown-vs-limit is not a comparison.\n\t\t\t\texceedsProviderLimit: count != null && providerLimit != null && count > providerLimit,\n\t\t\t\tproviderLimit: providerLimit,\n\t\t\t});\n\t\t}\n\t}\n\n\t// A provider that reported nothing for any scope and raised no warning: say so explicitly rather than letting it\n\t// read as \"every scope matched nothing\".\n\tif (items.length === 0 && warnings.length === 0) {\n\t\treturn refused(\n\t\t\tunsupportedPullRequestSearchCriteriaWarning(options.providerId, domain, options.connectionId, {\n\t\t\t\treason: 'unsupported-search',\n\t\t\t}),\n\t\t);\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/**\n * Why one pull-request scope can't be counted, as the warning to report — or `undefined` when it can. The PR twin\n * of {@link rejectScope}, validating against the same {@link resolvePullRequestSearchScope} /\n * {@link resolvePullRequestSearchCriteria} the read uses, so a count always previews the constraints the read\n * would apply.\n */\nfunction rejectPullRequestScope(\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tscope: PullRequestCountScope,\n): ProviderWarning | undefined {\n\tconst scoping = resolvePullRequestSearchScope(providerId, scope.repos, scope.org, scope.criteria);\n\tswitch (scoping.rejection?.reason) {\n\t\tcase 'repo-ids':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Pull request count scope '${scope.key}' cannot be scoped by repository id; pass repository descriptors (namespace + name) instead.`,\n\t\t\t);\n\t\tcase 'unscoped':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t`Pull request count scope '${scope.key}' is unscoped; pass \\`repos\\`, \\`org\\`, or at least one current-user relationship.`,\n\t\t\t);\n\t\tcase 'unsupported-repository-scope':\n\t\tcase 'unsupported-organization-scope':\n\t\t\treturn unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, {\n\t\t\t\treason: 'unsupported-criteria',\n\t\t\t\tcriteria: [\n\t\t\t\t\tscoping.rejection.reason === 'unsupported-repository-scope'\n\t\t\t\t\t\t? 'repositoryScope'\n\t\t\t\t\t\t: 'organizationScope',\n\t\t\t\t],\n\t\t\t});\n\t\t// See the issue twin, and `isUsableSearchScopeName` for the rule.\n\t\tcase 'unusable-scope':\n\t\t\treturn otherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\tunusableSearchScopeMessage(\n\t\t\t\t\t'scopes',\n\t\t\t\t\tscoping.rejection.scopes,\n\t\t\t\t\t`Pull request count scope '${scope.key}'`,\n\t\t\t\t),\n\t\t\t);\n\t}\n\n\tconst resolved = resolvePullRequestSearchCriteria(providerId, scope.criteria);\n\tif (resolved.rejection != null) {\n\t\treturn unsupportedPullRequestSearchCriteriaWarning(providerId, domain, connectionId, resolved.rejection);\n\t}\n\n\t// Count-only, with no counterpart in the read: a relationship set is an OR across several searches, which one\n\t// count can't express — summing would double-count overlaps and max would under-report. Ask the caller to count\n\t// each relationship as its own scope, where the keys make the OR explicit. (States are disjoint, so a scope may\n\t// still name several — the provider counts them as the max, not a refusal.) A provider that counts by reading\n\t// deduplicates the matching rows themselves, so it counts the OR exactly, as the search it previews returns it.\n\tif ((scope.criteria?.relationships?.length ?? 0) > 1 && !countsPullRequestsByReading(providerId)) {\n\t\treturn otherWarning(\n\t\t\tproviderId,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t`Pull request count scope '${scope.key}' requests several relationships, which a single count can't express (they are OR-ed, so overlapping matches would be double-counted); pass one scope per relationship.`,\n\t\t);\n\t}\n\n\treturn undefined;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { hostFromDomain } from "../utils/domain.utils.js";
2
1
  import { toPageCursor } from "../providers/utils/providerPaging.js";
2
+ import { hostFromDomain } from "../utils/domain.utils.js";
3
3
  import { pageToCursor } from "./paging.js";
4
4
  //#region ../plus/integrations/src/reads/cursors.ts
5
5
  /** Positive safe integers only, deduped; `undefined` when nothing survives (so the field is omitted). */
@@ -1,8 +1,9 @@
1
1
  import { PullRequestFilter, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
2
2
  import { IntegrationIds } from "../constants.js";
3
+ import "../providerFilters.js";
4
+ import { ProviderWarning } from "../results.js";
3
5
  import { GitHostIntegration } from "../models/gitHostIntegration.js";
4
6
  import { IntegrationResult } from "../models/integration.js";
5
- import { ProviderWarning } from "../results.js";
6
7
  import { ProviderPullRequest, ProviderReposInput, ProviderRepository } from "../providers/models.js";
7
8
  import { ProviderSweepTarget, PullRequestSweepOptions } from "../manager.js";
8
9
  //#region ../plus/integrations/src/reads/drains.d.ts
@@ -1,6 +1,6 @@
1
+ import { supportedOrderedCloudIntegrationIds } from "../constants.js";
1
2
  import { appendDedupedWarning, reconcileOmissionsWithFailure, toProviderWarning } from "../results.js";
2
3
  import { mergeAssessmentInto } from "../collectionMetadata.js";
3
- import { supportedOrderedCloudIntegrationIds } from "../constants.js";
4
4
  import { isIssuesHostIntegrationId } from "../utils/integration.utils.js";
5
5
  import { getProviderPullRequestIdentity } from "../providers/models.js";
6
6
  import { noConnectionWarning, truncationWarning } from "./warnings.js";
@@ -167,7 +167,7 @@ export type IssueSearchScopeRejection =
167
167
  * Every scope name is checked for naming the same scope after the provider sanitizes it, not merely for being
168
168
  * non-empty, and that distinction is a SECURITY one rather than a nicety — see {@link isUsableSearchScopeName}.
169
169
  */
170
- export declare function resolveIssueSearchScope(repos: ProviderReposInput | undefined, org: string | undefined, criteria: IssueSearchCriteria | undefined): {
170
+ export declare function resolveIssueSearchScope(id: IntegrationIds, repos: ProviderReposInput | undefined, org: string | undefined, criteria: IssueSearchCriteria | undefined): {
171
171
  rejection?: IssueSearchScopeRejection;
172
172
  repos?: ProviderRepoInput[];
173
173
  };
@@ -72,7 +72,7 @@ function resolvePullRequestSearchScope(id, repos, org, criteria) {
72
72
  }
73
73
  const hasOrganizationScope = org != null && org.length > 0;
74
74
  if (hasOrganizationScope && supported?.organizationScope !== true) return { rejection: { reason: "unsupported-organization-scope" } };
75
- const unusable = unusableSearchScopeNames(org, resolvedRepos);
75
+ const unusable = unusableSearchScopeNames(id, org, resolvedRepos);
76
76
  if (unusable.length > 0) return { rejection: {
77
77
  reason: "unusable-scope",
78
78
  scopes: unusable
@@ -213,11 +213,15 @@ const unsupportedPullRequestSearchCapabilities = {
213
213
  *
214
214
  * Provider-NEUTRAL by design, and deliberately not an import of GitHub's `sanitizeGitHubQualifierValue`: this
215
215
  * module validates for every provider, and a GitHub-specific rule reaching in here would be wrong for the next
216
- * one that declares a search. Only GitHub and GHE declare one today, so the character class below is GitHub's in
217
- * practice; it is stated as the common part of any query language because each class breaks a query on its own
218
- * terms — a quote closes its own qualifier, a control character cannot appear at all, whitespace delimits the
219
- * next qualifier — but a provider whose names legitimately carry one (Azure DevOps project names can contain
220
- * spaces) needs its own rule alongside its `supported*Search` capability rather than an exception here.
216
+ * one that declares a search. The character class below is stated as the common part of any query language,
217
+ * because each class breaks a query on its own terms — a quote closes its own qualifier, a control character
218
+ * cannot appear at all, whitespace delimits the next qualifier.
219
+ *
220
+ * A provider that never puts a scope name into a query string declares
221
+ * {@link ProviderMetadata.exactSearchScopeNames}, and only blank names and control characters are refused for it:
222
+ * Azure DevOps Server addresses a collection or repository as an encoded URL segment and matches a project against
223
+ * the names discovery reported, so a space or a quote reaches it naming the same scope — and its names legitimately
224
+ * contain both.
221
225
  *
222
226
  * EDGES ARE STRIPPED before the test, which is what keeps the predicate no stricter than the provider's own
223
227
  * sanitizing — the invariant that makes refusing safe to add, since it means this can only reject a name the
@@ -230,8 +234,9 @@ const unsupportedPullRequestSearchCapabilities = {
230
234
  * empties, an INNER space or control character still splits (`'git\u0000kraken'` emits `git kraken`, two
231
235
  * tokens), and a quote still alters wherever it sits.
232
236
  */
233
- function isUsableSearchScopeName(name) {
237
+ function isUsableSearchScopeName(id, name) {
234
238
  const stripped = stripSearchScopeEdges(name);
239
+ if (providersMetadata[id]?.exactSearchScopeNames === true) return stripped.length > 0 && !/[\u0000-\u001f\u007f]/.test(stripped);
235
240
  return stripped.length > 0 && !/["\u0000-\u001f\u007f\s]/.test(stripped);
236
241
  }
237
242
  /** The leading/trailing run a provider's sanitizing removes — see {@link isUsableSearchScopeName}. */
@@ -242,14 +247,14 @@ function stripSearchScopeEdges(value) {
242
247
  * The scope names a search cannot carry as given, as the strings to name in the refusal — empty when every one is
243
248
  * usable. See {@link isUsableSearchScopeName} for the rule and why it refuses rather than sanitizes.
244
249
  */
245
- function unusableSearchScopeNames(org, repos) {
250
+ function unusableSearchScopeNames(id, org, repos) {
246
251
  const unusable = [];
247
- if (org != null && org.length > 0 && !isUsableSearchScopeName(org)) unusable.push(org);
252
+ if (org != null && org.length > 0 && !isUsableSearchScopeName(id, org)) unusable.push(org);
248
253
  for (const repo of repos ?? []) {
249
254
  const namespace = repo.namespace ?? "";
250
255
  const name = repo.name ?? "";
251
256
  const path = `${namespace}/${name}`;
252
- if (!isUsableSearchScopeName(path) || stripSearchScopeEdges(namespace).length === 0 || stripSearchScopeEdges(name).length === 0) unusable.push(path);
257
+ if (!isUsableSearchScopeName(id, path) || stripSearchScopeEdges(namespace).length === 0 || stripSearchScopeEdges(name).length === 0) unusable.push(path);
253
258
  }
254
259
  return unusable;
255
260
  }
@@ -269,13 +274,13 @@ function unusableSearchScopeNames(org, repos) {
269
274
  * Every scope name is checked for naming the same scope after the provider sanitizes it, not merely for being
270
275
  * non-empty, and that distinction is a SECURITY one rather than a nicety — see {@link isUsableSearchScopeName}.
271
276
  */
272
- function resolveIssueSearchScope(repos, org, criteria) {
277
+ function resolveIssueSearchScope(id, repos, org, criteria) {
273
278
  let resolvedRepos;
274
279
  if (repos?.length) {
275
280
  if (repos.some((r) => typeof r === "string" || typeof r === "number")) return { rejection: { reason: "repo-ids" } };
276
281
  resolvedRepos = repos;
277
282
  }
278
- const unusable = unusableSearchScopeNames(org, resolvedRepos);
283
+ const unusable = unusableSearchScopeNames(id, org, resolvedRepos);
279
284
  if (unusable.length > 0) return { rejection: {
280
285
  reason: "unusable-scope",
281
286
  scopes: unusable