@gitkraken/core-gitlens 0.7.1 → 0.8.0

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 (188) hide show
  1. package/CHANGELOG.md +48 -1
  2. package/dist/git/cache.d.ts +18 -5
  3. package/dist/git/cache.js +26 -8
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/context.d.ts +5 -1
  6. package/dist/git/context.js.map +1 -1
  7. package/dist/git/errors.d.ts +28 -1
  8. package/dist/git/errors.js +45 -1
  9. package/dist/git/errors.js.map +1 -1
  10. package/dist/git/providers/branches.d.ts +5 -1
  11. package/dist/git/providers/commits.d.ts +24 -1
  12. package/dist/git/providers/config.d.ts +7 -1
  13. package/dist/git/providers/operations.d.ts +6 -0
  14. package/dist/git/providers/provider.d.ts +17 -0
  15. package/dist/git/providers/refs.d.ts +38 -3
  16. package/dist/git/providers/revision.d.ts +5 -1
  17. package/dist/git/providers/staging.d.ts +12 -0
  18. package/dist/git/providers/worktrees.d.ts +23 -3
  19. package/dist/git/run.types.d.ts +6 -0
  20. package/dist/git-cli/cliGitProvider.d.ts +17 -1
  21. package/dist/git-cli/cliGitProvider.js +37 -1
  22. package/dist/git-cli/cliGitProvider.js.map +1 -1
  23. package/dist/git-cli/exec/git.d.ts +12 -2
  24. package/dist/git-cli/exec/git.js +48 -17
  25. package/dist/git-cli/exec/git.js.map +1 -1
  26. package/dist/git-cli/exec/gitQueue.d.ts +4 -1
  27. package/dist/git-cli/exec/gitQueue.js +1 -1
  28. package/dist/git-cli/exec/gitQueue.js.map +1 -1
  29. package/dist/git-cli/providers/branches.d.ts +10 -1
  30. package/dist/git-cli/providers/branches.js +15 -8
  31. package/dist/git-cli/providers/branches.js.map +1 -1
  32. package/dist/git-cli/providers/commits.d.ts +23 -1
  33. package/dist/git-cli/providers/commits.js +57 -8
  34. package/dist/git-cli/providers/commits.js.map +1 -1
  35. package/dist/git-cli/providers/config.js +4 -3
  36. package/dist/git-cli/providers/config.js.map +1 -1
  37. package/dist/git-cli/providers/operations.d.ts +1 -0
  38. package/dist/git-cli/providers/operations.js +39 -3
  39. package/dist/git-cli/providers/operations.js.map +1 -1
  40. package/dist/git-cli/providers/patch.js +20 -41
  41. package/dist/git-cli/providers/patch.js.map +1 -1
  42. package/dist/git-cli/providers/pausedOperations.js +2 -2
  43. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  44. package/dist/git-cli/providers/refs.d.ts +15 -3
  45. package/dist/git-cli/providers/refs.js +115 -10
  46. package/dist/git-cli/providers/refs.js.map +1 -1
  47. package/dist/git-cli/providers/revision.d.ts +3 -1
  48. package/dist/git-cli/providers/revision.js +9 -5
  49. package/dist/git-cli/providers/revision.js.map +1 -1
  50. package/dist/git-cli/providers/staging.d.ts +8 -0
  51. package/dist/git-cli/providers/staging.js +59 -7
  52. package/dist/git-cli/providers/staging.js.map +1 -1
  53. package/dist/git-cli/providers/worktrees.d.ts +31 -2
  54. package/dist/git-cli/providers/worktrees.js +75 -7
  55. package/dist/git-cli/providers/worktrees.js.map +1 -1
  56. package/dist/ipc/discovery.js +1 -1
  57. package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
  58. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  59. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -1
  60. package/dist/plus/git-github/api/github.d.ts +64 -3
  61. package/dist/plus/git-github/api/github.js +237 -24
  62. package/dist/plus/git-github/api/github.js.map +1 -1
  63. package/dist/plus/git-github/providers/github/branches.d.ts +3 -1
  64. package/dist/plus/git-github/providers/github/branches.js +2 -1
  65. package/dist/plus/git-github/providers/github/branches.js.map +1 -1
  66. package/dist/plus/git-github/providers/github/refs.d.ts +10 -3
  67. package/dist/plus/git-github/providers/github/refs.js +4 -4
  68. package/dist/plus/git-github/providers/github/refs.js.map +1 -1
  69. package/dist/plus/git-github/providers/github/revision.d.ts +3 -1
  70. package/dist/plus/git-github/providers/github/revision.js +1 -1
  71. package/dist/plus/git-github/providers/github/revision.js.map +1 -1
  72. package/dist/plus/integrations/collectionMetadata.d.ts +48 -3
  73. package/dist/plus/integrations/collectionMetadata.js +138 -8
  74. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  75. package/dist/plus/integrations/errors.d.ts +3 -1
  76. package/dist/plus/integrations/errors.js +7 -3
  77. package/dist/plus/integrations/errors.js.map +1 -1
  78. package/dist/plus/integrations/index.d.ts +6 -3
  79. package/dist/plus/integrations/index.js.map +1 -1
  80. package/dist/plus/integrations/integrationService.d.ts +36 -10
  81. package/dist/plus/integrations/integrationService.js +16 -3
  82. package/dist/plus/integrations/integrationService.js.map +1 -1
  83. package/dist/plus/integrations/manager.d.ts +107 -37
  84. package/dist/plus/integrations/models/gitHostIntegration.d.ts +129 -14
  85. package/dist/plus/integrations/models/gitHostIntegration.js +142 -18
  86. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  87. package/dist/plus/integrations/models/integration.d.ts +117 -11
  88. package/dist/plus/integrations/models/integration.js +175 -13
  89. package/dist/plus/integrations/models/integration.js.map +1 -1
  90. package/dist/plus/integrations/models/issuesIntegration.d.ts +18 -5
  91. package/dist/plus/integrations/models/issuesIntegration.js +42 -10
  92. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  93. package/dist/plus/integrations/providers/azure/azure.d.ts +1 -1
  94. package/dist/plus/integrations/providers/azure/azure.js +14 -18
  95. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  96. package/dist/plus/integrations/providers/azure/models.d.ts +29 -0
  97. package/dist/plus/integrations/providers/azure/models.js +112 -1
  98. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  99. package/dist/plus/integrations/providers/azureDevOps.d.ts +73 -3
  100. package/dist/plus/integrations/providers/azureDevOps.js +117 -3
  101. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  102. package/dist/plus/integrations/providers/azureDevOpsServer.js +2 -1
  103. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
  104. package/dist/plus/integrations/providers/bitbucket/bitbucket.d.ts +61 -0
  105. package/dist/plus/integrations/providers/bitbucket/bitbucket.js +165 -23
  106. package/dist/plus/integrations/providers/bitbucket/bitbucket.js.map +1 -1
  107. package/dist/plus/integrations/providers/bitbucket/models.d.ts +6 -1
  108. package/dist/plus/integrations/providers/bitbucket/models.js +4 -3
  109. package/dist/plus/integrations/providers/bitbucket/models.js.map +1 -1
  110. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js +2 -2
  111. package/dist/plus/integrations/providers/bitbucket-server/pullRequestSearch.js.map +1 -1
  112. package/dist/plus/integrations/providers/bitbucket-server.d.ts +48 -2
  113. package/dist/plus/integrations/providers/bitbucket-server.js +51 -1
  114. package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
  115. package/dist/plus/integrations/providers/bitbucket.d.ts +45 -2
  116. package/dist/plus/integrations/providers/bitbucket.js +41 -2
  117. package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
  118. package/dist/plus/integrations/providers/github.d.ts +49 -5
  119. package/dist/plus/integrations/providers/github.js +102 -7
  120. package/dist/plus/integrations/providers/github.js.map +1 -1
  121. package/dist/plus/integrations/providers/gitlab/gitlab.d.ts +35 -0
  122. package/dist/plus/integrations/providers/gitlab/gitlab.js +132 -15
  123. package/dist/plus/integrations/providers/gitlab/gitlab.js.map +1 -1
  124. package/dist/plus/integrations/providers/gitlab/models.d.ts +1 -0
  125. package/dist/plus/integrations/providers/gitlab/models.js +1 -1
  126. package/dist/plus/integrations/providers/gitlab/models.js.map +1 -1
  127. package/dist/plus/integrations/providers/gitlab.d.ts +49 -2
  128. package/dist/plus/integrations/providers/gitlab.js +80 -4
  129. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  130. package/dist/plus/integrations/providers/jira-server.d.ts +3 -3
  131. package/dist/plus/integrations/providers/jira-server.js +5 -8
  132. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  133. package/dist/plus/integrations/providers/jira.d.ts +9 -0
  134. package/dist/plus/integrations/providers/jira.js +13 -1
  135. package/dist/plus/integrations/providers/jira.js.map +1 -1
  136. package/dist/plus/integrations/providers/linear.js +1 -1
  137. package/dist/plus/integrations/providers/models.d.ts +13 -2
  138. package/dist/plus/integrations/providers/models.js +11 -1
  139. package/dist/plus/integrations/providers/models.js.map +1 -1
  140. package/dist/plus/integrations/providers/providerErrors.d.ts +9 -1
  141. package/dist/plus/integrations/providers/providerErrors.js +10 -1
  142. package/dist/plus/integrations/providers/providerErrors.js.map +1 -1
  143. package/dist/plus/integrations/providers/providersApi.d.ts +60 -1
  144. package/dist/plus/integrations/providers/providersApi.js +137 -2
  145. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  146. package/dist/plus/integrations/providers/utils/providerPaging.d.ts +42 -0
  147. package/dist/plus/integrations/providers/utils/providerPaging.js +58 -1
  148. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  149. package/dist/plus/integrations/providers/utils.d.ts +1 -1
  150. package/dist/plus/integrations/reads/broaden.js +1 -1
  151. package/dist/plus/integrations/reads/context.d.ts +1 -1
  152. package/dist/plus/integrations/reads/counts.js +1 -1
  153. package/dist/plus/integrations/reads/currentAccount.d.ts +35 -0
  154. package/dist/plus/integrations/reads/currentAccount.js +46 -0
  155. package/dist/plus/integrations/reads/currentAccount.js.map +1 -0
  156. package/dist/plus/integrations/reads/drains.d.ts +5 -2
  157. package/dist/plus/integrations/reads/drains.js +8 -4
  158. package/dist/plus/integrations/reads/drains.js.map +1 -1
  159. package/dist/plus/integrations/reads/hierarchy.js +1 -1
  160. package/dist/plus/integrations/reads/hierarchy.utils.js +1 -1
  161. package/dist/plus/integrations/reads/issueBatch.d.ts +72 -12
  162. package/dist/plus/integrations/reads/issueBatch.js +193 -48
  163. package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
  164. package/dist/plus/integrations/reads/issueTracker.js +1 -1
  165. package/dist/plus/integrations/reads/issues.js +1 -1
  166. package/dist/plus/integrations/reads/pullRequestBatch.d.ts +57 -0
  167. package/dist/plus/integrations/reads/pullRequestBatch.js +80 -0
  168. package/dist/plus/integrations/reads/pullRequestBatch.js.map +1 -0
  169. package/dist/plus/integrations/reads/pullRequestBranches.d.ts +58 -0
  170. package/dist/plus/integrations/reads/pullRequestBranches.js +111 -0
  171. package/dist/plus/integrations/reads/pullRequestBranches.js.map +1 -0
  172. package/dist/plus/integrations/reads/pullRequests.js +3 -3
  173. package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
  174. package/dist/plus/integrations/reads/resolveRepository.js +1 -1
  175. package/dist/plus/integrations/reads/sweeps.js +4 -4
  176. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  177. package/dist/plus/integrations/results.d.ts +87 -6
  178. package/dist/plus/integrations/results.js +9 -2
  179. package/dist/plus/integrations/results.js.map +1 -1
  180. package/dist/utils/env/node/exec.js +3 -3
  181. package/dist/utils/env/node/exec.js.map +1 -1
  182. package/docs/integrations.md +175 -35
  183. package/docs/kepler-git-write-api-parity.md +239 -0
  184. package/docs/kepler-read-api-parity.md +75 -28
  185. package/package.json +1 -1
  186. package/dist/plus/integrations/reads/trackerIssue.d.ts +0 -33
  187. package/dist/plus/integrations/reads/trackerIssue.js +0 -60
  188. package/dist/plus/integrations/reads/trackerIssue.js.map +0 -1
@@ -1,41 +1,73 @@
1
1
  import { IssueShape } from "../../../git/models/issue.js";
2
2
  import { IntegrationIds } from "../constants.js";
3
- import { ProviderResult } from "../results.js";
3
+ import { ProviderResult, ProviderWarning } from "../results.js";
4
+ import { BatchSlot } from "../models/integration.js";
4
5
  import { ProviderReadContext } from "./context.js";
5
6
  //#region ../plus/integrations/src/reads/issueBatch.d.ts
6
7
  /**
7
- * The BATCH issue read: resolve N `(owner, repo, number)` coordinates in one request.
8
+ * The BATCH issue read: resolve N issues BY IDENTITY in one call — `(owner, repo, number)` coordinates on a git
9
+ * host, `(resourceId, ABC-123)` identifiers on an issue tracker.
8
10
  *
9
11
  * A sibling of the issue searches rather than a mode of one, because it answers a different question. A search
10
12
  * asks "what matches"; this asks "does this exact issue exist", which is what a caller correlating a branch name
11
13
  * to the issue it references is actually asking. Three consequences follow, and they are the reason this exists:
12
14
  *
13
- * - It resolves by EXACT NUMBER, so relevance never enters into it.
15
+ * - It resolves by EXACT IDENTIFIER, so relevance never enters into it.
14
16
  * - No result ceiling applies, so there is no partial window to reason about and no omission to report.
15
17
  * - An absent slot is a PROVEN ABSENCE, not "not found within a page budget". That is the property that matters
16
18
  * most: a caller can CACHE a miss. Emulating this with a paged list cannot prove absence without walking the
17
19
  * whole scope, so a miss stays unproven and the walk repeats on every pass, forever.
18
20
  *
19
- * Modeled on `countIssues` for its shape — caller-owned `key` echoed back, per-target isolation, chunked into as
20
- * few requests as possible — because both take a set of independent questions and answer them together.
21
+ * Modeled on `countIssues` for its shape — caller-owned `key` echoed back, per-target isolation, as few requests
22
+ * as the provider allows — because both take a set of independent questions and answer them together.
23
+ *
24
+ * ONE integration call per invocation, whatever the target count, as in `getPullRequestsBatch`: the integration
25
+ * fans its targets out (chunked for GitHub/GHE, one request per target with bounded concurrency on GitLab and
26
+ * Azure DevOps and on the trackers) and settles each independently, so failing targets spend at most one strike
27
+ * of the integration's failure budget.
28
+ */
29
+ /** One issue to resolve, echoed back under the caller's own `key`. Which form a call takes depends on its provider. */
30
+ export type IssueBatchTarget =
31
+ /**
32
+ * A git host's issue, by repository coordinate. GitHub/GHE, GitLab (and self-managed) and Azure DevOps (and
33
+ * Server); Bitbucket and Bitbucket Data Center have no issues and refuse.
21
34
  */
22
- /** One issue to resolve, identified by coordinate and echoed back under the caller's own `key`. */
23
- export interface IssueBatchTarget {
24
- /** Caller-owned identifier, echoed on the result so no positional matching is needed. Must be unique. */
35
+ {
25
36
  key: string;
37
+ /** GitHub owner, GitLab namespace path (may be nested), or Azure DevOps organization (or collection). */
26
38
  owner: string;
39
+ /**
40
+ * Repository name. Required, but ignored on Azure DevOps, whose work items belong to the project rather
41
+ * than to a repository.
42
+ */
27
43
  repo: string;
44
+ /** Issue number, GitLab issue iid, or Azure DevOps work item id. */
28
45
  number: number;
29
- }
46
+ /** Azure DevOps project. Required there, ignored elsewhere. */
47
+ project?: string;
48
+ } |
49
+ /**
50
+ * An issue tracker's issue, by its own identifier (e.g. `ABC-123`) within a resource. Jira (Cloud and Data
51
+ * Center) and Linear. For Jira Data Center `resourceId` is the host — the instance's single resource, as
52
+ * `listOrgs` reports it — and `resourceUrl` is not needed, since the browser link is built from the
53
+ * connection's own base URL; Jira Cloud requires it.
54
+ */
55
+ {
56
+ key: string;
57
+ resourceId: string;
58
+ resourceUrl?: string;
59
+ identifier: string;
60
+ };
30
61
  /** The answer for one {@link IssueBatchTarget}. */
31
62
  export interface IssueBatchResult {
32
63
  key: string;
33
64
  /**
34
65
  * The resolved issue, or `undefined` when it PROVABLY does not exist (or is not visible to this connection).
35
66
  *
36
- * Absent is an answer here, unlike every paged read on this facade: a target whose chunk FAILED is not
37
- * returned at all and sets `fetchFailed`, so a caller can tell "proven absent" from "unknown" and cache the
38
- * first without ever caching the second.
67
+ * Absent is an answer here, unlike every paged read on this facade: a target that FAILED — its own request
68
+ * outright, or just its own alias within an otherwise-answering GitHub chunk (e.g. an org enforcing SAML SSO
69
+ * the token isn't authorized for) — is not returned at all and sets `fetchFailed`, so a caller can tell
70
+ * "proven absent" from "unknown" and cache the first without ever caching the second.
39
71
  */
40
72
  issue?: IssueShape;
41
73
  }
@@ -49,5 +81,33 @@ export declare function getIssuesBatch(ctx: ProviderReadContext, options: {
49
81
  */
50
82
  domain?: string;
51
83
  }): Promise<ProviderResult<IssueBatchResult>>;
84
+ /**
85
+ * Trims `owner`, `repo` and `project` once, shared by every coordinate-form batch read (this one, the pull request
86
+ * batch and the pull-requests-by-branch read): leading/trailing whitespace can't change which repository a
87
+ * coordinate names, so it's stripped and accepted rather than refused, matching #5821's scope-name precedent.
88
+ */
89
+ export declare function trimCoordinateFields<T extends {
90
+ owner: string;
91
+ repo: string;
92
+ project?: string;
93
+ }>(target: T): T;
94
+ /**
95
+ * The answer when no integration resolves for a batch read. Targets were asked about, so this is never a silent
96
+ * empty success: a supplied connection or domain that no longer resolves gets its own warning, and the untargeted
97
+ * primary path (e.g. a self-managed provider with no configured host) a connection warning, as `getCurrentAccount`
98
+ * gives — either way `fetchFailed`, so no caller mistakes the dropped targets for an answer.
99
+ */
100
+ export declare function unresolvedIntegration<T>(ctx: ProviderReadContext, providerId: IntegrationIds, connectionId: string | undefined, domain: string | undefined): ProviderResult<T>;
101
+ /**
102
+ * Appends the warning for a batch target that could not be checked, shared by every batch read: for a refusal, the
103
+ * scope failure it was recorded as (see `IntegrationBase.settleBatchRefusals`), published as an account-wide read
104
+ * publishes one, scoped and with its cause; otherwise, the reason itself.
105
+ */
106
+ export declare function appendBatchSlotWarning(warnings: ProviderWarning[], providerId: IntegrationIds, domain: string | undefined, connectionId: string | undefined, slot: Extract<BatchSlot<unknown>, {
107
+ status: 'rejected';
108
+ }>): void;
109
+ export declare function findDuplicateKey(targets: readonly {
110
+ key: string;
111
+ }[]): string | undefined;
52
112
  //#endregion
53
113
  //# sourceMappingURL=issueBatch.d.ts.map
@@ -1,74 +1,137 @@
1
- import { chunk } from "../../../utils/array.js";
2
- import { mapBounded } from "../../../utils/promise.js";
3
- import { appendDedupedWarning } from "../results.js";
4
1
  import "../constants.js";
5
- import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
6
- import { gitHostOnlySurfaceWarning, issuesUnsupportedWarning, otherWarning } from "./warnings.js";
2
+ import { githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId } from "../providers/providerErrors.js";
3
+ import { appendDedupedWarning, toProviderWarning } from "../results.js";
4
+ import { mergeAssessmentInto } from "../collectionMetadata.js";
5
+ import { areDomainsOnSameHost, hostFromDomain } from "../utils/domain.utils.js";
6
+ import { isGitHostIntegration, isIssuesHostIntegrationId, isIssuesSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
7
+ import { gitHostOnlySurfaceWarning, issueTrackerOnlySurfaceWarning, issuesUnsupportedWarning, noConnectionWarning, otherWarning } from "./warnings.js";
7
8
  import { runCaptured } from "./drains.js";
9
+ import { isIssuesIntegration } from "../models/issuesIntegration.js";
8
10
  //#region ../plus/integrations/src/reads/issueBatch.ts
9
- /**
10
- * How many coordinates go into one upstream request.
11
- *
12
- * Measured rather than inherited from `issueCountChunkSize`, since that one rests on a zero-node selection and
13
- * this carries the full issue projection per alias. Against the live API on all-resolving coordinates: 10 took
14
- * 875ms, 25 took 890ms — so up to 25 is effectively free — 50 took 1.3s, and 75 took 2.7s. No complexity refusal
15
- * appeared up to 100.
16
- *
17
- * 25 is where latency is still flat, with room before it degrades. It also puts any realistic correlation batch
18
- * in ONE request, which is the whole point of the read.
19
- */
20
- const issueBatchChunkSize = 25;
21
- async function getIssuesBatch(ctx, options) {
22
- const refused = (warning) => ({
11
+ const surface = "Batch issue resolution";
12
+ function refused(warning) {
13
+ return {
23
14
  items: [],
24
15
  warnings: [warning],
25
16
  fetchFailed: true
26
- });
27
- if (isIssuesHostIntegrationId(options.providerId)) return refused(gitHostOnlySurfaceWarning(options.providerId, void 0, options.connectionId, "Batch issue resolution"));
17
+ };
18
+ }
19
+ async function getIssuesBatch(ctx, options) {
28
20
  if (options.targets.length === 0) return {
29
21
  items: [],
30
22
  warnings: []
31
23
  };
32
24
  const duplicateKey = findDuplicateKey(options.targets);
33
25
  if (duplicateKey != null) return refused(otherWarning(options.providerId, void 0, options.connectionId, `Duplicate issue batch target key '${duplicateKey}'; keys identify results, so each must be unique.`));
26
+ if (isIssuesHostIntegrationId(options.providerId)) return getIssuesBatchForTracker(ctx, options.providerId, options.targets, options.connectionId, options.domain);
27
+ const targets = options.targets;
28
+ if (!targets.every(isCoordinateTarget)) return refused(otherWarning(options.providerId, void 0, options.connectionId, `${surface} for '${options.providerId}' takes repository coordinates { key, owner, repo, number, project? }, not tracker identifiers.`));
29
+ const trimmedTargets = targets.map(trimCoordinateFields);
30
+ const invalid = findInvalidCoordinateTarget(options.providerId, trimmedTargets);
31
+ if (invalid != null) return refused(otherWarning(options.providerId, void 0, options.connectionId, invalid));
34
32
  const integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);
35
- if (integration == null) {
36
- const early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);
37
- return {
38
- items: [],
39
- warnings: early.warnings,
40
- fetchFailed: early.fetchFailed || void 0
41
- };
42
- }
43
- if (!isGitHostIntegration(integration)) return refused(gitHostOnlySurfaceWarning(options.providerId, void 0, options.connectionId, "Batch issue resolution"));
33
+ if (integration == null) return unresolvedIntegration(ctx, options.providerId, options.connectionId, options.domain);
34
+ if (!isGitHostIntegration(integration)) return refused(gitHostOnlySurfaceWarning(options.providerId, void 0, options.connectionId, surface));
44
35
  const domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);
45
- const warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);
46
36
  if (!integration.supportsIssues) return refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));
47
37
  const warnings = [];
48
38
  let fetchFailed = false;
49
- const batches = await mapBounded(chunk([...options.targets], issueBatchChunkSize), 6, (batch) => runCaptured(options.providerId, domain, options.connectionId, () => integration.getIssuesBatchResult(batch.map((t) => ({
39
+ const { value: slots, warning } = await runCaptured(options.providerId, domain, options.connectionId, () => integration.getIssuesBatchResult(trimmedTargets.map((t) => ({
50
40
  owner: t.owner,
51
41
  repo: t.repo,
52
- number: t.number
53
- })), void 0, options.connectionId), { warnOnMissingSession }).then((result) => ({
54
- batch,
55
- ...result
56
- })));
42
+ number: t.number,
43
+ project: t.project
44
+ })), void 0, options.connectionId), { warnOnMissingSession: true });
45
+ if (warning != null) appendDedupedWarning(warnings, warning);
46
+ if (slots == null) {
47
+ if (warning == null) appendDedupedWarning(warnings, otherWarning(options.providerId, domain, options.connectionId, `${surface} is not supported by '${options.providerId}'; resolve issues individually instead.`));
48
+ return {
49
+ items: [],
50
+ warnings,
51
+ fetchFailed: true
52
+ };
53
+ }
57
54
  const items = [];
58
- for (const { batch, value, warning } of batches) {
59
- if (warning != null) appendDedupedWarning(warnings, warning);
60
- if (value == null) {
61
- if (warning == null) appendDedupedWarning(warnings, otherWarning(options.providerId, domain, options.connectionId, `Batch issue resolution is not supported by '${options.providerId}'; resolve issues individually instead.`));
55
+ for (let i = 0; i < trimmedTargets.length; i++) {
56
+ const slot = slots[i];
57
+ if (slot.status === "rejected") {
58
+ appendBatchSlotWarning(warnings, options.providerId, domain, options.connectionId, slot);
62
59
  fetchFailed = true;
63
60
  continue;
64
61
  }
65
- for (let i = 0; i < batch.length; i++) {
66
- const issue = value[i];
67
- items.push({
68
- key: batch[i].key,
69
- ...issue != null ? { issue } : {}
70
- });
62
+ const issue = slot.value;
63
+ items.push({
64
+ key: trimmedTargets[i].key,
65
+ ...issue != null ? { issue } : {}
66
+ });
67
+ }
68
+ return {
69
+ items,
70
+ warnings,
71
+ fetchFailed: fetchFailed || void 0
72
+ };
73
+ }
74
+ /**
75
+ * The tracker form. ONE integration call per invocation, whatever the target count: the integration makes one
76
+ * single-issue request per target, with bounded concurrency, and settles each independently — see
77
+ * `IssuesIntegration.getIssuesByResourceIdBatchResult`.
78
+ *
79
+ * `resourceId` is trusted and the read does no resource discovery. It is handed to the provider's by-resource-id
80
+ * read as-is, never wrapped into a synthesized descriptor: Linear and Trello answer `undefined` for a descriptor
81
+ * that fails `isIssueResourceDescriptor`, which would be published as a proven absence.
82
+ *
83
+ * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for
84
+ * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host. Unlike
85
+ * the paged reads, this one never falls back to the primary connection for a self-managed tracker: it requires a
86
+ * `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted instances routinely
87
+ * issue the same project and issue keys, and `issue: undefined` is a proven absence a caller may cache — an answer
88
+ * from whichever host happens to be primary would be cached under a key that names a different instance (#5872).
89
+ */
90
+ async function getIssuesBatchForTracker(ctx, providerId, targets, connectionId, domain) {
91
+ if (!targets.every(isTrackerTarget)) return refused(otherWarning(providerId, void 0, connectionId, `${surface} for '${providerId}' takes tracker identifiers { key, resourceId, resourceUrl?, identifier }, not repository coordinates.`));
92
+ const trimmedTargets = targets.map(trimTrackerTarget);
93
+ const invalid = findInvalidTrackerTarget(providerId, trimmedTargets);
94
+ if (invalid != null) return refused(otherWarning(providerId, void 0, connectionId, invalid));
95
+ if (isIssuesSelfManagedHostIntegrationId(providerId) && (domain != null ? hostFromDomain(domain) == null : connectionId == null || !ctx.getConfigured(providerId).some((c) => c.id === connectionId && hostFromDomain(c.domain) != null))) return refused(otherWarning(providerId, void 0, connectionId, `${surface} requires a domain or a configured connection id for '${providerId}': every configured host can hold a different issue under the same key, so the read does not answer from the primary connection.`));
96
+ const integration = await ctx.getIntegrationForRead(providerId, connectionId, domain);
97
+ if (integration == null) return unresolvedIntegration(ctx, providerId, connectionId, domain);
98
+ if (!isIssuesIntegration(integration)) return refused(issueTrackerOnlySurfaceWarning(providerId, connectionId, surface));
99
+ const unsupported = () => otherWarning(providerId, void 0, connectionId, `${surface} is not supported by '${providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`);
100
+ if (!integration.supportsIssueLookupByResourceId) return refused(unsupported());
101
+ if (isIssuesSelfManagedHostIntegrationId(providerId)) {
102
+ const mismatched = trimmedTargets.find((t) => !areDomainsOnSameHost(t.resourceId, integration.domain));
103
+ if (mismatched != null) return refused(otherWarning(providerId, void 0, connectionId, `Issue batch target '${mismatched.key}' requires the resource id of '${providerId}' to name the host the read resolved to.`));
104
+ }
105
+ const resolvedDomain = ctx.domainForRead(integration, providerId, connectionId, domain);
106
+ const warnings = [];
107
+ let fetchFailed = false;
108
+ const { value: slots, warning } = await runCaptured(providerId, resolvedDomain, connectionId, () => integration.getIssuesByResourceIdBatchResult(trimmedTargets.map((t) => ({
109
+ resourceId: t.resourceId,
110
+ identifier: t.identifier,
111
+ resourceUrl: t.resourceUrl
112
+ })), connectionId), { warnOnMissingSession: true });
113
+ if (warning != null) appendDedupedWarning(warnings, warning);
114
+ if (slots == null) {
115
+ if (warning == null) appendDedupedWarning(warnings, unsupported());
116
+ return {
117
+ items: [],
118
+ warnings,
119
+ fetchFailed: true
120
+ };
121
+ }
122
+ const items = [];
123
+ for (let i = 0; i < trimmedTargets.length; i++) {
124
+ const slot = slots[i];
125
+ if (slot.status === "rejected") {
126
+ appendBatchSlotWarning(warnings, providerId, resolvedDomain, connectionId, slot);
127
+ fetchFailed = true;
128
+ continue;
71
129
  }
130
+ const issue = slot.value;
131
+ items.push({
132
+ key: trimmedTargets[i].key,
133
+ ...issue != null ? { issue } : {}
134
+ });
72
135
  }
73
136
  return {
74
137
  items,
@@ -76,6 +139,88 @@ async function getIssuesBatch(ctx, options) {
76
139
  fetchFailed: fetchFailed || void 0
77
140
  };
78
141
  }
142
+ function isCoordinateTarget(target) {
143
+ return "owner" in target;
144
+ }
145
+ function isTrackerTarget(target) {
146
+ return "resourceId" in target;
147
+ }
148
+ /**
149
+ * Trims `owner`, `repo` and `project` once, shared by every coordinate-form batch read (this one, the pull request
150
+ * batch and the pull-requests-by-branch read): leading/trailing whitespace can't change which repository a
151
+ * coordinate names, so it's stripped and accepted rather than refused, matching #5821's scope-name precedent.
152
+ */
153
+ function trimCoordinateFields(target) {
154
+ return {
155
+ ...target,
156
+ owner: target.owner.trim(),
157
+ repo: target.repo.trim(),
158
+ project: target.project?.trim()
159
+ };
160
+ }
161
+ /** Trims `resourceId`, `resourceUrl` and `identifier` once; the validated value must be the one sent to the provider. */
162
+ function trimTrackerTarget(target) {
163
+ return {
164
+ ...target,
165
+ resourceId: target.resourceId.trim(),
166
+ resourceUrl: target.resourceUrl?.trim() || void 0,
167
+ identifier: target.identifier.trim()
168
+ };
169
+ }
170
+ function findInvalidCoordinateTarget(providerId, targets) {
171
+ const isAzure = isAzureProviderId(providerId);
172
+ const isGitHub = isGitHubProviderId(providerId);
173
+ for (const target of targets) {
174
+ if (!Number.isSafeInteger(target.number) || target.number <= 0) return `Issue batch target '${target.key}' has number ${target.number}; expected a positive integer.`;
175
+ if (isGitHub && target.number > 2147483647) return `Issue batch target '${target.key}' has number ${target.number}; GitHub numbers are 32-bit and cannot exceed ${githubGraphQLInt32Max}.`;
176
+ if (target.owner.length === 0) return `Issue batch target '${target.key}' requires a non-empty owner.`;
177
+ if (isAzure) {
178
+ if (!target.project) return `Issue batch target '${target.key}' requires a project for '${providerId}'.`;
179
+ } else if (target.repo.length === 0) return `Issue batch target '${target.key}' requires a non-empty repo.`;
180
+ }
181
+ }
182
+ function findInvalidTrackerTarget(providerId, targets) {
183
+ for (const target of targets) {
184
+ if (target.resourceId.length === 0) return `Issue batch target '${target.key}' requires a resource id.`;
185
+ if (target.identifier.length === 0) return `Issue batch target '${target.key}' requires an issue identifier.`;
186
+ if (providerId === "jira" && !target.resourceUrl) return `Issue batch target '${target.key}' requires the Jira resource URL so the result contains a browser link without resource discovery.`;
187
+ }
188
+ }
189
+ /**
190
+ * The answer when no integration resolves for a batch read. Targets were asked about, so this is never a silent
191
+ * empty success: a supplied connection or domain that no longer resolves gets its own warning, and the untargeted
192
+ * primary path (e.g. a self-managed provider with no configured host) a connection warning, as `getCurrentAccount`
193
+ * gives — either way `fetchFailed`, so no caller mistakes the dropped targets for an answer.
194
+ */
195
+ function unresolvedIntegration(ctx, providerId, connectionId, domain) {
196
+ const early = ctx.earlyReturnConnectionWarnings(providerId, connectionId, domain);
197
+ if (early.warnings.length > 0) return {
198
+ items: [],
199
+ warnings: early.warnings,
200
+ fetchFailed: true
201
+ };
202
+ const resolvedDomain = ctx.resolveDomainForRead(providerId, connectionId, domain);
203
+ return {
204
+ items: [],
205
+ warnings: [noConnectionWarning(providerId, resolvedDomain, connectionId)],
206
+ fetchFailed: true
207
+ };
208
+ }
209
+ /**
210
+ * Appends the warning for a batch target that could not be checked, shared by every batch read: for a refusal, the
211
+ * scope failure it was recorded as (see `IntegrationBase.settleBatchRefusals`), published as an account-wide read
212
+ * publishes one, scoped and with its cause; otherwise, the reason itself.
213
+ */
214
+ function appendBatchSlotWarning(warnings, providerId, domain, connectionId, slot) {
215
+ if (slot.failure != null) {
216
+ mergeAssessmentInto(warnings, providerId, domain, connectionId, {
217
+ completeness: "partial",
218
+ failures: [slot.failure]
219
+ });
220
+ return;
221
+ }
222
+ appendDedupedWarning(warnings, toProviderWarning(providerId, domain, connectionId, slot.reason));
223
+ }
79
224
  function findDuplicateKey(targets) {
80
225
  const seen = /* @__PURE__ */ new Set();
81
226
  for (const target of targets) {
@@ -84,6 +229,6 @@ function findDuplicateKey(targets) {
84
229
  }
85
230
  }
86
231
  //#endregion
87
- export { getIssuesBatch };
232
+ export { appendBatchSlotWarning, findDuplicateKey, getIssuesBatch, trimCoordinateFields, unresolvedIntegration };
88
233
 
89
234
  //# sourceMappingURL=issueBatch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"issueBatch.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueBatch.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.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 { 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 { gitHostOnlySurfaceWarning, issuesUnsupportedWarning, otherWarning } from './warnings.js';\n\n/**\n * The BATCH issue read: resolve N `(owner, repo, number)` coordinates in one request.\n *\n * A sibling of the issue searches rather than a mode of one, because it answers a different question. A search\n * asks \"what matches\"; this asks \"does this exact issue exist\", which is what a caller correlating a branch name\n * to the issue it references is actually asking. Three consequences follow, and they are the reason this exists:\n *\n * - It resolves by EXACT NUMBER, so relevance never enters into it.\n * - No result ceiling applies, so there is no partial window to reason about and no omission to report.\n * - An absent slot is a PROVEN ABSENCE, not \"not found within a page budget\". That is the property that matters\n * most: a caller can CACHE a miss. Emulating this with a paged list cannot prove absence without walking the\n * whole scope, so a miss stays unproven and the walk repeats on every pass, forever.\n *\n * Modeled on `countIssues` for its shape — caller-owned `key` echoed back, per-target isolation, chunked into as\n * few requests as possible — because both take a set of independent questions and answer them together.\n */\n\n/** One issue to resolve, identified by coordinate and echoed back under the caller's own `key`. */\nexport interface IssueBatchTarget {\n\t/** Caller-owned identifier, echoed on the result so no positional matching is needed. Must be unique. */\n\tkey: string;\n\towner: string;\n\trepo: string;\n\tnumber: number;\n}\n\n/** The answer for one {@link IssueBatchTarget}. */\nexport interface IssueBatchResult {\n\tkey: string;\n\t/**\n\t * The resolved issue, or `undefined` when it PROVABLY does not exist (or is not visible to this connection).\n\t *\n\t * Absent is an answer here, unlike every paged read on this facade: a target whose chunk FAILED is not\n\t * returned at all and sets `fetchFailed`, so a caller can tell \"proven absent\" from \"unknown\" and cache the\n\t * first without ever caching the second.\n\t */\n\tissue?: IssueShape;\n}\n\n/**\n * How many coordinates go into one upstream request.\n *\n * Measured rather than inherited from `issueCountChunkSize`, since that one rests on a zero-node selection and\n * this carries the full issue projection per alias. Against the live API on all-resolving coordinates: 10 took\n * 875ms, 25 took 890ms — so up to 25 is effectively free — 50 took 1.3s, and 75 took 2.7s. No complexity refusal\n * appeared up to 100.\n *\n * 25 is where latency is still flat, with room before it degrades. It also puts any realistic correlation batch\n * in ONE request, which is the whole point of the read.\n */\nconst issueBatchChunkSize = 25;\n\nexport async function getIssuesBatch(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\ttargets: readonly IssueBatchTarget[];\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<IssueBatchResult>> {\n\tconst refused = (warning: ProviderWarning): ProviderResult<IssueBatchResult> => ({\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, 'Batch issue resolution'),\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.targets.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.targets);\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 target, not just\n\t\t// the 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 batch target 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, 'Batch issue resolution'),\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\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\t// Chunks are independent requests over their own slice of targets — nothing in one reads what another\n\t// produced, and `runCaptured` never throws — so they run concurrently, bounded like every other fan-out here.\n\t// `mapBounded` returns in input order, so `items` stays in target order.\n\tconst batches = await mapBounded(\n\t\tchunk([...options.targets], issueBatchChunkSize),\n\t\tproviderFanOutConcurrency,\n\t\tbatch =>\n\t\t\trunCaptured(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t() =>\n\t\t\t\t\tintegration.getIssuesBatchResult(\n\t\t\t\t\t\tbatch.map(t => ({ owner: t.owner, repo: t.repo, number: t.number })),\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t).then(result => ({ batch: batch, ...result })),\n\t);\n\n\tconst items: IssueBatchResult[] = [];\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// A provider that doesn't implement the batch hook answers `undefined` with no error. Either way this\n\t\t\t// chunk contributes nothing while the chunks around it still do, so its targets are DROPPED rather\n\t\t\t// than reported as absent — the difference between \"unknown\" and \"proven absent\" is the read's whole\n\t\t\t// value, and a failure must never be cached as an answer.\n\t\t\tif (warning == null) {\n\t\t\t\tappendDedupedWarning(\n\t\t\t\t\twarnings,\n\t\t\t\t\totherWarning(\n\t\t\t\t\t\toptions.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t\t`Batch issue resolution is not supported by '${options.providerId}'; resolve issues individually instead.`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\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 issue = value[i];\n\t\t\titems.push({ key: batch[i].key, ...(issue != null ? { issue: issue } : {}) });\n\t\t}\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\nfunction findDuplicateKey(targets: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const target of targets) {\n\t\tif (seen.has(target.key)) return target.key;\n\n\t\tseen.add(target.key);\n\t}\n\treturn undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkEA,MAAM,sBAAsB;AAE5B,eAAsB,eACrB,KACA,SAU4C;CAC5C,MAAM,WAAW,aAAgE;EAChF,OAAO,CAAC;EACR,UAAU,CAAC,OAAO;EAClB,aAAa;CACd;CAEA,IAAI,0BAA0B,QAAQ,UAAU,GAC/C,OAAO,QACN,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,wBAAwB,CACxG;CAID,IAAI,QAAQ,QAAQ,WAAW,GAAG,OAAO;EAAE,OAAO,CAAC;EAAG,UAAU,CAAC;CAAE;CAEnE,MAAM,eAAe,iBAAiB,QAAQ,OAAO;CACrD,IAAI,gBAAgB,MAInB,OAAO,QACN,aACC,QAAQ,YACR,KAAA,GACA,QAAQ,cACR,qCAAqC,aAAa,kDACnD,CACD;CAGD,MAAM,cAAc,MAAM,IAAI,sBAAsB,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CAC5G,IAAI,eAAe,MAAM;EAExB,MAAM,QAAQ,IAAI,8BAA8B,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;EACxG,OAAO;GAAE,OAAO,CAAC;GAAG,UAAU,MAAM;GAAU,aAAa,MAAM,eAAe,KAAA;EAAU;CAC3F;CACA,IAAI,CAAC,qBAAqB,WAAW,GACpC,OAAO,QACN,0BAA0B,QAAQ,YAAY,KAAA,GAAW,QAAQ,cAAc,wBAAwB,CACxG;CAGD,MAAM,SAAS,IAAI,cAAc,aAAa,QAAQ,YAAY,QAAQ,cAAc,QAAQ,MAAM;CACtG,MAAM,uBAAuB,8BAA8B,QAAQ,YAAY,QAAQ,MAAM;CAE7F,IAAI,CAAC,YAAY,gBAChB,OAAO,QAAQ,yBAAyB,QAAQ,YAAY,QAAQ,QAAQ,YAAY,CAAC;CAG1F,MAAM,WAA8B,CAAC;CACrC,IAAI,cAAc;CAKlB,MAAM,UAAU,MAAM,WACrB,MAAM,CAAC,GAAG,QAAQ,OAAO,GAAG,mBAAmB,GAAA,IAE/C,UACC,YACC,QAAQ,YACR,QACA,QAAQ,oBAEP,YAAY,qBACX,MAAM,KAAI,OAAM;EAAE,OAAO,EAAE;EAAO,MAAM,EAAE;EAAM,QAAQ,EAAE;CAAO,EAAE,GACnE,KAAA,GACA,QAAQ,YACT,GACD,EAAwB,qBAAqB,CAC9C,CAAC,CAAC,MAAK,YAAW;EAAS;EAAO,GAAG;CAAO,EAAE,CAChD;CAEA,MAAM,QAA4B,CAAC;CACnC,KAAK,MAAM,EAAE,OAAO,OAAO,aAAa,SAAS;EAChD,IAAI,WAAW,MACd,qBAAqB,UAAU,OAAO;EAEvC,IAAI,SAAS,MAAM;GAKlB,IAAI,WAAW,MACd,qBACC,UACA,aACC,QAAQ,YACR,QACA,QAAQ,cACR,+CAA+C,QAAQ,WAAW,wCACnE,CACD;GAED,cAAc;GACd;EACD;EAEA,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;GACtC,MAAM,QAAQ,MAAM;GACpB,MAAM,KAAK;IAAE,KAAK,MAAM,EAAE,CAAC;IAAK,GAAI,SAAS,OAAO,EAAS,MAAM,IAAI,CAAC;GAAG,CAAC;EAC7E;CACD;CAEA,OAAO;EAAS;EAAiB;EAAU,aAAa,eAAe,KAAA;CAAU;AAClF;AAEA,SAAS,iBAAiB,SAAyD;CAClF,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,UAAU,SAAS;EAC7B,IAAI,KAAK,IAAI,OAAO,GAAG,GAAG,OAAO,OAAO;EAExC,KAAK,IAAI,OAAO,GAAG;CACpB;AAED"}
1
+ {"version":3,"file":"issueBatch.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueBatch.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds, IssuesHostIntegrationIds } from '../constants.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport type { BatchSlot } from '../models/integration.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId } from '../providers/providerErrors.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning, toProviderWarning } from '../results.js';\nimport { areDomainsOnSameHost, hostFromDomain } from '../utils/domain.utils.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\tisIssuesSelfManagedHostIntegrationId,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\tissueTrackerOnlySurfaceWarning,\n\tnoConnectionWarning,\n\totherWarning,\n} from './warnings.js';\n\n/**\n * The BATCH issue read: resolve N issues BY IDENTITY in one call — `(owner, repo, number)` coordinates on a git\n * host, `(resourceId, ABC-123)` identifiers on an issue tracker.\n *\n * A sibling of the issue searches rather than a mode of one, because it answers a different question. A search\n * asks \"what matches\"; this asks \"does this exact issue exist\", which is what a caller correlating a branch name\n * to the issue it references is actually asking. Three consequences follow, and they are the reason this exists:\n *\n * - It resolves by EXACT IDENTIFIER, so relevance never enters into it.\n * - No result ceiling applies, so there is no partial window to reason about and no omission to report.\n * - An absent slot is a PROVEN ABSENCE, not \"not found within a page budget\". That is the property that matters\n * most: a caller can CACHE a miss. Emulating this with a paged list cannot prove absence without walking the\n * whole scope, so a miss stays unproven and the walk repeats on every pass, forever.\n *\n * Modeled on `countIssues` for its shape — caller-owned `key` echoed back, per-target isolation, as few requests\n * as the provider allows — because both take a set of independent questions and answer them together.\n *\n * ONE integration call per invocation, whatever the target count, as in `getPullRequestsBatch`: the integration\n * fans its targets out (chunked for GitHub/GHE, one request per target with bounded concurrency on GitLab and\n * Azure DevOps and on the trackers) and settles each independently, so failing targets spend at most one strike\n * of the integration's failure budget.\n */\n\n/** One issue to resolve, echoed back under the caller's own `key`. Which form a call takes depends on its provider. */\nexport type IssueBatchTarget =\n\t/**\n\t * A git host's issue, by repository coordinate. GitHub/GHE, GitLab (and self-managed) and Azure DevOps (and\n\t * Server); Bitbucket and Bitbucket Data Center have no issues and refuse.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\t/** GitHub owner, GitLab namespace path (may be nested), or Azure DevOps organization (or collection). */\n\t\t\towner: string;\n\t\t\t/**\n\t\t\t * Repository name. Required, but ignored on Azure DevOps, whose work items belong to the project rather\n\t\t\t * than to a repository.\n\t\t\t */\n\t\t\trepo: string;\n\t\t\t/** Issue number, GitLab issue iid, or Azure DevOps work item id. */\n\t\t\tnumber: number;\n\t\t\t/** Azure DevOps project. Required there, ignored elsewhere. */\n\t\t\tproject?: string;\n\t }\n\t/**\n\t * An issue tracker's issue, by its own identifier (e.g. `ABC-123`) within a resource. Jira (Cloud and Data\n\t * Center) and Linear. For Jira Data Center `resourceId` is the host — the instance's single resource, as\n\t * `listOrgs` reports it — and `resourceUrl` is not needed, since the browser link is built from the\n\t * connection's own base URL; Jira Cloud requires it.\n\t */\n\t| { key: string; resourceId: string; resourceUrl?: string; identifier: string };\n\ntype CoordinateTarget = Extract<IssueBatchTarget, { owner: string }>;\ntype TrackerTarget = Extract<IssueBatchTarget, { resourceId: string }>;\n\n/** The answer for one {@link IssueBatchTarget}. */\nexport interface IssueBatchResult {\n\tkey: string;\n\t/**\n\t * The resolved issue, or `undefined` when it PROVABLY does not exist (or is not visible to this connection).\n\t *\n\t * Absent is an answer here, unlike every paged read on this facade: a target that FAILED — its own request\n\t * outright, or just its own alias within an otherwise-answering GitHub chunk (e.g. an org enforcing SAML SSO\n\t * the token isn't authorized for) — is not returned at all and sets `fetchFailed`, so a caller can tell\n\t * \"proven absent\" from \"unknown\" and cache the first without ever caching the second.\n\t */\n\tissue?: IssueShape;\n}\n\nconst surface = 'Batch issue resolution';\n\nfunction refused(warning: ProviderWarning): ProviderResult<IssueBatchResult> {\n\treturn { items: [], warnings: [warning], fetchFailed: true };\n}\n\nexport async function getIssuesBatch(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\ttargets: readonly IssueBatchTarget[];\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<IssueBatchResult>> {\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.targets.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.targets);\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 target, not just\n\t\t// the 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 batch target key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn getIssuesBatchForTracker(ctx, options.providerId, options.targets, options.connectionId, options.domain);\n\t}\n\n\tconst targets = options.targets;\n\tif (!targets.every(isCoordinateTarget)) {\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`${surface} for '${options.providerId}' takes repository coordinates { key, owner, repo, number, project? }, not tracker identifiers.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimCoordinateFields);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream, where a blank owner or repo\n\t// would come back as a missing repository — a proven absence the caller would cache.\n\tconst invalid = findInvalidCoordinateTarget(options.providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(options.providerId, undefined, options.connectionId, invalid));\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\treturn unresolvedIntegration(ctx, options.providerId, options.connectionId, options.domain);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, surface));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\tconst { value: slots, warning } = await runCaptured(\n\t\toptions.providerId,\n\t\tdomain,\n\t\toptions.connectionId,\n\t\t() =>\n\t\t\tintegration.getIssuesBatchResult(\n\t\t\t\ttrimmedTargets.map(t => ({ owner: t.owner, repo: t.repo, number: t.number, project: t.project })),\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t),\n\t\t// A missing session must surface as a connection warning, including on the primary path, which\n\t\t// otherwise reads it as \"not connected\" and says nothing — and this read would then call it unsupported.\n\t\t{ warnOnMissingSession: true },\n\t);\n\tif (warning != null) {\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\n\tif (slots == null) {\n\t\t// A provider that doesn't implement the batch hook answers `undefined` with no error. Either way its\n\t\t// targets are DROPPED rather than reported as absent — the difference between \"unknown\" and \"proven\n\t\t// absent\" is the read's whole value, and a failure must never be cached as an answer.\n\t\tif (warning == null) {\n\t\t\tappendDedupedWarning(\n\t\t\t\twarnings,\n\t\t\t\totherWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t`${surface} is not supported by '${options.providerId}'; resolve issues individually instead.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\treturn { items: [], warnings: warnings, fetchFailed: true };\n\t}\n\n\tconst items: IssueBatchResult[] = [];\n\tfor (let i = 0; i < trimmedTargets.length; i++) {\n\t\tconst slot = slots[i];\n\t\tif (slot.status === 'rejected') {\n\t\t\t// Dropped, never reported absent, like a whole-call failure: only THIS target failed.\n\t\t\tappendBatchSlotWarning(warnings, options.providerId, domain, options.connectionId, slot);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst issue = slot.value;\n\t\titems.push({ key: trimmedTargets[i].key, ...(issue != null ? { issue: issue } : {}) });\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\n/**\n * The tracker form. ONE integration call per invocation, whatever the target count: the integration makes one\n * single-issue request per target, with bounded concurrency, and settles each independently — see\n * `IssuesIntegration.getIssuesByResourceIdBatchResult`.\n *\n * `resourceId` is trusted and the read does no resource discovery. It is handed to the provider's by-resource-id\n * read as-is, never wrapped into a synthesized descriptor: Linear and Trello answer `undefined` for a descriptor\n * that fails `isIssueResourceDescriptor`, which would be published as a proven absence.\n *\n * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for\n * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host. Unlike\n * the paged reads, this one never falls back to the primary connection for a self-managed tracker: it requires a\n * `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted instances routinely\n * issue the same project and issue keys, and `issue: undefined` is a proven absence a caller may cache — an answer\n * from whichever host happens to be primary would be cached under a key that names a different instance (#5872).\n */\nasync function getIssuesBatchForTracker(\n\tctx: ProviderReadContext,\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly IssueBatchTarget[],\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n): Promise<ProviderResult<IssueBatchResult>> {\n\tif (!targets.every(isTrackerTarget)) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} for '${providerId}' takes tracker identifiers { key, resourceId, resourceUrl?, identifier }, not repository coordinates.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimTrackerTarget);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream to fail one by one.\n\tconst invalid = findInvalidTrackerTarget(providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(providerId, undefined, connectionId, invalid));\n\t}\n\n\t// A `domain` only selects a host when it parses to one, and a `connectionId` only when it names a configured\n\t// connection that has one; anything else would resolve the primary host instead, which is the fallback this\n\t// read refuses.\n\tif (\n\t\tisIssuesSelfManagedHostIntegrationId(providerId) &&\n\t\t(domain != null\n\t\t\t? hostFromDomain(domain) == null\n\t\t\t: connectionId == null ||\n\t\t\t\t!ctx.getConfigured(providerId).some(c => c.id === connectionId && hostFromDomain(c.domain) != null))\n\t) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} requires a domain or a configured connection id for '${providerId}': every configured host can hold a different issue under the same key, so the read does not answer from the primary connection.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(providerId, connectionId, domain);\n\tif (integration == null) return unresolvedIntegration(ctx, providerId, connectionId, domain);\n\tif (!isIssuesIntegration(integration)) {\n\t\treturn refused(issueTrackerOnlySurfaceWarning(providerId, connectionId, surface));\n\t}\n\n\tconst unsupported = (): ProviderWarning =>\n\t\totherWarning(\n\t\t\tproviderId,\n\t\t\tundefined,\n\t\t\tconnectionId,\n\t\t\t`${surface} is not supported by '${providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`,\n\t\t);\n\tif (!integration.supportsIssueLookupByResourceId) return refused(unsupported());\n\n\t// A self-managed tracker's single resource is its host, and a caller keys (and caches) each answer by\n\t// `resourceId` while the read is addressed to the host `domain`/`connectionId` resolved. When they disagree,\n\t// host B's answer — including a cacheable absence — would be stored under host A's key. Refused whole, like\n\t// the caller bugs above: a target naming another host means the call selected a host its caller did not mean.\n\tif (isIssuesSelfManagedHostIntegrationId(providerId)) {\n\t\tconst mismatched = trimmedTargets.find(t => !areDomainsOnSameHost(t.resourceId, integration.domain));\n\t\tif (mismatched != null) {\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\tproviderId,\n\t\t\t\t\tundefined,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t`Issue batch target '${mismatched.key}' requires the resource id of '${providerId}' to name the host the read resolved to.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\tconst resolvedDomain = ctx.domainForRead(integration, providerId, connectionId, domain);\n\tconst warnings: ProviderWarning[] = [];\n\tlet fetchFailed = false;\n\n\tconst { value: slots, warning } = await runCaptured(\n\t\tproviderId,\n\t\tresolvedDomain,\n\t\tconnectionId,\n\t\t() =>\n\t\t\tintegration.getIssuesByResourceIdBatchResult(\n\t\t\t\ttrimmedTargets.map(t => ({\n\t\t\t\t\tresourceId: t.resourceId,\n\t\t\t\t\tidentifier: t.identifier,\n\t\t\t\t\tresourceUrl: t.resourceUrl,\n\t\t\t\t})),\n\t\t\t\tconnectionId,\n\t\t\t),\n\t\t// The read core answers `undefined` when it cannot resolve a session. That must surface as a connection\n\t\t// warning, including on the primary path, rather than as nothing — or worse, be read as absence.\n\t\t{ warnOnMissingSession: true },\n\t);\n\tif (warning != null) {\n\t\tappendDedupedWarning(warnings, warning);\n\t}\n\n\tif (slots == null) {\n\t\t// Dropped, never reported absent: \"unknown\" and \"proven absent\" must stay distinguishable.\n\t\tif (warning == null) {\n\t\t\tappendDedupedWarning(warnings, unsupported());\n\t\t}\n\t\treturn { items: [], warnings: warnings, fetchFailed: true };\n\t}\n\n\tconst items: IssueBatchResult[] = [];\n\tfor (let i = 0; i < trimmedTargets.length; i++) {\n\t\tconst slot = slots[i];\n\t\tif (slot.status === 'rejected') {\n\t\t\t// Dropped, never reported absent, like a whole-call failure.\n\t\t\tappendBatchSlotWarning(warnings, providerId, resolvedDomain, connectionId, slot);\n\t\t\tfetchFailed = true;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst issue = slot.value;\n\t\titems.push({ key: trimmedTargets[i].key, ...(issue != null ? { issue: issue } : {}) });\n\t}\n\n\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed || undefined };\n}\n\nfunction isCoordinateTarget(target: IssueBatchTarget): target is CoordinateTarget {\n\treturn 'owner' in target;\n}\n\nfunction isTrackerTarget(target: IssueBatchTarget): target is TrackerTarget {\n\treturn 'resourceId' in target;\n}\n\n/**\n * Trims `owner`, `repo` and `project` once, shared by every coordinate-form batch read (this one, the pull request\n * batch and the pull-requests-by-branch read): leading/trailing whitespace can't change which repository a\n * coordinate names, so it's stripped and accepted rather than refused, matching #5821's scope-name precedent.\n */\nexport function trimCoordinateFields<T extends { owner: string; repo: string; project?: string }>(target: T): T {\n\treturn { ...target, owner: target.owner.trim(), repo: target.repo.trim(), project: target.project?.trim() };\n}\n\n/** Trims `resourceId`, `resourceUrl` and `identifier` once; the validated value must be the one sent to the provider. */\nfunction trimTrackerTarget(target: TrackerTarget): TrackerTarget {\n\treturn {\n\t\t...target,\n\t\tresourceId: target.resourceId.trim(),\n\t\tresourceUrl: target.resourceUrl?.trim() || undefined,\n\t\tidentifier: target.identifier.trim(),\n\t};\n}\n\nfunction findInvalidCoordinateTarget(\n\tproviderId: IntegrationIds,\n\ttargets: readonly CoordinateTarget[],\n): string | undefined {\n\tconst isAzure = isAzureProviderId(providerId);\n\tconst isGitHub = isGitHubProviderId(providerId);\n\tfor (const target of targets) {\n\t\tif (!Number.isSafeInteger(target.number) || target.number <= 0) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; expected a positive integer.`;\n\t\t}\n\t\tif (isGitHub && target.number > githubGraphQLInt32Max) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; GitHub numbers are 32-bit and cannot exceed ${githubGraphQLInt32Max}.`;\n\t\t}\n\t\tif (target.owner.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty owner.`;\n\t\t}\n\t\tif (isAzure) {\n\t\t\tif (!target.project) {\n\t\t\t\treturn `Issue batch target '${target.key}' requires a project for '${providerId}'.`;\n\t\t\t}\n\t\t} else if (target.repo.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty repo.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction findInvalidTrackerTarget(\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly TrackerTarget[],\n): string | undefined {\n\tfor (const target of targets) {\n\t\tif (target.resourceId.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a resource id.`;\n\t\t}\n\t\tif (target.identifier.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires an issue identifier.`;\n\t\t}\n\t\tif (providerId === IssuesCloudHostIntegrationId.Jira && !target.resourceUrl) {\n\t\t\treturn `Issue batch target '${target.key}' requires the Jira resource URL so the result contains a browser link without resource discovery.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The answer when no integration resolves for a batch read. Targets were asked about, so this is never a silent\n * empty success: a supplied connection or domain that no longer resolves gets its own warning, and the untargeted\n * primary path (e.g. a self-managed provider with no configured host) a connection warning, as `getCurrentAccount`\n * gives — either way `fetchFailed`, so no caller mistakes the dropped targets for an answer.\n */\nexport function unresolvedIntegration<T>(\n\tctx: ProviderReadContext,\n\tproviderId: IntegrationIds,\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n): ProviderResult<T> {\n\tconst early = ctx.earlyReturnConnectionWarnings(providerId, connectionId, domain);\n\tif (early.warnings.length > 0) return { items: [], warnings: early.warnings, fetchFailed: true };\n\n\tconst resolvedDomain = ctx.resolveDomainForRead(providerId, connectionId, domain);\n\treturn { items: [], warnings: [noConnectionWarning(providerId, resolvedDomain, connectionId)], fetchFailed: true };\n}\n\n/**\n * Appends the warning for a batch target that could not be checked, shared by every batch read: for a refusal, the\n * scope failure it was recorded as (see `IntegrationBase.settleBatchRefusals`), published as an account-wide read\n * publishes one, scoped and with its cause; otherwise, the reason itself.\n */\nexport function appendBatchSlotWarning(\n\twarnings: ProviderWarning[],\n\tproviderId: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tslot: Extract<BatchSlot<unknown>, { status: 'rejected' }>,\n): void {\n\tif (slot.failure != null) {\n\t\tmergeAssessmentInto(warnings, providerId, domain, connectionId, {\n\t\t\tcompleteness: 'partial',\n\t\t\tfailures: [slot.failure],\n\t\t});\n\t\treturn;\n\t}\n\n\tappendDedupedWarning(warnings, toProviderWarning(providerId, domain, connectionId, slot.reason));\n}\n\nexport function findDuplicateKey(targets: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const target of targets) {\n\t\tif (seen.has(target.key)) return target.key;\n\n\t\tseen.add(target.key);\n\t}\n\treturn undefined;\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { mapBounded } from "../../../utils/promise.js";
2
+ import "../constants.js";
2
3
  import { appendDedupedWarning, reconcileOmissionsWithFailure } from "../results.js";
3
4
  import { assessCollectionMetadata, mergeAssessmentInto } from "../collectionMetadata.js";
4
- import "../constants.js";
5
5
  import "../providerFilters.js";
6
6
  import { mergeCollectionMetadata, parsePageCursor } from "../providers/utils/providerPaging.js";
7
7
  import { isIssuesHostIntegrationId } from "../utils/integration.utils.js";
@@ -1,6 +1,6 @@
1
+ import "../constants.js";
1
2
  import { reconcileOmissionsWithFailure } from "../results.js";
2
3
  import { mergeAssessmentInto } from "../collectionMetadata.js";
3
- import "../constants.js";
4
4
  import { mergeCollectionMetadata } from "../providers/utils/providerPaging.js";
5
5
  import { isGitHostIntegration, isIssuesHostIntegrationId, warnOnMissingSessionForDomain } from "../utils/integration.utils.js";
6
6
  import { isRepoIdsInput, providersMetadata } from "../providers/models.js";
@@ -0,0 +1,57 @@
1
+ import { PullRequestShape } from "../../../git/models/pullRequest.js";
2
+ import { IntegrationIds } from "../constants.js";
3
+ import { ProviderResult } from "../results.js";
4
+ import { ProviderReadContext } from "./context.js";
5
+ //#region ../plus/integrations/src/reads/pullRequestBatch.d.ts
6
+ /**
7
+ * The BATCH pull request read: resolve N pull requests, in any state, BY COORDINATE — the pull-request twin of
8
+ * `getIssuesBatch`, and for the same reason. "Does this exact pull request exist" is an identity question; a
9
+ * paged list can only answer it by walking the whole scope, so a miss stays unproven and the walk repeats forever.
10
+ * Here an absent slot is a PROVEN ABSENCE a caller can cache.
11
+ *
12
+ * Deliberately NOT routed through `Integration.getPullRequest`: that read answers `undefined` both when not
13
+ * connected and on any failure, and caches through `IntegrationCacheProvider.getPullRequest`, whose key carries no
14
+ * connection and whose by-id bucket never expires a miss.
15
+ *
16
+ * ONE integration call per invocation, whatever the target count: the manager makes a single
17
+ * `getPullRequestsBatchResult` call and the integration fans its targets out (chunked for GitHub/GHE, one request
18
+ * per target with bounded concurrency everywhere else) and settles each independently, so one bad target never
19
+ * spends more than its own slot — see `GitHostIntegration.getPullRequestsBatchResult`.
20
+ */
21
+ /** One pull request to resolve, identified by coordinate and echoed back under the caller's own `key`. */
22
+ export interface PullRequestBatchTarget {
23
+ /** Caller-owned identifier, echoed on the result so no positional matching is needed. Must be unique. */
24
+ key: string;
25
+ /**
26
+ * GitHub owner, GitLab namespace path (may be nested), Bitbucket workspace, Bitbucket Data Center project key, or
27
+ * Azure DevOps organization.
28
+ */
29
+ owner: string;
30
+ /** Repository name or slug. */
31
+ repo: string;
32
+ /** Pull request number, GitLab merge request iid, or Azure DevOps pull request id. */
33
+ number: number;
34
+ /** Azure DevOps project. Required there, ignored elsewhere. */
35
+ project?: string;
36
+ }
37
+ /** The answer for one {@link PullRequestBatchTarget}. */
38
+ export interface PullRequestBatchResult {
39
+ key: string;
40
+ /**
41
+ * The resolved pull request, whatever its state, or `undefined` when it PROVABLY does not exist (or is not
42
+ * visible to this connection). A target whose read FAILED is not returned at all and sets `fetchFailed`.
43
+ */
44
+ pullRequest?: PullRequestShape;
45
+ }
46
+ export declare function getPullRequestsBatch(ctx: ProviderReadContext, options: {
47
+ providerId: IntegrationIds;
48
+ targets: readonly PullRequestBatchTarget[];
49
+ connectionId?: string;
50
+ /**
51
+ * Explicit self-managed host domain. Used only when the requested connection has no configured domain;
52
+ * it must come from the trusted authentication configuration, not repository or remote data.
53
+ */
54
+ domain?: string;
55
+ }): Promise<ProviderResult<PullRequestBatchResult>>;
56
+ //#endregion
57
+ //# sourceMappingURL=pullRequestBatch.d.ts.map