@gitkraken/core-gitlens 0.7.0 → 0.7.1
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.
- package/CHANGELOG.md +21 -2
- package/dist/git/remotes/azure-devops.js +3 -3
- package/dist/git/remotes/azure-devops.js.map +1 -1
- package/dist/git-cli/exec/git.js +1 -1
- package/dist/git-cli/providers/config.js +2 -2
- package/dist/git-cli/providers/pausedOperations.js +2 -2
- package/dist/git-cli/providers/pausedOperations.js.map +1 -1
- package/dist/git-cli/providers/staging.js +1 -1
- package/dist/ipc/discovery.js +1 -1
- package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
- package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
- package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -1
- package/dist/plus/integrations/authentication/configuredIntegrationService.js +9 -2
- package/dist/plus/integrations/authentication/configuredIntegrationService.js.map +1 -1
- package/dist/plus/integrations/authentication/integrationAuthenticationProvider.js +1 -1
- package/dist/plus/integrations/index.d.ts +2 -2
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +34 -0
- package/dist/plus/integrations/integrationService.js +90 -43
- package/dist/plus/integrations/integrationService.js.map +1 -1
- package/dist/plus/integrations/lite.js +5 -4
- package/dist/plus/integrations/lite.js.map +1 -1
- package/dist/plus/integrations/manager.d.ts +68 -23
- package/dist/plus/integrations/models/gitHostIntegration.d.ts +30 -14
- package/dist/plus/integrations/models/gitHostIntegration.js +59 -21
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +3 -3
- package/dist/plus/integrations/models/integration.js +1 -1
- package/dist/plus/integrations/models/integration.js.map +1 -1
- package/dist/plus/integrations/models/issueReads.d.ts +17 -0
- package/dist/plus/integrations/models/pullRequestReads.d.ts +26 -0
- package/dist/plus/integrations/providers/azure/azure.d.ts +66 -1
- package/dist/plus/integrations/providers/azure/azure.js +192 -12
- package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
- package/dist/plus/integrations/providers/azure/search.d.ts +144 -0
- package/dist/plus/integrations/providers/azure/search.js +250 -0
- package/dist/plus/integrations/providers/azure/search.js.map +1 -0
- package/dist/plus/integrations/providers/azureDevOps.d.ts +54 -35
- package/dist/plus/integrations/providers/azureDevOps.js +154 -121
- package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
- package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +176 -0
- package/dist/plus/integrations/providers/azureDevOpsServer.js +560 -0
- package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.d.ts +12 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.js +128 -0
- package/dist/plus/integrations/providers/bitbucket-server/discovery.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server/models.d.ts +10 -1
- package/dist/plus/integrations/providers/bitbucket-server/models.js +3 -2
- package/dist/plus/integrations/providers/bitbucket-server/models.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.d.ts +58 -0
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +485 -0
- package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -0
- package/dist/plus/integrations/providers/bitbucket-server.d.ts +52 -3
- package/dist/plus/integrations/providers/bitbucket-server.js +96 -20
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.js +1 -1
- package/dist/plus/integrations/providers/github.d.ts +5 -5
- package/dist/plus/integrations/providers/github.js +34 -33
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +5 -3
- package/dist/plus/integrations/providers/gitlab.js +31 -27
- package/dist/plus/integrations/providers/gitlab.js.map +1 -1
- package/dist/plus/integrations/providers/issueSorts.d.ts +1 -0
- package/dist/plus/integrations/providers/jira-server.d.ts +9 -0
- package/dist/plus/integrations/providers/jira-server.js +13 -2
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.js +1 -1
- package/dist/plus/integrations/providers/linear.js +1 -1
- package/dist/plus/integrations/providers/models.d.ts +9 -0
- package/dist/plus/integrations/providers/models.js +86 -0
- package/dist/plus/integrations/providers/models.js.map +1 -1
- package/dist/plus/integrations/providers/providersApi.d.ts +10 -0
- package/dist/plus/integrations/providers/providersApi.js +16 -1
- package/dist/plus/integrations/providers/providersApi.js.map +1 -1
- package/dist/plus/integrations/providers/utils.d.ts +12 -1
- package/dist/plus/integrations/providers/utils.js +36 -6
- package/dist/plus/integrations/providers/utils.js.map +1 -1
- package/dist/plus/integrations/reads/broaden.js +5 -5
- package/dist/plus/integrations/reads/broaden.js.map +1 -1
- package/dist/plus/integrations/reads/counts.d.ts +18 -4
- package/dist/plus/integrations/reads/counts.js +66 -15
- package/dist/plus/integrations/reads/counts.js.map +1 -1
- package/dist/plus/integrations/reads/cursors.js +1 -1
- package/dist/plus/integrations/reads/drains.d.ts +1 -0
- package/dist/plus/integrations/reads/filters.d.ts +1 -1
- package/dist/plus/integrations/reads/filters.js +17 -12
- package/dist/plus/integrations/reads/filters.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.js +1 -1
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +1 -1
- package/dist/plus/integrations/reads/issues.js +1 -1
- package/dist/plus/integrations/reads/paging.js +1 -1
- package/dist/plus/integrations/reads/resolveRepository.d.ts +12 -11
- package/dist/plus/integrations/reads/resolveRepository.js +109 -18
- package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
- package/dist/plus/integrations/reads/searchIssues.js +4 -2
- package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
- package/dist/plus/integrations/reads/searchPullRequests.js +9 -3
- package/dist/plus/integrations/reads/searchPullRequests.js.map +1 -1
- package/dist/plus/integrations/reads/trackerIssue.d.ts +9 -5
- package/dist/plus/integrations/reads/trackerIssue.js +15 -9
- package/dist/plus/integrations/reads/trackerIssue.js.map +1 -1
- package/dist/plus/integrations/reads/warnings.d.ts +6 -2
- package/dist/plus/integrations/reads/warnings.js +15 -19
- package/dist/plus/integrations/reads/warnings.js.map +1 -1
- package/dist/plus/integrations/utils/domain.utils.d.ts +19 -0
- package/dist/plus/integrations/utils/domain.utils.js +36 -1
- package/dist/plus/integrations/utils/domain.utils.js.map +1 -1
- package/dist/plus/integrations/utils/integration.utils.d.ts +1 -0
- package/dist/plus/integrations/utils/integration.utils.js +12 -1
- package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
- package/dist/utils/env/node/exec.js +2 -2
- package/docs/integrations.md +176 -73
- package/docs/kepler-read-api-parity.md +19 -6
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { chunk } from "../../../utils/array.js";
|
|
2
2
|
import { mapBounded } from "../../../utils/promise.js";
|
|
3
|
-
import { appendDedupedWarning } from "../results.js";
|
|
3
|
+
import { appendDedupedWarning, toProviderWarning } from "../results.js";
|
|
4
4
|
import "../constants.js";
|
|
5
5
|
import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
|
|
6
6
|
import { providersMetadata } from "../providers/models.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
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
|
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
|
|
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,5 +1,6 @@
|
|
|
1
1
|
import { PullRequestFilter, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
|
|
2
2
|
import { IntegrationIds } from "../constants.js";
|
|
3
|
+
import "../providerFilters.js";
|
|
3
4
|
import { GitHostIntegration } from "../models/gitHostIntegration.js";
|
|
4
5
|
import { IntegrationResult } from "../models/integration.js";
|
|
5
6
|
import { ProviderWarning } from "../results.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.
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
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
|