@gitkraken/core-gitlens 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/dist/git/cache.d.ts +84 -29
  3. package/dist/git/cache.js +188 -54
  4. package/dist/git/cache.js.map +1 -1
  5. package/dist/git/features.d.ts +1 -1
  6. package/dist/git/features.js +4 -0
  7. package/dist/git/features.js.map +1 -1
  8. package/dist/git/models/branch.d.ts +15 -2
  9. package/dist/git/models/branch.js +27 -5
  10. package/dist/git/models/branch.js.map +1 -1
  11. package/dist/git/models/status.js +1 -1
  12. package/dist/git/models/worktree.d.ts +4 -1
  13. package/dist/git/models/worktree.js +4 -2
  14. package/dist/git/models/worktree.js.map +1 -1
  15. package/dist/git/providers/branches.d.ts +73 -8
  16. package/dist/git/providers/pausedOperations.d.ts +6 -0
  17. package/dist/git/providers/provider.d.ts +34 -2
  18. package/dist/git/providers/status.d.ts +8 -2
  19. package/dist/git/remotes/azure-devops.js +61 -53
  20. package/dist/git/remotes/azure-devops.js.map +1 -1
  21. package/dist/git/service.d.ts +8 -5
  22. package/dist/git/service.js +16 -8
  23. package/dist/git/service.js.map +1 -1
  24. package/dist/git/utils/branch.utils.d.ts +8 -1
  25. package/dist/git/utils/branch.utils.js +30 -1
  26. package/dist/git/utils/branch.utils.js.map +1 -1
  27. package/dist/git/watching/changeEvent.d.ts +13 -2
  28. package/dist/git/watching/changeEvent.js +17 -3
  29. package/dist/git/watching/changeEvent.js.map +1 -1
  30. package/dist/git/watching/gitDirPoller.d.ts +111 -0
  31. package/dist/git/watching/gitDirPoller.js +266 -0
  32. package/dist/git/watching/gitDirPoller.js.map +1 -0
  33. package/dist/git/watching/provider.d.ts +16 -0
  34. package/dist/git/watching/watchGroup.d.ts +40 -4
  35. package/dist/git/watching/watchGroup.js +143 -17
  36. package/dist/git/watching/watchGroup.js.map +1 -1
  37. package/dist/git/watching/watchService.d.ts +70 -1
  38. package/dist/git/watching/watchService.js +158 -13
  39. package/dist/git/watching/watchService.js.map +1 -1
  40. package/dist/git/watching/watchSession.d.ts +44 -0
  41. package/dist/git/watching/watchSession.js +91 -6
  42. package/dist/git/watching/watchSession.js.map +1 -1
  43. package/dist/git/watching/watcherPatterns.d.ts +21 -0
  44. package/dist/git/watching/watcherPatterns.js +67 -1
  45. package/dist/git/watching/watcherPatterns.js.map +1 -1
  46. package/dist/git/watching/worktreeSummary.d.ts +34 -7
  47. package/dist/git/watching/worktreeSummary.js +39 -12
  48. package/dist/git/watching/worktreeSummary.js.map +1 -1
  49. package/dist/git-cli/cliGitProvider.d.ts +34 -2
  50. package/dist/git-cli/cliGitProvider.js +119 -17
  51. package/dist/git-cli/cliGitProvider.js.map +1 -1
  52. package/dist/git-cli/exec/commandClassifier.d.ts +15 -0
  53. package/dist/git-cli/exec/commandClassifier.js +48 -2
  54. package/dist/git-cli/exec/commandClassifier.js.map +1 -1
  55. package/dist/git-cli/exec/git.d.ts +15 -3
  56. package/dist/git-cli/exec/git.js +38 -17
  57. package/dist/git-cli/exec/git.js.map +1 -1
  58. package/dist/git-cli/exec/gitQueue.d.ts +2 -0
  59. package/dist/git-cli/exec/gitQueue.js +2 -1
  60. package/dist/git-cli/exec/gitQueue.js.map +1 -1
  61. package/dist/git-cli/exec/indexLock.d.ts +38 -0
  62. package/dist/git-cli/exec/indexLock.js +150 -0
  63. package/dist/git-cli/exec/indexLock.js.map +1 -0
  64. package/dist/git-cli/parsers/worktreeParser.js +42 -2
  65. package/dist/git-cli/parsers/worktreeParser.js.map +1 -1
  66. package/dist/git-cli/providers/branches.d.ts +109 -10
  67. package/dist/git-cli/providers/branches.js +624 -131
  68. package/dist/git-cli/providers/branches.js.map +1 -1
  69. package/dist/git-cli/providers/config.d.ts +9 -5
  70. package/dist/git-cli/providers/config.js +60 -27
  71. package/dist/git-cli/providers/config.js.map +1 -1
  72. package/dist/git-cli/providers/operations.d.ts +17 -0
  73. package/dist/git-cli/providers/operations.js +153 -96
  74. package/dist/git-cli/providers/operations.js.map +1 -1
  75. package/dist/git-cli/providers/pausedOperations.d.ts +8 -0
  76. package/dist/git-cli/providers/pausedOperations.js +60 -40
  77. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  78. package/dist/git-cli/providers/refs.js +11 -8
  79. package/dist/git-cli/providers/refs.js.map +1 -1
  80. package/dist/git-cli/providers/remotes.js +19 -10
  81. package/dist/git-cli/providers/remotes.js.map +1 -1
  82. package/dist/git-cli/providers/staging.js +40 -15
  83. package/dist/git-cli/providers/staging.js.map +1 -1
  84. package/dist/git-cli/providers/stash.d.ts +2 -0
  85. package/dist/git-cli/providers/stash.js +33 -18
  86. package/dist/git-cli/providers/stash.js.map +1 -1
  87. package/dist/git-cli/providers/status.d.ts +3 -1
  88. package/dist/git-cli/providers/status.js +38 -20
  89. package/dist/git-cli/providers/status.js.map +1 -1
  90. package/dist/git-cli/providers/tags.js +6 -4
  91. package/dist/git-cli/providers/tags.js.map +1 -1
  92. package/dist/git-cli/providers/worktrees.d.ts +1 -0
  93. package/dist/git-cli/providers/worktrees.js +60 -31
  94. package/dist/git-cli/providers/worktrees.js.map +1 -1
  95. package/dist/git-cli/service.d.ts +6 -0
  96. package/dist/git-cli/service.js +5 -2
  97. package/dist/git-cli/service.js.map +1 -1
  98. package/dist/ipc/discovery.js +1 -1
  99. package/dist/plus/agents/providers/claudeCodeTranscript.js +6 -6
  100. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  101. package/dist/plus/ai/providers/openAICompatibleProviderBase.js +1 -1
  102. package/dist/plus/git-github/providers/github/status.d.ts +3 -1
  103. package/dist/plus/git-github/providers/github/status.js +1 -1
  104. package/dist/plus/git-github/providers/github/status.js.map +1 -1
  105. package/dist/plus/integrations/collectionMetadata.d.ts +13 -2
  106. package/dist/plus/integrations/collectionMetadata.js +14 -2
  107. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  108. package/dist/plus/integrations/index.d.ts +2 -2
  109. package/dist/plus/integrations/index.js.map +1 -1
  110. package/dist/plus/integrations/integrationService.d.ts +1 -0
  111. package/dist/plus/integrations/integrationService.js +1 -1
  112. package/dist/plus/integrations/integrationService.js.map +1 -1
  113. package/dist/plus/integrations/manager.d.ts +1 -0
  114. package/dist/plus/integrations/models/gitHostIntegration.d.ts +41 -5
  115. package/dist/plus/integrations/models/gitHostIntegration.js +106 -29
  116. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  117. package/dist/plus/integrations/models/issueReads.d.ts +17 -4
  118. package/dist/plus/integrations/providers/apiClients.d.ts +2 -0
  119. package/dist/plus/integrations/providers/apiClients.js +7 -1
  120. package/dist/plus/integrations/providers/apiClients.js.map +1 -1
  121. package/dist/plus/integrations/providers/azure/azure.d.ts +5 -4
  122. package/dist/plus/integrations/providers/azure/azure.js +37 -77
  123. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  124. package/dist/plus/integrations/providers/azure/azure.utils.js +20 -17
  125. package/dist/plus/integrations/providers/azure/azure.utils.js.map +1 -1
  126. package/dist/plus/integrations/providers/azure/models.d.ts +2 -2
  127. package/dist/plus/integrations/providers/azure/models.js +4 -4
  128. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  129. package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +18 -14
  130. package/dist/plus/integrations/providers/azure/workItemStateCategories.js +69 -17
  131. package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -1
  132. package/dist/plus/integrations/providers/azureDevOps.d.ts +44 -0
  133. package/dist/plus/integrations/providers/azureDevOps.js +108 -17
  134. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  135. package/dist/plus/integrations/providers/azureDevOpsServer.d.ts +45 -1
  136. package/dist/plus/integrations/providers/azureDevOpsServer.js +121 -6
  137. package/dist/plus/integrations/providers/azureDevOpsServer.js.map +1 -1
  138. package/dist/plus/integrations/providers/jira-server.js +18 -14
  139. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  140. package/dist/plus/integrations/providers/jira.js +18 -14
  141. package/dist/plus/integrations/providers/jira.js.map +1 -1
  142. package/dist/plus/integrations/providers/models.d.ts +1 -2
  143. package/dist/plus/integrations/providers/models.js +9 -2
  144. package/dist/plus/integrations/providers/models.js.map +1 -1
  145. package/dist/plus/integrations/providers/providersApi.d.ts +7 -17
  146. package/dist/plus/integrations/providers/providersApi.js +85 -78
  147. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  148. package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +22 -0
  149. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +69 -9
  150. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -1
  151. package/dist/plus/integrations/providers/utils/providerPaging.js +8 -2
  152. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  153. package/dist/plus/integrations/reads/filters.d.ts +3 -3
  154. package/dist/plus/integrations/reads/filters.js.map +1 -1
  155. package/dist/plus/integrations/reads/issueBatch.d.ts +2 -1
  156. package/dist/plus/integrations/reads/issueBatch.js +1 -1
  157. package/dist/plus/integrations/reads/issueBatch.js.map +1 -1
  158. package/dist/plus/integrations/reads/issueTracker.js +23 -14
  159. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  160. package/dist/plus/integrations/reads/issues.d.ts +2 -1
  161. package/dist/plus/integrations/reads/issues.js +18 -3
  162. package/dist/plus/integrations/reads/issues.js.map +1 -1
  163. package/dist/plus/integrations/reads/pullRequestBatch.js +1 -1
  164. package/dist/plus/integrations/reads/resolveRepository.js +12 -3
  165. package/dist/plus/integrations/reads/resolveRepository.js.map +1 -1
  166. package/dist/plus/integrations/reads/warnings.d.ts +11 -4
  167. package/dist/plus/integrations/reads/warnings.js +15 -5
  168. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  169. package/dist/plus/integrations/results.d.ts +36 -2
  170. package/dist/plus/integrations/results.js +33 -13
  171. package/dist/plus/integrations/results.js.map +1 -1
  172. package/dist/utils/env/node/exec.js +2 -2
  173. package/dist/utils/promiseCache.d.ts +10 -0
  174. package/dist/utils/promiseCache.js +17 -2
  175. package/dist/utils/promiseCache.js.map +1 -1
  176. package/docs/integrations.md +52 -20
  177. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"filters.js","names":[],"sources":["../../../../../plus/integrations/src/reads/filters.ts"],"sourcesContent":["import type { IntegrationIds } from '../constants.js';\nimport type {\n\tIssueFilter,\n\tIssueSearchCapabilities,\n\tIssueSearchCriteria,\n\tIssueSearchRelationship,\n\tIssueSorting,\n\tPullRequestFilter,\n\tPullRequestSearchCapabilities,\n\tPullRequestSearchCriteria,\n} from '../providerFilters.js';\nimport type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';\nimport { providersMetadata } from '../providers/models.js';\n\n/**\n * Validation of a caller-supplied relationship filter set against what a provider can express server-side.\n *\n * Every one of these is ALL-OR-NOTHING, and deliberately so: dropping the members a provider can't express\n * would silently widen the read past what was asked for (requesting Author+Mention on a host without Mention\n * would return every authored item rather than the requested set), and narrowing the returned page afterward\n * would leave `items` describing a different result set than the `hasMore`/`cursor` the provider produced with\n * it. So an inexpressible set is refused whole, and the caller surfaces a warning instead of reading unfiltered.\n *\n * The three differ only in WHICH capability they check, because they govern three different provider queries —\n * see `IntegrationManager.getSupportedFilters`, which exposes the same capability table so a consumer can\n * intersect against it and never reach a refusal.\n */\nexport interface ResolvedFilters<T> {\n\tfilters?: T[];\n\tunsupported: boolean;\n}\n\n/**\n * Validates a repo-scoped PR filter set, so an unsupported filter never trips the read core's \"Unsupported\n * filters\" guard. `unsupported: true` when the caller DID request filters and the provider can't express even\n * ONE of them — the exact negation of the read core's `providerSupportsPullRequestFilters` (`every`), so this\n * can only ever pre-empt that guard, never disagree with it.\n *\n * Returns `{ filters }` (possibly undefined when none were requested — an unfiltered read is intended). Genuine\n * \"my pull requests\" self-scoping is delivered by the account-wide path\n * (`GitHostIntegration.getMyPullRequestsForUserResult`); this only governs the optional repo-scoped narrowing.\n */\nexport function resolvePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedPullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/**\n * Validates an account-wide PR relationship union independently from the repo-scoped capability. Dropping an\n * unsupported member would change the requested OR set, so validation is all-or-nothing.\n */\nexport function resolveAccountWidePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWidePullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: [...new Set(filters)], unsupported: false };\n}\n\n/** Why {@link resolvePullRequestSearchCriteria} refused a criteria set. */\nexport type PullRequestSearchCriteriaRejection =\n\t/** The provider has no filtered pull-request search. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but cannot express every requested criterion server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] };\n\n/**\n * Validates filtered pull-request search criteria all-or-nothing. Dropping an unsupported criterion would\n * widen the provider query while leaving its page and cursor describing the wider set, so the facade refuses the\n * request before any upstream call instead.\n */\nexport function resolvePullRequestSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tif (supported == null || supported.relationships.length === 0) {\n\t\treturn { rejection: { reason: 'unsupported-search' } };\n\t}\n\tif (criteria == null) return {};\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of criteria.relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\tfor (const state of criteria.states ?? []) {\n\t\tif (!supported.states.includes(state)) {\n\t\t\tunsupported.push(`states:${state}`);\n\t\t}\n\t}\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.includeArchived === true && !supported.includeArchived) {\n\t\tunsupported.push('includeArchived');\n\t}\n\t// `!= null`, not truthy: `draft: false` (only ready-for-review) is as much a request as `draft: true`.\n\tif (criteria.draft != null && !supported.draft) {\n\t\tunsupported.push('draft');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than its own reason, so a caller asking for one\n\t// inexpressible filter and one inexpressible sort learns about both at once. `updated:desc` is always in\n\t// `sorts` for a usable search, so the omitted (default) case never rejects.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\treturn unsupported.length > 0 ? { rejection: { reason: 'unsupported-criteria', criteria: unsupported } } : {};\n}\n\n/** Why a filtered pull-request search's repository/organization boundary was refused. */\nexport type PullRequestSearchScopeRejection =\n\t| { reason: 'unscoped' }\n\t| { reason: 'repo-ids' }\n\t| { reason: 'unsupported-repository-scope' }\n\t| { reason: 'unsupported-organization-scope' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Validates the search boundary independently from its criteria. A current-user relationship is itself a safe\n * account-wide boundary; without one, a repository or organization scope is mandatory.\n *\n * Scope names go through the same {@link unusableSearchScopeNames} rule the issue search uses — shared rather\n * than re-derived, because the defect is identical on both reads.\n */\nexport function resolvePullRequestSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\t\tif (supported?.repositoryScope !== true) return { rejection: { reason: 'unsupported-repository-scope' } };\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\tconst hasOrganizationScope = org != null && org.length > 0;\n\tif (hasOrganizationScope && supported?.organizationScope !== true) {\n\t\treturn { rejection: { reason: 'unsupported-organization-scope' } };\n\t}\n\n\t// After the capability checks and before the \"is it scoped at all\" rule: a scope the provider cannot express\n\t// at all is the more fundamental refusal, and an unusable value that WAS supplied must not be reported as a\n\t// missing one. `org` is passed bare, matching the issue twin — the helper applies its own empty-means-\n\t// unsupplied guard, so re-testing `hasOrganizationScope` here would only make the two resolvers look like\n\t// they differ.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null || hasOrganizationScope || (criteria?.relationships?.length ?? 0) > 0) {\n\t\treturn { repos: resolvedRepos };\n\t}\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * Validates a caller-provided issue filter set against what the provider's ACCOUNT-WIDE issue read can express\n * server-side (`ProviderMetadata.supportedAccountWideIssueFilters`), which is a different — usually narrower —\n * set than the repo-scoped `ProviderMetadata.supportedIssueFilters`.\n *\n * Dropping the unexpressible members would silently widen the read back toward the provider's own union\n * (authored ∪ assigned ∪ mentioned for GitHub), which is the opposite of what a caller narrowing to\n * `[Assignee]` asked for.\n */\nexport function resolveAccountWideIssueFilters(\n\tid: IntegrationIds,\n\tfilters: IssueFilter[] | undefined,\n): ResolvedFilters<IssueFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWideIssueFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/** Why {@link resolveIssueSearchCriteria} refused a criteria set, so the caller can word the refusal exactly. */\nexport type IssueSearchCriteriaRejection =\n\t/** The provider has no filtered issue search at all. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but can't express these criteria server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] }\n\t/** `any-assignee` and `unassigned` partition the scope between them; asking for both asks for nothing. */\n\t| { reason: 'contradictory-relationships' };\n\n/**\n * Whether a provider exposes the FILTERED issue search at all — the one predicate a caller can check BEFORE\n * building criteria, so a read that would only be refused is never issued.\n *\n * The same test {@link resolveIssueSearchCriteria} makes for its `unsupported-search` rejection, named rather\n * than re-derived at each call site: a caller writing `supportedIssueSearch != null` itself is a copy free to\n * disagree with the validator about what \"has a search\" means. `broadenIssues` reads it to pick its engine —\n * the org-scoped search where there is one, the repository drain where there isn't — which is a CHOICE rather\n * than a refusal, so it needs the predicate without the rejection.\n */\nexport function supportsFilteredIssueSearch(id: IntegrationIds): boolean {\n\treturn providersMetadata[id]?.supportedIssueSearch != null;\n}\n\n/**\n * Validates a filtered issue search's criteria against {@link ProviderMetadata.supportedIssueSearch}.\n *\n * All-or-nothing like its three siblings above, and for the same reason: a criterion dropped because the\n * provider can't express it would serve a WIDER result than was asked for, and narrowing the returned page\n * afterward would leave `items` describing a different result set than the `hasMore`/`cursor` the provider\n * produced with it. So an inexpressible set is refused whole and the caller surfaces a warning.\n *\n * Unlike the sibling validators this also rejects a set that is internally contradictory, because this criteria\n * model is the only one with two members that partition the scope between them (`any-assignee` ∪ `unassigned` =\n * everything, ∩ = nothing). Silently keeping one would answer a question the caller didn't ask.\n *\n * `undefined` (or an empty relationship list plus no other criterion) is a valid UNNARROWED search of the given\n * scope, not an error — the caller's `repos`/`org` is what bounds it.\n */\nexport function resolveIssueSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedIssueSearch;\n\tif (supported == null) return { rejection: { reason: 'unsupported-search' } };\n\tif (criteria == null) return {};\n\n\tconst relationships = criteria.relationships;\n\tif (relationships?.includes('any-assignee') && relationships.includes('unassigned')) {\n\t\treturn { rejection: { reason: 'contradictory-relationships' } };\n\t}\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\t// Only a criterion the caller actually SET can be unsupported. `false`/empty means \"don't narrow on this\",\n\t// which every provider can honor by doing nothing, so it must not be validated as a request.\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.labels?.length && !supported.labels) {\n\t\tunsupported.push('labels');\n\t}\n\tif (criteria.milestone != null && !supported.milestone) {\n\t\tunsupported.push('milestone');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.withoutLinkedPullRequest === true && !supported.withoutLinkedPullRequest) {\n\t\tunsupported.push('withoutLinkedPullRequest');\n\t}\n\t// `'open'` is every provider's own default, so asking for it needs no state capability.\n\tif (criteria.state != null && criteria.state !== 'open' && !supported.states) {\n\t\tunsupported.push('state');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than given its own reason, so a caller asking\n\t// for one inexpressible filter and one inexpressible sort learns about both at once instead of fixing them one\n\t// refusal at a time. The `sorts` list is `supported`'s own, so this needs no branch on which surface it is.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\tif (unsupported.length > 0) return { rejection: { reason: 'unsupported-criteria', criteria: unsupported } };\n\n\treturn {};\n}\n\n/**\n * The relationships that bound a filtered issue search to the CURRENT USER, and so can stand in for a\n * repository/org scope.\n *\n * Derived from the union rather than spelled out per call site, because the distinction it encodes is the whole\n * reason the scope rule exists: `any-assignee` and `unassigned` are deliberately absent — they describe the\n * ISSUE, not the caller, so neither reduces the search to anyone's own world (measured: unscoped `no:assignee`\n * matches ~45 million issues on GitHub).\n */\nconst userScopingIssueSearchRelationships: readonly IssueSearchRelationship[] = ['authored', 'assigned', 'mentioned'];\n\n/** What a provider with NO filtered issue search reports: nothing expressible, spelled out rather than absent. */\nconst unsupportedIssueSearchCapabilities: IssueSearchCapabilities = {\n\trelationships: [],\n\ttext: false,\n\tlabels: false,\n\tmilestone: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\twithoutLinkedPullRequest: false,\n\tstates: false,\n\tsorts: [],\n};\n\n/** What a provider with no filtered pull-request search reports. */\nconst unsupportedPullRequestSearchCapabilities: PullRequestSearchCapabilities = {\n\trelationships: [],\n\tstates: [],\n\ttext: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\tincludeArchived: false,\n\tdraft: false,\n\trepositoryScope: false,\n\torganizationScope: false,\n\tsorts: [],\n};\n\n/** Why a filtered issue search's scope was refused, or `undefined` when it is usable. */\nexport type IssueSearchScopeRejection =\n\t/** No repositories, no org, and no user-relative relationship: a search of the whole host. */\n\t| { reason: 'unscoped' }\n\t/** Repositories given as ids. A search names repositories by PATH, so ids can't express a scope. */\n\t| { reason: 'repo-ids' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Whether a scope name reaches the provider NAMING THE SAME SCOPE — the one rule behind every `unusable-scope`\n * refusal, and the canonical home for its reasoning: the call sites point here rather than restating it.\n *\n * The defect it exists to prevent: the boundary is checked against the value AS SUPPLIED while the request is\n * built from the value AFTER the provider sanitizes it, so a name a query cannot spell produces a read that is no\n * longer the read that was authorized. Three outcomes, all of which LOOK LIKE SUCCESS, so there is nothing for a\n * consumer to branch on:\n * - **emptied** — a value of only quotes/whitespace/control characters emits no scope qualifier at all. A scope is\n * also what makes a relationship-less read legal, so the request carries neither and every item on the host\n * matches: measured at 52 million issues across unrelated accounts.\n * - **altered** — a quote inside a real name sanitizes to a real but DIFFERENT scope (`git\"kraken` -> `gitkraken`),\n * whose answer looks entirely normal.\n * - **split** — whitespace delimits qualifiers, so `my org` emits `org:my org`: a search of `my` additionally\n * filtered by the free text `org`. Measured against the live API this is a wrong NARROWING rather than a\n * widening — `org:gitkraken bar` returns 12 where `org:gitkraken` returns 379.\n *\n * Refusing beats sanitizing, and the count probes are why it matters most: they deliberately apply exactly the\n * qualifiers their search would, so a sanitized scope makes the count AGREE with the wrong search rather than\n * disagree with it, and a consumer cross-checking \"N matched\" cannot detect it by construction. Nor can it\n * pre-empt the rule — the free-text sanitizing rules are published precisely so a caller can mirror them, the\n * scope rules are not — so only the caller knows which scope it meant, and the refusal goes back to it NAMING the\n * offending value.\n *\n * Provider-NEUTRAL by design, and deliberately not an import of GitHub's `sanitizeGitHubQualifierValue`: this\n * module validates for every provider, and a GitHub-specific rule reaching in here would be wrong for the next\n * one that declares a search. The character class below is stated as the common part of any query language,\n * because each class breaks a query on its own terms — a quote closes its own qualifier, a control character\n * cannot appear at all, whitespace delimits the next qualifier.\n *\n * A provider that never puts a scope name into a query string declares\n * {@link ProviderMetadata.exactSearchScopeNames}, and only blank names and control characters are refused for it:\n * Azure DevOps Server addresses a collection or repository as an encoded URL segment and matches a project against\n * the names discovery reported, so a space or a quote reaches it naming the same scope — and its names legitimately\n * contain both.\n *\n * EDGES ARE STRIPPED before the test, which is what keeps the predicate no stricter than the provider's own\n * sanitizing — the invariant that makes refusing safe to add, since it means this can only reject a name the\n * provider would have altered, never one it would have resolved correctly. Leading and trailing whitespace AND\n * control characters both qualify: a sanitizer maps a control character to a space, then collapses and trims, so\n * `'gitkraken\\n'` and `'gitkraken\\u0000'` alike emit `org:gitkraken` — the scope that was asked for. Note this\n * is wider than `String.trim()`, which leaves control characters in place.\n *\n * None of the three outcomes above survives the stripping, so the rule loses nothing: an all-edge value still\n * empties, an INNER space or control character still splits (`'git\\u0000kraken'` emits `git kraken`, two\n * tokens), and a quote still alters wherever it sits.\n */\nfunction isUsableSearchScopeName(id: IntegrationIds, name: string): boolean {\n\tconst stripped = stripSearchScopeEdges(name);\n\tif (providersMetadata[id]?.exactSearchScopeNames === true) {\n\t\t// eslint-disable-next-line no-control-regex\n\t\treturn stripped.length > 0 && !/[\\u0000-\\u001f\\u007f]/.test(stripped);\n\t}\n\n\t// eslint-disable-next-line no-control-regex\n\treturn stripped.length > 0 && !/[\"\\u0000-\\u001f\\u007f\\s]/.test(stripped);\n}\n\n/** The leading/trailing run a provider's sanitizing removes — see {@link isUsableSearchScopeName}. */\nfunction stripSearchScopeEdges(value: string): string {\n\t// eslint-disable-next-line no-control-regex\n\treturn value.replace(/^[\\s\\u0000-\\u001f\\u007f]+|[\\s\\u0000-\\u001f\\u007f]+$/g, '');\n}\n\n/**\n * The scope names a search cannot carry as given, as the strings to name in the refusal — empty when every one is\n * usable. See {@link isUsableSearchScopeName} for the rule and why it refuses rather than sanitizes.\n */\nfunction unusableSearchScopeNames(\n\tid: IntegrationIds,\n\torg: string | undefined,\n\trepos: readonly ProviderRepoInput[] | undefined,\n): string[] {\n\tconst unusable: string[] = [];\n\n\t// An EMPTY org is \"no org supplied\" and falls through to the remaining scopes; any other unusable value WAS\n\t// supplied, so it is refused rather than dropped.\n\tif (org != null && org.length > 0 && !isUsableSearchScopeName(id, org)) {\n\t\tunusable.push(org);\n\t}\n\n\t// A `repo:` qualifier names a repository by its JOINED `namespace/name` path, which is the value the rule\n\t// below is applied to. Both halves are read defensively, through the SAME locals the label is built from: the\n\t// descriptor form is only narrowed from a union by an element-type check, so a half-built descriptor reaches\n\t// here as `undefined` and must refuse rather than throw out of a facade that reports refusals as warnings —\n\t// and reporting it as `undefined/a` would name a value the caller never passed.\n\tfor (const repo of repos ?? []) {\n\t\tconst namespace = repo.namespace ?? '';\n\t\tconst name = repo.name ?? '';\n\t\tconst path = `${namespace}/${name}`;\n\t\t// BOTH the composite and each half, because neither sees what the other does:\n\t\t// - the composite catches an offender the halves cannot, since an edge of a half is an INTERIOR character\n\t\t// of the path — `{ 'git ', 'kraken' }` has two usable-looking halves and emits `repo:git /kraken`.\n\t\t// - the halves catch a BLANK one the composite cannot, since that offender sits at a composite EDGE where\n\t\t// stripping removes it — `' /a'` strips to `'/a'`, a perfectly spellable qualifier naming no repository.\n\t\t// Each half is measured after the same stripping, so `' '` and `''` are one case rather than two.\n\t\tif (\n\t\t\t!isUsableSearchScopeName(id, path) ||\n\t\t\tstripSearchScopeEdges(namespace).length === 0 ||\n\t\t\tstripSearchScopeEdges(name).length === 0\n\t\t) {\n\t\t\tunusable.push(path);\n\t\t}\n\t}\n\n\treturn unusable;\n}\n\n/**\n * Validates that a filtered issue search is scoped at all, and narrows `repos` to the descriptor form the\n * provider query needs.\n *\n * Shared by `searchIssuesPage` and `countIssues` because a count computed under different constraints than the\n * read it previews is a WRONG number rather than a missing one. The rejection is returned as a reason code, not a\n * warning: the two callers word it differently (whole-read vs naming the offending scope's key), and wording is\n * the warning layer's business.\n *\n * `repo-ids` and `unscoped` are mutually exclusive — one requires repositories and the other requires none — so\n * their relative order is free. `unusable-scope` is NOT: it must precede `unscoped`, so a value that was supplied\n * but cannot be used is never reported as a missing one.\n *\n * Every scope name is checked for naming the same scope after the provider sanitizes it, not merely for being\n * non-empty, and that distinction is a SECURITY one rather than a nicety — see {@link isUsableSearchScopeName}.\n */\nexport function resolveIssueSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\t// `ProviderReposInput` is a union of descriptor and id arrays; only the descriptor form is usable here.\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\t// Checked BEFORE the \"is it scoped at all\" rule below, so an unusable value is never reported as a missing\n\t// one: it was supplied, and telling the caller to pass a scope it already passed names the wrong defect.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null) return { repos: resolvedRepos };\n\tif (org != null && org.length > 0) return {};\n\tif (criteria?.relationships?.some(r => userScopingIssueSearchRelationships.includes(r)) === true) return {};\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * What {@link getSupportedFilters} reports: every vocabulary a provider's reads can express, so a consumer can\n * narrow to it BEFORE issuing a read. See that function for the capability table itself and why intersecting\n * against this is what keeps a filtered read from being refused.\n *\n * Named rather than written inline, because the same shape is the declared return type of THREE declarations that\n * all describe one value — this function, `IntegrationService.getSupportedFilters`, which only forwards to it, and\n * `IntegrationManager.getSupportedFilters`, which publishes it. Spelled out three times, a member added here\n * reached one of them and left the others a compile error away from the truth; that is exactly how the two sort\n * members below came to be written out three times.\n */\nexport type SupportedFilters = {\n\tpullRequests: PullRequestFilter[];\n\tpullRequestsAccountWide: PullRequestFilter[];\n\t/**\n\t * Criteria and scopes the filtered pull-request search can express. Always present; an empty relationship list\n\t * means the provider exposes no filtered pull-request search.\n\t */\n\tpullRequestSearch: PullRequestSearchCapabilities;\n\t/**\n\t * Filters the repo-scoped issue read accepts — and, for an issue tracker (Jira/Linear/Trello), its\n\t * project-scoped read, that being its only issue surface. A tracker therefore reports here and leaves\n\t * `issuesAccountWide` empty, so intersecting a tracker against that field would read \"cannot filter\" for a\n\t * provider that filters fine.\n\t */\n\tissues: IssueFilter[];\n\tissuesAccountWide: IssueFilter[];\n\t/**\n\t * What the filtered issue search (and the count probe over the same criteria) can express. Always present: a\n\t * provider with no filtered issue search reports an empty `relationships` and all-false flags, which is the\n\t * signal to hide that surface rather than to hide individual chips.\n\t */\n\tissueSearch: IssueSearchCapabilities;\n\t/**\n\t * Sort keys the repo-scoped issue read can express — and, for an issue tracker, its project-scoped read. Empty\n\t * means the read can't be ordered, so pass no `sort`.\n\t *\n\t * A key here is expressible on ONE provider query. A page spanning several repositories or projects is a merge,\n\t * and a merge can only order by what a normalized issue carries, so `priority`/`dueDate`/`resolved` are refused\n\t * there even where they are listed. Read one scope at a time to use them.\n\t */\n\tissueSorts: IssueSorting[];\n\t/**\n\t * Sort keys the account-wide issue read can express — a different vocabulary, not a subset: for GitLab the two\n\t * reads are different APIs. Needs no mergeability caveat, because every account-wide read is a union of several\n\t * queries, so only keys a merge can honor are listed at all. Empty for an issue tracker, which reports under\n\t * `issueSorts`.\n\t */\n\tissueSortsAccountWide: IssueSorting[];\n\t/**\n\t * Whether an issue tracker's reads can be asked for a `state` other than open (`closed`, `all`). False means\n\t * only open issues are readable, and the read refuses the other two rather than serving the open list.\n\t *\n\t * Tracker-only, like `issues`: the git hosts' state support belongs to the filtered search, under\n\t * `issueSearch.states`.\n\t */\n\tissueStates: boolean;\n};\n\n/**\n * The filters `listPullRequestsPage`/`listIssuesPage` (and the sweeps) accept for a provider, so a caller can\n * narrow to what the provider can express BEFORE issuing the read.\n *\n * This matters because the filter contract is all-or-nothing: a set containing even one unsupported filter is\n * refused outright ({@link resolvePullRequestFilters}) rather than silently narrowed, since falling through to\n * an unfiltered fetch would return every PR instead of the user's. Without this accessor a consumer had to\n * hardcode its own copy of the table — reachable only by importing the internal `providers/models.js`\n * subpath — and a copy that drifts turns a supported read into an empty page with `fetchFailed`.\n *\n * Empty means \"no filter of that kind is expressible\": either the provider has no such surface (issue trackers\n * have no pull requests; Bitbucket exposes no issues) or its metadata declares none. Callers should treat it as\n * \"don't pass filters\", not as an error. Returns copies, so mutating the result can't corrupt the metadata.\n *\n * `issues` and `issuesAccountWide` are separate because the repo-scoped and account-wide issue reads are\n * different provider queries with different filter surfaces, and the same `filters` input is validated against\n * whichever one the read uses (`repos` present or not). `issuesAccountWide` is generally the narrower of the\n * two: GitLab, for instance, can express `Assignee` and `Author` account-wide, but not `Mention`.\n *\n * That split describes the GIT-HOST reads only. An issue tracker (Jira/Linear/Trello) has neither — its issues\n * live under resource → project — so it reports its filters under `issues`, which is what\n * {@link IntegrationService.listIssueTrackerIssuesPage} validates against, and leaves `issuesAccountWide`\n * empty. Reading a tracker's capability off `issuesAccountWide` therefore under-reports it.\n *\n * `issueSearch` is a third, wider surface: the FILTERED issue search (`searchIssuesPage`, and the `countIssues`\n * probe over the same criteria), which is not bound to the user at all. It is a record of per-criterion flags\n * rather than a list, because its criteria aren't a single kind. An empty `relationships` means the provider has\n * no filtered issue search — hide the surface, not just its chips.\n *\n * `pullRequestSearch` describes the separate filtered PR search. Its table declares each criteria vocabulary\n * plus repository/organization scope support; an empty relationship list means the search itself is absent.\n *\n * `issueSorts` / `issueSortsAccountWide` / `issueSearch.sorts` are the ORDERING vocabulary of those same three\n * issue reads, split the same way and for the same reason (GitLab's repo-scoped read is GraphQL and its\n * account-wide read is REST, with genuinely different sort vocabularies). Empty means the read can't be ordered,\n * so pass no `sort`; a key not listed refuses the whole read exactly like an inexpressible filter. Note a key\n * listed here is expressible on ONE provider query: a read that fans out across projects can only honor a key\n * derivable from a normalized issue, so it refuses `priority`/`dueDate`/`resolved` on top of this table.\n *\n * Note this is a CAPABILITY table — \"what the provider can express\" — not a recommendation. A consumer\n * matching another tool's behavior may deliberately pass fewer filters than are listed here (or none, where an\n * already-scoped read would only be narrowed by them). Intersecting against this table is what keeps a\n * filtered read from being refused; it isn't a directive to use every filter in it.\n */\nexport function getSupportedFilters(providerId: IntegrationIds): SupportedFilters {\n\tconst metadata = providersMetadata[providerId];\n\tconst issueSearch = metadata?.supportedIssueSearch;\n\tconst pullRequestSearch = metadata?.supportedPullRequestSearch;\n\treturn {\n\t\tpullRequests: [...(metadata?.supportedPullRequestFilters ?? [])],\n\t\tpullRequestsAccountWide: [...(metadata?.supportedAccountWidePullRequestFilters ?? [])],\n\t\tpullRequestSearch: {\n\t\t\t...unsupportedPullRequestSearchCapabilities,\n\t\t\t...pullRequestSearch,\n\t\t\trelationships: [...(pullRequestSearch?.relationships ?? [])],\n\t\t\tstates: [...(pullRequestSearch?.states ?? [])],\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\tsorts: [...(pullRequestSearch?.sorts ?? [])],\n\t\t},\n\t\tissues: [...(metadata?.supportedIssueFilters ?? [])],\n\t\tissuesAccountWide: [...(metadata?.supportedAccountWideIssueFilters ?? [])],\n\t\t// Always an object, never `undefined`: a provider WITHOUT a filtered issue search reports one whose\n\t\t// `relationships` is empty and whose flags are all false, so a consumer reads capabilities the same way\n\t\t// for every provider (and an empty `relationships` is the signal to hide the surface itself).\n\t\t//\n\t\t// Spread over the all-false baseline rather than defaulting each flag: a criterion added to\n\t\t// `IssueSearchCapabilities` then can't be forgotten here and silently reported as `undefined` (which a\n\t\t// consumer's `if (caps.x)` would read as unsupported — right answer, wrong reason, and untyped).\n\t\t// `supportedIssueSearch` is typed as the complete shape, so no member can arrive as an explicit undefined.\n\t\tissueSearch: {\n\t\t\t...unsupportedIssueSearchCapabilities,\n\t\t\t...issueSearch,\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\trelationships: [...(issueSearch?.relationships ?? [])],\n\t\t\tsorts: [...(issueSearch?.sorts ?? [])],\n\t\t},\n\t\tissueSorts: [...(metadata?.supportedIssueSorts ?? [])],\n\t\tissueSortsAccountWide: [...(metadata?.supportedAccountWideIssueSorts ?? [])],\n\t\tissueStates: metadata?.supportsIssueStates === true,\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"filters.js","names":[],"sources":["../../../../../plus/integrations/src/reads/filters.ts"],"sourcesContent":["import type { IntegrationIds } from '../constants.js';\nimport type {\n\tIssueFilter,\n\tIssueSearchCapabilities,\n\tIssueSearchCriteria,\n\tIssueSearchRelationship,\n\tIssueSorting,\n\tPullRequestFilter,\n\tPullRequestSearchCapabilities,\n\tPullRequestSearchCriteria,\n} from '../providerFilters.js';\nimport type { ProviderRepoInput, ProviderReposInput } from '../providers/models.js';\nimport { providersMetadata } from '../providers/models.js';\n\n/**\n * Validation of a caller-supplied relationship filter set against what a provider can express server-side.\n *\n * Every one of these is ALL-OR-NOTHING, and deliberately so: dropping the members a provider can't express\n * would silently widen the read past what was asked for (requesting Author+Mention on a host without Mention\n * would return every authored item rather than the requested set), and narrowing the returned page afterward\n * would leave `items` describing a different result set than the `hasMore`/`cursor` the provider produced with\n * it. So an inexpressible set is refused whole, and the caller surfaces a warning instead of reading unfiltered.\n *\n * The three differ only in WHICH capability they check, because they govern three different provider queries —\n * see `IntegrationManager.getSupportedFilters`, which exposes the same capability table so a consumer can\n * intersect against it and never reach a refusal.\n */\nexport interface ResolvedFilters<T> {\n\tfilters?: T[];\n\tunsupported: boolean;\n}\n\n/**\n * Validates a repo-scoped PR filter set, so an unsupported filter never trips the read core's \"Unsupported\n * filters\" guard. `unsupported: true` when the caller DID request filters and the provider can't express even\n * ONE of them — the exact negation of the read core's `providerSupportsPullRequestFilters` (`every`), so this\n * can only ever pre-empt that guard, never disagree with it.\n *\n * Returns `{ filters }` (possibly undefined when none were requested — an unfiltered read is intended). Genuine\n * \"my pull requests\" self-scoping is delivered by the account-wide path\n * (`GitHostIntegration.getMyPullRequestsForUserResult`); this only governs the optional repo-scoped narrowing.\n */\nexport function resolvePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedPullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/**\n * Validates an account-wide PR relationship union independently from the repo-scoped capability. Dropping an\n * unsupported member would change the requested OR set, so validation is all-or-nothing.\n */\nexport function resolveAccountWidePullRequestFilters(\n\tid: IntegrationIds,\n\tfilters: PullRequestFilter[] | undefined,\n): ResolvedFilters<PullRequestFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWidePullRequestFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: [...new Set(filters)], unsupported: false };\n}\n\n/** Why {@link resolvePullRequestSearchCriteria} refused a criteria set. */\nexport type PullRequestSearchCriteriaRejection =\n\t/** The provider has no filtered pull-request search. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but cannot express every requested criterion server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] };\n\n/**\n * Validates filtered pull-request search criteria all-or-nothing. Dropping an unsupported criterion would\n * widen the provider query while leaving its page and cursor describing the wider set, so the facade refuses the\n * request before any upstream call instead.\n */\nexport function resolvePullRequestSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tif (supported == null || supported.relationships.length === 0) {\n\t\treturn { rejection: { reason: 'unsupported-search' } };\n\t}\n\tif (criteria == null) return {};\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of criteria.relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\tfor (const state of criteria.states ?? []) {\n\t\tif (!supported.states.includes(state)) {\n\t\t\tunsupported.push(`states:${state}`);\n\t\t}\n\t}\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.includeArchived === true && !supported.includeArchived) {\n\t\tunsupported.push('includeArchived');\n\t}\n\t// `!= null`, not truthy: `draft: false` (only ready-for-review) is as much a request as `draft: true`.\n\tif (criteria.draft != null && !supported.draft) {\n\t\tunsupported.push('draft');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than its own reason, so a caller asking for one\n\t// inexpressible filter and one inexpressible sort learns about both at once. `updated:desc` is always in\n\t// `sorts` for a usable search, so the omitted (default) case never rejects.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\treturn unsupported.length > 0 ? { rejection: { reason: 'unsupported-criteria', criteria: unsupported } } : {};\n}\n\n/** Why a filtered pull-request search's repository/organization boundary was refused. */\nexport type PullRequestSearchScopeRejection =\n\t| { reason: 'unscoped' }\n\t| { reason: 'repo-ids' }\n\t| { reason: 'unsupported-repository-scope' }\n\t| { reason: 'unsupported-organization-scope' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Validates the search boundary independently from its criteria. A current-user relationship is itself a safe\n * account-wide boundary; without one, a repository or organization scope is mandatory.\n *\n * Scope names go through the same {@link unusableSearchScopeNames} rule the issue search uses — shared rather\n * than re-derived, because the defect is identical on both reads.\n */\nexport function resolvePullRequestSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: PullRequestSearchCriteria | undefined,\n): { rejection?: PullRequestSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tconst supported = providersMetadata[id]?.supportedPullRequestSearch;\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\t\tif (supported?.repositoryScope !== true) return { rejection: { reason: 'unsupported-repository-scope' } };\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\tconst hasOrganizationScope = org != null && org.length > 0;\n\tif (hasOrganizationScope && supported?.organizationScope !== true) {\n\t\treturn { rejection: { reason: 'unsupported-organization-scope' } };\n\t}\n\n\t// After the capability checks and before the \"is it scoped at all\" rule: a scope the provider cannot express\n\t// at all is the more fundamental refusal, and an unusable value that WAS supplied must not be reported as a\n\t// missing one. `org` is passed bare, matching the issue twin — the helper applies its own empty-means-\n\t// unsupplied guard, so re-testing `hasOrganizationScope` here would only make the two resolvers look like\n\t// they differ.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null || hasOrganizationScope || (criteria?.relationships?.length ?? 0) > 0) {\n\t\treturn { repos: resolvedRepos };\n\t}\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * Validates a caller-provided issue filter set against what the provider's ACCOUNT-WIDE issue read can express\n * server-side (`ProviderMetadata.supportedAccountWideIssueFilters`), which is a different — usually narrower —\n * set than the repo-scoped `ProviderMetadata.supportedIssueFilters`.\n *\n * Dropping the unexpressible members would silently widen the read back toward the provider's own union\n * (authored ∪ assigned ∪ mentioned for GitHub), which is the opposite of what a caller narrowing to\n * `[Assignee]` asked for.\n */\nexport function resolveAccountWideIssueFilters(\n\tid: IntegrationIds,\n\tfilters: IssueFilter[] | undefined,\n): ResolvedFilters<IssueFilter> {\n\tif (filters == null || filters.length === 0) return { unsupported: false };\n\n\tconst supported = providersMetadata[id]?.supportedAccountWideIssueFilters;\n\tif (supported == null || filters.some(f => !supported.includes(f))) return { unsupported: true };\n\n\treturn { filters: filters, unsupported: false };\n}\n\n/** Why {@link resolveIssueSearchCriteria} refused a criteria set, so the caller can word the refusal exactly. */\nexport type IssueSearchCriteriaRejection =\n\t/** The provider has no filtered issue search at all. */\n\t| { reason: 'unsupported-search' }\n\t/** The provider has one, but can't express these criteria server-side. */\n\t| { reason: 'unsupported-criteria'; criteria: string[] }\n\t/** `any-assignee` and `unassigned` partition the scope between them; asking for both asks for nothing. */\n\t| { reason: 'contradictory-relationships' };\n\n/**\n * Whether a provider exposes the FILTERED issue search at all — the one predicate a caller can check BEFORE\n * building criteria, so a read that would only be refused is never issued.\n *\n * The same test {@link resolveIssueSearchCriteria} makes for its `unsupported-search` rejection, named rather\n * than re-derived at each call site: a caller writing `supportedIssueSearch != null` itself is a copy free to\n * disagree with the validator about what \"has a search\" means. `broadenIssues` reads it to pick its engine —\n * the org-scoped search where there is one, the repository drain where there isn't — which is a CHOICE rather\n * than a refusal, so it needs the predicate without the rejection.\n */\nexport function supportsFilteredIssueSearch(id: IntegrationIds): boolean {\n\treturn providersMetadata[id]?.supportedIssueSearch != null;\n}\n\n/**\n * Validates a filtered issue search's criteria against {@link ProviderMetadata.supportedIssueSearch}.\n *\n * All-or-nothing like its three siblings above, and for the same reason: a criterion dropped because the\n * provider can't express it would serve a WIDER result than was asked for, and narrowing the returned page\n * afterward would leave `items` describing a different result set than the `hasMore`/`cursor` the provider\n * produced with it. So an inexpressible set is refused whole and the caller surfaces a warning.\n *\n * Unlike the sibling validators this also rejects a set that is internally contradictory, because this criteria\n * model is the only one with two members that partition the scope between them (`any-assignee` ∪ `unassigned` =\n * everything, ∩ = nothing). Silently keeping one would answer a question the caller didn't ask.\n *\n * `undefined` (or an empty relationship list plus no other criterion) is a valid UNNARROWED search of the given\n * scope, not an error — the caller's `repos`/`org` is what bounds it.\n */\nexport function resolveIssueSearchCriteria(\n\tid: IntegrationIds,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchCriteriaRejection } {\n\tconst supported = providersMetadata[id]?.supportedIssueSearch;\n\tif (supported == null) return { rejection: { reason: 'unsupported-search' } };\n\tif (criteria == null) return {};\n\n\tconst relationships = criteria.relationships;\n\tif (relationships?.includes('any-assignee') && relationships.includes('unassigned')) {\n\t\treturn { rejection: { reason: 'contradictory-relationships' } };\n\t}\n\n\tconst unsupported: string[] = [];\n\tfor (const relationship of relationships ?? []) {\n\t\tif (!supported.relationships.includes(relationship)) {\n\t\t\tunsupported.push(`relationships:${relationship}`);\n\t\t}\n\t}\n\t// Only a criterion the caller actually SET can be unsupported. `false`/empty means \"don't narrow on this\",\n\t// which every provider can honor by doing nothing, so it must not be validated as a request.\n\tif (criteria.text != null && criteria.text.trim().length > 0 && !supported.text) {\n\t\tunsupported.push('text');\n\t}\n\tif (criteria.labels?.length && !supported.labels) {\n\t\tunsupported.push('labels');\n\t}\n\tif (criteria.milestone != null && !supported.milestone) {\n\t\tunsupported.push('milestone');\n\t}\n\tif (criteria.updatedAfter != null && !supported.updatedAfter) {\n\t\tunsupported.push('updatedAfter');\n\t}\n\tif (criteria.createdAfter != null && !supported.createdAfter) {\n\t\tunsupported.push('createdAfter');\n\t}\n\tif (criteria.withoutLinkedPullRequest === true && !supported.withoutLinkedPullRequest) {\n\t\tunsupported.push('withoutLinkedPullRequest');\n\t}\n\t// `'open'` is every provider's own default, so asking for it needs no state capability.\n\tif (criteria.state != null && criteria.state !== 'open' && !supported.states) {\n\t\tunsupported.push('state');\n\t}\n\t// Folded into the same `unsupported-criteria` rejection rather than given its own reason, so a caller asking\n\t// for one inexpressible filter and one inexpressible sort learns about both at once instead of fixing them one\n\t// refusal at a time. The `sorts` list is `supported`'s own, so this needs no branch on which surface it is.\n\tif (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {\n\t\tunsupported.push(`sort:${criteria.sort}`);\n\t}\n\n\tif (unsupported.length > 0) return { rejection: { reason: 'unsupported-criteria', criteria: unsupported } };\n\n\treturn {};\n}\n\n/**\n * The relationships that bound a filtered issue search to the CURRENT USER, and so can stand in for a\n * repository/org scope.\n *\n * Derived from the union rather than spelled out per call site, because the distinction it encodes is the whole\n * reason the scope rule exists: `any-assignee` and `unassigned` are deliberately absent — they describe the\n * ISSUE, not the caller, so neither reduces the search to anyone's own world (measured: unscoped `no:assignee`\n * matches ~45 million issues on GitHub).\n */\nconst userScopingIssueSearchRelationships: readonly IssueSearchRelationship[] = ['authored', 'assigned', 'mentioned'];\n\n/** What a provider with NO filtered issue search reports: nothing expressible, spelled out rather than absent. */\nconst unsupportedIssueSearchCapabilities: IssueSearchCapabilities = {\n\trelationships: [],\n\ttext: false,\n\tlabels: false,\n\tmilestone: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\twithoutLinkedPullRequest: false,\n\tstates: false,\n\tsorts: [],\n};\n\n/** What a provider with no filtered pull-request search reports. */\nconst unsupportedPullRequestSearchCapabilities: PullRequestSearchCapabilities = {\n\trelationships: [],\n\tstates: [],\n\ttext: false,\n\tupdatedAfter: false,\n\tcreatedAfter: false,\n\tincludeArchived: false,\n\tdraft: false,\n\trepositoryScope: false,\n\torganizationScope: false,\n\tsorts: [],\n};\n\n/** Why a filtered issue search's scope was refused, or `undefined` when it is usable. */\nexport type IssueSearchScopeRejection =\n\t/** No repositories, no org, and no user-relative relationship: a search of the whole host. */\n\t| { reason: 'unscoped' }\n\t/** Repositories given as ids. A search names repositories by PATH, so ids can't express a scope. */\n\t| { reason: 'repo-ids' }\n\t/** Scope names a query cannot carry AS GIVEN; see {@link unusableSearchScopeNames}. */\n\t| { reason: 'unusable-scope'; scopes: string[] };\n\n/**\n * Whether a scope name reaches the provider NAMING THE SAME SCOPE — the one rule behind every `unusable-scope`\n * refusal, and the canonical home for its reasoning: the call sites point here rather than restating it.\n *\n * The defect it exists to prevent: the boundary is checked against the value AS SUPPLIED while the request is\n * built from the value AFTER the provider sanitizes it, so a name a query cannot spell produces a read that is no\n * longer the read that was authorized. Three outcomes, all of which LOOK LIKE SUCCESS, so there is nothing for a\n * consumer to branch on:\n * - **emptied** — a value of only quotes/whitespace/control characters emits no scope qualifier at all. A scope is\n * also what makes a relationship-less read legal, so the request carries neither and every item on the host\n * matches: measured at 52 million issues across unrelated accounts.\n * - **altered** — a quote inside a real name sanitizes to a real but DIFFERENT scope (`git\"kraken` -> `gitkraken`),\n * whose answer looks entirely normal.\n * - **split** — whitespace delimits qualifiers, so `my org` emits `org:my org`: a search of `my` additionally\n * filtered by the free text `org`. Measured against the live API this is a wrong NARROWING rather than a\n * widening — `org:gitkraken bar` returns 12 where `org:gitkraken` returns 379.\n *\n * Refusing beats sanitizing, and the count probes are why it matters most: they deliberately apply exactly the\n * qualifiers their search would, so a sanitized scope makes the count AGREE with the wrong search rather than\n * disagree with it, and a consumer cross-checking \"N matched\" cannot detect it by construction. Nor can it\n * pre-empt the rule — the free-text sanitizing rules are published precisely so a caller can mirror them, the\n * scope rules are not — so only the caller knows which scope it meant, and the refusal goes back to it NAMING the\n * offending value.\n *\n * Provider-NEUTRAL by design, and deliberately not an import of GitHub's `sanitizeGitHubQualifierValue`: this\n * module validates for every provider, and a GitHub-specific rule reaching in here would be wrong for the next\n * one that declares a search. The character class below is stated as the common part of any query language,\n * because each class breaks a query on its own terms — a quote closes its own qualifier, a control character\n * cannot appear at all, whitespace delimits the next qualifier.\n *\n * A provider that never puts a scope name into a query string declares\n * {@link ProviderMetadata.exactSearchScopeNames}, and only blank names and control characters are refused for it:\n * Azure DevOps Server addresses a collection or repository as an encoded URL segment and matches a project against\n * the names discovery reported, so a space or a quote reaches it naming the same scope — and its names legitimately\n * contain both.\n *\n * EDGES ARE STRIPPED before the test, which is what keeps the predicate no stricter than the provider's own\n * sanitizing — the invariant that makes refusing safe to add, since it means this can only reject a name the\n * provider would have altered, never one it would have resolved correctly. Leading and trailing whitespace AND\n * control characters both qualify: a sanitizer maps a control character to a space, then collapses and trims, so\n * `'gitkraken\\n'` and `'gitkraken\\u0000'` alike emit `org:gitkraken` — the scope that was asked for. Note this\n * is wider than `String.trim()`, which leaves control characters in place.\n *\n * None of the three outcomes above survives the stripping, so the rule loses nothing: an all-edge value still\n * empties, an INNER space or control character still splits (`'git\\u0000kraken'` emits `git kraken`, two\n * tokens), and a quote still alters wherever it sits.\n */\nfunction isUsableSearchScopeName(id: IntegrationIds, name: string): boolean {\n\tconst stripped = stripSearchScopeEdges(name);\n\tif (providersMetadata[id]?.exactSearchScopeNames === true) {\n\t\t// eslint-disable-next-line no-control-regex\n\t\treturn stripped.length > 0 && !/[\\u0000-\\u001f\\u007f]/.test(stripped);\n\t}\n\n\t// eslint-disable-next-line no-control-regex\n\treturn stripped.length > 0 && !/[\"\\u0000-\\u001f\\u007f\\s]/.test(stripped);\n}\n\n/** The leading/trailing run a provider's sanitizing removes — see {@link isUsableSearchScopeName}. */\nfunction stripSearchScopeEdges(value: string): string {\n\t// eslint-disable-next-line no-control-regex\n\treturn value.replace(/^[\\s\\u0000-\\u001f\\u007f]+|[\\s\\u0000-\\u001f\\u007f]+$/g, '');\n}\n\n/**\n * The scope names a search cannot carry as given, as the strings to name in the refusal — empty when every one is\n * usable. See {@link isUsableSearchScopeName} for the rule and why it refuses rather than sanitizes.\n */\nfunction unusableSearchScopeNames(\n\tid: IntegrationIds,\n\torg: string | undefined,\n\trepos: readonly ProviderRepoInput[] | undefined,\n): string[] {\n\tconst unusable: string[] = [];\n\n\t// An EMPTY org is \"no org supplied\" and falls through to the remaining scopes; any other unusable value WAS\n\t// supplied, so it is refused rather than dropped.\n\tif (org != null && org.length > 0 && !isUsableSearchScopeName(id, org)) {\n\t\tunusable.push(org);\n\t}\n\n\t// A `repo:` qualifier names a repository by its JOINED `namespace/name` path, which is the value the rule\n\t// below is applied to. Both halves are read defensively, through the SAME locals the label is built from: the\n\t// descriptor form is only narrowed from a union by an element-type check, so a half-built descriptor reaches\n\t// here as `undefined` and must refuse rather than throw out of a facade that reports refusals as warnings —\n\t// and reporting it as `undefined/a` would name a value the caller never passed.\n\tfor (const repo of repos ?? []) {\n\t\tconst namespace = repo.namespace ?? '';\n\t\tconst name = repo.name ?? '';\n\t\tconst path = `${namespace}/${name}`;\n\t\t// BOTH the composite and each half, because neither sees what the other does:\n\t\t// - the composite catches an offender the halves cannot, since an edge of a half is an INTERIOR character\n\t\t// of the path — `{ 'git ', 'kraken' }` has two usable-looking halves and emits `repo:git /kraken`.\n\t\t// - the halves catch a BLANK one the composite cannot, since that offender sits at a composite EDGE where\n\t\t// stripping removes it — `' /a'` strips to `'/a'`, a perfectly spellable qualifier naming no repository.\n\t\t// Each half is measured after the same stripping, so `' '` and `''` are one case rather than two.\n\t\tif (\n\t\t\t!isUsableSearchScopeName(id, path) ||\n\t\t\tstripSearchScopeEdges(namespace).length === 0 ||\n\t\t\tstripSearchScopeEdges(name).length === 0\n\t\t) {\n\t\t\tunusable.push(path);\n\t\t}\n\t}\n\n\treturn unusable;\n}\n\n/**\n * Validates that a filtered issue search is scoped at all, and narrows `repos` to the descriptor form the\n * provider query needs.\n *\n * Shared by `searchIssuesPage` and `countIssues` because a count computed under different constraints than the\n * read it previews is a WRONG number rather than a missing one. The rejection is returned as a reason code, not a\n * warning: the two callers word it differently (whole-read vs naming the offending scope's key), and wording is\n * the warning layer's business.\n *\n * `repo-ids` and `unscoped` are mutually exclusive — one requires repositories and the other requires none — so\n * their relative order is free. `unusable-scope` is NOT: it must precede `unscoped`, so a value that was supplied\n * but cannot be used is never reported as a missing one.\n *\n * Every scope name is checked for naming the same scope after the provider sanitizes it, not merely for being\n * non-empty, and that distinction is a SECURITY one rather than a nicety — see {@link isUsableSearchScopeName}.\n */\nexport function resolveIssueSearchScope(\n\tid: IntegrationIds,\n\trepos: ProviderReposInput | undefined,\n\torg: string | undefined,\n\tcriteria: IssueSearchCriteria | undefined,\n): { rejection?: IssueSearchScopeRejection; repos?: ProviderRepoInput[] } {\n\tlet resolvedRepos: ProviderRepoInput[] | undefined;\n\n\tif (repos?.length) {\n\t\t// `ProviderReposInput` is a union of descriptor and id arrays; only the descriptor form is usable here.\n\t\tif (repos.some(r => typeof r === 'string' || typeof r === 'number')) {\n\t\t\treturn { rejection: { reason: 'repo-ids' } };\n\t\t}\n\n\t\tresolvedRepos = repos as ProviderRepoInput[];\n\t}\n\n\t// Checked BEFORE the \"is it scoped at all\" rule below, so an unusable value is never reported as a missing\n\t// one: it was supplied, and telling the caller to pass a scope it already passed names the wrong defect.\n\tconst unusable = unusableSearchScopeNames(id, org, resolvedRepos);\n\tif (unusable.length > 0) return { rejection: { reason: 'unusable-scope', scopes: unusable } };\n\n\tif (resolvedRepos != null) return { repos: resolvedRepos };\n\tif (org != null && org.length > 0) return {};\n\tif (criteria?.relationships?.some(r => userScopingIssueSearchRelationships.includes(r)) === true) return {};\n\n\treturn { rejection: { reason: 'unscoped' } };\n}\n\n/**\n * What {@link getSupportedFilters} reports: every vocabulary a provider's reads can express, so a consumer can\n * narrow to it BEFORE issuing a read. See that function for the capability table itself and why intersecting\n * against this is what keeps a filtered read from being refused.\n *\n * Named rather than written inline, because the same shape is the declared return type of THREE declarations that\n * all describe one value — this function, `IntegrationService.getSupportedFilters`, which only forwards to it, and\n * `IntegrationManager.getSupportedFilters`, which publishes it. Spelled out three times, a member added here\n * reached one of them and left the others a compile error away from the truth; that is exactly how the two sort\n * members below came to be written out three times.\n */\nexport type SupportedFilters = {\n\tpullRequests: PullRequestFilter[];\n\tpullRequestsAccountWide: PullRequestFilter[];\n\t/**\n\t * Criteria and scopes the filtered pull-request search can express. Always present; an empty relationship list\n\t * means the provider exposes no filtered pull-request search.\n\t */\n\tpullRequestSearch: PullRequestSearchCapabilities;\n\t/**\n\t * Filters the repo-scoped issue read accepts — and, for an issue tracker (Jira/Linear/Trello), its\n\t * project-scoped read, that being its only issue surface. A tracker therefore reports here and leaves\n\t * `issuesAccountWide` empty, so intersecting a tracker against that field would read \"cannot filter\" for a\n\t * provider that filters fine.\n\t */\n\tissues: IssueFilter[];\n\tissuesAccountWide: IssueFilter[];\n\t/**\n\t * What the filtered issue search (and the count probe over the same criteria) can express. Always present: a\n\t * provider with no filtered issue search reports an empty `relationships` and all-false flags, which is the\n\t * signal to hide that surface rather than to hide individual chips.\n\t */\n\tissueSearch: IssueSearchCapabilities;\n\t/**\n\t * Sort keys the repo-scoped issue read can express — and, for an issue tracker, its project-scoped read. Empty\n\t * means the read can't be ordered, so pass no `sort`.\n\t *\n\t * A key here is expressible on ONE provider query. A page spanning several repositories or projects is a merge,\n\t * and a merge can only order by what a normalized issue carries, so `priority`/`dueDate`/`resolved` are refused\n\t * there even where they are listed. Read one scope at a time to use them.\n\t */\n\tissueSorts: IssueSorting[];\n\t/**\n\t * Sort keys the account-wide issue read can express — a different vocabulary, not a subset: for GitLab the two\n\t * reads are different APIs. Needs no mergeability caveat, because every account-wide read is a union of several\n\t * queries, so only keys a merge can honor are listed at all. Empty for an issue tracker, which reports under\n\t * `issueSorts`.\n\t */\n\tissueSortsAccountWide: IssueSorting[];\n\t/**\n\t * Whether issue list reads can be asked for a `state` other than open (`closed`, `all`). False means\n\t * only open issues are readable, and the read refuses the other two rather than serving the open list.\n\t *\n\t * Includes Azure DevOps Services and Server list reads. Filtered search reports its independent state\n\t * capability under `issueSearch.states`.\n\t */\n\tissueStates: boolean;\n};\n\n/**\n * The filters `listPullRequestsPage`/`listIssuesPage` (and the sweeps) accept for a provider, so a caller can\n * narrow to what the provider can express BEFORE issuing the read.\n *\n * This matters because the filter contract is all-or-nothing: a set containing even one unsupported filter is\n * refused outright ({@link resolvePullRequestFilters}) rather than silently narrowed, since falling through to\n * an unfiltered fetch would return every PR instead of the user's. Without this accessor a consumer had to\n * hardcode its own copy of the table — reachable only by importing the internal `providers/models.js`\n * subpath — and a copy that drifts turns a supported read into an empty page with `fetchFailed`.\n *\n * Empty means \"no filter of that kind is expressible\": either the provider has no such surface (issue trackers\n * have no pull requests; Bitbucket exposes no issues) or its metadata declares none. Callers should treat it as\n * \"don't pass filters\", not as an error. Returns copies, so mutating the result can't corrupt the metadata.\n *\n * `issues` and `issuesAccountWide` are separate because the repo-scoped and account-wide issue reads are\n * different provider queries with different filter surfaces, and the same `filters` input is validated against\n * whichever one the read uses (`repos` present or not). `issuesAccountWide` is generally the narrower of the\n * two: GitLab, for instance, can express `Assignee` and `Author` account-wide, but not `Mention`.\n *\n * That split describes the GIT-HOST reads only. An issue tracker (Jira/Linear/Trello) has neither — its issues\n * live under resource → project — so it reports its filters under `issues`, which is what\n * {@link IntegrationService.listIssueTrackerIssuesPage} validates against, and leaves `issuesAccountWide`\n * empty. Reading a tracker's capability off `issuesAccountWide` therefore under-reports it.\n *\n * `issueSearch` is a third, wider surface: the FILTERED issue search (`searchIssuesPage`, and the `countIssues`\n * probe over the same criteria), which is not bound to the user at all. It is a record of per-criterion flags\n * rather than a list, because its criteria aren't a single kind. An empty `relationships` means the provider has\n * no filtered issue search — hide the surface, not just its chips.\n *\n * `pullRequestSearch` describes the separate filtered PR search. Its table declares each criteria vocabulary\n * plus repository/organization scope support; an empty relationship list means the search itself is absent.\n *\n * `issueSorts` / `issueSortsAccountWide` / `issueSearch.sorts` are the ORDERING vocabulary of those same three\n * issue reads, split the same way and for the same reason (GitLab's repo-scoped read is GraphQL and its\n * account-wide read is REST, with genuinely different sort vocabularies). Empty means the read can't be ordered,\n * so pass no `sort`; a key not listed refuses the whole read exactly like an inexpressible filter. Note a key\n * listed here is expressible on ONE provider query: a read that fans out across projects can only honor a key\n * derivable from a normalized issue, so it refuses `priority`/`dueDate`/`resolved` on top of this table.\n *\n * Note this is a CAPABILITY table — \"what the provider can express\" — not a recommendation. A consumer\n * matching another tool's behavior may deliberately pass fewer filters than are listed here (or none, where an\n * already-scoped read would only be narrowed by them). Intersecting against this table is what keeps a\n * filtered read from being refused; it isn't a directive to use every filter in it.\n */\nexport function getSupportedFilters(providerId: IntegrationIds): SupportedFilters {\n\tconst metadata = providersMetadata[providerId];\n\tconst issueSearch = metadata?.supportedIssueSearch;\n\tconst pullRequestSearch = metadata?.supportedPullRequestSearch;\n\treturn {\n\t\tpullRequests: [...(metadata?.supportedPullRequestFilters ?? [])],\n\t\tpullRequestsAccountWide: [...(metadata?.supportedAccountWidePullRequestFilters ?? [])],\n\t\tpullRequestSearch: {\n\t\t\t...unsupportedPullRequestSearchCapabilities,\n\t\t\t...pullRequestSearch,\n\t\t\trelationships: [...(pullRequestSearch?.relationships ?? [])],\n\t\t\tstates: [...(pullRequestSearch?.states ?? [])],\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\tsorts: [...(pullRequestSearch?.sorts ?? [])],\n\t\t},\n\t\tissues: [...(metadata?.supportedIssueFilters ?? [])],\n\t\tissuesAccountWide: [...(metadata?.supportedAccountWideIssueFilters ?? [])],\n\t\t// Always an object, never `undefined`: a provider WITHOUT a filtered issue search reports one whose\n\t\t// `relationships` is empty and whose flags are all false, so a consumer reads capabilities the same way\n\t\t// for every provider (and an empty `relationships` is the signal to hide the surface itself).\n\t\t//\n\t\t// Spread over the all-false baseline rather than defaulting each flag: a criterion added to\n\t\t// `IssueSearchCapabilities` then can't be forgotten here and silently reported as `undefined` (which a\n\t\t// consumer's `if (caps.x)` would read as unsupported — right answer, wrong reason, and untyped).\n\t\t// `supportedIssueSearch` is typed as the complete shape, so no member can arrive as an explicit undefined.\n\t\tissueSearch: {\n\t\t\t...unsupportedIssueSearchCapabilities,\n\t\t\t...issueSearch,\n\t\t\t// Copied, so mutating the result can't corrupt the metadata table.\n\t\t\trelationships: [...(issueSearch?.relationships ?? [])],\n\t\t\tsorts: [...(issueSearch?.sorts ?? [])],\n\t\t},\n\t\tissueSorts: [...(metadata?.supportedIssueSorts ?? [])],\n\t\tissueSortsAccountWide: [...(metadata?.supportedAccountWideIssueSorts ?? [])],\n\t\tissueStates: metadata?.supportsIssueStates === true,\n\t};\n}\n"],"mappings":"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"}
@@ -43,7 +43,8 @@ export type IssueBatchTarget =
43
43
  owner: string;
44
44
  /**
45
45
  * Repository name. Required, but ignored on Azure DevOps, whose work items belong to the project rather
46
- * than to a repository.
46
+ * than to a repository; there it only names the project of a project-less Azure DevOps Server path read
47
+ * behind a virtual directory (`{vdir}/{collection}/_git/{repo}` as owner `{vdir}`, project `{collection}`).
47
48
  */
48
49
  repo: string;
49
50
  /** Issue number, GitLab issue iid, or Azure DevOps work item id. */
@@ -2,9 +2,9 @@ import "../constants.js";
2
2
  import { githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId } from "../providers/providerErrors.js";
3
3
  import { areDomainsOnSameHost, hostFromDomain } from "../utils/domain.utils.js";
4
4
  import { isGitHostIntegration, isIssuesHostIntegrationId, isIssuesSelfManagedHostIntegrationId } from "../utils/integration.utils.js";
5
+ import { issueEtagIncludes } from "../models/integration.js";
5
6
  import { gitHostOnlySurfaceWarning, issueTrackerOnlySurfaceWarning, issuesUnsupportedWarning, noConnectionWarning, otherWarning, unsupportedWarning } from "./warnings.js";
6
7
  import { runCaptured } from "./drains.js";
7
- import { issueEtagIncludes } from "../models/integration.js";
8
8
  import { isIssuesIntegration } from "../models/issuesIntegration.js";
9
9
  import { readEtaggedBatch } from "./batchEtags.js";
10
10
  import { findInvalidIssueEtagInclude, issueEtag, issueEtagFieldsFromShape, normalizeIssueEtagIncludes } from "./etag.js";
@@ -1 +1 @@
1
- {"version":3,"file":"issueBatch.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueBatch.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IntegrationIds, IssuesHostIntegrationIds } from '../constants.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport type { IssueEtagInclude } from '../models/integration.js';\nimport { issueEtagIncludes } from '../models/integration.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId } from '../providers/providerErrors.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { areDomainsOnSameHost, hostFromDomain } from '../utils/domain.utils.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\tisIssuesSelfManagedHostIntegrationId,\n} from '../utils/integration.utils.js';\nimport type { EtaggedBatchRow } from './batchEtags.js';\nimport { readEtaggedBatch } from './batchEtags.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tfindInvalidIssueEtagInclude,\n\tissueEtag,\n\tissueEtagFieldsFromShape,\n\tnormalizeIssueEtagIncludes,\n} from './etag.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\tissueTrackerOnlySurfaceWarning,\n\tnoConnectionWarning,\n\totherWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\n/**\n * The BATCH issue read: resolve N issues BY IDENTITY in one call — `(owner, repo, number)` coordinates on a git\n * host, `(resourceId, ABC-123)` identifiers on an issue tracker.\n *\n * A sibling of the issue searches rather than a mode of one, because it answers a different question. A search\n * asks \"what matches\"; this asks \"does this exact issue exist\", which is what a caller correlating a branch name\n * to the issue it references is actually asking. Three consequences follow, and they are the reason this exists:\n *\n * - It resolves by EXACT IDENTIFIER, so relevance never enters into it.\n * - No result ceiling applies, so there is no partial window to reason about and no omission to report.\n * - An absent slot is a PROVEN ABSENCE, not \"not found within a page budget\". That is the property that matters\n * most: a caller can CACHE a miss. Emulating this with a paged list cannot prove absence without walking the\n * whole scope, so a miss stays unproven and the walk repeats on every pass, forever.\n *\n * Modeled on `countIssues` for its shape — caller-owned `key` echoed back, per-target isolation, as few requests\n * as the provider allows — because both take a set of independent questions and answer them together.\n *\n * ONE integration call per invocation when no target carries an `etag`, whatever the target count, as in\n * `getPullRequestsBatch`: the integration fans its targets out (chunked for GitHub/GHE, one request per target with\n * bounded concurrency on GitLab and Azure DevOps and on the trackers) and settles each independently, so failing\n * targets spend at most one strike of the integration's failure budget.\n *\n * With etags, up to THREE on a host with a cheap check (GitHub/GHE, GitLab and Azure DevOps; Jira Cloud and Linear\n * among the trackers): the cheap check of the targets that carry one, a full read of the rest started alongside it,\n * and a full read of the targets whose etag no longer matches — see `readEtaggedBatch`. Every other tracker still makes\n * one call and etags its rows.\n */\n\n/** One issue to resolve, echoed back under the caller's own `key`. Which form a call takes depends on its provider. */\nexport type IssueBatchTarget =\n\t/**\n\t * A git host's issue, by repository coordinate. GitHub/GHE, GitLab (and self-managed) and Azure DevOps (and\n\t * Server); Bitbucket and Bitbucket Data Center have no issues and refuse.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\t/** GitHub owner, GitLab namespace path (may be nested), or Azure DevOps organization (or collection). */\n\t\t\towner: string;\n\t\t\t/**\n\t\t\t * Repository name. Required, but ignored on Azure DevOps, whose work items belong to the project rather\n\t\t\t * than to a repository.\n\t\t\t */\n\t\t\trepo: string;\n\t\t\t/** Issue number, GitLab issue iid, or Azure DevOps work item id. */\n\t\t\tnumber: number;\n\t\t\t/** Azure DevOps project. Required there, ignored elsewhere. */\n\t\t\tproject?: string;\n\t\t\t/** The {@link IssueBatchResult.etag} of the copy the caller holds; see {@link IssueBatchResult}. */\n\t\t\tetag?: string;\n\t }\n\t/**\n\t * An issue tracker's issue, by its own identifier (e.g. `ABC-123`) within a resource. Jira (Cloud and Data\n\t * Center) and Linear. For Jira Data Center `resourceId` is the host — the instance's single resource, as\n\t * `listOrgs` reports it — and `resourceUrl` is not needed, since the browser link is built from the\n\t * connection's own base URL; Jira Cloud requires it.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\tresourceId: string;\n\t\t\tresourceUrl?: string;\n\t\t\tidentifier: string;\n\t\t\t/**\n\t\t\t * The {@link IssueBatchResult.etag} of the copy the caller holds. Checked cheaply on Jira Cloud and Linear;\n\t\t\t * every other tracker accepts it but reads the issue in full.\n\t\t\t */\n\t\t\tetag?: string;\n\t };\n\ntype CoordinateTarget = Extract<IssueBatchTarget, { owner: string }>;\ntype TrackerTarget = Extract<IssueBatchTarget, { resourceId: string }>;\n\n/**\n * The answer for one {@link IssueBatchTarget}, in one of four states:\n * - `{ key, issue, etag }` — read in full. Every fully read row carries an `etag`, whether or not the caller sent\n * one, so a caller can seed its etags from the reads it already makes.\n * - `{ key, unchanged: true, etag }` — the cheap check proved the caller's copy current, and nothing else was\n * fetched. Only for a target that sent an `etag`, on a host with a cheap check.\n * - `{ key }` — PROVEN ABSENT.\n * - no row, with `fetchFailed` and a warning — the read could not check. Never treat that as absent.\n */\nexport interface IssueBatchResult {\n\tkey: string;\n\t/**\n\t * The resolved issue, or `undefined` when it PROVABLY does not exist (or is not visible to this connection) —\n\t * or when it is `unchanged`.\n\t *\n\t * Absent is an answer here, unlike every paged read on this facade: a target that FAILED — its own request\n\t * outright, or just its own alias within an otherwise-answering GitHub chunk (e.g. an org enforcing SAML SSO\n\t * the token isn't authorized for) — is not returned at all and sets `fetchFailed`, so a caller can tell\n\t * \"proven absent\" from \"unknown\" and cache the first without ever caching the second.\n\t */\n\tissue?: IssueShape;\n\t/**\n\t * Opaque: compare for equality only, never parse. Computed by core from the issue's change state — its state and\n\t * update time, plus each input the call's `etagIncludes` listed — and NOT the provider's HTTP ETag. Send it back\n\t * as the target's `etag`. An etag from another scheme or another `etagIncludes` set simply compares unequal and\n\t * costs a full read, never a false `unchanged`. A host can add a reaction without moving the update time, so\n\t * unless `'reactions'` is listed, an `unchanged` copy's `thumbsUpCount` may be stale.\n\t */\n\tetag?: string;\n\t/** The caller's copy is current (its `etag` matched); `issue` is absent because nothing was read. */\n\tunchanged?: true;\n}\n\nconst surface = 'Batch issue resolution';\n\nfunction refused(warning: ProviderWarning): ProviderResult<IssueBatchResult> {\n\treturn { items: [], warnings: [warning], fetchFailed: true };\n}\n\nexport async function getIssuesBatch(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\ttargets: readonly IssueBatchTarget[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t\t/**\n\t\t * Widens every etag to the listed inputs, each of which a host changes without moving the issue's update\n\t\t * time, and each of which costs its own fields in the cheap check. `'reactions'` is the thumbs-up count; it\n\t\t * widens nothing on a host whose rows carry no real count (Azure DevOps work items and the trackers). Order\n\t\t * and repeats don't matter. An unknown value refuses the whole call. Changes only which etag is computed; the\n\t\t * full read is the same. Calls that differ in this set never match each other's etags.\n\t\t */\n\t\tetagIncludes?: readonly IssueEtagInclude[];\n\t},\n): Promise<ProviderResult<IssueBatchResult>> {\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.targets.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.targets);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY target, not just\n\t\t// the repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate issue batch target key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Same as a bad target: refused whole before any request, naming the value and what's allowed.\n\tconst invalidInclude = findInvalidIssueEtagInclude(options.etagIncludes ?? []);\n\tif (invalidInclude != null) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Unknown issue etag include '${invalidInclude}'; expected one of ${issueEtagIncludes.map(i => `'${i}'`).join(', ')}.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst etagIncludes = normalizeIssueEtagIncludes(options.etagIncludes ?? []);\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn getIssuesBatchForTracker(\n\t\t\tctx,\n\t\t\toptions.providerId,\n\t\t\toptions.targets,\n\t\t\toptions.connectionId,\n\t\t\toptions.domain,\n\t\t\tetagIncludes,\n\t\t);\n\t}\n\n\tconst targets = options.targets;\n\tif (!targets.every(isCoordinateTarget)) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} for '${options.providerId}' takes repository coordinates { key, owner, repo, number, project? }, not tracker identifiers.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimCoordinateFields);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream, where a blank owner or repo\n\t// would come back as a missing repository — a proven absence the caller would cache.\n\tconst invalid = findInvalidCoordinateTarget(options.providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(options.providerId, undefined, options.connectionId, invalid));\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\treturn unresolvedIntegration(ctx, options.providerId, options.connectionId, options.domain);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, surface));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\tconst toCoordinate = (t: CoordinateTarget) => ({\n\t\towner: t.owner,\n\t\trepo: t.repo,\n\t\tnumber: t.number,\n\t\tproject: t.project,\n\t});\n\tconst result = await readEtaggedBatch({\n\t\tproviderId: options.providerId,\n\t\tdomain: domain,\n\t\tconnectionId: options.connectionId,\n\t\ttargets: trimmedTargets,\n\t\tsupportsEtags: integration.supportsIssueEtags,\n\t\treadFull: batch =>\n\t\t\trunCaptured(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t() => integration.getIssuesBatchResult(batch.map(toCoordinate), undefined, options.connectionId),\n\t\t\t\t// A missing session must surface as a connection warning, including on the primary path, which\n\t\t\t\t// otherwise reads it as \"not connected\" and says nothing — and this read would then call it\n\t\t\t\t// unsupported.\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\treadEtagFields: batch =>\n\t\t\trunCaptured(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t() =>\n\t\t\t\t\tintegration.getIssuesEtagFieldsResult(\n\t\t\t\t\t\tbatch.map(toCoordinate),\n\t\t\t\t\t\t{ etagIncludes: etagIncludes },\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t),\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\titemEtag: issue => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),\n\t\tfieldsEtag: fields => issueEtag(fields, etagIncludes),\n\t\t// A provider that doesn't implement the batch hook answers `undefined` with no error. Either way its targets\n\t\t// are DROPPED rather than reported as absent — the difference between \"unknown\" and \"proven absent\" is the\n\t\t// read's whole value, and a failure must never be cached as an answer.\n\t\tunsupportedWarning: () =>\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} is not supported by '${options.providerId}'; resolve issues individually instead.`,\n\t\t\t),\n\t});\n\n\treturn toIssueBatchResult(result);\n}\n\n/**\n * The tracker form. ONE integration call per invocation when no target carries an `etag`, whatever the target count:\n * the integration makes one single-issue request per target, with bounded concurrency, and settles each independently\n * — see `IssuesIntegration.getIssuesByResourceIdBatchResult`. With etags, a tracker with a cheap check (Jira Cloud's\n * bulk fetch, Linear's per-team query) takes the coordinate form's `readEtaggedBatch` flow; every other tracker\n * ignores a target's `etag` and reads and etags every found row in full.\n *\n * `resourceId` is trusted and the read does no resource discovery. It is handed to the provider's by-resource-id\n * read as-is, never wrapped into a synthesized descriptor: Linear and Trello answer `undefined` for a descriptor\n * that fails `isIssueResourceDescriptor`, which would be published as a proven absence.\n *\n * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for\n * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host. Unlike\n * the paged reads, this one never falls back to the primary connection for a self-managed tracker: it requires a\n * `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted instances routinely\n * issue the same project and issue keys, and `issue: undefined` is a proven absence a caller may cache — an answer\n * from whichever host happens to be primary would be cached under a key that names a different instance (#5872).\n */\nasync function getIssuesBatchForTracker(\n\tctx: ProviderReadContext,\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly IssueBatchTarget[],\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n\tetagIncludes: readonly IssueEtagInclude[],\n): Promise<ProviderResult<IssueBatchResult>> {\n\tif (!targets.every(isTrackerTarget)) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} for '${providerId}' takes tracker identifiers { key, resourceId, resourceUrl?, identifier }, not repository coordinates.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimTrackerTarget);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream to fail one by one.\n\tconst invalid = findInvalidTrackerTarget(providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(providerId, undefined, connectionId, invalid));\n\t}\n\n\t// A `domain` only selects a host when it parses to one, and a `connectionId` only when it names a configured\n\t// connection that has one; anything else would resolve the primary host instead, which is the fallback this\n\t// read refuses.\n\tif (\n\t\tisIssuesSelfManagedHostIntegrationId(providerId) &&\n\t\t(domain != null\n\t\t\t? hostFromDomain(domain) == null\n\t\t\t: connectionId == null ||\n\t\t\t\t!ctx.getConfigured(providerId).some(c => c.id === connectionId && hostFromDomain(c.domain) != null))\n\t) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} requires a domain or a configured connection id for '${providerId}': every configured host can hold a different issue under the same key, so the read does not answer from the primary connection.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(providerId, connectionId, domain);\n\tif (integration == null) return unresolvedIntegration(ctx, providerId, connectionId, domain);\n\tif (!isIssuesIntegration(integration)) {\n\t\treturn refused(issueTrackerOnlySurfaceWarning(providerId, connectionId, surface));\n\t}\n\n\tconst unsupported = (): ProviderWarning =>\n\t\tunsupportedWarning(\n\t\t\tproviderId,\n\t\t\tundefined,\n\t\t\tconnectionId,\n\t\t\t`${surface} is not supported by '${providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`,\n\t\t);\n\tif (!integration.supportsIssueLookupByResourceId) return refused(unsupported());\n\n\t// A self-managed tracker's single resource is its host, and a caller keys (and caches) each answer by\n\t// `resourceId` while the read is addressed to the host `domain`/`connectionId` resolved. When they disagree,\n\t// host B's answer — including a cacheable absence — would be stored under host A's key. Refused whole, like\n\t// the caller bugs above: a target naming another host means the call selected a host its caller did not mean.\n\tif (isIssuesSelfManagedHostIntegrationId(providerId)) {\n\t\tconst mismatched = trimmedTargets.find(t => !areDomainsOnSameHost(t.resourceId, integration.domain));\n\t\tif (mismatched != null) {\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\tproviderId,\n\t\t\t\t\tundefined,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t`Issue batch target '${mismatched.key}' requires the resource id of '${providerId}' to name the host the read resolved to.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\tconst resolvedDomain = ctx.domainForRead(integration, providerId, connectionId, domain);\n\n\tconst toResourceTarget = (t: TrackerTarget) => ({\n\t\tresourceId: t.resourceId,\n\t\tidentifier: t.identifier,\n\t\tresourceUrl: t.resourceUrl,\n\t});\n\tconst result = await readEtaggedBatch({\n\t\tproviderId: providerId,\n\t\tdomain: resolvedDomain,\n\t\tconnectionId: connectionId,\n\t\ttargets: trimmedTargets,\n\t\tsupportsEtags: integration.supportsIssueEtagsByResourceId,\n\t\treadFull: batch =>\n\t\t\trunCaptured(\n\t\t\t\tproviderId,\n\t\t\t\tresolvedDomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getIssuesByResourceIdBatchResult(batch.map(toResourceTarget), connectionId),\n\t\t\t\t// The read core answers `undefined` when it cannot resolve a session. That must surface as a connection\n\t\t\t\t// warning, including on the primary path, rather than as nothing — or worse, be read as absence.\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\treadEtagFields: batch =>\n\t\t\trunCaptured(\n\t\t\t\tproviderId,\n\t\t\t\tresolvedDomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() =>\n\t\t\t\t\tintegration.getIssuesEtagFieldsByResourceIdBatchResult(\n\t\t\t\t\t\tbatch.map(toResourceTarget),\n\t\t\t\t\t\t{ etagIncludes: etagIncludes },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\titemEtag: issue => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),\n\t\tfieldsEtag: fields => issueEtag(fields, etagIncludes),\n\t\tunsupportedWarning: unsupported,\n\t});\n\n\treturn toIssueBatchResult(result);\n}\n\n/** The etag flow's rows as this read's results, under the `issue` name the result type gives the item. */\nfunction toIssueBatchResult(result: ProviderResult<EtaggedBatchRow<IssueShape>>): ProviderResult<IssueBatchResult> {\n\treturn {\n\t\titems: result.items.map(row => ({\n\t\t\tkey: row.key,\n\t\t\t...(row.value != null ? { issue: row.value } : {}),\n\t\t\t...(row.unchanged ? { unchanged: true as const } : {}),\n\t\t\t...(row.etag != null ? { etag: row.etag } : {}),\n\t\t})),\n\t\twarnings: result.warnings,\n\t\tfetchFailed: result.fetchFailed,\n\t};\n}\n\nfunction isCoordinateTarget(target: IssueBatchTarget): target is CoordinateTarget {\n\treturn 'owner' in target;\n}\n\nfunction isTrackerTarget(target: IssueBatchTarget): target is TrackerTarget {\n\treturn 'resourceId' in target;\n}\n\n/**\n * Trims `owner`, `repo` and `project` once, shared by every coordinate-form batch read (this one, the pull request\n * batch and the pull-requests-by-branch read): leading/trailing whitespace can't change which repository a\n * coordinate names, so it's stripped and accepted rather than refused, matching #5821's scope-name precedent.\n */\nexport function trimCoordinateFields<T extends { owner: string; repo: string; project?: string }>(target: T): T {\n\treturn { ...target, owner: target.owner.trim(), repo: target.repo.trim(), project: target.project?.trim() };\n}\n\n/** Trims `resourceId`, `resourceUrl` and `identifier` once; the validated value must be the one sent to the provider. */\nfunction trimTrackerTarget(target: TrackerTarget): TrackerTarget {\n\treturn {\n\t\t...target,\n\t\tresourceId: target.resourceId.trim(),\n\t\tresourceUrl: target.resourceUrl?.trim() || undefined,\n\t\tidentifier: target.identifier.trim(),\n\t};\n}\n\nfunction findInvalidCoordinateTarget(\n\tproviderId: IntegrationIds,\n\ttargets: readonly CoordinateTarget[],\n): string | undefined {\n\tconst isAzure = isAzureProviderId(providerId);\n\tconst isGitHub = isGitHubProviderId(providerId);\n\tfor (const target of targets) {\n\t\tif (!Number.isSafeInteger(target.number) || target.number <= 0) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; expected a positive integer.`;\n\t\t}\n\t\tif (isGitHub && target.number > githubGraphQLInt32Max) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; GitHub numbers are 32-bit and cannot exceed ${githubGraphQLInt32Max}.`;\n\t\t}\n\t\tif (target.owner.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty owner.`;\n\t\t}\n\t\tif (isAzure) {\n\t\t\tif (!target.project) {\n\t\t\t\treturn `Issue batch target '${target.key}' requires a project for '${providerId}'.`;\n\t\t\t}\n\t\t} else if (target.repo.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty repo.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction findInvalidTrackerTarget(\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly TrackerTarget[],\n): string | undefined {\n\tfor (const target of targets) {\n\t\tif (target.resourceId.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a resource id.`;\n\t\t}\n\t\tif (target.identifier.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires an issue identifier.`;\n\t\t}\n\t\tif (providerId === IssuesCloudHostIntegrationId.Jira && !target.resourceUrl) {\n\t\t\treturn `Issue batch target '${target.key}' requires the Jira resource URL so the result contains a browser link without resource discovery.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The answer when no integration resolves for a batch read. Targets were asked about, so this is never a silent\n * empty success: a supplied connection or domain that no longer resolves gets its own warning, and the untargeted\n * primary path (e.g. a self-managed provider with no configured host) a connection warning, as `getCurrentAccount`\n * gives — either way `fetchFailed`, so no caller mistakes the dropped targets for an answer.\n */\nexport function unresolvedIntegration<T>(\n\tctx: ProviderReadContext,\n\tproviderId: IntegrationIds,\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n): ProviderResult<T> {\n\tconst early = ctx.earlyReturnConnectionWarnings(providerId, connectionId, domain);\n\tif (early.warnings.length > 0) return { items: [], warnings: early.warnings, fetchFailed: true };\n\n\tconst resolvedDomain = ctx.resolveDomainForRead(providerId, connectionId, domain);\n\treturn { items: [], warnings: [noConnectionWarning(providerId, resolvedDomain, connectionId)], fetchFailed: true };\n}\n\nexport function findDuplicateKey(targets: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const target of targets) {\n\t\tif (seen.has(target.key)) return target.key;\n\n\t\tseen.add(target.key);\n\t}\n\treturn undefined;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"issueBatch.js","names":[],"sources":["../../../../../plus/integrations/src/reads/issueBatch.ts"],"sourcesContent":["import type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { IntegrationIds, IssuesHostIntegrationIds } from '../constants.js';\nimport { IssuesCloudHostIntegrationId } from '../constants.js';\nimport type { IssueEtagInclude } from '../models/integration.js';\nimport { issueEtagIncludes } from '../models/integration.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport { githubGraphQLInt32Max, isAzureProviderId, isGitHubProviderId } from '../providers/providerErrors.js';\nimport type { ProviderResult, ProviderWarning } from '../results.js';\nimport { areDomainsOnSameHost, hostFromDomain } from '../utils/domain.utils.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\tisIssuesSelfManagedHostIntegrationId,\n} from '../utils/integration.utils.js';\nimport type { EtaggedBatchRow } from './batchEtags.js';\nimport { readEtaggedBatch } from './batchEtags.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tfindInvalidIssueEtagInclude,\n\tissueEtag,\n\tissueEtagFieldsFromShape,\n\tnormalizeIssueEtagIncludes,\n} from './etag.js';\nimport {\n\tgitHostOnlySurfaceWarning,\n\tissuesUnsupportedWarning,\n\tissueTrackerOnlySurfaceWarning,\n\tnoConnectionWarning,\n\totherWarning,\n\tunsupportedWarning,\n} from './warnings.js';\n\n/**\n * The BATCH issue read: resolve N issues BY IDENTITY in one call — `(owner, repo, number)` coordinates on a git\n * host, `(resourceId, ABC-123)` identifiers on an issue tracker.\n *\n * A sibling of the issue searches rather than a mode of one, because it answers a different question. A search\n * asks \"what matches\"; this asks \"does this exact issue exist\", which is what a caller correlating a branch name\n * to the issue it references is actually asking. Three consequences follow, and they are the reason this exists:\n *\n * - It resolves by EXACT IDENTIFIER, so relevance never enters into it.\n * - No result ceiling applies, so there is no partial window to reason about and no omission to report.\n * - An absent slot is a PROVEN ABSENCE, not \"not found within a page budget\". That is the property that matters\n * most: a caller can CACHE a miss. Emulating this with a paged list cannot prove absence without walking the\n * whole scope, so a miss stays unproven and the walk repeats on every pass, forever.\n *\n * Modeled on `countIssues` for its shape — caller-owned `key` echoed back, per-target isolation, as few requests\n * as the provider allows — because both take a set of independent questions and answer them together.\n *\n * ONE integration call per invocation when no target carries an `etag`, whatever the target count, as in\n * `getPullRequestsBatch`: the integration fans its targets out (chunked for GitHub/GHE, one request per target with\n * bounded concurrency on GitLab and Azure DevOps and on the trackers) and settles each independently, so failing\n * targets spend at most one strike of the integration's failure budget.\n *\n * With etags, up to THREE on a host with a cheap check (GitHub/GHE, GitLab and Azure DevOps; Jira Cloud and Linear\n * among the trackers): the cheap check of the targets that carry one, a full read of the rest started alongside it,\n * and a full read of the targets whose etag no longer matches — see `readEtaggedBatch`. Every other tracker still makes\n * one call and etags its rows.\n */\n\n/** One issue to resolve, echoed back under the caller's own `key`. Which form a call takes depends on its provider. */\nexport type IssueBatchTarget =\n\t/**\n\t * A git host's issue, by repository coordinate. GitHub/GHE, GitLab (and self-managed) and Azure DevOps (and\n\t * Server); Bitbucket and Bitbucket Data Center have no issues and refuse.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\t/** GitHub owner, GitLab namespace path (may be nested), or Azure DevOps organization (or collection). */\n\t\t\towner: string;\n\t\t\t/**\n\t\t\t * Repository name. Required, but ignored on Azure DevOps, whose work items belong to the project rather\n\t\t\t * than to a repository; there it only names the project of a project-less Azure DevOps Server path read\n\t\t\t * behind a virtual directory (`{vdir}/{collection}/_git/{repo}` as owner `{vdir}`, project `{collection}`).\n\t\t\t */\n\t\t\trepo: string;\n\t\t\t/** Issue number, GitLab issue iid, or Azure DevOps work item id. */\n\t\t\tnumber: number;\n\t\t\t/** Azure DevOps project. Required there, ignored elsewhere. */\n\t\t\tproject?: string;\n\t\t\t/** The {@link IssueBatchResult.etag} of the copy the caller holds; see {@link IssueBatchResult}. */\n\t\t\tetag?: string;\n\t }\n\t/**\n\t * An issue tracker's issue, by its own identifier (e.g. `ABC-123`) within a resource. Jira (Cloud and Data\n\t * Center) and Linear. For Jira Data Center `resourceId` is the host — the instance's single resource, as\n\t * `listOrgs` reports it — and `resourceUrl` is not needed, since the browser link is built from the\n\t * connection's own base URL; Jira Cloud requires it.\n\t */\n\t| {\n\t\t\tkey: string;\n\t\t\tresourceId: string;\n\t\t\tresourceUrl?: string;\n\t\t\tidentifier: string;\n\t\t\t/**\n\t\t\t * The {@link IssueBatchResult.etag} of the copy the caller holds. Checked cheaply on Jira Cloud and Linear;\n\t\t\t * every other tracker accepts it but reads the issue in full.\n\t\t\t */\n\t\t\tetag?: string;\n\t };\n\ntype CoordinateTarget = Extract<IssueBatchTarget, { owner: string }>;\ntype TrackerTarget = Extract<IssueBatchTarget, { resourceId: string }>;\n\n/**\n * The answer for one {@link IssueBatchTarget}, in one of four states:\n * - `{ key, issue, etag }` — read in full. Every fully read row carries an `etag`, whether or not the caller sent\n * one, so a caller can seed its etags from the reads it already makes.\n * - `{ key, unchanged: true, etag }` — the cheap check proved the caller's copy current, and nothing else was\n * fetched. Only for a target that sent an `etag`, on a host with a cheap check.\n * - `{ key }` — PROVEN ABSENT.\n * - no row, with `fetchFailed` and a warning — the read could not check. Never treat that as absent.\n */\nexport interface IssueBatchResult {\n\tkey: string;\n\t/**\n\t * The resolved issue, or `undefined` when it PROVABLY does not exist (or is not visible to this connection) —\n\t * or when it is `unchanged`.\n\t *\n\t * Absent is an answer here, unlike every paged read on this facade: a target that FAILED — its own request\n\t * outright, or just its own alias within an otherwise-answering GitHub chunk (e.g. an org enforcing SAML SSO\n\t * the token isn't authorized for) — is not returned at all and sets `fetchFailed`, so a caller can tell\n\t * \"proven absent\" from \"unknown\" and cache the first without ever caching the second.\n\t */\n\tissue?: IssueShape;\n\t/**\n\t * Opaque: compare for equality only, never parse. Computed by core from the issue's change state — its state and\n\t * update time, plus each input the call's `etagIncludes` listed — and NOT the provider's HTTP ETag. Send it back\n\t * as the target's `etag`. An etag from another scheme or another `etagIncludes` set simply compares unequal and\n\t * costs a full read, never a false `unchanged`. A host can add a reaction without moving the update time, so\n\t * unless `'reactions'` is listed, an `unchanged` copy's `thumbsUpCount` may be stale.\n\t */\n\tetag?: string;\n\t/** The caller's copy is current (its `etag` matched); `issue` is absent because nothing was read. */\n\tunchanged?: true;\n}\n\nconst surface = 'Batch issue resolution';\n\nfunction refused(warning: ProviderWarning): ProviderResult<IssueBatchResult> {\n\treturn { items: [], warnings: [warning], fetchFailed: true };\n}\n\nexport async function getIssuesBatch(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\ttargets: readonly IssueBatchTarget[];\n\t\tconnectionId?: string;\n\t\t/**\n\t\t * Explicit self-managed host domain. Used only when the requested connection has no configured domain;\n\t\t * it must come from the trusted authentication configuration, not repository or remote data.\n\t\t */\n\t\tdomain?: string;\n\t\t/**\n\t\t * Widens every etag to the listed inputs, each of which a host changes without moving the issue's update\n\t\t * time, and each of which costs its own fields in the cheap check. `'reactions'` is the thumbs-up count; it\n\t\t * widens nothing on a host whose rows carry no real count (Azure DevOps work items and the trackers). Order\n\t\t * and repeats don't matter. An unknown value refuses the whole call. Changes only which etag is computed; the\n\t\t * full read is the same. Calls that differ in this set never match each other's etags.\n\t\t */\n\t\tetagIncludes?: readonly IssueEtagInclude[];\n\t},\n): Promise<ProviderResult<IssueBatchResult>> {\n\t// Nothing was asked for, so nothing is missing: an empty success, not a refusal.\n\tif (options.targets.length === 0) return { items: [], warnings: [] };\n\n\tconst duplicateKey = findDuplicateKey(options.targets);\n\tif (duplicateKey != null) {\n\t\t// Refuses the whole call rather than deduping: `key` exists so the caller can match results without\n\t\t// positional bookkeeping, and two results under one key make that ambiguous for EVERY target, not just\n\t\t// the repeated one.\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Duplicate issue batch target key '${duplicateKey}'; keys identify results, so each must be unique.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// Same as a bad target: refused whole before any request, naming the value and what's allowed.\n\tconst invalidInclude = findInvalidIssueEtagInclude(options.etagIncludes ?? []);\n\tif (invalidInclude != null) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`Unknown issue etag include '${invalidInclude}'; expected one of ${issueEtagIncludes.map(i => `'${i}'`).join(', ')}.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst etagIncludes = normalizeIssueEtagIncludes(options.etagIncludes ?? []);\n\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn getIssuesBatchForTracker(\n\t\t\tctx,\n\t\t\toptions.providerId,\n\t\t\toptions.targets,\n\t\t\toptions.connectionId,\n\t\t\toptions.domain,\n\t\t\tetagIncludes,\n\t\t);\n\t}\n\n\tconst targets = options.targets;\n\tif (!targets.every(isCoordinateTarget)) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tundefined,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} for '${options.providerId}' takes repository coordinates { key, owner, repo, number, project? }, not tracker identifiers.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimCoordinateFields);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream, where a blank owner or repo\n\t// would come back as a missing repository — a proven absence the caller would cache.\n\tconst invalid = findInvalidCoordinateTarget(options.providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(options.providerId, undefined, options.connectionId, invalid));\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\treturn unresolvedIntegration(ctx, options.providerId, options.connectionId, options.domain);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refused(gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, surface));\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tif (!integration.supportsIssues) {\n\t\treturn refused(issuesUnsupportedWarning(options.providerId, domain, options.connectionId));\n\t}\n\n\tconst toCoordinate = (t: CoordinateTarget) => ({\n\t\towner: t.owner,\n\t\trepo: t.repo,\n\t\tnumber: t.number,\n\t\tproject: t.project,\n\t});\n\tconst result = await readEtaggedBatch({\n\t\tproviderId: options.providerId,\n\t\tdomain: domain,\n\t\tconnectionId: options.connectionId,\n\t\ttargets: trimmedTargets,\n\t\tsupportsEtags: integration.supportsIssueEtags,\n\t\treadFull: batch =>\n\t\t\trunCaptured(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t() => integration.getIssuesBatchResult(batch.map(toCoordinate), undefined, options.connectionId),\n\t\t\t\t// A missing session must surface as a connection warning, including on the primary path, which\n\t\t\t\t// otherwise reads it as \"not connected\" and says nothing — and this read would then call it\n\t\t\t\t// unsupported.\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\treadEtagFields: batch =>\n\t\t\trunCaptured(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t() =>\n\t\t\t\t\tintegration.getIssuesEtagFieldsResult(\n\t\t\t\t\t\tbatch.map(toCoordinate),\n\t\t\t\t\t\t{ etagIncludes: etagIncludes },\n\t\t\t\t\t\tundefined,\n\t\t\t\t\t\toptions.connectionId,\n\t\t\t\t\t),\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\titemEtag: issue => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),\n\t\tfieldsEtag: fields => issueEtag(fields, etagIncludes),\n\t\t// A provider that doesn't implement the batch hook answers `undefined` with no error. Either way its targets\n\t\t// are DROPPED rather than reported as absent — the difference between \"unknown\" and \"proven absent\" is the\n\t\t// read's whole value, and a failure must never be cached as an answer.\n\t\tunsupportedWarning: () =>\n\t\t\tunsupportedWarning(\n\t\t\t\toptions.providerId,\n\t\t\t\tdomain,\n\t\t\t\toptions.connectionId,\n\t\t\t\t`${surface} is not supported by '${options.providerId}'; resolve issues individually instead.`,\n\t\t\t),\n\t});\n\n\treturn toIssueBatchResult(result);\n}\n\n/**\n * The tracker form. ONE integration call per invocation when no target carries an `etag`, whatever the target count:\n * the integration makes one single-issue request per target, with bounded concurrency, and settles each independently\n * — see `IssuesIntegration.getIssuesByResourceIdBatchResult`. With etags, a tracker with a cheap check (Jira Cloud's\n * bulk fetch, Linear's per-team query) takes the coordinate form's `readEtaggedBatch` flow; every other tracker\n * ignores a target's `etag` and reads and etags every found row in full.\n *\n * `resourceId` is trusted and the read does no resource discovery. It is handed to the provider's by-resource-id\n * read as-is, never wrapped into a synthesized descriptor: Linear and Trello answer `undefined` for a descriptor\n * that fails `isIssueResourceDescriptor`, which would be published as a proven absence.\n *\n * `domain` selects the host of a self-managed tracker (Jira Data Center), as it does for\n * `listIssueTrackerIssuesPage`, and is ignored for the cloud trackers, which have a single canonical host. Unlike\n * the paged reads, this one never falls back to the primary connection for a self-managed tracker: it requires a\n * `domain` or a `connectionId` with a configured host and refuses otherwise. Two self-hosted instances routinely\n * issue the same project and issue keys, and `issue: undefined` is a proven absence a caller may cache — an answer\n * from whichever host happens to be primary would be cached under a key that names a different instance (#5872).\n */\nasync function getIssuesBatchForTracker(\n\tctx: ProviderReadContext,\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly IssueBatchTarget[],\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n\tetagIncludes: readonly IssueEtagInclude[],\n): Promise<ProviderResult<IssueBatchResult>> {\n\tif (!targets.every(isTrackerTarget)) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} for '${providerId}' takes tracker identifiers { key, resourceId, resourceUrl?, identifier }, not repository coordinates.`,\n\t\t\t),\n\t\t);\n\t}\n\n\t// The validated value must be the one sent to the provider, so every string field is trimmed once here.\n\tconst trimmedTargets = targets.map(trimTrackerTarget);\n\n\t// Deterministic caller bugs, refused like a duplicate key rather than sent upstream to fail one by one.\n\tconst invalid = findInvalidTrackerTarget(providerId, trimmedTargets);\n\tif (invalid != null) {\n\t\treturn refused(otherWarning(providerId, undefined, connectionId, invalid));\n\t}\n\n\t// A `domain` only selects a host when it parses to one, and a `connectionId` only when it names a configured\n\t// connection that has one; anything else would resolve the primary host instead, which is the fallback this\n\t// read refuses.\n\tif (\n\t\tisIssuesSelfManagedHostIntegrationId(providerId) &&\n\t\t(domain != null\n\t\t\t? hostFromDomain(domain) == null\n\t\t\t: connectionId == null ||\n\t\t\t\t!ctx.getConfigured(providerId).some(c => c.id === connectionId && hostFromDomain(c.domain) != null))\n\t) {\n\t\treturn refused(\n\t\t\totherWarning(\n\t\t\t\tproviderId,\n\t\t\t\tundefined,\n\t\t\t\tconnectionId,\n\t\t\t\t`${surface} requires a domain or a configured connection id for '${providerId}': every configured host can hold a different issue under the same key, so the read does not answer from the primary connection.`,\n\t\t\t),\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(providerId, connectionId, domain);\n\tif (integration == null) return unresolvedIntegration(ctx, providerId, connectionId, domain);\n\tif (!isIssuesIntegration(integration)) {\n\t\treturn refused(issueTrackerOnlySurfaceWarning(providerId, connectionId, surface));\n\t}\n\n\tconst unsupported = (): ProviderWarning =>\n\t\tunsupportedWarning(\n\t\t\tproviderId,\n\t\t\tundefined,\n\t\t\tconnectionId,\n\t\t\t`${surface} is not supported by '${providerId}'; its single-issue read cannot prove an absence, so a miss would not be safe to cache.`,\n\t\t);\n\tif (!integration.supportsIssueLookupByResourceId) return refused(unsupported());\n\n\t// A self-managed tracker's single resource is its host, and a caller keys (and caches) each answer by\n\t// `resourceId` while the read is addressed to the host `domain`/`connectionId` resolved. When they disagree,\n\t// host B's answer — including a cacheable absence — would be stored under host A's key. Refused whole, like\n\t// the caller bugs above: a target naming another host means the call selected a host its caller did not mean.\n\tif (isIssuesSelfManagedHostIntegrationId(providerId)) {\n\t\tconst mismatched = trimmedTargets.find(t => !areDomainsOnSameHost(t.resourceId, integration.domain));\n\t\tif (mismatched != null) {\n\t\t\treturn refused(\n\t\t\t\totherWarning(\n\t\t\t\t\tproviderId,\n\t\t\t\t\tundefined,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\t`Issue batch target '${mismatched.key}' requires the resource id of '${providerId}' to name the host the read resolved to.`,\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t}\n\n\tconst resolvedDomain = ctx.domainForRead(integration, providerId, connectionId, domain);\n\n\tconst toResourceTarget = (t: TrackerTarget) => ({\n\t\tresourceId: t.resourceId,\n\t\tidentifier: t.identifier,\n\t\tresourceUrl: t.resourceUrl,\n\t});\n\tconst result = await readEtaggedBatch({\n\t\tproviderId: providerId,\n\t\tdomain: resolvedDomain,\n\t\tconnectionId: connectionId,\n\t\ttargets: trimmedTargets,\n\t\tsupportsEtags: integration.supportsIssueEtagsByResourceId,\n\t\treadFull: batch =>\n\t\t\trunCaptured(\n\t\t\t\tproviderId,\n\t\t\t\tresolvedDomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getIssuesByResourceIdBatchResult(batch.map(toResourceTarget), connectionId),\n\t\t\t\t// The read core answers `undefined` when it cannot resolve a session. That must surface as a connection\n\t\t\t\t// warning, including on the primary path, rather than as nothing — or worse, be read as absence.\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\treadEtagFields: batch =>\n\t\t\trunCaptured(\n\t\t\t\tproviderId,\n\t\t\t\tresolvedDomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() =>\n\t\t\t\t\tintegration.getIssuesEtagFieldsByResourceIdBatchResult(\n\t\t\t\t\t\tbatch.map(toResourceTarget),\n\t\t\t\t\t\t{ etagIncludes: etagIncludes },\n\t\t\t\t\t\tconnectionId,\n\t\t\t\t\t),\n\t\t\t\t{ warnOnMissingSession: true },\n\t\t\t),\n\t\titemEtag: issue => issueEtag(issueEtagFieldsFromShape(issue), etagIncludes),\n\t\tfieldsEtag: fields => issueEtag(fields, etagIncludes),\n\t\tunsupportedWarning: unsupported,\n\t});\n\n\treturn toIssueBatchResult(result);\n}\n\n/** The etag flow's rows as this read's results, under the `issue` name the result type gives the item. */\nfunction toIssueBatchResult(result: ProviderResult<EtaggedBatchRow<IssueShape>>): ProviderResult<IssueBatchResult> {\n\treturn {\n\t\titems: result.items.map(row => ({\n\t\t\tkey: row.key,\n\t\t\t...(row.value != null ? { issue: row.value } : {}),\n\t\t\t...(row.unchanged ? { unchanged: true as const } : {}),\n\t\t\t...(row.etag != null ? { etag: row.etag } : {}),\n\t\t})),\n\t\twarnings: result.warnings,\n\t\tfetchFailed: result.fetchFailed,\n\t};\n}\n\nfunction isCoordinateTarget(target: IssueBatchTarget): target is CoordinateTarget {\n\treturn 'owner' in target;\n}\n\nfunction isTrackerTarget(target: IssueBatchTarget): target is TrackerTarget {\n\treturn 'resourceId' in target;\n}\n\n/**\n * Trims `owner`, `repo` and `project` once, shared by every coordinate-form batch read (this one, the pull request\n * batch and the pull-requests-by-branch read): leading/trailing whitespace can't change which repository a\n * coordinate names, so it's stripped and accepted rather than refused, matching #5821's scope-name precedent.\n */\nexport function trimCoordinateFields<T extends { owner: string; repo: string; project?: string }>(target: T): T {\n\treturn { ...target, owner: target.owner.trim(), repo: target.repo.trim(), project: target.project?.trim() };\n}\n\n/** Trims `resourceId`, `resourceUrl` and `identifier` once; the validated value must be the one sent to the provider. */\nfunction trimTrackerTarget(target: TrackerTarget): TrackerTarget {\n\treturn {\n\t\t...target,\n\t\tresourceId: target.resourceId.trim(),\n\t\tresourceUrl: target.resourceUrl?.trim() || undefined,\n\t\tidentifier: target.identifier.trim(),\n\t};\n}\n\nfunction findInvalidCoordinateTarget(\n\tproviderId: IntegrationIds,\n\ttargets: readonly CoordinateTarget[],\n): string | undefined {\n\tconst isAzure = isAzureProviderId(providerId);\n\tconst isGitHub = isGitHubProviderId(providerId);\n\tfor (const target of targets) {\n\t\tif (!Number.isSafeInteger(target.number) || target.number <= 0) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; expected a positive integer.`;\n\t\t}\n\t\tif (isGitHub && target.number > githubGraphQLInt32Max) {\n\t\t\treturn `Issue batch target '${target.key}' has number ${target.number}; GitHub numbers are 32-bit and cannot exceed ${githubGraphQLInt32Max}.`;\n\t\t}\n\t\tif (target.owner.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty owner.`;\n\t\t}\n\t\tif (isAzure) {\n\t\t\tif (!target.project) {\n\t\t\t\treturn `Issue batch target '${target.key}' requires a project for '${providerId}'.`;\n\t\t\t}\n\t\t} else if (target.repo.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a non-empty repo.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\nfunction findInvalidTrackerTarget(\n\tproviderId: IssuesHostIntegrationIds,\n\ttargets: readonly TrackerTarget[],\n): string | undefined {\n\tfor (const target of targets) {\n\t\tif (target.resourceId.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires a resource id.`;\n\t\t}\n\t\tif (target.identifier.length === 0) {\n\t\t\treturn `Issue batch target '${target.key}' requires an issue identifier.`;\n\t\t}\n\t\tif (providerId === IssuesCloudHostIntegrationId.Jira && !target.resourceUrl) {\n\t\t\treturn `Issue batch target '${target.key}' requires the Jira resource URL so the result contains a browser link without resource discovery.`;\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The answer when no integration resolves for a batch read. Targets were asked about, so this is never a silent\n * empty success: a supplied connection or domain that no longer resolves gets its own warning, and the untargeted\n * primary path (e.g. a self-managed provider with no configured host) a connection warning, as `getCurrentAccount`\n * gives — either way `fetchFailed`, so no caller mistakes the dropped targets for an answer.\n */\nexport function unresolvedIntegration<T>(\n\tctx: ProviderReadContext,\n\tproviderId: IntegrationIds,\n\tconnectionId: string | undefined,\n\tdomain: string | undefined,\n): ProviderResult<T> {\n\tconst early = ctx.earlyReturnConnectionWarnings(providerId, connectionId, domain);\n\tif (early.warnings.length > 0) return { items: [], warnings: early.warnings, fetchFailed: true };\n\n\tconst resolvedDomain = ctx.resolveDomainForRead(providerId, connectionId, domain);\n\treturn { items: [], warnings: [noConnectionWarning(providerId, resolvedDomain, connectionId)], fetchFailed: true };\n}\n\nexport function findDuplicateKey(targets: readonly { key: string }[]): string | undefined {\n\tconst seen = new Set<string>();\n\tfor (const target of targets) {\n\t\tif (seen.has(target.key)) return target.key;\n\n\t\tseen.add(target.key);\n\t}\n\treturn undefined;\n}\n"],"mappings":"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"}
@@ -8,7 +8,7 @@ import { isIssuesHostIntegrationId } from "../utils/integration.utils.js";
8
8
  import { mergeCollectionMetadata, parsePageCursor } from "../providers/utils/providerPaging.js";
9
9
  import { providersMetadata } from "../providers/models.js";
10
10
  import { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from "./cursors.js";
11
- import { incompleteReadWarning, issueTrackerOnlySurfaceWarning, otherWarning, unmergeableIssueSortWarning, unsupportedIssueSortWarning, unsupportedWarning } from "./warnings.js";
11
+ import { incompleteReadWarning, issueTrackerOnlySurfaceWarning, otherWarning, projectCutWarning, unmergeableIssueSortWarning, unsupportedIssueSortWarning, unsupportedWarning } from "./warnings.js";
12
12
  import { runCaptured } from "./drains.js";
13
13
  import { isIssuesIntegration } from "../models/issuesIntegration.js";
14
14
  import { projectKey, resourceIdForProject, resourceLabel, selectorMatches } from "./hierarchy.utils.js";
@@ -230,7 +230,9 @@ async function listIssueTrackerIssuesPage(ctx, options) {
230
230
  ...await runCaptured(options.providerId, domain, options.connectionId, () => Promise.resolve(projectReads[index]))
231
231
  })));
232
232
  let fetchFailed = projectDiscoveryFailed || accountLookupFailed;
233
- let truncationRecovery = projectDiscoveryTruncated ? "none" : void 0;
233
+ let requestFailed = fetchFailed;
234
+ let truncated = projectDiscoveryTruncated;
235
+ const cuts = [];
234
236
  let drainMetadata;
235
237
  for (const { project, value: result, warning } of perProject) {
236
238
  const key = projectKey(project);
@@ -238,14 +240,22 @@ async function listIssueTrackerIssuesPage(ctx, options) {
238
240
  if (warning != null) {
239
241
  appendDedupedWarning(warnings, warning);
240
242
  fetchFailed = true;
241
- truncationRecovery = "none";
243
+ requestFailed = true;
244
+ truncated = true;
245
+ }
246
+ if (result == null) {
247
+ fetchFailed = true;
248
+ requestFailed = true;
242
249
  }
243
- if (result == null) fetchFailed = true;
244
250
  if (result != null) {
245
251
  items.push(...result.values);
246
252
  if (result.truncated) {
247
- const recovery = result.recovery;
248
- truncationRecovery = truncationRecovery == null || truncationRecovery === recovery ? recovery : "none";
253
+ truncated = true;
254
+ if (result.scope != null && !result.metadata?.failures?.length && !result.metadata?.omissions?.length) cuts.push({
255
+ project,
256
+ recovery: result.recovery,
257
+ scope: result.scope
258
+ });
249
259
  }
250
260
  if (result.metadata != null) drainMetadata = mergeCollectionMetadata(drainMetadata, result.metadata);
251
261
  }
@@ -255,18 +265,17 @@ async function listIssueTrackerIssuesPage(ctx, options) {
255
265
  } else completedProjectKeys.add(key);
256
266
  }
257
267
  const orderedItems = ordering.order(items);
258
- const projectReadReportedTruncation = truncationRecovery != null;
259
268
  const drainAssessment = assessCollectionMetadata(options.providerId, domain, options.connectionId, drainMetadata);
260
269
  for (const warning of drainAssessment.warnings) {
261
- if (projectReadReportedTruncation && !drainAssessment.fetchFailed && warning.omission == null) continue;
270
+ if (truncated && !drainAssessment.fetchFailed && warning.omission == null) continue;
262
271
  appendDedupedWarning(warnings, warning);
263
272
  }
264
273
  fetchFailed = fetchFailed || drainAssessment.fetchFailed;
265
- if (drainAssessment.truncated) truncationRecovery = "none";
266
- const projectTruncated = truncationRecovery != null;
267
- const scopeTooLarge = !fetchFailed && truncationRecovery === "narrow-scope";
268
- if (scopeTooLarge || !fetchFailed && projectTruncated && !warnings.some((warning) => warning.omission != null)) appendDedupedWarning(warnings, incompleteReadWarning(options.providerId, domain, options.connectionId, scopeTooLarge ? "Some projects hold more issues than one read can return; narrow the scope to read the rest." : "Some issues were omitted; the provider returned an incomplete result.", scopeTooLarge ? "scope-too-large" : "exhausted"));
269
- reconcileOmissionsWithFailure(warnings, fetchFailed);
274
+ requestFailed = requestFailed || drainAssessment.requestFailed;
275
+ truncated = truncated || drainAssessment.truncated;
276
+ for (const { project, recovery, scope } of cuts) appendDedupedWarning(warnings, projectCutWarning(options.providerId, domain, options.connectionId, resourceLabel(project), recovery, scope));
277
+ if (!fetchFailed && truncated && !warnings.some((warning) => warning.omission != null)) appendDedupedWarning(warnings, incompleteReadWarning(options.providerId, domain, options.connectionId, "Some issues were omitted; the provider returned an incomplete result.", "exhausted"));
278
+ reconcileOmissionsWithFailure(warnings, requestFailed);
270
279
  const retryPages = retryWindowPages();
271
280
  const cursor = toIssueTrackerPageCursor({
272
281
  currentPage: page + 1,
@@ -283,7 +292,7 @@ async function listIssueTrackerIssuesPage(ctx, options) {
283
292
  page: {
284
293
  currentPage: page,
285
294
  itemsPerPage: orderedItems.length,
286
- truncated: projectTruncated || void 0
295
+ truncated: truncated || void 0
287
296
  },
288
297
  hasMore: nextPage != null && cursor != null,
289
298
  cursor,