@gitkraken/core-gitlens 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -2
- package/dist/git/remotes/azure-devops.js +3 -3
- package/dist/git/remotes/azure-devops.js.map +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/collectionMetadata.d.ts +48 -3
- package/dist/plus/integrations/collectionMetadata.js +138 -8
- package/dist/plus/integrations/collectionMetadata.js.map +1 -1
- package/dist/plus/integrations/errors.d.ts +3 -1
- package/dist/plus/integrations/errors.js +7 -3
- package/dist/plus/integrations/errors.js.map +1 -1
- package/dist/plus/integrations/index.d.ts +3 -3
- package/dist/plus/integrations/index.js.map +1 -1
- package/dist/plus/integrations/integrationService.d.ts +35 -1
- 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 +31 -15
- package/dist/plus/integrations/models/gitHostIntegration.js +71 -22
- package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
- package/dist/plus/integrations/models/integration.d.ts +70 -12
- package/dist/plus/integrations/models/integration.js +56 -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/issuesIntegration.js +2 -0
- package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
- 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 +73 -36
- package/dist/plus/integrations/providers/azureDevOps.js +191 -122
- 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 +61 -3
- package/dist/plus/integrations/providers/bitbucket-server.js +114 -20
- package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
- package/dist/plus/integrations/providers/bitbucket.d.ts +9 -1
- package/dist/plus/integrations/providers/bitbucket.js +13 -2
- package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
- package/dist/plus/integrations/providers/github.d.ts +6 -6
- package/dist/plus/integrations/providers/github.js +35 -34
- package/dist/plus/integrations/providers/github.js.map +1 -1
- package/dist/plus/integrations/providers/gitlab.d.ts +6 -4
- package/dist/plus/integrations/providers/gitlab.js +32 -28
- 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 +15 -7
- package/dist/plus/integrations/providers/jira-server.js.map +1 -1
- package/dist/plus/integrations/providers/jira.d.ts +9 -0
- package/dist/plus/integrations/providers/jira.js +14 -2
- package/dist/plus/integrations/providers/jira.js.map +1 -1
- package/dist/plus/integrations/providers/linear.js +2 -2
- package/dist/plus/integrations/providers/models.d.ts +10 -1
- 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 +18 -3
- 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 +6 -6
- package/dist/plus/integrations/reads/broaden.js.map +1 -1
- package/dist/plus/integrations/reads/context.d.ts +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 +2 -1
- package/dist/plus/integrations/reads/drains.js +1 -1
- 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 +2 -2
- package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
- package/dist/plus/integrations/reads/hierarchy.utils.js +1 -1
- package/dist/plus/integrations/reads/issueBatch.js +1 -1
- package/dist/plus/integrations/reads/issueTracker.js +2 -2
- package/dist/plus/integrations/reads/issues.js +2 -2
- 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 +110 -19
- 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/sweeps.js +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/results.d.ts +84 -6
- package/dist/plus/integrations/results.js +9 -2
- package/dist/plus/integrations/results.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/docs/integrations.md +263 -74
- package/docs/kepler-read-api-parity.md +19 -6
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Logger as _logger } from "../../../utils/logger.js";
|
|
2
2
|
import { isIssueResourceDescriptor, isRepositoryDescriptor } from "../../../git/utils/resourceDescriptor.utils.js";
|
|
3
3
|
import "../constants.js";
|
|
4
|
-
import {
|
|
4
|
+
import { getSingleConfiguredDomain } from "../utils/domain.utils.js";
|
|
5
|
+
import { isCloudGitSelfManagedHostIntegrationId, isGitSelfManagedHostIntegrationId, isSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
|
|
5
6
|
import { isGitHubDotCom, isGitLabDotCom } from "./models.js";
|
|
6
7
|
//#region ../plus/integrations/src/providers/utils.ts
|
|
7
8
|
const EntityIdentifierProviderType = {
|
|
@@ -42,6 +43,7 @@ function getEntityIdentifierInput(entity) {
|
|
|
42
43
|
provider = EntityIdentifierProviderType.GitlabSelfHosted;
|
|
43
44
|
domain = entity.provider.domain ?? null;
|
|
44
45
|
}
|
|
46
|
+
if (entityType === EntityType.Issue && domain == null && (provider === EntityIdentifierProviderType.GithubEnterprise || provider === EntityIdentifierProviderType.GitlabSelfHosted || provider === EntityIdentifierProviderType.BitbucketServer)) domain = entity.provider.domain ?? null;
|
|
45
47
|
if (provider === EntityIdentifierProviderType.AzureDevOpsServer) domain = entity.provider.domain ?? null;
|
|
46
48
|
if (provider === EntityIdentifierProviderType.JiraServer) domain = entity.provider.domain ?? null;
|
|
47
49
|
let projectId = null;
|
|
@@ -152,18 +154,45 @@ function decodeEntityIdentifiersFromGitConfig(str) {
|
|
|
152
154
|
_logger.error("Invalid entity identifiers in git config");
|
|
153
155
|
return [];
|
|
154
156
|
}
|
|
155
|
-
for (const decodedEntity of decoded)
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
for (const decodedEntity of decoded) {
|
|
158
|
+
if (decodedEntity.provider === EntityIdentifierProviderType.Jira && (decodedEntity.resourceId == null || decodedEntity.projectId == null)) {
|
|
159
|
+
debugger;
|
|
160
|
+
_logger.error("Invalid Jira issue in git config");
|
|
161
|
+
}
|
|
162
|
+
if (decodedEntity.provider === EntityIdentifierProviderType.JiraServer && (getDomainFromEntityIdentifier(decodedEntity) == null || decodedEntity.projectId == null)) {
|
|
163
|
+
debugger;
|
|
164
|
+
_logger.error("Invalid Jira Server issue in git config");
|
|
165
|
+
}
|
|
158
166
|
}
|
|
159
167
|
return decoded;
|
|
160
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* The host a self-managed identifier names, or `undefined` for a cloud one (or a malformed self-managed one).
|
|
171
|
+
* `domain` lives on some arms of `AnyEntityIdentifierInput` and on the catch-all, so the read is structural.
|
|
172
|
+
*/
|
|
173
|
+
function getDomainFromEntityIdentifier(identifier) {
|
|
174
|
+
const domain = "domain" in identifier ? identifier.domain : void 0;
|
|
175
|
+
return typeof domain === "string" && domain.trim().length > 0 ? domain : void 0;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Resolves the issue a branch association names.
|
|
179
|
+
*
|
|
180
|
+
* `resolveIntegration` receives the identifier's `domain` alongside the integration id, so a host-keyed
|
|
181
|
+
* provider resolves the instance the association was encoded for rather than whichever connection is primary
|
|
182
|
+
* (#5872). It is `undefined` for a cloud provider, which has a single host.
|
|
183
|
+
*/
|
|
161
184
|
async function getIssueFromGitConfigEntityIdentifier(resolveIntegration, identifier, options) {
|
|
162
185
|
if (identifier.entityType !== EntityType.Issue) return;
|
|
163
|
-
if (identifier.provider !== EntityIdentifierProviderType.Jira && identifier.provider !== EntityIdentifierProviderType.Linear && identifier.provider !== EntityIdentifierProviderType.Github && identifier.provider !== EntityIdentifierProviderType.Gitlab && identifier.provider !== EntityIdentifierProviderType.GithubEnterprise && identifier.provider !== EntityIdentifierProviderType.GitlabSelfHosted && identifier.provider !== EntityIdentifierProviderType.Bitbucket && identifier.provider !== EntityIdentifierProviderType.BitbucketServer && identifier.provider !== EntityIdentifierProviderType.AzureDevOpsServer && identifier.provider !== EntityIdentifierProviderType.Azure && identifier.provider !== EntityIdentifierProviderType.Trello) return;
|
|
186
|
+
if (identifier.provider !== EntityIdentifierProviderType.Jira && identifier.provider !== EntityIdentifierProviderType.JiraServer && identifier.provider !== EntityIdentifierProviderType.Linear && identifier.provider !== EntityIdentifierProviderType.Github && identifier.provider !== EntityIdentifierProviderType.Gitlab && identifier.provider !== EntityIdentifierProviderType.GithubEnterprise && identifier.provider !== EntityIdentifierProviderType.GitlabSelfHosted && identifier.provider !== EntityIdentifierProviderType.Bitbucket && identifier.provider !== EntityIdentifierProviderType.BitbucketServer && identifier.provider !== EntityIdentifierProviderType.AzureDevOpsServer && identifier.provider !== EntityIdentifierProviderType.Azure && identifier.provider !== EntityIdentifierProviderType.Trello) return;
|
|
164
187
|
const integrationId = getProviderIdFromEntityIdentifier(identifier);
|
|
165
188
|
if (integrationId == null) return;
|
|
166
|
-
|
|
189
|
+
let domain = getDomainFromEntityIdentifier(identifier);
|
|
190
|
+
if (domain == null && isGitSelfManagedHostIntegrationId(integrationId)) domain = getSingleConfiguredDomain(options?.getConfiguredIntegrations?.(integrationId) ?? []);
|
|
191
|
+
if (domain == null && isSelfManagedHostIntegrationId(integrationId)) {
|
|
192
|
+
_logger.error(`Cannot resolve a '${integrationId}' issue from git config without an unambiguous domain`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const integration = await resolveIntegration(integrationId, domain);
|
|
167
196
|
const resource = {
|
|
168
197
|
id: identifier.metadata.owner.id,
|
|
169
198
|
key: identifier.metadata.owner.key,
|
|
@@ -172,6 +201,7 @@ async function getIssueFromGitConfigEntityIdentifier(resolveIntegration, identif
|
|
|
172
201
|
};
|
|
173
202
|
const remoteLookupId = identifier.provider === EntityIdentifierProviderType.Trello ? identifier.entityId : identifier.metadata.id;
|
|
174
203
|
if (options?.cached) {
|
|
204
|
+
if (integration == null && isSelfManagedHostIntegrationId(integrationId)) return void 0;
|
|
175
205
|
const cachedIssue = options.peekCachedIssue?.(integration, resource, identifier.metadata.id);
|
|
176
206
|
if (cachedIssue != null || identifier.provider !== EntityIdentifierProviderType.Trello && identifier.provider !== EntityIdentifierProviderType.Linear) return cachedIssue;
|
|
177
207
|
return options.peekCachedIssue?.(integration, resource, identifier.entityId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","names":[],"sources":["../../../../../plus/integrations/src/providers/utils.ts"],"sourcesContent":["import type { AnyEntityIdentifierInput, EntityIdentifier } from '@gitkraken/provider-apis';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { PullRequest } from '@gitlens/git/models/pullRequest.js';\nimport type {\n\tIssueResourceDescriptor,\n\tRepositoryDescriptor,\n\tResourceDescriptor,\n} from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor, isRepositoryDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n\tIssuesSelfManagedHostIntegrationId,\n} from '../constants.js';\nimport { isCloudGitSelfManagedHostIntegrationId } from '../utils/integration.utils.js';\nimport type { AzureProjectInputDescriptor } from './azure/models.js';\nimport type { GitConfigEntityIdentifier } from './models.js';\nimport { isGitHubDotCom, isGitLabDotCom } from './models.js';\n\n// Local runtime copies of the `@gitkraken/provider-apis` entity-identifier string enums, duplicated for\n// the same CJS-from-ESM reason as the enums in `models.ts`. Exported so the enum-parity test can guard\n// them against upstream drift. `EntityVersion` is an intentional subset: the package only ever writes\n// version 1, so the parity test asserts each local entry matches the SDK rather than a full mirror.\nexport const EntityIdentifierProviderType = {\n\tAzure: 'azure',\n\tAzureDevOpsServer: 'azureDevOpsServer',\n\tGithub: 'github',\n\tGithubEnterprise: 'githubEnterprise',\n\tGitlab: 'gitlab',\n\tGitlabSelfHosted: 'gitlabSelfHosted',\n\tBitbucket: 'bitbucket',\n\tBitbucketServer: 'bitbucketServer',\n\tJira: 'jira',\n\tJiraServer: 'jiraServer',\n\tLinear: 'linear',\n\tTrello: 'trello',\n} as const;\n\nexport const EntityType = {\n\tPullRequest: 'pr',\n\tIssue: 'issue',\n} as const;\n\nexport const EntityVersion = {\n\tOne: '1',\n} as const;\n\n/**\n * Forward-compatible structural shape of the host's `LaunchpadItem`. The package\n * uses this for narrowing inside {@link getEntityIdentifierInput}; the host's\n * actual `LaunchpadItem` (in `src/plus/launchpad/launchpadProvider.ts`) carries\n * many more fields. The declared shape only includes fields integrations inspect.\n */\ntype LaunchpadItem = {\n\tuuid: string;\n\ttype: 'issue' | 'pullrequest';\n\tgraphQLId?: string;\n\tprovider: { id: string; domain?: string };\n\tunderlyingPullRequest?: {\n\t\tid: string;\n\t\tproject?: { id: string; resourceName?: string };\n\t\trepository?: { id?: string };\n\t};\n};\n\nfunction isLaunchpadItem(item: IssueOrPullRequest | LaunchpadItem): item is LaunchpadItem {\n\treturn (item as LaunchpadItem).uuid !== undefined;\n}\n\nfunction isIssue(item: IssueOrPullRequest | LaunchpadItem): item is Issue {\n\treturn item.type === 'issue';\n}\n\nexport function getEntityIdentifierInput(entity: Issue | PullRequest | LaunchpadItem): AnyEntityIdentifierInput {\n\tlet entityType: (typeof EntityType)[keyof typeof EntityType] = EntityType.Issue;\n\tif (entity.type === 'pullrequest') {\n\t\tentityType = EntityType.PullRequest;\n\t}\n\n\tlet provider = fromStringToEntityIdentifierProviderType(entity.provider.id);\n\tlet domain = null;\n\tif (provider === EntityIdentifierProviderType.Github && !isGitHubDotCom(entity.provider.domain)) {\n\t\tprovider = EntityIdentifierProviderType.GithubEnterprise;\n\t\tdomain = entity.provider.domain ?? null;\n\t}\n\tif (provider === EntityIdentifierProviderType.Gitlab && !isGitLabDotCom(entity.provider.domain)) {\n\t\tprovider = EntityIdentifierProviderType.GitlabSelfHosted;\n\t\tdomain = entity.provider.domain ?? null;\n\t}\n\tif (provider === EntityIdentifierProviderType.AzureDevOpsServer) {\n\t\tdomain = entity.provider.domain ?? null;\n\t}\n\tif (provider === EntityIdentifierProviderType.JiraServer) {\n\t\t// The host is the identity here, not a decoration: two self-hosted instances routinely issue the same\n\t\t// project and issue keys, so an identifier without it collides across them. The SDK's\n\t\t// `JiraIssueEntityIdentifierInput` says as much — its `JiraServer` arm requires `domain` where the\n\t\t// `Jira` arm requires `resourceId`.\n\t\tdomain = entity.provider.domain ?? null;\n\t}\n\n\tlet projectId = null;\n\tlet resourceId = null;\n\tlet accountOrOrgId = null;\n\tlet organizationName = null;\n\tlet repoId = null;\n\tif (provider === EntityIdentifierProviderType.Jira) {\n\t\tif (!isIssue(entity) || entity.project == null) {\n\t\t\tthrow new Error('Jira issues must have a project');\n\t\t}\n\n\t\tprojectId = entity.project.id;\n\t\tresourceId = entity.project.resourceId;\n\t} else if (provider === EntityIdentifierProviderType.JiraServer) {\n\t\tif (!isIssue(entity) || entity.project == null) {\n\t\t\tthrow new Error('Jira Server issues must have a project');\n\t\t}\n\n\t\t// `projectId` only; the SDK's `JiraServer` arm carries `domain` where Cloud carries `resourceId`, and a\n\t\t// self-hosted instance's single synthetic resource is the host the `domain` above already names.\n\t\tprojectId = entity.project.id;\n\t} else if (\n\t\tprovider === EntityIdentifierProviderType.Azure ||\n\t\tprovider === EntityIdentifierProviderType.AzureDevOpsServer\n\t) {\n\t\tconst project = isLaunchpadItem(entity) ? entity.underlyingPullRequest?.project : entity.project;\n\t\tif (project == null) {\n\t\t\tthrow new Error('Azure issues and PRs must have a project to be encoded');\n\t\t}\n\n\t\tprojectId = project.id;\n\t\torganizationName = project.resourceName;\n\t\trepoId = isLaunchpadItem(entity) ? entity.underlyingPullRequest?.repository?.id : entity.repository?.id;\n\t\tif (entityType === EntityType.PullRequest && repoId == null) {\n\t\t\tthrow new Error('Azure PRs must have a repository ID to be encoded');\n\t\t}\n\t} else if (provider === EntityIdentifierProviderType.Trello) {\n\t\tif (!isIssue(entity) || entity.project == null) {\n\t\t\tthrow new Error('Trello issues must have a board project to be encoded');\n\t\t}\n\n\t\tprojectId = entity.project.id;\n\t\t// Trello currently exposes the board but not a separate workspace/org id here. Reuse the board id in\n\t\t// both serialization slots so branch associations remain round-trippable until the upstream shape grows.\n\t\taccountOrOrgId = entity.project.resourceId || entity.project.id;\n\t} else if (\n\t\tprovider === EntityIdentifierProviderType.Bitbucket ||\n\t\tprovider === EntityIdentifierProviderType.BitbucketServer\n\t) {\n\t\trepoId = isLaunchpadItem(entity) ? entity.underlyingPullRequest?.repository?.id : entity.repository?.id;\n\t}\n\n\tlet entityId = isLaunchpadItem(entity) ? entity.graphQLId! : entity.nodeId!;\n\tif (\n\t\tprovider === EntityIdentifierProviderType.Azure ||\n\t\tprovider === EntityIdentifierProviderType.AzureDevOpsServer\n\t) {\n\t\tentityId = (isLaunchpadItem(entity) ? entity.underlyingPullRequest?.id : entity.id) as string;\n\t}\n\n\t// `AnyEntityIdentifierInput` includes the catch-all `EntityIdentifier`\n\t// variant which requires all fields, while each provider-specific variant\n\t// requires a different subset. The function builds a polymorphic value\n\t// that's correct at runtime per `provider` but the literal can't be\n\t// structurally narrowed to any single variant. The 2-step cast through\n\t// `unknown` is the documented escape for this discriminated-union pattern.\n\treturn {\n\t\taccountOrOrgId: accountOrOrgId,\n\t\torganizationName: organizationName, // needed for Azure issues and PRs, once supported\n\t\tprojectId: projectId,\n\t\trepoId: repoId ?? null, // needed for Azure and BitBucket PRs, once supported\n\t\tresourceId: resourceId, // needed for Jira issues\n\t\tprovider: provider,\n\t\tentityType: entityType,\n\t\tversion: EntityVersion.One,\n\t\tdomain: domain,\n\t\tentityId: entityId,\n\t} as unknown as AnyEntityIdentifierInput;\n}\n\nexport function getProviderIdFromEntityIdentifier(\n\tentityIdentifier: EntityIdentifier | AnyEntityIdentifierInput | GitConfigEntityIdentifier,\n): IntegrationIds | undefined {\n\tswitch (entityIdentifier.provider) {\n\t\tcase EntityIdentifierProviderType.Github:\n\t\t\treturn GitCloudHostIntegrationId.GitHub;\n\t\tcase EntityIdentifierProviderType.GithubEnterprise:\n\t\t\treturn GitSelfManagedHostIntegrationId.CloudGitHubEnterprise;\n\t\tcase EntityIdentifierProviderType.Gitlab:\n\t\t\treturn GitCloudHostIntegrationId.GitLab;\n\t\tcase EntityIdentifierProviderType.GitlabSelfHosted:\n\t\t\treturn GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted;\n\t\tcase EntityIdentifierProviderType.Jira:\n\t\t\treturn IssuesCloudHostIntegrationId.Jira;\n\t\tcase EntityIdentifierProviderType.JiraServer:\n\t\t\treturn IssuesSelfManagedHostIntegrationId.JiraServer;\n\t\tcase EntityIdentifierProviderType.Linear:\n\t\t\treturn IssuesCloudHostIntegrationId.Linear;\n\t\tcase EntityIdentifierProviderType.Trello:\n\t\t\treturn IssuesCloudHostIntegrationId.Trello;\n\t\tcase EntityIdentifierProviderType.Azure:\n\t\t\treturn GitCloudHostIntegrationId.AzureDevOps;\n\t\tcase EntityIdentifierProviderType.AzureDevOpsServer:\n\t\t\treturn GitSelfManagedHostIntegrationId.AzureDevOpsServer;\n\t\tcase EntityIdentifierProviderType.Bitbucket:\n\t\t\treturn GitCloudHostIntegrationId.Bitbucket;\n\t\tcase EntityIdentifierProviderType.BitbucketServer:\n\t\t\treturn isGitConfigEntityIdentifier(entityIdentifier) && entityIdentifier.metadata.isCloudEnterprise\n\t\t\t\t? GitSelfManagedHostIntegrationId.BitbucketServer\n\t\t\t\t: undefined;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nfunction fromStringToEntityIdentifierProviderType(\n\tstr: string,\n): (typeof EntityIdentifierProviderType)[keyof typeof EntityIdentifierProviderType] {\n\tswitch (str) {\n\t\tcase 'github':\n\t\t\treturn EntityIdentifierProviderType.Github;\n\t\tcase 'cloud-github-enterprise':\n\t\t\treturn EntityIdentifierProviderType.GithubEnterprise;\n\t\tcase 'cloud-gitlab-self-hosted':\n\t\t\treturn EntityIdentifierProviderType.GitlabSelfHosted;\n\t\tcase 'gitlab':\n\t\t\treturn EntityIdentifierProviderType.Gitlab;\n\t\tcase 'jira':\n\t\t\treturn EntityIdentifierProviderType.Jira;\n\t\tcase 'jira-server':\n\t\t\treturn EntityIdentifierProviderType.JiraServer;\n\t\tcase 'linear':\n\t\t\treturn EntityIdentifierProviderType.Linear;\n\t\tcase 'trello':\n\t\t\treturn EntityIdentifierProviderType.Trello;\n\t\tcase 'azure':\n\t\tcase 'azureDevOps':\n\t\tcase 'azure-devops':\n\t\t\treturn EntityIdentifierProviderType.Azure;\n\t\tcase GitSelfManagedHostIntegrationId.AzureDevOpsServer:\n\t\t\treturn EntityIdentifierProviderType.AzureDevOpsServer;\n\t\tcase 'bitbucket':\n\t\t\treturn EntityIdentifierProviderType.Bitbucket;\n\t\tcase 'bitbucket-server':\n\t\t\treturn EntityIdentifierProviderType.BitbucketServer;\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown provider type '${str}'`);\n\t}\n}\n\nexport function encodeIssueOrPullRequestForGitConfig(\n\tentity: Issue | PullRequest,\n\towner: RepositoryDescriptor | IssueResourceDescriptor,\n): GitConfigEntityIdentifier {\n\tconst encodedOwner: GitConfigEntityIdentifier['metadata']['owner'] = {\n\t\tkey: owner.key,\n\t\tname: owner.name,\n\t\tid: undefined,\n\t\towner: undefined,\n\t};\n\tif (isRepositoryDescriptor(owner)) {\n\t\tencodedOwner.owner = owner.owner;\n\t} else if (isIssueResourceDescriptor(owner)) {\n\t\tencodedOwner.id = owner.id;\n\t} else {\n\t\tthrow new Error('Invalid owner');\n\t}\n\n\treturn {\n\t\t...getEntityIdentifierInput(entity),\n\t\tmetadata: {\n\t\t\tid: entity.id,\n\t\t\towner: encodedOwner,\n\t\t\tcreatedDate: new Date().toISOString(),\n\t\t\tisCloudEnterprise: isCloudGitSelfManagedHostIntegrationId(entity.provider.id as IntegrationIds),\n\t\t},\n\t};\n}\n\nexport function isGitConfigEntityIdentifier(entity: unknown): entity is GitConfigEntityIdentifier {\n\treturn (\n\t\tentity != null &&\n\t\ttypeof entity === 'object' &&\n\t\t'provider' in entity &&\n\t\tentity.provider != null &&\n\t\t'entityType' in entity &&\n\t\tentity.entityType != null &&\n\t\t'version' in entity &&\n\t\tentity.version != null &&\n\t\t'entityId' in entity &&\n\t\tentity.entityId != null &&\n\t\t'metadata' in entity &&\n\t\tentity.metadata != null &&\n\t\ttypeof entity.metadata === 'object' &&\n\t\t'id' in entity.metadata &&\n\t\tentity.metadata.id != null &&\n\t\t'owner' in entity.metadata &&\n\t\tentity.metadata.owner != null &&\n\t\t'createdDate' in entity.metadata &&\n\t\tentity.metadata.createdDate != null\n\t);\n}\n\nexport function isGitConfigEntityIdentifiers(entities: unknown): entities is GitConfigEntityIdentifier[] {\n\treturn Array.isArray(entities) && entities.every(entity => isGitConfigEntityIdentifier(entity));\n}\n\nexport function decodeEntityIdentifiersFromGitConfig(str: string): GitConfigEntityIdentifier[] {\n\tconst decoded = JSON.parse(str);\n\n\tif (!isGitConfigEntityIdentifiers(decoded)) {\n\t\tdebugger;\n\t\tLogger.error('Invalid entity identifiers in git config');\n\t\treturn [];\n\t}\n\n\tfor (const decodedEntity of decoded) {\n\t\tif (\n\t\t\tdecodedEntity.provider === EntityIdentifierProviderType.Jira &&\n\t\t\t(decodedEntity.resourceId == null || decodedEntity.projectId == null)\n\t\t) {\n\t\t\tdebugger;\n\t\t\tLogger.error('Invalid Jira issue in git config');\n\t\t}\n\t}\n\n\treturn decoded;\n}\n\ninterface IssueResolvableIntegration {\n\tgetIssue(owner: unknown, id: string): Promise<Issue | undefined>;\n}\n\nexport async function getIssueFromGitConfigEntityIdentifier(\n\tresolveIntegration: (id: IntegrationIds) => Promise<IssueResolvableIntegration | undefined>,\n\tidentifier: GitConfigEntityIdentifier,\n\toptions?: {\n\t\t/** Only return a value already in the local cache. No remote fetch — returns undefined on cache miss. */\n\t\tcached?: boolean;\n\t\t/**\n\t\t * Host-supplied cache reader used when {@link cached} is set. The package is cache-agnostic, so the\n\t\t * host (which owns the cache) provides this to satisfy a cache-only read. When omitted, `cached`\n\t\t * yields undefined rather than falling through to a remote fetch.\n\t\t */\n\t\tpeekCachedIssue?: (\n\t\t\tintegration: IssueResolvableIntegration | undefined,\n\t\t\tresource: ResourceDescriptor,\n\t\t\tid: string,\n\t\t) => Issue | undefined;\n\t},\n): Promise<Issue | undefined> {\n\tif (identifier.entityType !== EntityType.Issue) {\n\t\treturn undefined;\n\t}\n\n\t// TODO: Centralize where we represent all supported providers for issues\n\t//\n\t// `JiraServer` is deliberately absent, even though `getEntityIdentifierInput` now encodes it: resolution\n\t// goes through `resolveIntegration(id)`, which takes no domain and therefore answers from whichever\n\t// connection is primary. For a host-keyed tracker that silently reads the wrong instance — the same trap\n\t// `getTrackerIssue` refuses at (see `reads/trackerIssue.ts`). Encoding without decoding loses a branch\n\t// association; decoding without a domain returns someone else's issue under the right key. Add it here\n\t// once the resolver carries the identifier's `domain` (#5872).\n\tif (\n\t\tidentifier.provider !== EntityIdentifierProviderType.Jira &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Linear &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Github &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Gitlab &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.GithubEnterprise &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.GitlabSelfHosted &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Bitbucket &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.BitbucketServer &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.AzureDevOpsServer &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Azure &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Trello\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst integrationId = getProviderIdFromEntityIdentifier(identifier);\n\tif (integrationId == null) {\n\t\treturn undefined;\n\t}\n\n\tconst integration = await resolveIntegration(integrationId);\n\n\tconst resource: ResourceDescriptor = {\n\t\tid: identifier.metadata.owner.id,\n\t\tkey: identifier.metadata.owner.key,\n\t\towner: identifier.metadata.owner.owner,\n\t\tname: identifier.metadata.owner.name,\n\t};\n\tconst remoteLookupId =\n\t\tidentifier.provider === EntityIdentifierProviderType.Trello ? identifier.entityId : identifier.metadata.id;\n\n\t// Cache-only read (no remote fetch). The package can't reach the host cache directly, so defer to the\n\t// host-supplied reader; without one, honor the no-fetch contract by returning undefined. The cache key\n\t// is resource+id (the integration only affects the etag), so peek even when the integration is\n\t// unresolvable — a still-cached issue must survive an unconfigured/disconnected integration.\n\tif (options?.cached) {\n\t\tconst cachedIssue = options.peekCachedIssue?.(integration, resource, identifier.metadata.id);\n\t\tif (\n\t\t\tcachedIssue != null ||\n\t\t\t(identifier.provider !== EntityIdentifierProviderType.Trello &&\n\t\t\t\tidentifier.provider !== EntityIdentifierProviderType.Linear)\n\t\t) {\n\t\t\treturn cachedIssue;\n\t\t}\n\n\t\t// Two id generations coexist for these providers: Trello identifiers persisted before the stable-id\n\t\t// switch carry the board-local `idShort` in `metadata.id` while the cache is keyed by the stable card\n\t\t// id (`entityId`); Linear identifiers persisted before `Issue.id` became the human identifier carry\n\t\t// the UUID in `metadata.id` while fresh cache entries key by the identifier — and vice versa, the\n\t\t// UUID always survives in `entityId` (via `nodeId`). Peeking both keys resolves either generation.\n\t\treturn options.peekCachedIssue?.(integration, resource, identifier.entityId);\n\t}\n\n\tif (integration == null) {\n\t\treturn undefined;\n\t}\n\n\treturn integration.getIssue(resource, remoteLookupId);\n}\n\nexport function getIssueOwner(\n\tissue: IssueShape,\n): RepositoryDescriptor | IssueResourceDescriptor | AzureProjectInputDescriptor | undefined {\n\tconst isAzure = ['azure', GitCloudHostIntegrationId.AzureDevOps, 'azure-devops'].includes(issue.provider.id);\n\treturn issue.repository\n\t\t? {\n\t\t\t\tkey: `${issue.repository.owner}/${issue.repository.repo}`,\n\t\t\t\towner: issue.repository.owner,\n\t\t\t\tname: issue.repository.repo,\n\t\t\t}\n\t\t: issue.project\n\t\t\t? {\n\t\t\t\t\tkey: isAzure ? issue.project.id : issue.project.resourceId,\n\t\t\t\t\tid: isAzure ? issue.project.id : issue.project.resourceId,\n\t\t\t\t\towner: isAzure ? issue.project.resourceName : undefined,\n\t\t\t\t\tname: isAzure ? issue.project.name : issue.project.resourceName,\n\t\t\t\t}\n\t\t\t: undefined;\n}\n"],"mappings":";;;;;;AA2BA,MAAa,+BAA+B;CAC3C,OAAO;CACP,mBAAmB;CACnB,QAAQ;CACR,kBAAkB;CAClB,QAAQ;CACR,kBAAkB;CAClB,WAAW;CACX,iBAAiB;CACjB,MAAM;CACN,YAAY;CACZ,QAAQ;CACR,QAAQ;AACT;AAEA,MAAa,aAAa;CACzB,aAAa;CACb,OAAO;AACR;AAEA,MAAa,gBAAgB,EAC5B,KAAK,IACN;AAoBA,SAAS,gBAAgB,MAAiE;CACzF,OAAQ,KAAuB,SAAS,KAAA;AACzC;AAEA,SAAS,QAAQ,MAAyD;CACzE,OAAO,KAAK,SAAS;AACtB;AAEA,SAAgB,yBAAyB,QAAuE;CAC/G,IAAI,aAA2D,WAAW;CAC1E,IAAI,OAAO,SAAS,eACnB,aAAa,WAAW;CAGzB,IAAI,WAAW,yCAAyC,OAAO,SAAS,EAAE;CAC1E,IAAI,SAAS;CACb,IAAI,aAAa,6BAA6B,UAAU,CAAC,eAAe,OAAO,SAAS,MAAM,GAAG;EAChG,WAAW,6BAA6B;EACxC,SAAS,OAAO,SAAS,UAAU;CACpC;CACA,IAAI,aAAa,6BAA6B,UAAU,CAAC,eAAe,OAAO,SAAS,MAAM,GAAG;EAChG,WAAW,6BAA6B;EACxC,SAAS,OAAO,SAAS,UAAU;CACpC;CACA,IAAI,aAAa,6BAA6B,mBAC7C,SAAS,OAAO,SAAS,UAAU;CAEpC,IAAI,aAAa,6BAA6B,YAK7C,SAAS,OAAO,SAAS,UAAU;CAGpC,IAAI,YAAY;CAChB,IAAI,aAAa;CACjB,IAAI,iBAAiB;CACrB,IAAI,mBAAmB;CACvB,IAAI,SAAS;CACb,IAAI,aAAa,6BAA6B,MAAM;EACnD,IAAI,CAAC,QAAQ,MAAM,KAAK,OAAO,WAAW,MACzC,MAAM,IAAI,MAAM,iCAAiC;EAGlD,YAAY,OAAO,QAAQ;EAC3B,aAAa,OAAO,QAAQ;CAC7B,OAAO,IAAI,aAAa,6BAA6B,YAAY;EAChE,IAAI,CAAC,QAAQ,MAAM,KAAK,OAAO,WAAW,MACzC,MAAM,IAAI,MAAM,wCAAwC;EAKzD,YAAY,OAAO,QAAQ;CAC5B,OAAO,IACN,aAAa,6BAA6B,SAC1C,aAAa,6BAA6B,mBACzC;EACD,MAAM,UAAU,gBAAgB,MAAM,IAAI,OAAO,uBAAuB,UAAU,OAAO;EACzF,IAAI,WAAW,MACd,MAAM,IAAI,MAAM,wDAAwD;EAGzE,YAAY,QAAQ;EACpB,mBAAmB,QAAQ;EAC3B,SAAS,gBAAgB,MAAM,IAAI,OAAO,uBAAuB,YAAY,KAAK,OAAO,YAAY;EACrG,IAAI,eAAe,WAAW,eAAe,UAAU,MACtD,MAAM,IAAI,MAAM,mDAAmD;CAErE,OAAO,IAAI,aAAa,6BAA6B,QAAQ;EAC5D,IAAI,CAAC,QAAQ,MAAM,KAAK,OAAO,WAAW,MACzC,MAAM,IAAI,MAAM,uDAAuD;EAGxE,YAAY,OAAO,QAAQ;EAG3B,iBAAiB,OAAO,QAAQ,cAAc,OAAO,QAAQ;CAC9D,OAAO,IACN,aAAa,6BAA6B,aAC1C,aAAa,6BAA6B,iBAE1C,SAAS,gBAAgB,MAAM,IAAI,OAAO,uBAAuB,YAAY,KAAK,OAAO,YAAY;CAGtG,IAAI,WAAW,gBAAgB,MAAM,IAAI,OAAO,YAAa,OAAO;CACpE,IACC,aAAa,6BAA6B,SAC1C,aAAa,6BAA6B,mBAE1C,WAAY,gBAAgB,MAAM,IAAI,OAAO,uBAAuB,KAAK,OAAO;CASjF,OAAO;EACU;EACE;EACP;EACX,QAAQ,UAAU;EACN;EACF;EACE;EACZ,SAAS,cAAc;EACf;EACE;CACX;AACD;AAEA,SAAgB,kCACf,kBAC6B;CAC7B,QAAQ,iBAAiB,UAAzB;EACC,KAAK,6BAA6B,QACjC,OAAA;EACD,KAAK,6BAA6B,kBACjC,OAAA;EACD,KAAK,6BAA6B,QACjC,OAAA;EACD,KAAK,6BAA6B,kBACjC,OAAA;EACD,KAAK,6BAA6B,MACjC,OAAA;EACD,KAAK,6BAA6B,YACjC,OAAA;EACD,KAAK,6BAA6B,QACjC,OAAA;EACD,KAAK,6BAA6B,QACjC,OAAA;EACD,KAAK,6BAA6B,OACjC,OAAA;EACD,KAAK,6BAA6B,mBACjC,OAAA;EACD,KAAK,6BAA6B,WACjC,OAAA;EACD,KAAK,6BAA6B,iBACjC,OAAO,4BAA4B,gBAAgB,KAAK,iBAAiB,SAAS,oBAAA,qBAE/E,KAAA;EACJ,SACC;CACF;AACD;AAEA,SAAS,yCACR,KACmF;CACnF,QAAQ,KAAR;EACC,KAAK,UACJ,OAAO,6BAA6B;EACrC,KAAK,2BACJ,OAAO,6BAA6B;EACrC,KAAK,4BACJ,OAAO,6BAA6B;EACrC,KAAK,UACJ,OAAO,6BAA6B;EACrC,KAAK,QACJ,OAAO,6BAA6B;EACrC,KAAK,eACJ,OAAO,6BAA6B;EACrC,KAAK,UACJ,OAAO,6BAA6B;EACrC,KAAK,UACJ,OAAO,6BAA6B;EACrC,KAAK;EACL,KAAK;EACL,KAAK,gBACJ,OAAO,6BAA6B;EACrC,KAAA,uBACC,OAAO,6BAA6B;EACrC,KAAK,aACJ,OAAO,6BAA6B;EACrC,KAAK,oBACJ,OAAO,6BAA6B;EACrC,SACC,MAAM,IAAI,MAAM,0BAA0B,IAAI,EAAE;CAClD;AACD;AAEA,SAAgB,qCACf,QACA,OAC4B;CAC5B,MAAM,eAA+D;EACpE,KAAK,MAAM;EACX,MAAM,MAAM;EACZ,IAAI,KAAA;EACJ,OAAO,KAAA;CACR;CACA,IAAI,uBAAuB,KAAK,GAC/B,aAAa,QAAQ,MAAM;MACrB,IAAI,0BAA0B,KAAK,GACzC,aAAa,KAAK,MAAM;MAExB,MAAM,IAAI,MAAM,eAAe;CAGhC,OAAO;EACN,GAAG,yBAAyB,MAAM;EAClC,UAAU;GACT,IAAI,OAAO;GACX,OAAO;GACP,8BAAa,IAAI,KAAK,EAAA,CAAE,YAAY;GACpC,mBAAmB,uCAAuC,OAAO,SAAS,EAAoB;EAC/F;CACD;AACD;AAEA,SAAgB,4BAA4B,QAAsD;CACjG,OACC,UAAU,QACV,OAAO,WAAW,YAClB,cAAc,UACd,OAAO,YAAY,QACnB,gBAAgB,UAChB,OAAO,cAAc,QACrB,aAAa,UACb,OAAO,WAAW,QAClB,cAAc,UACd,OAAO,YAAY,QACnB,cAAc,UACd,OAAO,YAAY,QACnB,OAAO,OAAO,aAAa,YAC3B,QAAQ,OAAO,YACf,OAAO,SAAS,MAAM,QACtB,WAAW,OAAO,YAClB,OAAO,SAAS,SAAS,QACzB,iBAAiB,OAAO,YACxB,OAAO,SAAS,eAAe;AAEjC;AAEA,SAAgB,6BAA6B,UAA4D;CACxG,OAAO,MAAM,QAAQ,QAAQ,KAAK,SAAS,OAAM,WAAU,4BAA4B,MAAM,CAAC;AAC/F;AAEA,SAAgB,qCAAqC,KAA0C;CAC9F,MAAM,UAAU,KAAK,MAAM,GAAG;CAE9B,IAAI,CAAC,6BAA6B,OAAO,GAAG;EAC3C;EACA,QAAO,MAAM,0CAA0C;EACvD,OAAO,CAAC;CACT;CAEA,KAAK,MAAM,iBAAiB,SAC3B,IACC,cAAc,aAAa,6BAA6B,SACvD,cAAc,cAAc,QAAQ,cAAc,aAAa,OAC/D;EACD;EACA,QAAO,MAAM,kCAAkC;CAChD;CAGD,OAAO;AACR;AAMA,eAAsB,sCACrB,oBACA,YACA,SAc6B;CAC7B,IAAI,WAAW,eAAe,WAAW,OACxC;CAWD,IACC,WAAW,aAAa,6BAA6B,QACrD,WAAW,aAAa,6BAA6B,UACrD,WAAW,aAAa,6BAA6B,UACrD,WAAW,aAAa,6BAA6B,UACrD,WAAW,aAAa,6BAA6B,oBACrD,WAAW,aAAa,6BAA6B,oBACrD,WAAW,aAAa,6BAA6B,aACrD,WAAW,aAAa,6BAA6B,mBACrD,WAAW,aAAa,6BAA6B,qBACrD,WAAW,aAAa,6BAA6B,SACrD,WAAW,aAAa,6BAA6B,QAErD;CAGD,MAAM,gBAAgB,kCAAkC,UAAU;CAClE,IAAI,iBAAiB,MACpB;CAGD,MAAM,cAAc,MAAM,mBAAmB,aAAa;CAE1D,MAAM,WAA+B;EACpC,IAAI,WAAW,SAAS,MAAM;EAC9B,KAAK,WAAW,SAAS,MAAM;EAC/B,OAAO,WAAW,SAAS,MAAM;EACjC,MAAM,WAAW,SAAS,MAAM;CACjC;CACA,MAAM,iBACL,WAAW,aAAa,6BAA6B,SAAS,WAAW,WAAW,WAAW,SAAS;CAMzG,IAAI,SAAS,QAAQ;EACpB,MAAM,cAAc,QAAQ,kBAAkB,aAAa,UAAU,WAAW,SAAS,EAAE;EAC3F,IACC,eAAe,QACd,WAAW,aAAa,6BAA6B,UACrD,WAAW,aAAa,6BAA6B,QAEtD,OAAO;EAQR,OAAO,QAAQ,kBAAkB,aAAa,UAAU,WAAW,QAAQ;CAC5E;CAEA,IAAI,eAAe,MAClB;CAGD,OAAO,YAAY,SAAS,UAAU,cAAc;AACrD;AAEA,SAAgB,cACf,OAC2F;CAC3F,MAAM,UAAU;EAAC;;EAAgD;CAAc,CAAC,CAAC,SAAS,MAAM,SAAS,EAAE;CAC3G,OAAO,MAAM,aACV;EACA,KAAK,GAAG,MAAM,WAAW,MAAM,GAAG,MAAM,WAAW;EACnD,OAAO,MAAM,WAAW;EACxB,MAAM,MAAM,WAAW;CACxB,IACC,MAAM,UACL;EACA,KAAK,UAAU,MAAM,QAAQ,KAAK,MAAM,QAAQ;EAChD,IAAI,UAAU,MAAM,QAAQ,KAAK,MAAM,QAAQ;EAC/C,OAAO,UAAU,MAAM,QAAQ,eAAe,KAAA;EAC9C,MAAM,UAAU,MAAM,QAAQ,OAAO,MAAM,QAAQ;CACpD,IACC,KAAA;AACL"}
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../../../../../plus/integrations/src/providers/utils.ts"],"sourcesContent":["import type { AnyEntityIdentifierInput, EntityIdentifier } from '@gitkraken/provider-apis';\nimport type { Issue, IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IssueOrPullRequest } from '@gitlens/git/models/issueOrPullRequest.js';\nimport type { PullRequest } from '@gitlens/git/models/pullRequest.js';\nimport type {\n\tIssueResourceDescriptor,\n\tRepositoryDescriptor,\n\tResourceDescriptor,\n} from '@gitlens/git/models/resourceDescriptor.js';\nimport { isIssueResourceDescriptor, isRepositoryDescriptor } from '@gitlens/git/utils/resourceDescriptor.utils.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tIssuesCloudHostIntegrationId,\n\tIssuesSelfManagedHostIntegrationId,\n} from '../constants.js';\nimport { getSingleConfiguredDomain } from '../utils/domain.utils.js';\nimport {\n\tisCloudGitSelfManagedHostIntegrationId,\n\tisGitSelfManagedHostIntegrationId,\n\tisSelfManagedHostIntegrationId,\n} from '../utils/integration.utils.js';\nimport type { AzureProjectInputDescriptor } from './azure/models.js';\nimport type { GitConfigEntityIdentifier } from './models.js';\nimport { isGitHubDotCom, isGitLabDotCom } from './models.js';\n\n// Local runtime copies of the `@gitkraken/provider-apis` entity-identifier string enums, duplicated for\n// the same CJS-from-ESM reason as the enums in `models.ts`. Exported so the enum-parity test can guard\n// them against upstream drift. `EntityVersion` is an intentional subset: the package only ever writes\n// version 1, so the parity test asserts each local entry matches the SDK rather than a full mirror.\nexport const EntityIdentifierProviderType = {\n\tAzure: 'azure',\n\tAzureDevOpsServer: 'azureDevOpsServer',\n\tGithub: 'github',\n\tGithubEnterprise: 'githubEnterprise',\n\tGitlab: 'gitlab',\n\tGitlabSelfHosted: 'gitlabSelfHosted',\n\tBitbucket: 'bitbucket',\n\tBitbucketServer: 'bitbucketServer',\n\tJira: 'jira',\n\tJiraServer: 'jiraServer',\n\tLinear: 'linear',\n\tTrello: 'trello',\n} as const;\n\nexport const EntityType = {\n\tPullRequest: 'pr',\n\tIssue: 'issue',\n} as const;\n\nexport const EntityVersion = {\n\tOne: '1',\n} as const;\n\n/**\n * Forward-compatible structural shape of the host's `LaunchpadItem`. The package\n * uses this for narrowing inside {@link getEntityIdentifierInput}; the host's\n * actual `LaunchpadItem` (in `src/plus/launchpad/launchpadProvider.ts`) carries\n * many more fields. The declared shape only includes fields integrations inspect.\n */\ntype LaunchpadItem = {\n\tuuid: string;\n\ttype: 'issue' | 'pullrequest';\n\tgraphQLId?: string;\n\tprovider: { id: string; domain?: string };\n\tunderlyingPullRequest?: {\n\t\tid: string;\n\t\tproject?: { id: string; resourceName?: string };\n\t\trepository?: { id?: string };\n\t};\n};\n\nfunction isLaunchpadItem(item: IssueOrPullRequest | LaunchpadItem): item is LaunchpadItem {\n\treturn (item as LaunchpadItem).uuid !== undefined;\n}\n\nfunction isIssue(item: IssueOrPullRequest | LaunchpadItem): item is Issue {\n\treturn item.type === 'issue';\n}\n\nexport function getEntityIdentifierInput(entity: Issue | PullRequest | LaunchpadItem): AnyEntityIdentifierInput {\n\tlet entityType: (typeof EntityType)[keyof typeof EntityType] = EntityType.Issue;\n\tif (entity.type === 'pullrequest') {\n\t\tentityType = EntityType.PullRequest;\n\t}\n\n\tlet provider = fromStringToEntityIdentifierProviderType(entity.provider.id);\n\tlet domain = null;\n\tif (provider === EntityIdentifierProviderType.Github && !isGitHubDotCom(entity.provider.domain)) {\n\t\tprovider = EntityIdentifierProviderType.GithubEnterprise;\n\t\tdomain = entity.provider.domain ?? null;\n\t}\n\tif (provider === EntityIdentifierProviderType.Gitlab && !isGitLabDotCom(entity.provider.domain)) {\n\t\tprovider = EntityIdentifierProviderType.GitlabSelfHosted;\n\t\tdomain = entity.provider.domain ?? null;\n\t}\n\t// An issue read through the self-managed integration already carries the enterprise id, so the rewrites above\n\t// never see it and its host was dropped — for exactly the issues that have one. A branch association has to\n\t// name it to be read back from the right server, since two hosts routinely share `owner/repo#number`. Issues\n\t// only: a pull request's identifier is also its Launchpad uuid, which keys stored pins and snoozes, so giving it\n\t// a host would orphan every existing one.\n\tif (\n\t\tentityType === EntityType.Issue &&\n\t\tdomain == null &&\n\t\t(provider === EntityIdentifierProviderType.GithubEnterprise ||\n\t\t\tprovider === EntityIdentifierProviderType.GitlabSelfHosted ||\n\t\t\tprovider === EntityIdentifierProviderType.BitbucketServer)\n\t) {\n\t\tdomain = entity.provider.domain ?? null;\n\t}\n\tif (provider === EntityIdentifierProviderType.AzureDevOpsServer) {\n\t\tdomain = entity.provider.domain ?? null;\n\t}\n\tif (provider === EntityIdentifierProviderType.JiraServer) {\n\t\t// The host is the identity here, not a decoration: two self-hosted instances routinely issue the same\n\t\t// project and issue keys, so an identifier without it collides across them. The SDK's\n\t\t// `JiraIssueEntityIdentifierInput` says as much — its `JiraServer` arm requires `domain` where the\n\t\t// `Jira` arm requires `resourceId`.\n\t\tdomain = entity.provider.domain ?? null;\n\t}\n\n\tlet projectId = null;\n\tlet resourceId = null;\n\tlet accountOrOrgId = null;\n\tlet organizationName = null;\n\tlet repoId = null;\n\tif (provider === EntityIdentifierProviderType.Jira) {\n\t\tif (!isIssue(entity) || entity.project == null) {\n\t\t\tthrow new Error('Jira issues must have a project');\n\t\t}\n\n\t\tprojectId = entity.project.id;\n\t\tresourceId = entity.project.resourceId;\n\t} else if (provider === EntityIdentifierProviderType.JiraServer) {\n\t\tif (!isIssue(entity) || entity.project == null) {\n\t\t\tthrow new Error('Jira Server issues must have a project');\n\t\t}\n\n\t\t// `projectId` only; the SDK's `JiraServer` arm carries `domain` where Cloud carries `resourceId`, and a\n\t\t// self-hosted instance's single synthetic resource is the host the `domain` above already names.\n\t\tprojectId = entity.project.id;\n\t} else if (\n\t\tprovider === EntityIdentifierProviderType.Azure ||\n\t\tprovider === EntityIdentifierProviderType.AzureDevOpsServer\n\t) {\n\t\tconst project = isLaunchpadItem(entity) ? entity.underlyingPullRequest?.project : entity.project;\n\t\tif (project == null) {\n\t\t\tthrow new Error('Azure issues and PRs must have a project to be encoded');\n\t\t}\n\n\t\tprojectId = project.id;\n\t\torganizationName = project.resourceName;\n\t\trepoId = isLaunchpadItem(entity) ? entity.underlyingPullRequest?.repository?.id : entity.repository?.id;\n\t\tif (entityType === EntityType.PullRequest && repoId == null) {\n\t\t\tthrow new Error('Azure PRs must have a repository ID to be encoded');\n\t\t}\n\t} else if (provider === EntityIdentifierProviderType.Trello) {\n\t\tif (!isIssue(entity) || entity.project == null) {\n\t\t\tthrow new Error('Trello issues must have a board project to be encoded');\n\t\t}\n\n\t\tprojectId = entity.project.id;\n\t\t// Trello currently exposes the board but not a separate workspace/org id here. Reuse the board id in\n\t\t// both serialization slots so branch associations remain round-trippable until the upstream shape grows.\n\t\taccountOrOrgId = entity.project.resourceId || entity.project.id;\n\t} else if (\n\t\tprovider === EntityIdentifierProviderType.Bitbucket ||\n\t\tprovider === EntityIdentifierProviderType.BitbucketServer\n\t) {\n\t\trepoId = isLaunchpadItem(entity) ? entity.underlyingPullRequest?.repository?.id : entity.repository?.id;\n\t}\n\n\tlet entityId = isLaunchpadItem(entity) ? entity.graphQLId! : entity.nodeId!;\n\tif (\n\t\tprovider === EntityIdentifierProviderType.Azure ||\n\t\tprovider === EntityIdentifierProviderType.AzureDevOpsServer\n\t) {\n\t\tentityId = (isLaunchpadItem(entity) ? entity.underlyingPullRequest?.id : entity.id) as string;\n\t}\n\n\t// `AnyEntityIdentifierInput` includes the catch-all `EntityIdentifier`\n\t// variant which requires all fields, while each provider-specific variant\n\t// requires a different subset. The function builds a polymorphic value\n\t// that's correct at runtime per `provider` but the literal can't be\n\t// structurally narrowed to any single variant. The 2-step cast through\n\t// `unknown` is the documented escape for this discriminated-union pattern.\n\treturn {\n\t\taccountOrOrgId: accountOrOrgId,\n\t\torganizationName: organizationName, // needed for Azure issues and PRs, once supported\n\t\tprojectId: projectId,\n\t\trepoId: repoId ?? null, // needed for Azure and BitBucket PRs, once supported\n\t\tresourceId: resourceId, // needed for Jira issues\n\t\tprovider: provider,\n\t\tentityType: entityType,\n\t\tversion: EntityVersion.One,\n\t\tdomain: domain,\n\t\tentityId: entityId,\n\t} as unknown as AnyEntityIdentifierInput;\n}\n\nexport function getProviderIdFromEntityIdentifier(\n\tentityIdentifier: EntityIdentifier | AnyEntityIdentifierInput | GitConfigEntityIdentifier,\n): IntegrationIds | undefined {\n\tswitch (entityIdentifier.provider) {\n\t\tcase EntityIdentifierProviderType.Github:\n\t\t\treturn GitCloudHostIntegrationId.GitHub;\n\t\tcase EntityIdentifierProviderType.GithubEnterprise:\n\t\t\treturn GitSelfManagedHostIntegrationId.CloudGitHubEnterprise;\n\t\tcase EntityIdentifierProviderType.Gitlab:\n\t\t\treturn GitCloudHostIntegrationId.GitLab;\n\t\tcase EntityIdentifierProviderType.GitlabSelfHosted:\n\t\t\treturn GitSelfManagedHostIntegrationId.CloudGitLabSelfHosted;\n\t\tcase EntityIdentifierProviderType.Jira:\n\t\t\treturn IssuesCloudHostIntegrationId.Jira;\n\t\tcase EntityIdentifierProviderType.JiraServer:\n\t\t\treturn IssuesSelfManagedHostIntegrationId.JiraServer;\n\t\tcase EntityIdentifierProviderType.Linear:\n\t\t\treturn IssuesCloudHostIntegrationId.Linear;\n\t\tcase EntityIdentifierProviderType.Trello:\n\t\t\treturn IssuesCloudHostIntegrationId.Trello;\n\t\tcase EntityIdentifierProviderType.Azure:\n\t\t\treturn GitCloudHostIntegrationId.AzureDevOps;\n\t\tcase EntityIdentifierProviderType.AzureDevOpsServer:\n\t\t\treturn GitSelfManagedHostIntegrationId.AzureDevOpsServer;\n\t\tcase EntityIdentifierProviderType.Bitbucket:\n\t\t\treturn GitCloudHostIntegrationId.Bitbucket;\n\t\tcase EntityIdentifierProviderType.BitbucketServer:\n\t\t\treturn isGitConfigEntityIdentifier(entityIdentifier) && entityIdentifier.metadata.isCloudEnterprise\n\t\t\t\t? GitSelfManagedHostIntegrationId.BitbucketServer\n\t\t\t\t: undefined;\n\t\tdefault:\n\t\t\treturn undefined;\n\t}\n}\n\nfunction fromStringToEntityIdentifierProviderType(\n\tstr: string,\n): (typeof EntityIdentifierProviderType)[keyof typeof EntityIdentifierProviderType] {\n\tswitch (str) {\n\t\tcase 'github':\n\t\t\treturn EntityIdentifierProviderType.Github;\n\t\tcase 'cloud-github-enterprise':\n\t\t\treturn EntityIdentifierProviderType.GithubEnterprise;\n\t\tcase 'cloud-gitlab-self-hosted':\n\t\t\treturn EntityIdentifierProviderType.GitlabSelfHosted;\n\t\tcase 'gitlab':\n\t\t\treturn EntityIdentifierProviderType.Gitlab;\n\t\tcase 'jira':\n\t\t\treturn EntityIdentifierProviderType.Jira;\n\t\tcase 'jira-server':\n\t\t\treturn EntityIdentifierProviderType.JiraServer;\n\t\tcase 'linear':\n\t\t\treturn EntityIdentifierProviderType.Linear;\n\t\tcase 'trello':\n\t\t\treturn EntityIdentifierProviderType.Trello;\n\t\tcase 'azure':\n\t\tcase 'azureDevOps':\n\t\tcase 'azure-devops':\n\t\t\treturn EntityIdentifierProviderType.Azure;\n\t\tcase GitSelfManagedHostIntegrationId.AzureDevOpsServer:\n\t\t\treturn EntityIdentifierProviderType.AzureDevOpsServer;\n\t\tcase 'bitbucket':\n\t\t\treturn EntityIdentifierProviderType.Bitbucket;\n\t\tcase 'bitbucket-server':\n\t\t\treturn EntityIdentifierProviderType.BitbucketServer;\n\t\tdefault:\n\t\t\tthrow new Error(`Unknown provider type '${str}'`);\n\t}\n}\n\nexport function encodeIssueOrPullRequestForGitConfig(\n\tentity: Issue | PullRequest,\n\towner: RepositoryDescriptor | IssueResourceDescriptor,\n): GitConfigEntityIdentifier {\n\tconst encodedOwner: GitConfigEntityIdentifier['metadata']['owner'] = {\n\t\tkey: owner.key,\n\t\tname: owner.name,\n\t\tid: undefined,\n\t\towner: undefined,\n\t};\n\tif (isRepositoryDescriptor(owner)) {\n\t\tencodedOwner.owner = owner.owner;\n\t} else if (isIssueResourceDescriptor(owner)) {\n\t\tencodedOwner.id = owner.id;\n\t} else {\n\t\tthrow new Error('Invalid owner');\n\t}\n\n\treturn {\n\t\t...getEntityIdentifierInput(entity),\n\t\tmetadata: {\n\t\t\tid: entity.id,\n\t\t\towner: encodedOwner,\n\t\t\tcreatedDate: new Date().toISOString(),\n\t\t\tisCloudEnterprise: isCloudGitSelfManagedHostIntegrationId(entity.provider.id as IntegrationIds),\n\t\t},\n\t};\n}\n\nexport function isGitConfigEntityIdentifier(entity: unknown): entity is GitConfigEntityIdentifier {\n\treturn (\n\t\tentity != null &&\n\t\ttypeof entity === 'object' &&\n\t\t'provider' in entity &&\n\t\tentity.provider != null &&\n\t\t'entityType' in entity &&\n\t\tentity.entityType != null &&\n\t\t'version' in entity &&\n\t\tentity.version != null &&\n\t\t'entityId' in entity &&\n\t\tentity.entityId != null &&\n\t\t'metadata' in entity &&\n\t\tentity.metadata != null &&\n\t\ttypeof entity.metadata === 'object' &&\n\t\t'id' in entity.metadata &&\n\t\tentity.metadata.id != null &&\n\t\t'owner' in entity.metadata &&\n\t\tentity.metadata.owner != null &&\n\t\t'createdDate' in entity.metadata &&\n\t\tentity.metadata.createdDate != null\n\t);\n}\n\nexport function isGitConfigEntityIdentifiers(entities: unknown): entities is GitConfigEntityIdentifier[] {\n\treturn Array.isArray(entities) && entities.every(entity => isGitConfigEntityIdentifier(entity));\n}\n\nexport function decodeEntityIdentifiersFromGitConfig(str: string): GitConfigEntityIdentifier[] {\n\tconst decoded = JSON.parse(str);\n\n\tif (!isGitConfigEntityIdentifiers(decoded)) {\n\t\tdebugger;\n\t\tLogger.error('Invalid entity identifiers in git config');\n\t\treturn [];\n\t}\n\n\tfor (const decodedEntity of decoded) {\n\t\tif (\n\t\t\tdecodedEntity.provider === EntityIdentifierProviderType.Jira &&\n\t\t\t(decodedEntity.resourceId == null || decodedEntity.projectId == null)\n\t\t) {\n\t\t\tdebugger;\n\t\t\tLogger.error('Invalid Jira issue in git config');\n\t\t}\n\n\t\t// The host is the identity of a self-hosted issue (see `getEntityIdentifierInput`); without it the\n\t\t// identifier cannot be resolved against any instance and `getIssueFromGitConfigEntityIdentifier` drops it.\n\t\tif (\n\t\t\tdecodedEntity.provider === EntityIdentifierProviderType.JiraServer &&\n\t\t\t(getDomainFromEntityIdentifier(decodedEntity) == null || decodedEntity.projectId == null)\n\t\t) {\n\t\t\tdebugger;\n\t\t\tLogger.error('Invalid Jira Server issue in git config');\n\t\t}\n\t}\n\n\treturn decoded;\n}\n\n/**\n * The host a self-managed identifier names, or `undefined` for a cloud one (or a malformed self-managed one).\n * `domain` lives on some arms of `AnyEntityIdentifierInput` and on the catch-all, so the read is structural.\n */\nfunction getDomainFromEntityIdentifier(identifier: GitConfigEntityIdentifier): string | undefined {\n\tconst domain = 'domain' in identifier ? identifier.domain : undefined;\n\treturn typeof domain === 'string' && domain.trim().length > 0 ? domain : undefined;\n}\n\ninterface IssueResolvableIntegration {\n\tgetIssue(owner: unknown, id: string): Promise<Issue | undefined>;\n}\n\n/**\n * Resolves the issue a branch association names.\n *\n * `resolveIntegration` receives the identifier's `domain` alongside the integration id, so a host-keyed\n * provider resolves the instance the association was encoded for rather than whichever connection is primary\n * (#5872). It is `undefined` for a cloud provider, which has a single host.\n */\nexport async function getIssueFromGitConfigEntityIdentifier(\n\tresolveIntegration: (id: IntegrationIds, domain?: string) => Promise<IssueResolvableIntegration | undefined>,\n\tidentifier: GitConfigEntityIdentifier,\n\toptions?: {\n\t\t/** Legacy git-host associations need configured connections to prove their host is unambiguous. */\n\t\tgetConfiguredIntegrations?: (id: IntegrationIds) => readonly { domain?: string }[];\n\t\t/** Only return a value already in the local cache. No remote fetch — returns undefined on cache miss. */\n\t\tcached?: boolean;\n\t\t/**\n\t\t * Host-supplied cache reader used when {@link cached} is set. The package is cache-agnostic, so the\n\t\t * host (which owns the cache) provides this to satisfy a cache-only read. When omitted, `cached`\n\t\t * yields undefined rather than falling through to a remote fetch.\n\t\t */\n\t\tpeekCachedIssue?: (\n\t\t\tintegration: IssueResolvableIntegration | undefined,\n\t\t\tresource: ResourceDescriptor,\n\t\t\tid: string,\n\t\t) => Issue | undefined;\n\t},\n): Promise<Issue | undefined> {\n\tif (identifier.entityType !== EntityType.Issue) {\n\t\treturn undefined;\n\t}\n\n\t// TODO: Centralize where we represent all supported providers for issues\n\tif (\n\t\tidentifier.provider !== EntityIdentifierProviderType.Jira &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.JiraServer &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Linear &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Github &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Gitlab &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.GithubEnterprise &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.GitlabSelfHosted &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Bitbucket &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.BitbucketServer &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.AzureDevOpsServer &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Azure &&\n\t\tidentifier.provider !== EntityIdentifierProviderType.Trello\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst integrationId = getProviderIdFromEntityIdentifier(identifier);\n\tif (integrationId == null) {\n\t\treturn undefined;\n\t}\n\n\tlet domain = getDomainFromEntityIdentifier(identifier);\n\tif (domain == null && isGitSelfManagedHostIntegrationId(integrationId)) {\n\t\tdomain = getSingleConfiguredDomain(options?.getConfiguredIntegrations?.(integrationId) ?? []);\n\t}\n\t// Hosts can share issue keys. Legacy git associations may omit the host, but can only be recovered when\n\t// configuration identifies exactly one; self-hosted tracker associations always require an explicit host.\n\tif (domain == null && isSelfManagedHostIntegrationId(integrationId)) {\n\t\tLogger.error(`Cannot resolve a '${integrationId}' issue from git config without an unambiguous domain`);\n\t\treturn undefined;\n\t}\n\n\tconst integration = await resolveIntegration(integrationId, domain);\n\n\tconst resource: ResourceDescriptor = {\n\t\tid: identifier.metadata.owner.id,\n\t\tkey: identifier.metadata.owner.key,\n\t\towner: identifier.metadata.owner.owner,\n\t\tname: identifier.metadata.owner.name,\n\t};\n\tconst remoteLookupId =\n\t\tidentifier.provider === EntityIdentifierProviderType.Trello ? identifier.entityId : identifier.metadata.id;\n\n\t// Cache-only read (no remote fetch). The package can't reach the host cache directly, so defer to the\n\t// host-supplied reader; without one, honor the no-fetch contract by returning undefined. For a cloud\n\t// provider the cache key is resource+id (the integration only affects the etag), so peek even when the\n\t// integration is unresolvable — a still-cached issue must survive an unconfigured/disconnected integration.\n\t// A self-managed host's key also names the host, which only a resolved integration can supply: without one\n\t// the peek would fall back to the unscoped key a cloud provider writes for the same resource, so skip it.\n\tif (options?.cached) {\n\t\tif (integration == null && isSelfManagedHostIntegrationId(integrationId)) return undefined;\n\n\t\tconst cachedIssue = options.peekCachedIssue?.(integration, resource, identifier.metadata.id);\n\t\tif (\n\t\t\tcachedIssue != null ||\n\t\t\t(identifier.provider !== EntityIdentifierProviderType.Trello &&\n\t\t\t\tidentifier.provider !== EntityIdentifierProviderType.Linear)\n\t\t) {\n\t\t\treturn cachedIssue;\n\t\t}\n\n\t\t// Two id generations coexist for these providers: Trello identifiers persisted before the stable-id\n\t\t// switch carry the board-local `idShort` in `metadata.id` while the cache is keyed by the stable card\n\t\t// id (`entityId`); Linear identifiers persisted before `Issue.id` became the human identifier carry\n\t\t// the UUID in `metadata.id` while fresh cache entries key by the identifier — and vice versa, the\n\t\t// UUID always survives in `entityId` (via `nodeId`). Peeking both keys resolves either generation.\n\t\treturn options.peekCachedIssue?.(integration, resource, identifier.entityId);\n\t}\n\n\tif (integration == null) {\n\t\treturn undefined;\n\t}\n\n\treturn integration.getIssue(resource, remoteLookupId);\n}\n\nexport function getIssueOwner(\n\tissue: IssueShape,\n): RepositoryDescriptor | IssueResourceDescriptor | AzureProjectInputDescriptor | undefined {\n\tconst isAzure = ['azure', GitCloudHostIntegrationId.AzureDevOps, 'azure-devops'].includes(issue.provider.id);\n\treturn issue.repository\n\t\t? {\n\t\t\t\tkey: `${issue.repository.owner}/${issue.repository.repo}`,\n\t\t\t\towner: issue.repository.owner,\n\t\t\t\tname: issue.repository.repo,\n\t\t\t}\n\t\t: issue.project\n\t\t\t? {\n\t\t\t\t\tkey: isAzure ? issue.project.id : issue.project.resourceId,\n\t\t\t\t\tid: isAzure ? issue.project.id : issue.project.resourceId,\n\t\t\t\t\towner: isAzure ? issue.project.resourceName : undefined,\n\t\t\t\t\tname: isAzure ? issue.project.name : issue.project.resourceName,\n\t\t\t\t}\n\t\t\t: undefined;\n}\n"],"mappings":"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"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { mapBounded } from "../../../utils/promise.js";
|
|
2
2
|
import { effectiveIssueSort } from "../../../git/utils/issue.utils.js";
|
|
3
|
+
import "../constants.js";
|
|
3
4
|
import { appendDedupedWarning } from "../results.js";
|
|
4
5
|
import { mergeAssessmentInto } from "../collectionMetadata.js";
|
|
5
|
-
import "../constants.js";
|
|
6
|
-
import { isGitHostIntegration, isIssuesHostIntegrationId } from "../utils/integration.utils.js";
|
|
7
6
|
import { hostFromDomain } from "../utils/domain.utils.js";
|
|
7
|
+
import { isGitHostIntegration, isIssuesHostIntegrationId } from "../utils/integration.utils.js";
|
|
8
8
|
import { resolveContinuation, toProviderPageInfo, usableCursor } from "./paging.js";
|
|
9
9
|
import { getBroadenIssuesCursor, isBroadenIssuesOrgExhausted, toBroadenIssuesCursor } from "./cursors.js";
|
|
10
10
|
import { gitHostOnlySurfaceWarning, issueSearchCapResultWarning, issuesUnsupportedWarning, noConnectionWarning, otherWarning, truncationWarning, unsupportedIssueSearchCriteriaWarning, unusableSearchScopeMessage } from "./warnings.js";
|
|
@@ -195,7 +195,7 @@ async function readOrgIssuesViaSearch(integration, org, domain, cursor) {
|
|
|
195
195
|
cursor: usableCursor(page.cursor),
|
|
196
196
|
truncated: page.truncated
|
|
197
197
|
};
|
|
198
|
-
if (page.truncated && warnings.length === 0) warnings.push(issueSearchCapResultWarning(org.providerId, domain, connectionId, page.totalCount, effectiveIssueSort(void 0)) ?? truncationWarning(org.providerId, domain, connectionId, "Issue search", "exhausted"));
|
|
198
|
+
if (page.truncated && warnings.length === 0) warnings.push(issueSearchCapResultWarning(org.providerId, domain, connectionId, page.totalCount, effectiveIssueSort(void 0), page.limitReached) ?? truncationWarning(org.providerId, domain, connectionId, "Issue search", "exhausted"));
|
|
199
199
|
}
|
|
200
200
|
return {
|
|
201
201
|
value,
|
|
@@ -208,8 +208,8 @@ async function readOrgIssuesViaSearch(integration, org, domain, cursor) {
|
|
|
208
208
|
/**
|
|
209
209
|
* The original engine: drain the org's repositories, then read the issues across them.
|
|
210
210
|
*
|
|
211
|
-
* Kept for a provider that declares NO filtered issue search (Azure DevOps, GitLab), where refusing the
|
|
212
|
-
* be a regression rather than a saving. Its cost, and the arbitrary subset it serves past the result ceiling, are
|
|
211
|
+
* Kept for a provider that declares NO filtered issue search (Azure DevOps Services, GitLab), where refusing the
|
|
212
|
+
* org would be a regression rather than a saving. Its cost, and the arbitrary subset it serves past the result ceiling, are
|
|
213
213
|
* what {@link readOrgIssuesViaSearch} exists to avoid — see the module docstring's measurement. Those providers
|
|
214
214
|
* keep both, because on them there is nothing better to switch to.
|
|
215
215
|
*/
|
|
@@ -279,7 +279,7 @@ async function readOrgSlice(ctx, options, org, page) {
|
|
|
279
279
|
}
|
|
280
280
|
if (!isGitHostIntegration(integration)) return barrenSlice([gitHostOnlySurfaceWarning(org.providerId, requestedDomain, connectionId, "issue broadening")], { fetchFailed: true });
|
|
281
281
|
if (!integration.supportsIssues) return barrenSlice([issuesUnsupportedWarning(org.providerId, ctx.domainForRead(integration, org.providerId, connectionId, requestedDomain), connectionId)], { fetchFailed: true });
|
|
282
|
-
const scopeRejection = resolveIssueSearchScope(void 0, org.name, void 0).rejection;
|
|
282
|
+
const scopeRejection = resolveIssueSearchScope(org.providerId, void 0, org.name, void 0).rejection;
|
|
283
283
|
if (scopeRejection != null) return barrenSlice([otherWarning(org.providerId, ctx.domainForRead(integration, org.providerId, connectionId, requestedDomain), connectionId, scopeRejection.reason === "unusable-scope" ? unusableSearchScopeMessage("issue broadening scopes", scopeRejection.scopes) : "Issue broadening must name an organization; one was supplied with an empty name.")], { fetchFailed: true });
|
|
284
284
|
if (isBroadenIssuesOrgExhausted(options.cursor, org, options.orgs.length)) return barrenSlice([], { exhausted: true });
|
|
285
285
|
await ctx.forceRefreshIfRequested(integration, options.forceSync, connectionId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"broaden.js","names":[],"sources":["../../../../../plus/integrations/src/reads/broaden.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport { effectiveIssueSort } from '@gitlens/git/utils/issue.utils.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport type { ProviderBroadenOrg } from '../manager.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { ProviderReposInput } from '../providers/models.js';\nimport type { ProviderBroadenResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport { hostFromDomain } from '../utils/domain.utils.js';\nimport { isGitHostIntegration, isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport type { BroadenIssuesExhaustedOrg, BroadenIssuesOrgCursor } from './cursors.js';\nimport { getBroadenIssuesCursor, isBroadenIssuesOrgExhausted, toBroadenIssuesCursor } from './cursors.js';\nimport { drainRepositories, runCaptured } from './drains.js';\nimport { resolveIssueSearchScope, supportsFilteredIssueSearch } from './filters.js';\nimport { resolveContinuation, toProviderPageInfo, usableCursor } from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissueSearchCapResultWarning,\n\tissuesUnsupportedWarning,\n\tnoConnectionWarning,\n\totherWarning,\n\ttruncationWarning,\n\tunsupportedIssueSearchCriteriaWarning,\n\tunusableSearchScopeMessage,\n} from './warnings.js';\n\n/**\n * The issue-broadening fan-out: every visible issue across a set of orgs, one page at a time.\n *\n * What makes this its own read rather than a variant of `listIssuesPage` is that one logical page spans several\n * provider positions — one per org — so its continuation is a per-org cursor BUNDLE (see `cursors.ts`) and its\n * failure attribution is per provider across those orgs. Both are the reason a page number alone can't address a\n * later page, and why {@link broadenIssues} walks prior pages itself when given only `page`.\n *\n * Each org slice runs on ONE OF TWO ENGINES, chosen by what the provider declares (#5804):\n * - the org-scoped FILTERED SEARCH where the provider has one (GitHub/GHE): one request per page, no repositories\n * needed at all, since the search reaches the scope by naming the org;\n * - the REPOSITORY DRAIN plus the SDK's repo-scoped read where it doesn't (Azure DevOps, GitLab): up to 100\n * requests to enumerate the org, and a read whose over-limit recovery walk can spend 128 more per repository\n * and still return an incomplete set.\n *\n * Measured by running this read against the live API, three times per engine per org (median shown; each\n * engine returned a byte-identical result set on all three, so the sets below are not sampling noise):\n *\n * | | repo drain + SDK read | org-scoped search |\n * | --- | --- | --- |\n * | BELOW the ceiling (146 issues) | 9.3s, 11 requests, 146 issues | 6.2s, 2 requests, 146 issues |\n * | ABOVE it (1.475 issues, 217 repos) | 111s, 133 requests, 1.118 issues | 40s, 10 requests, 1.000 issues |\n * | ...of the 1.000 most recently updated | 644 | 1.000 |\n * | ...omissions reported | 99 × `recovery-budget`, no count | 1, carrying `totalCount: 1474` |\n *\n * BELOW the ceiling the two return the IDENTICAL SET (verified as set equality), so there the swap is a straight\n * saving and nothing else. ABOVE it they return DIFFERENT SETS — overlapping on 644 items, so the divergence is\n * not confined to the edge.\n *\n * Above the ceiling the search returns FEWER items and is nonetheless the more useful of the two, which is the\n * whole reason to prefer it: its 1.000 are exactly the 1.000 most recently updated (again set equality against a\n * ceiling-free enumeration, not a count), whereas the SDK read's 1.118 are an arbitrary subset — 474 of them from\n * beyond that window, at the cost of omitting 356 issues that ARE in it. Its recovery walk does not close the gap\n * it spends the budget on: all 99 of its omissions were exhausted budget.\n *\n * The second difference is what a consumer can SAY about the gap. At the ceiling the search succeeds and reports\n * one quantified omission (`issueSearchCapResultWarning` carries `totalCount`, `limit` and the order the window\n * was selected under), so a consumer can render \"1.474 matched, showing the 1.000 most recently updated\". The\n * SDK read reports one `recovery-budget` omission per exhausted repository, none of which carries how many issues\n * were missed — incompleteness that can be announced but not quantified.\n *\n * Note the time saving is the SMALLER half of this: 1.5× below the ceiling and 2.8× above it, against 5.5× and\n * 13.3× fewer requests. The request count is what matters for a rate limit shared with every other read.\n *\n * The public surface is the SAME on both: the options, the per-org cursor bundle, the `ProviderAttribution`\n * split, the warning dedupe and the return type. A consumer needs to know which engine ran only insofar as the\n * warnings say so.\n *\n * Broadening means ALL VISIBLE, and each engine expresses that differently — `includeAllAssignees: true` on the\n * SDK read, an OMITTED `relationships` on the search. Both resolve to no assignee constraint at all, so\n * unassigned issues ARE included. The search's equivalent is NOT `['any-assignee']`: `assignee:*` means \"has some\n * assignee\" and would silently exclude every unassigned issue, which is the opposite of broadening.\n *\n * Takes no `sort`, unlike `listIssuesPage` and `searchIssuesPage`. One logical page here spans several orgs, each\n * at its own provider position in a cursor bundle, so honoring an order across them would need a k-way merge with\n * a buffer per org rather than a sort of what arrived — the page is a slice of several independent walks, not a\n * union of one round's results. The search engine still orders each org's OWN query (it must: a ceiling policy of\n * \"the N most recent\" is only correct under a guaranteed order), so a slice is ordered where the merged page is\n * not. `searchIssuesPage({ repos, criteria: { sort } })` answers the ordered version of this question for a\n * caller that knows its repositories.\n */\n\nexport interface BroadenIssuesOptions {\n\torgs: ProviderBroadenOrg[];\n\tpage?: number;\n\tcursor?: string;\n\tforceSync?: boolean;\n}\n\n/** One org slice of the fan-out: its issues plus the position and health the aggregation keys the bundle by. */\ninterface BroadenSlice {\n\titems: IssueShape[];\n\twarnings: ProviderWarning[];\n\tbroadenedProviderIds: IntegrationIds[];\n\tproviderId: IntegrationIds;\n\torg: string;\n\tconnectionId?: string;\n\tdomain?: string;\n\tnextCursor?: string;\n\tretryPage?: number;\n\thasMore: boolean;\n\texhausted: boolean;\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n}\n\n/**\n * Accumulates provider attribution across the rounds of a page-number traversal.\n *\n * The three sets are mutually exclusive per provider and the ordering rules are what keep them so: a provider\n * that failed in one round but broadened in another is INCOMPLETE, not failed, whichever round reported first.\n */\nclass ProviderAttribution {\n\tprivate readonly broadened = new Set<IntegrationIds>();\n\tprivate readonly failed = new Set<IntegrationIds>();\n\tprivate readonly incomplete = new Set<IntegrationIds>();\n\n\tmerge(result: ProviderBroadenResult<IssueShape>): void {\n\t\tfor (const providerId of result.failedProviderIds) {\n\t\t\tif (this.broadened.has(providerId) || result.broadenedProviderIds.includes(providerId)) {\n\t\t\t\tthis.incomplete.add(providerId);\n\t\t\t} else {\n\t\t\t\tthis.failed.add(providerId);\n\t\t\t}\n\t\t}\n\t\tfor (const providerId of result.incompleteProviderIds) {\n\t\t\tthis.failed.delete(providerId);\n\t\t\tthis.incomplete.add(providerId);\n\t\t}\n\t\tfor (const providerId of result.broadenedProviderIds) {\n\t\t\tthis.broadened.add(providerId);\n\t\t\tif (this.failed.delete(providerId)) {\n\t\t\t\tthis.incomplete.add(providerId);\n\t\t\t}\n\t\t}\n\t}\n\n\ttoResultFields(): Pick<\n\t\tProviderBroadenResult<IssueShape>,\n\t\t'broadenedProviderIds' | 'failedProviderIds' | 'incompleteProviderIds'\n\t> {\n\t\treturn {\n\t\t\tbroadenedProviderIds: [...this.broadened],\n\t\t\tfailedProviderIds: [...this.failed],\n\t\t\tincompleteProviderIds: [...this.incomplete],\n\t\t};\n\t}\n}\n\nexport async function broadenIssues(\n\tctx: ProviderReadContext,\n\toptions: BroadenIssuesOptions,\n): Promise<ProviderBroadenResult<IssueShape>> {\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\n\t// Kepler's existing contract persists only a page number. When no opaque continuation was supplied,\n\t// advance through prior pages internally so cursor-only providers still return the requested page.\n\t// Each recursive call below carries a cursor, so it bypasses this block and performs exactly one round.\n\tif (options.cursor == null && page > 1) {\n\t\treturn traverseToRequestedPage(ctx, options, page);\n\t}\n\n\tconst results = await mapBounded(options.orgs, providerFanOutConcurrency, org =>\n\t\treadOrgSlice(ctx, options, org, page),\n\t);\n\n\tconst items: IssueShape[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tconst broadenedProviderIds = new Set<IntegrationIds>();\n\tconst problemProviderIds = new Set<IntegrationIds>();\n\tconst cursors: BroadenIssuesOrgCursor[] = [];\n\tconst exhausted: BroadenIssuesExhaustedOrg[] = [];\n\tlet hasMore = false;\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tfor (const result of results) {\n\t\tif (result == null) {\n\t\t\tcontinue;\n\t\t}\n\n\t\titems.push(...result.items);\n\t\t// Deduped across orgs: several orgs of one provider commonly fail the same way (one expired token, one\n\t\t// rate limit), producing the identical warning per slice, and the array's contract is that no two entries\n\t\t// are equal — `traverseToRequestedPage` already dedupes when it folds these same warnings across rounds.\n\t\tfor (const warning of result.warnings) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tfor (const id of result.broadenedProviderIds) {\n\t\t\tbroadenedProviderIds.add(id);\n\t\t}\n\t\tif (result.nextCursor != null || result.retryPage != null) {\n\t\t\tcursors.push({\n\t\t\t\tproviderId: result.providerId,\n\t\t\t\torg: result.org,\n\t\t\t\tconnectionId: result.connectionId,\n\t\t\t\tdomain: result.domain,\n\t\t\t\tcursor: result.nextCursor,\n\t\t\t\tretryPage: result.retryPage,\n\t\t\t});\n\t\t}\n\t\tif (result.exhausted) {\n\t\t\texhausted.push({\n\t\t\t\tproviderId: result.providerId,\n\t\t\t\torg: result.org,\n\t\t\t\tconnectionId: result.connectionId,\n\t\t\t\tdomain: result.domain,\n\t\t\t});\n\t\t}\n\t\tif (result.hasMore) {\n\t\t\thasMore = true;\n\t\t}\n\t\tif (result.fetchFailed) {\n\t\t\tfetchFailed = true;\n\t\t}\n\t\tif (result.truncated) {\n\t\t\ttruncated = true;\n\t\t}\n\t\tif (result.fetchFailed || result.truncated) {\n\t\t\tproblemProviderIds.add(result.providerId);\n\t\t}\n\t}\n\n\tconst cursor = toBroadenIssuesCursor(cursors, exhausted, options.orgs.length);\n\tconst failedProviderIds: IntegrationIds[] = [];\n\tconst incompleteProviderIds: IntegrationIds[] = [];\n\tfor (const providerId of problemProviderIds) {\n\t\tif (broadenedProviderIds.has(providerId)) {\n\t\t\tincompleteProviderIds.push(providerId);\n\t\t} else {\n\t\t\tfailedProviderIds.push(providerId);\n\t\t}\n\t}\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\t// `currentPage` is positional, per ProviderPageInfo.currentPage: this fan-out has no provider-reported\n\t\t// page of its own (its cursor is a per-org bundle, not a page), so the position is the one the caller\n\t\t// addressed — the `page` it supplied, or the page the internal traversal advanced to.\n\t\tpage: { currentPage: page, itemsPerPage: items.length, truncated: truncated || undefined },\n\t\t// `hasMore` promises a resumable continuation, so it must be true ONLY when a real cursor was\n\t\t// produced. Repo-drain truncation (a backstop hit with no persisted repo cursor) can't be resumed —\n\t\t// re-invoking would re-drain the same repos and repeat issues — so it is surfaced as the terminal\n\t\t// `page.truncated` incompleteness signal instead of `hasMore`, matching listRepos. Guard `hasMore`\n\t\t// against a missing cursor so we never advertise a continuation the caller can't make.\n\t\thasMore: hasMore && cursor != null,\n\t\tcursor: cursor,\n\t\tfetchFailed: fetchFailed || undefined,\n\t\tbroadenedProviderIds: [...broadenedProviderIds],\n\t\tfailedProviderIds: failedProviderIds,\n\t\tincompleteProviderIds: incompleteProviderIds,\n\t\tfanOutCount: options.orgs.length,\n\t};\n}\n\n/**\n * Advances to page N by re-running the fan-out for each prior page, threading each round's cursor bundle into the\n * next. Only the requested page's items are returned; warnings and provider attribution accumulate across the\n * whole walked prefix, since a failure in an earlier round still means this result is not authoritative.\n */\nasync function traverseToRequestedPage(\n\tctx: ProviderReadContext,\n\toptions: BroadenIssuesOptions,\n\tpage: number,\n): Promise<ProviderBroadenResult<IssueShape>> {\n\tlet cursor: string | undefined;\n\tconst traversalWarnings: ProviderWarning[] = [];\n\tconst attribution = new ProviderAttribution();\n\tlet traversalFetchFailed = false;\n\tlet traversalTruncated = false;\n\n\tfor (let currentPage = 1; currentPage < page; currentPage++) {\n\t\tconst previous = await broadenIssues(ctx, {\n\t\t\t...options,\n\t\t\tpage: currentPage,\n\t\t\tcursor: cursor,\n\t\t\t// A forced refresh belongs to the logical read, not every cursor-advancement round.\n\t\t\tforceSync: currentPage === 1 ? options.forceSync : false,\n\t\t});\n\t\tfor (const warning of previous.warnings) {\n\t\t\tappendDedupedWarning(traversalWarnings, warning);\n\t\t}\n\t\tattribution.merge(previous);\n\t\ttraversalFetchFailed ||= previous.fetchFailed === true;\n\t\ttraversalTruncated ||= previous.page.truncated === true;\n\t\tif (!previous.hasMore || previous.cursor == null) {\n\t\t\t// The requested page lies past the last continuation: an empty page N, never the last page relabeled.\n\t\t\treturn {\n\t\t\t\titems: [],\n\t\t\t\twarnings: traversalWarnings,\n\t\t\t\tpage: { currentPage: page, itemsPerPage: 0, truncated: traversalTruncated || undefined },\n\t\t\t\thasMore: false,\n\t\t\t\tfetchFailed: traversalFetchFailed || undefined,\n\t\t\t\t...attribution.toResultFields(),\n\t\t\t\tfanOutCount: options.orgs.length,\n\t\t\t};\n\t\t}\n\n\t\tcursor = previous.cursor;\n\t}\n\n\tconst requested = await broadenIssues(ctx, { ...options, page: page, cursor: cursor, forceSync: false });\n\tfor (const warning of requested.warnings) {\n\t\tappendDedupedWarning(traversalWarnings, warning);\n\t}\n\tattribution.merge(requested);\n\treturn {\n\t\t...requested,\n\t\twarnings: traversalWarnings,\n\t\tpage: {\n\t\t\t...requested.page,\n\t\t\ttruncated: traversalTruncated || requested.page.truncated === true || undefined,\n\t\t},\n\t\tfetchFailed: traversalFetchFailed || requested.fetchFailed === true || undefined,\n\t\t...attribution.toResultFields(),\n\t};\n}\n\n/**\n * One org's issues as either engine below reports them, normalized to the single shape {@link readOrgSlice}'s\n * bookkeeping consumes.\n *\n * The two engines return different envelopes — the SDK read's `PagedResult` + `paging`, the search's flat page —\n * and normalizing HERE rather than per engine is what keeps the continuation, retry, attribution and exhaustion\n * rules literally the same code on both paths. A second copy of those rules per engine is how one path would come\n * to advertise a continuation the other calls terminal.\n */\ninterface OrgIssuesRead {\n\t/** The page the provider served, or `undefined` when the read returned none (which `warning` explains). */\n\tvalue?: { items: IssueShape[]; hasMore: boolean; cursor?: string; truncated: boolean };\n\t/** The read's own failure. Already included in `warnings`; carried separately because the retry rules key off it. */\n\twarning?: ProviderWarning;\n\t/** Everything the engine produced, the read's own failure included. */\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\t/**\n\t * The engine established there is NOTHING to read for this org — the repository drain returned no\n\t * repositories — so the slice is terminal and empty. Distinct from `value == null`, which means a read WAS\n\t * issued and came back with no page, and so is a position to retry.\n\t */\n\tnothingToRead?: boolean;\n}\n\n/**\n * The org-scoped filtered search: ONE request per page, whatever the org contains.\n *\n * Replaces the drain + SDK read for every provider that declares a filtered issue search — the repository drain\n * costs up to 100 requests per org, and the SDK's repo-scoped read past GitHub's result ceiling enters a\n * per-repository recovery walk that can spend 128 more and still return an incomplete set. Measured on an org\n * past that ceiling: 133 requests and 111s, against this read's 10 and 40s. The search reaches the same scope by\n * NAMING the org, so it needs no repositories at all.\n *\n * It is also the more COMPLETE of the two over the window a consumer reads first, despite returning fewer items —\n * see the module docstring's table for the measurement and why that is not a contradiction.\n *\n * `criteria` is omitted entirely, and each half of that is deliberate:\n * - `relationships` OMITTED is what drops the assignee constraint, which is the substitution `includeAllAssignees:\n * true` performs on the read this replaces. NOT `['any-assignee']`, which means \"assigned to somebody\" and would\n * exclude every unassigned issue — the opposite of broadening.\n * - every other criterion left unset resolves to the provider's own default, which is the state this read already\n * served (open issues only, archived repositories excluded).\n *\n * At the result ceiling this SUCCEEDS and reports how many matched instead of walking for the rest — see the\n * ceiling warning below.\n */\nasync function readOrgIssuesViaSearch(\n\tintegration: GitHostIntegration,\n\torg: ProviderBroadenOrg,\n\tdomain: string | undefined,\n\tcursor: string | undefined,\n): Promise<OrgIssuesRead> {\n\tconst connectionId = org.connectionId;\n\tconst captured = await runCaptured(\n\t\torg.providerId,\n\t\tdomain,\n\t\tconnectionId,\n\t\t() => integration.searchIssuesPageResult({ org: org.name, cursor: cursor }, undefined, connectionId),\n\t\t{ warnOnMissingSession: true },\n\t);\n\n\t// A provider that declares the capability but implements no search hook answers `undefined` with no error.\n\t// Reported as the explicit unsupported failure rather than left as a silent empty page, so it can't be mistaken\n\t// for an org with no issues — and as the read's `warning`, so the slice treats it as the read failure it is.\n\tconst warning =\n\t\tcaptured.warning ??\n\t\t(captured.value == null\n\t\t\t? unsupportedIssueSearchCriteriaWarning(org.providerId, domain, connectionId, {\n\t\t\t\t\treason: 'unsupported-search',\n\t\t\t\t})\n\t\t\t: undefined);\n\tconst warnings = warning != null ? [warning] : [];\n\n\tlet value: OrgIssuesRead['value'];\n\tif (captured.value != null) {\n\t\tconst page = captured.value;\n\t\tvalue = {\n\t\t\titems: page.values,\n\t\t\thasMore: page.hasMore,\n\t\t\tcursor: usableCursor(page.cursor),\n\t\t\ttruncated: page.truncated,\n\t\t};\n\t\t// The result ceiling is the one incompleteness this read can QUANTIFY, so it carries the total rather than\n\t\t// a bare \"may be incomplete\" — which is what lets a consumer say \"1.474 matched, showing the 1.000 most\n\t\t// recently updated\" instead of silently serving a truncated list. Only when nothing else already explained\n\t\t// the incompleteness, and only the ceiling gets the number: any other cause falls back to the generic\n\t\t// wording rather than reporting a limit it didn't hit. `exhausted`: nothing this read exposes would return\n\t\t// the withheld items.\n\t\tif (page.truncated && warnings.length === 0) {\n\t\t\twarnings.push(\n\t\t\t\tissueSearchCapResultWarning(\n\t\t\t\t\torg.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\tpage.totalCount,\n\t\t\t\t\teffectiveIssueSort(undefined),\n\t\t\t\t) ?? truncationWarning(org.providerId, domain, connectionId, 'Issue search', 'exhausted'),\n\t\t\t);\n\t\t}\n\t}\n\n\treturn { value: value, warning: warning, warnings: warnings, fetchFailed: false, truncated: false };\n}\n\n/**\n * The original engine: drain the org's repositories, then read the issues across them.\n *\n * Kept for a provider that declares NO filtered issue search (Azure DevOps, GitLab), where refusing the org would\n * be a regression rather than a saving. Its cost, and the arbitrary subset it serves past the result ceiling, are\n * what {@link readOrgIssuesViaSearch} exists to avoid — see the module docstring's measurement. Those providers\n * keep both, because on them there is nothing better to switch to.\n */\nasync function readOrgIssuesViaRepoDrain(\n\tintegration: GitHostIntegration,\n\torg: ProviderBroadenOrg,\n\tdomain: string | undefined,\n\tcursor: string | undefined,\n): Promise<OrgIssuesRead> {\n\tconst connectionId = org.connectionId;\n\tconst reposDrain = await drainRepositories(\n\t\tintegration,\n\t\torg.providerId,\n\t\tdomain,\n\t\torg.name,\n\t\tundefined,\n\t\tconnectionId,\n\t\t100,\n\t);\n\tconst warnings: ProviderWarning[] = [...reposDrain.warnings];\n\n\tconst repos: ProviderReposInput = reposDrain.repos.map(r => ({ ...r }));\n\tif (repos.length === 0) {\n\t\treturn {\n\t\t\twarnings: warnings,\n\t\t\tfetchFailed: reposDrain.fetchFailed,\n\t\t\ttruncated: reposDrain.truncated,\n\t\t\tnothingToRead: true,\n\t\t};\n\t}\n\n\t// Broaden = \"all visible\": drop the assigned-to-me filter so unassigned issues are included.\n\tconst captured = await runCaptured(\n\t\torg.providerId,\n\t\tdomain,\n\t\tconnectionId,\n\t\t() =>\n\t\t\t// Normalized shapes seam, uniform with listIssuesPage.\n\t\t\tintegration.getMyIssuesForReposAsShapesResult(\n\t\t\t\trepos,\n\t\t\t\t{ includeAllAssignees: true, cursor: cursor },\n\t\t\t\tconnectionId,\n\t\t\t),\n\t\t{ warnOnMissingSession: true },\n\t);\n\tif (captured.warning != null) {\n\t\twarnings.push(captured.warning);\n\t}\n\tconst assessment = mergeAssessmentInto(warnings, org.providerId, domain, connectionId, captured.value?.metadata);\n\n\tlet value: OrgIssuesRead['value'];\n\tif (captured.value != null) {\n\t\tconst paged = toProviderPageInfo(captured.value.values.length, captured.value.paging);\n\t\tvalue = {\n\t\t\titems: captured.value.values,\n\t\t\thasMore: paged.hasMore,\n\t\t\tcursor: paged.cursor,\n\t\t\t// Carry a truncation signal from the issue read too: a provider that couldn't confirm it drained a\n\t\t\t// repo (`paging.truncated`) means this org's issues may be incomplete, on top of any repo-drain\n\t\t\t// truncation already captured above.\n\t\t\ttruncated: paged.truncated || assessment.truncated,\n\t\t};\n\t}\n\n\treturn {\n\t\tvalue: value,\n\t\twarning: captured.warning,\n\t\twarnings: warnings,\n\t\tfetchFailed: reposDrain.fetchFailed || assessment.fetchFailed,\n\t\ttruncated: reposDrain.truncated,\n\t};\n}\n\n/**\n * Reads one org's visible issues, through the org-scoped filtered search where the provider has one and through\n * the repository drain where it doesn't. Everything below the engine — the position, the retry slot, the\n * attribution and the exhaustion mark — is the same on both paths.\n */\nasync function readOrgSlice(\n\tctx: ProviderReadContext,\n\toptions: BroadenIssuesOptions,\n\torg: ProviderBroadenOrg,\n\tpage: number,\n): Promise<BroadenSlice> {\n\tconst connectionId = org.connectionId;\n\tconst requestedDomain = org.domain;\n\tconst cursorDomain = hostFromDomain(requestedDomain) ?? requestedDomain;\n\t// An org slice that yielded no issues and has nothing to continue: the org's identity (which the\n\t// aggregation above keys the per-org cursor bundle by) plus why it produced nothing. `exhausted` marks\n\t// an org a prior round already drained, so it stays skipped rather than being re-read from page 1.\n\tconst barrenSlice = (\n\t\twarnings: ProviderWarning[],\n\t\tflags?: { exhausted?: boolean; fetchFailed?: boolean; truncated?: boolean },\n\t): BroadenSlice => ({\n\t\titems: [],\n\t\twarnings: warnings,\n\t\tbroadenedProviderIds: [],\n\t\tproviderId: org.providerId,\n\t\torg: org.name,\n\t\tconnectionId: connectionId,\n\t\tdomain: cursorDomain,\n\t\tnextCursor: undefined,\n\t\thasMore: false,\n\t\texhausted: flags?.exhausted ?? false,\n\t\tfetchFailed: flags?.fetchFailed ?? false,\n\t\ttruncated: flags?.truncated ?? false,\n\t});\n\n\tif (isIssuesHostIntegrationId(org.providerId)) {\n\t\treturn barrenSlice(\n\t\t\t[gitHostOnlySurfaceWarning(org.providerId, requestedDomain, connectionId, 'issue broadening')],\n\t\t\t{\n\t\t\t\tfetchFailed: true,\n\t\t\t},\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(org.providerId, connectionId, requestedDomain);\n\tif (integration == null) {\n\t\t// A requested connection or domain that can't be resolved is a broken target — surface it as a\n\t\t// warning + fetchFailed rather than dropping the org silently.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(org.providerId, connectionId, requestedDomain);\n\t\treturn barrenSlice(\n\t\t\tearly.warnings.length > 0\n\t\t\t\t? early.warnings\n\t\t\t\t: [noConnectionWarning(org.providerId, requestedDomain, connectionId)],\n\t\t\t{ fetchFailed: true },\n\t\t);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn barrenSlice(\n\t\t\t[gitHostOnlySurfaceWarning(org.providerId, requestedDomain, connectionId, 'issue broadening')],\n\t\t\t{\n\t\t\t\tfetchFailed: true,\n\t\t\t},\n\t\t);\n\t}\n\t// A git host whose issue tracker is deprecated (Bitbucket) exposes no issues here — surface a\n\t// warning + fetchFailed and skip it (no repo drain), so broadening never serves a legacy source.\n\tif (!integration.supportsIssues) {\n\t\treturn barrenSlice(\n\t\t\t[\n\t\t\t\tissuesUnsupportedWarning(\n\t\t\t\t\torg.providerId,\n\t\t\t\t\tctx.domainForRead(integration, org.providerId, connectionId, requestedDomain),\n\t\t\t\t\tconnectionId,\n\t\t\t\t),\n\t\t\t],\n\t\t\t{ fetchFailed: true },\n\t\t);\n\t}\n\n\t// An org name NEITHER engine can use, refused before either is chosen. Declining the search for it is not\n\t// enough, because falling through to the repository drain is not the safe default it looks like: the drain\n\t// passes the SAME name to the org repository read, which matches nothing and reports `nothingToRead` — an\n\t// org that reads as empty, with no warning and no `fetchFailed`, after up to 100 requests. Indistinguishable\n\t// from an org that genuinely has no issues, and strictly worse than saying so.\n\t//\n\t// EVERY rejection, not just `unusable-scope`: with no repositories and no criteria the only other reachable\n\t// one is `unscoped`, which here means exactly `org.name === ''` — the same bad outcome by the same route, so\n\t// drawing the line between them would leave the emptiest case on the wrong side of the rule. (`repo-ids`\n\t// cannot arise: no repositories are passed.) Refusing them all is also what lets the engine choice below be\n\t// a plain capability question again, since no rejection survives this point.\n\tconst scopeRejection = resolveIssueSearchScope(undefined, org.name, undefined).rejection;\n\tif (scopeRejection != null) {\n\t\treturn barrenSlice(\n\t\t\t[\n\t\t\t\totherWarning(\n\t\t\t\t\torg.providerId,\n\t\t\t\t\tctx.domainForRead(integration, org.providerId, connectionId, requestedDomain),\n\t\t\t\t\tconnectionId,\n\t\t\t\t\tscopeRejection.reason === 'unusable-scope'\n\t\t\t\t\t\t? unusableSearchScopeMessage('issue broadening scopes', scopeRejection.scopes)\n\t\t\t\t\t\t: 'Issue broadening must name an organization; one was supplied with an empty name.',\n\t\t\t\t),\n\t\t\t],\n\t\t\t{ fetchFailed: true },\n\t\t);\n\t}\n\n\t// An org a prior round already drained must not be re-read: cursor-only providers would answer a\n\t// fresh page-1 request with their first page again, duplicating issues across rounds. Skip it\n\t// before any work (including the repo drain) and keep it marked exhausted so it stays skipped\n\t// for the rest of the fan-out.\n\tif (isBroadenIssuesOrgExhausted(options.cursor, org, options.orgs.length)) {\n\t\treturn barrenSlice([], { exhausted: true });\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, connectionId);\n\n\tconst domain = ctx.domainForRead(integration, org.providerId, connectionId, requestedDomain);\n\tconst cursor = getBroadenIssuesCursor(options.cursor, org, page, options.orgs.length);\n\t// A plain capability question, and only because the refusal above has already turned away every org whose\n\t// name the scope rule rejects. That order matters: `readOrgIssuesViaSearch` calls the integration's\n\t// `searchIssuesPageResult` directly rather than the `searchIssuesPage` read, and that method documents its\n\t// scope as already validated (\"the facade rejects it before here\") — so an unvalidated name reaching it would\n\t// go straight to the provider query that sanitizes it.\n\tconst read = supportsFilteredIssueSearch(org.providerId)\n\t\t? await readOrgIssuesViaSearch(integration, org, domain, cursor)\n\t\t: await readOrgIssuesViaRepoDrain(integration, org, domain, cursor);\n\tif (read.nothingToRead === true) {\n\t\treturn barrenSlice(read.warnings, { fetchFailed: read.fetchFailed, truncated: read.truncated });\n\t}\n\n\tconst warnings = read.warnings;\n\tlet issuesFetchFailed = read.fetchFailed || (read.warning != null && read.value == null);\n\tconst items: IssueShape[] = [];\n\tlet hasMore = false;\n\tlet nextCursor: string | undefined;\n\tlet retryPage: number | undefined;\n\tlet issuesTruncated = false;\n\tif (read.value != null) {\n\t\titems.push(...read.value.items);\n\t\t// An org that reports another page but no usable cursor can't be resumed: it would neither be\n\t\t// recorded in the composite cursor nor marked exhausted, so the next round would re-read its\n\t\t// page 1 and repeat every issue. Treat it as terminal-but-incomplete (which also marks the org\n\t\t// exhausted below, since `exhausted` keys off `!hasMore`).\n\t\tconst continuation = resolveContinuation(read.value, undefined);\n\t\thasMore = continuation.hasMore;\n\t\tnextCursor = continuation.cursor;\n\t\tissuesTruncated = continuation.truncated;\n\t} else if (read.warning != null || cursor != null) {\n\t\t// Keep the exact position that failed. Without this retry cursor a multi-org continuation would\n\t\t// omit this org from the bundle, then synthesize the next numbered page and silently skip the\n\t\t// failed page. The synthesized page cursor is also actionable for a first-page cursor-only read:\n\t\t// that provider ignores the page marker and retries its first page. A retry slot alone is NOT\n\t\t// forward progress, though: advertising `hasMore` for a persistent failure would make an infinite\n\t\t// query request it forever. Healthy sibling continuations set `hasMore` independently.\n\t\tif (cursor != null) {\n\t\t\tnextCursor = cursor;\n\t\t\tif (read.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\torg.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t\t'Issue continuation returned no result and must be retried',\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tissuesFetchFailed = true;\n\t\t\t\tissuesTruncated = true;\n\t\t\t}\n\t\t} else {\n\t\t\tretryPage = page;\n\t\t}\n\t}\n\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\tbroadenedProviderIds: read.value != null ? [org.providerId] : [],\n\t\tproviderId: org.providerId,\n\t\torg: org.name,\n\t\tconnectionId: connectionId,\n\t\tdomain: cursorDomain,\n\t\tnextCursor: nextCursor,\n\t\tretryPage: retryPage,\n\t\thasMore: hasMore,\n\t\t// Exhausted once a successful read reports no more pages — recorded in the cursor so later\n\t\t// rounds skip it while other orgs keep paging.\n\t\texhausted: read.value != null && !hasMore,\n\t\tfetchFailed: issuesFetchFailed,\n\t\ttruncated: read.truncated || issuesTruncated,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"broaden.js","names":[],"sources":["../../../../../plus/integrations/src/reads/broaden.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport { effectiveIssueSort } from '@gitlens/git/utils/issue.utils.js';\nimport { mapBounded } from '@gitlens/utils/promise.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport { providerFanOutConcurrency } from '../constants.js';\nimport type { ProviderBroadenOrg } from '../manager.js';\nimport type { GitHostIntegration } from '../models/gitHostIntegration.js';\nimport type { ProviderReposInput } from '../providers/models.js';\nimport type { ProviderBroadenResult, ProviderWarning } from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport { hostFromDomain } from '../utils/domain.utils.js';\nimport { isGitHostIntegration, isIssuesHostIntegrationId } from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport type { BroadenIssuesExhaustedOrg, BroadenIssuesOrgCursor } from './cursors.js';\nimport { getBroadenIssuesCursor, isBroadenIssuesOrgExhausted, toBroadenIssuesCursor } from './cursors.js';\nimport { drainRepositories, runCaptured } from './drains.js';\nimport { resolveIssueSearchScope, supportsFilteredIssueSearch } from './filters.js';\nimport { resolveContinuation, toProviderPageInfo, usableCursor } from './paging.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissueSearchCapResultWarning,\n\tissuesUnsupportedWarning,\n\tnoConnectionWarning,\n\totherWarning,\n\ttruncationWarning,\n\tunsupportedIssueSearchCriteriaWarning,\n\tunusableSearchScopeMessage,\n} from './warnings.js';\n\n/**\n * The issue-broadening fan-out: every visible issue across a set of orgs, one page at a time.\n *\n * What makes this its own read rather than a variant of `listIssuesPage` is that one logical page spans several\n * provider positions — one per org — so its continuation is a per-org cursor BUNDLE (see `cursors.ts`) and its\n * failure attribution is per provider across those orgs. Both are the reason a page number alone can't address a\n * later page, and why {@link broadenIssues} walks prior pages itself when given only `page`.\n *\n * Each org slice runs on ONE OF TWO ENGINES, chosen by what the provider declares (#5804):\n * - the org-scoped FILTERED SEARCH where the provider has one (GitHub/GHE): one request per page, no repositories\n * needed at all, since the search reaches the scope by naming the org;\n * - the REPOSITORY DRAIN plus the SDK's repo-scoped read where it doesn't (Azure DevOps, GitLab): up to 100\n * requests to enumerate the org, and a read whose over-limit recovery walk can spend 128 more per repository\n * and still return an incomplete set.\n *\n * Measured by running this read against the live API, three times per engine per org (median shown; each\n * engine returned a byte-identical result set on all three, so the sets below are not sampling noise):\n *\n * | | repo drain + SDK read | org-scoped search |\n * | --- | --- | --- |\n * | BELOW the ceiling (146 issues) | 9.3s, 11 requests, 146 issues | 6.2s, 2 requests, 146 issues |\n * | ABOVE it (1.475 issues, 217 repos) | 111s, 133 requests, 1.118 issues | 40s, 10 requests, 1.000 issues |\n * | ...of the 1.000 most recently updated | 644 | 1.000 |\n * | ...omissions reported | 99 × `recovery-budget`, no count | 1, carrying `totalCount: 1474` |\n *\n * BELOW the ceiling the two return the IDENTICAL SET (verified as set equality), so there the swap is a straight\n * saving and nothing else. ABOVE it they return DIFFERENT SETS — overlapping on 644 items, so the divergence is\n * not confined to the edge.\n *\n * Above the ceiling the search returns FEWER items and is nonetheless the more useful of the two, which is the\n * whole reason to prefer it: its 1.000 are exactly the 1.000 most recently updated (again set equality against a\n * ceiling-free enumeration, not a count), whereas the SDK read's 1.118 are an arbitrary subset — 474 of them from\n * beyond that window, at the cost of omitting 356 issues that ARE in it. Its recovery walk does not close the gap\n * it spends the budget on: all 99 of its omissions were exhausted budget.\n *\n * The second difference is what a consumer can SAY about the gap. At the ceiling the search succeeds and reports\n * one quantified omission (`issueSearchCapResultWarning` carries `totalCount`, `limit` and the order the window\n * was selected under), so a consumer can render \"1.474 matched, showing the 1.000 most recently updated\". The\n * SDK read reports one `recovery-budget` omission per exhausted repository, none of which carries how many issues\n * were missed — incompleteness that can be announced but not quantified.\n *\n * Note the time saving is the SMALLER half of this: 1.5× below the ceiling and 2.8× above it, against 5.5× and\n * 13.3× fewer requests. The request count is what matters for a rate limit shared with every other read.\n *\n * The public surface is the SAME on both: the options, the per-org cursor bundle, the `ProviderAttribution`\n * split, the warning dedupe and the return type. A consumer needs to know which engine ran only insofar as the\n * warnings say so.\n *\n * Broadening means ALL VISIBLE, and each engine expresses that differently — `includeAllAssignees: true` on the\n * SDK read, an OMITTED `relationships` on the search. Both resolve to no assignee constraint at all, so\n * unassigned issues ARE included. The search's equivalent is NOT `['any-assignee']`: `assignee:*` means \"has some\n * assignee\" and would silently exclude every unassigned issue, which is the opposite of broadening.\n *\n * Takes no `sort`, unlike `listIssuesPage` and `searchIssuesPage`. One logical page here spans several orgs, each\n * at its own provider position in a cursor bundle, so honoring an order across them would need a k-way merge with\n * a buffer per org rather than a sort of what arrived — the page is a slice of several independent walks, not a\n * union of one round's results. The search engine still orders each org's OWN query (it must: a ceiling policy of\n * \"the N most recent\" is only correct under a guaranteed order), so a slice is ordered where the merged page is\n * not. `searchIssuesPage({ repos, criteria: { sort } })` answers the ordered version of this question for a\n * caller that knows its repositories.\n */\n\nexport interface BroadenIssuesOptions {\n\torgs: ProviderBroadenOrg[];\n\tpage?: number;\n\tcursor?: string;\n\tforceSync?: boolean;\n}\n\n/** One org slice of the fan-out: its issues plus the position and health the aggregation keys the bundle by. */\ninterface BroadenSlice {\n\titems: IssueShape[];\n\twarnings: ProviderWarning[];\n\tbroadenedProviderIds: IntegrationIds[];\n\tproviderId: IntegrationIds;\n\torg: string;\n\tconnectionId?: string;\n\tdomain?: string;\n\tnextCursor?: string;\n\tretryPage?: number;\n\thasMore: boolean;\n\texhausted: boolean;\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n}\n\n/**\n * Accumulates provider attribution across the rounds of a page-number traversal.\n *\n * The three sets are mutually exclusive per provider and the ordering rules are what keep them so: a provider\n * that failed in one round but broadened in another is INCOMPLETE, not failed, whichever round reported first.\n */\nclass ProviderAttribution {\n\tprivate readonly broadened = new Set<IntegrationIds>();\n\tprivate readonly failed = new Set<IntegrationIds>();\n\tprivate readonly incomplete = new Set<IntegrationIds>();\n\n\tmerge(result: ProviderBroadenResult<IssueShape>): void {\n\t\tfor (const providerId of result.failedProviderIds) {\n\t\t\tif (this.broadened.has(providerId) || result.broadenedProviderIds.includes(providerId)) {\n\t\t\t\tthis.incomplete.add(providerId);\n\t\t\t} else {\n\t\t\t\tthis.failed.add(providerId);\n\t\t\t}\n\t\t}\n\t\tfor (const providerId of result.incompleteProviderIds) {\n\t\t\tthis.failed.delete(providerId);\n\t\t\tthis.incomplete.add(providerId);\n\t\t}\n\t\tfor (const providerId of result.broadenedProviderIds) {\n\t\t\tthis.broadened.add(providerId);\n\t\t\tif (this.failed.delete(providerId)) {\n\t\t\t\tthis.incomplete.add(providerId);\n\t\t\t}\n\t\t}\n\t}\n\n\ttoResultFields(): Pick<\n\t\tProviderBroadenResult<IssueShape>,\n\t\t'broadenedProviderIds' | 'failedProviderIds' | 'incompleteProviderIds'\n\t> {\n\t\treturn {\n\t\t\tbroadenedProviderIds: [...this.broadened],\n\t\t\tfailedProviderIds: [...this.failed],\n\t\t\tincompleteProviderIds: [...this.incomplete],\n\t\t};\n\t}\n}\n\nexport async function broadenIssues(\n\tctx: ProviderReadContext,\n\toptions: BroadenIssuesOptions,\n): Promise<ProviderBroadenResult<IssueShape>> {\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\n\t// Kepler's existing contract persists only a page number. When no opaque continuation was supplied,\n\t// advance through prior pages internally so cursor-only providers still return the requested page.\n\t// Each recursive call below carries a cursor, so it bypasses this block and performs exactly one round.\n\tif (options.cursor == null && page > 1) {\n\t\treturn traverseToRequestedPage(ctx, options, page);\n\t}\n\n\tconst results = await mapBounded(options.orgs, providerFanOutConcurrency, org =>\n\t\treadOrgSlice(ctx, options, org, page),\n\t);\n\n\tconst items: IssueShape[] = [];\n\tconst warnings: ProviderWarning[] = [];\n\tconst broadenedProviderIds = new Set<IntegrationIds>();\n\tconst problemProviderIds = new Set<IntegrationIds>();\n\tconst cursors: BroadenIssuesOrgCursor[] = [];\n\tconst exhausted: BroadenIssuesExhaustedOrg[] = [];\n\tlet hasMore = false;\n\tlet fetchFailed = false;\n\tlet truncated = false;\n\tfor (const result of results) {\n\t\tif (result == null) {\n\t\t\tcontinue;\n\t\t}\n\n\t\titems.push(...result.items);\n\t\t// Deduped across orgs: several orgs of one provider commonly fail the same way (one expired token, one\n\t\t// rate limit), producing the identical warning per slice, and the array's contract is that no two entries\n\t\t// are equal — `traverseToRequestedPage` already dedupes when it folds these same warnings across rounds.\n\t\tfor (const warning of result.warnings) {\n\t\t\tappendDedupedWarning(warnings, warning);\n\t\t}\n\t\tfor (const id of result.broadenedProviderIds) {\n\t\t\tbroadenedProviderIds.add(id);\n\t\t}\n\t\tif (result.nextCursor != null || result.retryPage != null) {\n\t\t\tcursors.push({\n\t\t\t\tproviderId: result.providerId,\n\t\t\t\torg: result.org,\n\t\t\t\tconnectionId: result.connectionId,\n\t\t\t\tdomain: result.domain,\n\t\t\t\tcursor: result.nextCursor,\n\t\t\t\tretryPage: result.retryPage,\n\t\t\t});\n\t\t}\n\t\tif (result.exhausted) {\n\t\t\texhausted.push({\n\t\t\t\tproviderId: result.providerId,\n\t\t\t\torg: result.org,\n\t\t\t\tconnectionId: result.connectionId,\n\t\t\t\tdomain: result.domain,\n\t\t\t});\n\t\t}\n\t\tif (result.hasMore) {\n\t\t\thasMore = true;\n\t\t}\n\t\tif (result.fetchFailed) {\n\t\t\tfetchFailed = true;\n\t\t}\n\t\tif (result.truncated) {\n\t\t\ttruncated = true;\n\t\t}\n\t\tif (result.fetchFailed || result.truncated) {\n\t\t\tproblemProviderIds.add(result.providerId);\n\t\t}\n\t}\n\n\tconst cursor = toBroadenIssuesCursor(cursors, exhausted, options.orgs.length);\n\tconst failedProviderIds: IntegrationIds[] = [];\n\tconst incompleteProviderIds: IntegrationIds[] = [];\n\tfor (const providerId of problemProviderIds) {\n\t\tif (broadenedProviderIds.has(providerId)) {\n\t\t\tincompleteProviderIds.push(providerId);\n\t\t} else {\n\t\t\tfailedProviderIds.push(providerId);\n\t\t}\n\t}\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\t// `currentPage` is positional, per ProviderPageInfo.currentPage: this fan-out has no provider-reported\n\t\t// page of its own (its cursor is a per-org bundle, not a page), so the position is the one the caller\n\t\t// addressed — the `page` it supplied, or the page the internal traversal advanced to.\n\t\tpage: { currentPage: page, itemsPerPage: items.length, truncated: truncated || undefined },\n\t\t// `hasMore` promises a resumable continuation, so it must be true ONLY when a real cursor was\n\t\t// produced. Repo-drain truncation (a backstop hit with no persisted repo cursor) can't be resumed —\n\t\t// re-invoking would re-drain the same repos and repeat issues — so it is surfaced as the terminal\n\t\t// `page.truncated` incompleteness signal instead of `hasMore`, matching listRepos. Guard `hasMore`\n\t\t// against a missing cursor so we never advertise a continuation the caller can't make.\n\t\thasMore: hasMore && cursor != null,\n\t\tcursor: cursor,\n\t\tfetchFailed: fetchFailed || undefined,\n\t\tbroadenedProviderIds: [...broadenedProviderIds],\n\t\tfailedProviderIds: failedProviderIds,\n\t\tincompleteProviderIds: incompleteProviderIds,\n\t\tfanOutCount: options.orgs.length,\n\t};\n}\n\n/**\n * Advances to page N by re-running the fan-out for each prior page, threading each round's cursor bundle into the\n * next. Only the requested page's items are returned; warnings and provider attribution accumulate across the\n * whole walked prefix, since a failure in an earlier round still means this result is not authoritative.\n */\nasync function traverseToRequestedPage(\n\tctx: ProviderReadContext,\n\toptions: BroadenIssuesOptions,\n\tpage: number,\n): Promise<ProviderBroadenResult<IssueShape>> {\n\tlet cursor: string | undefined;\n\tconst traversalWarnings: ProviderWarning[] = [];\n\tconst attribution = new ProviderAttribution();\n\tlet traversalFetchFailed = false;\n\tlet traversalTruncated = false;\n\n\tfor (let currentPage = 1; currentPage < page; currentPage++) {\n\t\tconst previous = await broadenIssues(ctx, {\n\t\t\t...options,\n\t\t\tpage: currentPage,\n\t\t\tcursor: cursor,\n\t\t\t// A forced refresh belongs to the logical read, not every cursor-advancement round.\n\t\t\tforceSync: currentPage === 1 ? options.forceSync : false,\n\t\t});\n\t\tfor (const warning of previous.warnings) {\n\t\t\tappendDedupedWarning(traversalWarnings, warning);\n\t\t}\n\t\tattribution.merge(previous);\n\t\ttraversalFetchFailed ||= previous.fetchFailed === true;\n\t\ttraversalTruncated ||= previous.page.truncated === true;\n\t\tif (!previous.hasMore || previous.cursor == null) {\n\t\t\t// The requested page lies past the last continuation: an empty page N, never the last page relabeled.\n\t\t\treturn {\n\t\t\t\titems: [],\n\t\t\t\twarnings: traversalWarnings,\n\t\t\t\tpage: { currentPage: page, itemsPerPage: 0, truncated: traversalTruncated || undefined },\n\t\t\t\thasMore: false,\n\t\t\t\tfetchFailed: traversalFetchFailed || undefined,\n\t\t\t\t...attribution.toResultFields(),\n\t\t\t\tfanOutCount: options.orgs.length,\n\t\t\t};\n\t\t}\n\n\t\tcursor = previous.cursor;\n\t}\n\n\tconst requested = await broadenIssues(ctx, { ...options, page: page, cursor: cursor, forceSync: false });\n\tfor (const warning of requested.warnings) {\n\t\tappendDedupedWarning(traversalWarnings, warning);\n\t}\n\tattribution.merge(requested);\n\treturn {\n\t\t...requested,\n\t\twarnings: traversalWarnings,\n\t\tpage: {\n\t\t\t...requested.page,\n\t\t\ttruncated: traversalTruncated || requested.page.truncated === true || undefined,\n\t\t},\n\t\tfetchFailed: traversalFetchFailed || requested.fetchFailed === true || undefined,\n\t\t...attribution.toResultFields(),\n\t};\n}\n\n/**\n * One org's issues as either engine below reports them, normalized to the single shape {@link readOrgSlice}'s\n * bookkeeping consumes.\n *\n * The two engines return different envelopes — the SDK read's `PagedResult` + `paging`, the search's flat page —\n * and normalizing HERE rather than per engine is what keeps the continuation, retry, attribution and exhaustion\n * rules literally the same code on both paths. A second copy of those rules per engine is how one path would come\n * to advertise a continuation the other calls terminal.\n */\ninterface OrgIssuesRead {\n\t/** The page the provider served, or `undefined` when the read returned none (which `warning` explains). */\n\tvalue?: { items: IssueShape[]; hasMore: boolean; cursor?: string; truncated: boolean };\n\t/** The read's own failure. Already included in `warnings`; carried separately because the retry rules key off it. */\n\twarning?: ProviderWarning;\n\t/** Everything the engine produced, the read's own failure included. */\n\twarnings: ProviderWarning[];\n\tfetchFailed: boolean;\n\ttruncated: boolean;\n\t/**\n\t * The engine established there is NOTHING to read for this org — the repository drain returned no\n\t * repositories — so the slice is terminal and empty. Distinct from `value == null`, which means a read WAS\n\t * issued and came back with no page, and so is a position to retry.\n\t */\n\tnothingToRead?: boolean;\n}\n\n/**\n * The org-scoped filtered search: ONE request per page, whatever the org contains.\n *\n * Replaces the drain + SDK read for every provider that declares a filtered issue search — the repository drain\n * costs up to 100 requests per org, and the SDK's repo-scoped read past GitHub's result ceiling enters a\n * per-repository recovery walk that can spend 128 more and still return an incomplete set. Measured on an org\n * past that ceiling: 133 requests and 111s, against this read's 10 and 40s. The search reaches the same scope by\n * NAMING the org, so it needs no repositories at all.\n *\n * It is also the more COMPLETE of the two over the window a consumer reads first, despite returning fewer items —\n * see the module docstring's table for the measurement and why that is not a contradiction.\n *\n * `criteria` is omitted entirely, and each half of that is deliberate:\n * - `relationships` OMITTED is what drops the assignee constraint, which is the substitution `includeAllAssignees:\n * true` performs on the read this replaces. NOT `['any-assignee']`, which means \"assigned to somebody\" and would\n * exclude every unassigned issue — the opposite of broadening.\n * - every other criterion left unset resolves to the provider's own default, which is the state this read already\n * served (open issues only, archived repositories excluded).\n *\n * At the result ceiling this SUCCEEDS and reports how many matched instead of walking for the rest — see the\n * ceiling warning below.\n */\nasync function readOrgIssuesViaSearch(\n\tintegration: GitHostIntegration,\n\torg: ProviderBroadenOrg,\n\tdomain: string | undefined,\n\tcursor: string | undefined,\n): Promise<OrgIssuesRead> {\n\tconst connectionId = org.connectionId;\n\tconst captured = await runCaptured(\n\t\torg.providerId,\n\t\tdomain,\n\t\tconnectionId,\n\t\t() => integration.searchIssuesPageResult({ org: org.name, cursor: cursor }, undefined, connectionId),\n\t\t{ warnOnMissingSession: true },\n\t);\n\n\t// A provider that declares the capability but implements no search hook answers `undefined` with no error.\n\t// Reported as the explicit unsupported failure rather than left as a silent empty page, so it can't be mistaken\n\t// for an org with no issues — and as the read's `warning`, so the slice treats it as the read failure it is.\n\tconst warning =\n\t\tcaptured.warning ??\n\t\t(captured.value == null\n\t\t\t? unsupportedIssueSearchCriteriaWarning(org.providerId, domain, connectionId, {\n\t\t\t\t\treason: 'unsupported-search',\n\t\t\t\t})\n\t\t\t: undefined);\n\tconst warnings = warning != null ? [warning] : [];\n\n\tlet value: OrgIssuesRead['value'];\n\tif (captured.value != null) {\n\t\tconst page = captured.value;\n\t\tvalue = {\n\t\t\titems: page.values,\n\t\t\thasMore: page.hasMore,\n\t\t\tcursor: usableCursor(page.cursor),\n\t\t\ttruncated: page.truncated,\n\t\t};\n\t\t// The result ceiling is the one incompleteness this read can QUANTIFY, so it carries the total rather than\n\t\t// a bare \"may be incomplete\" — which is what lets a consumer say \"1.474 matched, showing the 1.000 most\n\t\t// recently updated\" instead of silently serving a truncated list. Only when nothing else already explained\n\t\t// the incompleteness, and only the ceiling gets the number: any other cause falls back to the generic\n\t\t// wording rather than reporting a limit it didn't hit. `exhausted`: nothing this read exposes would return\n\t\t// the withheld items.\n\t\tif (page.truncated && warnings.length === 0) {\n\t\t\twarnings.push(\n\t\t\t\tissueSearchCapResultWarning(\n\t\t\t\t\torg.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\tpage.totalCount,\n\t\t\t\t\teffectiveIssueSort(undefined),\n\t\t\t\t\tpage.limitReached,\n\t\t\t\t) ?? truncationWarning(org.providerId, domain, connectionId, 'Issue search', 'exhausted'),\n\t\t\t);\n\t\t}\n\t}\n\n\treturn { value: value, warning: warning, warnings: warnings, fetchFailed: false, truncated: false };\n}\n\n/**\n * The original engine: drain the org's repositories, then read the issues across them.\n *\n * Kept for a provider that declares NO filtered issue search (Azure DevOps Services, GitLab), where refusing the\n * org would be a regression rather than a saving. Its cost, and the arbitrary subset it serves past the result ceiling, are\n * what {@link readOrgIssuesViaSearch} exists to avoid — see the module docstring's measurement. Those providers\n * keep both, because on them there is nothing better to switch to.\n */\nasync function readOrgIssuesViaRepoDrain(\n\tintegration: GitHostIntegration,\n\torg: ProviderBroadenOrg,\n\tdomain: string | undefined,\n\tcursor: string | undefined,\n): Promise<OrgIssuesRead> {\n\tconst connectionId = org.connectionId;\n\tconst reposDrain = await drainRepositories(\n\t\tintegration,\n\t\torg.providerId,\n\t\tdomain,\n\t\torg.name,\n\t\tundefined,\n\t\tconnectionId,\n\t\t100,\n\t);\n\tconst warnings: ProviderWarning[] = [...reposDrain.warnings];\n\n\tconst repos: ProviderReposInput = reposDrain.repos.map(r => ({ ...r }));\n\tif (repos.length === 0) {\n\t\treturn {\n\t\t\twarnings: warnings,\n\t\t\tfetchFailed: reposDrain.fetchFailed,\n\t\t\ttruncated: reposDrain.truncated,\n\t\t\tnothingToRead: true,\n\t\t};\n\t}\n\n\t// Broaden = \"all visible\": drop the assigned-to-me filter so unassigned issues are included.\n\tconst captured = await runCaptured(\n\t\torg.providerId,\n\t\tdomain,\n\t\tconnectionId,\n\t\t() =>\n\t\t\t// Normalized shapes seam, uniform with listIssuesPage.\n\t\t\tintegration.getMyIssuesForReposAsShapesResult(\n\t\t\t\trepos,\n\t\t\t\t{ includeAllAssignees: true, cursor: cursor },\n\t\t\t\tconnectionId,\n\t\t\t),\n\t\t{ warnOnMissingSession: true },\n\t);\n\tif (captured.warning != null) {\n\t\twarnings.push(captured.warning);\n\t}\n\tconst assessment = mergeAssessmentInto(warnings, org.providerId, domain, connectionId, captured.value?.metadata);\n\n\tlet value: OrgIssuesRead['value'];\n\tif (captured.value != null) {\n\t\tconst paged = toProviderPageInfo(captured.value.values.length, captured.value.paging);\n\t\tvalue = {\n\t\t\titems: captured.value.values,\n\t\t\thasMore: paged.hasMore,\n\t\t\tcursor: paged.cursor,\n\t\t\t// Carry a truncation signal from the issue read too: a provider that couldn't confirm it drained a\n\t\t\t// repo (`paging.truncated`) means this org's issues may be incomplete, on top of any repo-drain\n\t\t\t// truncation already captured above.\n\t\t\ttruncated: paged.truncated || assessment.truncated,\n\t\t};\n\t}\n\n\treturn {\n\t\tvalue: value,\n\t\twarning: captured.warning,\n\t\twarnings: warnings,\n\t\tfetchFailed: reposDrain.fetchFailed || assessment.fetchFailed,\n\t\ttruncated: reposDrain.truncated,\n\t};\n}\n\n/**\n * Reads one org's visible issues, through the org-scoped filtered search where the provider has one and through\n * the repository drain where it doesn't. Everything below the engine — the position, the retry slot, the\n * attribution and the exhaustion mark — is the same on both paths.\n */\nasync function readOrgSlice(\n\tctx: ProviderReadContext,\n\toptions: BroadenIssuesOptions,\n\torg: ProviderBroadenOrg,\n\tpage: number,\n): Promise<BroadenSlice> {\n\tconst connectionId = org.connectionId;\n\tconst requestedDomain = org.domain;\n\tconst cursorDomain = hostFromDomain(requestedDomain) ?? requestedDomain;\n\t// An org slice that yielded no issues and has nothing to continue: the org's identity (which the\n\t// aggregation above keys the per-org cursor bundle by) plus why it produced nothing. `exhausted` marks\n\t// an org a prior round already drained, so it stays skipped rather than being re-read from page 1.\n\tconst barrenSlice = (\n\t\twarnings: ProviderWarning[],\n\t\tflags?: { exhausted?: boolean; fetchFailed?: boolean; truncated?: boolean },\n\t): BroadenSlice => ({\n\t\titems: [],\n\t\twarnings: warnings,\n\t\tbroadenedProviderIds: [],\n\t\tproviderId: org.providerId,\n\t\torg: org.name,\n\t\tconnectionId: connectionId,\n\t\tdomain: cursorDomain,\n\t\tnextCursor: undefined,\n\t\thasMore: false,\n\t\texhausted: flags?.exhausted ?? false,\n\t\tfetchFailed: flags?.fetchFailed ?? false,\n\t\ttruncated: flags?.truncated ?? false,\n\t});\n\n\tif (isIssuesHostIntegrationId(org.providerId)) {\n\t\treturn barrenSlice(\n\t\t\t[gitHostOnlySurfaceWarning(org.providerId, requestedDomain, connectionId, 'issue broadening')],\n\t\t\t{\n\t\t\t\tfetchFailed: true,\n\t\t\t},\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(org.providerId, connectionId, requestedDomain);\n\tif (integration == null) {\n\t\t// A requested connection or domain that can't be resolved is a broken target — surface it as a\n\t\t// warning + fetchFailed rather than dropping the org silently.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(org.providerId, connectionId, requestedDomain);\n\t\treturn barrenSlice(\n\t\t\tearly.warnings.length > 0\n\t\t\t\t? early.warnings\n\t\t\t\t: [noConnectionWarning(org.providerId, requestedDomain, connectionId)],\n\t\t\t{ fetchFailed: true },\n\t\t);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn barrenSlice(\n\t\t\t[gitHostOnlySurfaceWarning(org.providerId, requestedDomain, connectionId, 'issue broadening')],\n\t\t\t{\n\t\t\t\tfetchFailed: true,\n\t\t\t},\n\t\t);\n\t}\n\t// A git host whose issue tracker is deprecated (Bitbucket) exposes no issues here — surface a\n\t// warning + fetchFailed and skip it (no repo drain), so broadening never serves a legacy source.\n\tif (!integration.supportsIssues) {\n\t\treturn barrenSlice(\n\t\t\t[\n\t\t\t\tissuesUnsupportedWarning(\n\t\t\t\t\torg.providerId,\n\t\t\t\t\tctx.domainForRead(integration, org.providerId, connectionId, requestedDomain),\n\t\t\t\t\tconnectionId,\n\t\t\t\t),\n\t\t\t],\n\t\t\t{ fetchFailed: true },\n\t\t);\n\t}\n\n\t// An org name NEITHER engine can use, refused before either is chosen. Declining the search for it is not\n\t// enough, because falling through to the repository drain is not the safe default it looks like: the drain\n\t// passes the SAME name to the org repository read, which matches nothing and reports `nothingToRead` — an\n\t// org that reads as empty, with no warning and no `fetchFailed`, after up to 100 requests. Indistinguishable\n\t// from an org that genuinely has no issues, and strictly worse than saying so.\n\t//\n\t// EVERY rejection, not just `unusable-scope`: with no repositories and no criteria the only other reachable\n\t// one is `unscoped`, which here means exactly `org.name === ''` — the same bad outcome by the same route, so\n\t// drawing the line between them would leave the emptiest case on the wrong side of the rule. (`repo-ids`\n\t// cannot arise: no repositories are passed.) Refusing them all is also what lets the engine choice below be\n\t// a plain capability question again, since no rejection survives this point.\n\tconst scopeRejection = resolveIssueSearchScope(org.providerId, undefined, org.name, undefined).rejection;\n\tif (scopeRejection != null) {\n\t\treturn barrenSlice(\n\t\t\t[\n\t\t\t\totherWarning(\n\t\t\t\t\torg.providerId,\n\t\t\t\t\tctx.domainForRead(integration, org.providerId, connectionId, requestedDomain),\n\t\t\t\t\tconnectionId,\n\t\t\t\t\tscopeRejection.reason === 'unusable-scope'\n\t\t\t\t\t\t? unusableSearchScopeMessage('issue broadening scopes', scopeRejection.scopes)\n\t\t\t\t\t\t: 'Issue broadening must name an organization; one was supplied with an empty name.',\n\t\t\t\t),\n\t\t\t],\n\t\t\t{ fetchFailed: true },\n\t\t);\n\t}\n\n\t// An org a prior round already drained must not be re-read: cursor-only providers would answer a\n\t// fresh page-1 request with their first page again, duplicating issues across rounds. Skip it\n\t// before any work (including the repo drain) and keep it marked exhausted so it stays skipped\n\t// for the rest of the fan-out.\n\tif (isBroadenIssuesOrgExhausted(options.cursor, org, options.orgs.length)) {\n\t\treturn barrenSlice([], { exhausted: true });\n\t}\n\n\tawait ctx.forceRefreshIfRequested(integration, options.forceSync, connectionId);\n\n\tconst domain = ctx.domainForRead(integration, org.providerId, connectionId, requestedDomain);\n\tconst cursor = getBroadenIssuesCursor(options.cursor, org, page, options.orgs.length);\n\t// A plain capability question, and only because the refusal above has already turned away every org whose\n\t// name the scope rule rejects. That order matters: `readOrgIssuesViaSearch` calls the integration's\n\t// `searchIssuesPageResult` directly rather than the `searchIssuesPage` read, and that method documents its\n\t// scope as already validated (\"the facade rejects it before here\") — so an unvalidated name reaching it would\n\t// go straight to the provider query that sanitizes it.\n\tconst read = supportsFilteredIssueSearch(org.providerId)\n\t\t? await readOrgIssuesViaSearch(integration, org, domain, cursor)\n\t\t: await readOrgIssuesViaRepoDrain(integration, org, domain, cursor);\n\tif (read.nothingToRead === true) {\n\t\treturn barrenSlice(read.warnings, { fetchFailed: read.fetchFailed, truncated: read.truncated });\n\t}\n\n\tconst warnings = read.warnings;\n\tlet issuesFetchFailed = read.fetchFailed || (read.warning != null && read.value == null);\n\tconst items: IssueShape[] = [];\n\tlet hasMore = false;\n\tlet nextCursor: string | undefined;\n\tlet retryPage: number | undefined;\n\tlet issuesTruncated = false;\n\tif (read.value != null) {\n\t\titems.push(...read.value.items);\n\t\t// An org that reports another page but no usable cursor can't be resumed: it would neither be\n\t\t// recorded in the composite cursor nor marked exhausted, so the next round would re-read its\n\t\t// page 1 and repeat every issue. Treat it as terminal-but-incomplete (which also marks the org\n\t\t// exhausted below, since `exhausted` keys off `!hasMore`).\n\t\tconst continuation = resolveContinuation(read.value, undefined);\n\t\thasMore = continuation.hasMore;\n\t\tnextCursor = continuation.cursor;\n\t\tissuesTruncated = continuation.truncated;\n\t} else if (read.warning != null || cursor != null) {\n\t\t// Keep the exact position that failed. Without this retry cursor a multi-org continuation would\n\t\t// omit this org from the bundle, then synthesize the next numbered page and silently skip the\n\t\t// failed page. The synthesized page cursor is also actionable for a first-page cursor-only read:\n\t\t// that provider ignores the page marker and retries its first page. A retry slot alone is NOT\n\t\t// forward progress, though: advertising `hasMore` for a persistent failure would make an infinite\n\t\t// query request it forever. Healthy sibling continuations set `hasMore` independently.\n\t\tif (cursor != null) {\n\t\t\tnextCursor = cursor;\n\t\t\tif (read.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\torg.providerId,\n\t\t\t\t\t\tdomain,\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t\t'Issue continuation returned no result and must be retried',\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tissuesFetchFailed = true;\n\t\t\t\tissuesTruncated = true;\n\t\t\t}\n\t\t} else {\n\t\t\tretryPage = page;\n\t\t}\n\t}\n\n\treturn {\n\t\titems: items,\n\t\twarnings: warnings,\n\t\tbroadenedProviderIds: read.value != null ? [org.providerId] : [],\n\t\tproviderId: org.providerId,\n\t\torg: org.name,\n\t\tconnectionId: connectionId,\n\t\tdomain: cursorDomain,\n\t\tnextCursor: nextCursor,\n\t\tretryPage: retryPage,\n\t\thasMore: hasMore,\n\t\t// Exhausted once a successful read reports no more pages — recorded in the cursor so later\n\t\t// rounds skip it while other orgs keep paging.\n\t\texhausted: read.value != null && !hasMore,\n\t\tfetchFailed: issuesFetchFailed,\n\t\ttruncated: read.truncated || issuesTruncated,\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ConfiguredIntegrationDescriptor } from "../authentication/models.js";
|
|
2
2
|
import { IntegrationIds } from "../constants.js";
|
|
3
|
-
import { Integration } from "../models/integration.js";
|
|
4
3
|
import { ProviderWarning } from "../results.js";
|
|
4
|
+
import { Integration } from "../models/integration.js";
|
|
5
5
|
import { IntegrationsRemoteConfig } from "../context.js";
|
|
6
6
|
//#region ../plus/integrations/src/reads/context.d.ts
|
|
7
7
|
/**
|
|
@@ -66,10 +66,21 @@ export interface PullRequestCountResult {
|
|
|
66
66
|
key: string;
|
|
67
67
|
/**
|
|
68
68
|
* Total matches the provider reports. `undefined` when the provider didn't report one for this scope — NEVER
|
|
69
|
-
* zero, which is a real answer.
|
|
70
|
-
*
|
|
69
|
+
* zero, which is a real answer.
|
|
70
|
+
*
|
|
71
|
+
* For a multi-state scope, GitHub/GHE report the LARGEST of their per-state counts (the same total
|
|
72
|
+
* `searchPullRequestsPage` surfaces, since each state is its own capped search), not their sum. Bitbucket Data
|
|
73
|
+
* Center has no ceiling to stay under, so it reports the exact union — the number of rows the search returns —
|
|
74
|
+
* and so does Azure DevOps Server, which reads every state in one drain.
|
|
71
75
|
*/
|
|
72
76
|
count?: number;
|
|
77
|
+
/**
|
|
78
|
+
* True when `count` is a FLOOR rather than the total: the provider has no count query (Bitbucket Data Center),
|
|
79
|
+
* so it counts by reading within a budget, and this scope had more than the budget read. Render it as "N+", and
|
|
80
|
+
* treat the scope as at least that expensive. Absent means `count` is exact — the provider's own total, or for a
|
|
81
|
+
* provider that counts by reading, every match it read.
|
|
82
|
+
*/
|
|
83
|
+
lowerBound?: boolean;
|
|
73
84
|
/**
|
|
74
85
|
* True when `count` exceeds the provider's own per-search result ceiling, so a full read CANNOT return
|
|
75
86
|
* everything no matter how it is paged. This is the signal to warn before starting an expensive fetch.
|
|
@@ -110,9 +121,12 @@ export declare function countIssues(ctx: ProviderReadContext, options: {
|
|
|
110
121
|
*
|
|
111
122
|
* Identical batching, per-scope isolation, and `key`-echo contract as {@link countIssues}; see it for the cost
|
|
112
123
|
* model and the `count: undefined` ≠ zero rule. The one difference is inherent to pull requests: a scope's criteria
|
|
113
|
-
* can name several STATES, which
|
|
124
|
+
* can name several STATES, which GitHub/GHE count as independent searches — the reported `count` is the LARGEST
|
|
114
125
|
* of them (mirroring `searchPullRequestsPage`'s total), so an unpaged read still can't exceed a single search's
|
|
115
|
-
* ceiling undetected.
|
|
126
|
+
* ceiling undetected. Bitbucket Data Center has no ceiling and no count query: it counts the exact union by reading
|
|
127
|
+
* each scope within a budget, and flags a scope that had more as `lowerBound`. Because it deduplicates rows, it is
|
|
128
|
+
* also the one provider that accepts several relationships in one scope. Azure DevOps Server counts the union of the
|
|
129
|
+
* states from the one drain its search pages through.
|
|
116
130
|
*/
|
|
117
131
|
export declare function countPullRequests(ctx: ProviderReadContext, options: {
|
|
118
132
|
providerId: IntegrationIds;
|