@gitkraken/core-gitlens 0.5.113 → 0.5.115

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 (349) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/dist/git/cache.d.ts +4 -0
  3. package/dist/git/cache.d.ts.map +1 -1
  4. package/dist/git/cache.js +33 -0
  5. package/dist/git/cache.js.map +1 -1
  6. package/dist/git/context.d.ts +10 -0
  7. package/dist/git/context.d.ts.map +1 -1
  8. package/dist/git/context.js.map +1 -1
  9. package/dist/git/errors.d.ts +6 -3
  10. package/dist/git/errors.d.ts.map +1 -1
  11. package/dist/git/errors.js +9 -1
  12. package/dist/git/errors.js.map +1 -1
  13. package/dist/git/features.d.ts +1 -1
  14. package/dist/git/features.d.ts.map +1 -1
  15. package/dist/git/features.js +38 -0
  16. package/dist/git/features.js.map +1 -1
  17. package/dist/git/gitHealth.d.ts +153 -0
  18. package/dist/git/gitHealth.d.ts.map +1 -0
  19. package/dist/git/gitHealth.js +353 -0
  20. package/dist/git/gitHealth.js.map +1 -0
  21. package/dist/git/models/search.d.ts +6 -3
  22. package/dist/git/models/search.d.ts.map +1 -1
  23. package/dist/git/models/search.js +5 -1
  24. package/dist/git/models/search.js.map +1 -1
  25. package/dist/git/parsers/diffParser.d.ts +8 -2
  26. package/dist/git/parsers/diffParser.d.ts.map +1 -1
  27. package/dist/git/parsers/diffParser.js +27 -4
  28. package/dist/git/parsers/diffParser.js.map +1 -1
  29. package/dist/git/providers/config.d.ts +36 -3
  30. package/dist/git/providers/config.d.ts.map +1 -1
  31. package/dist/git/providers/graph.d.ts +9 -0
  32. package/dist/git/providers/graph.d.ts.map +1 -1
  33. package/dist/git/providers/maintenance.d.ts +222 -0
  34. package/dist/git/providers/maintenance.d.ts.map +1 -0
  35. package/dist/git/providers/maintenance.js +20 -0
  36. package/dist/git/providers/maintenance.js.map +1 -0
  37. package/dist/git/providers/operations.d.ts +10 -0
  38. package/dist/git/providers/operations.d.ts.map +1 -1
  39. package/dist/git/providers/pausedOperations.d.ts +3 -0
  40. package/dist/git/providers/pausedOperations.d.ts.map +1 -1
  41. package/dist/git/providers/provider.d.ts +2 -0
  42. package/dist/git/providers/provider.d.ts.map +1 -1
  43. package/dist/git/providers/tags.d.ts +3 -0
  44. package/dist/git/providers/tags.d.ts.map +1 -1
  45. package/dist/git/repositoryService.d.ts +1 -0
  46. package/dist/git/repositoryService.d.ts.map +1 -1
  47. package/dist/git/repositoryService.js.map +1 -1
  48. package/dist/git/run.types.d.ts +8 -0
  49. package/dist/git/run.types.d.ts.map +1 -1
  50. package/dist/git/service.js +3 -0
  51. package/dist/git/service.js.map +1 -1
  52. package/dist/git/utils/search.utils.d.ts.map +1 -1
  53. package/dist/git/utils/search.utils.js +112 -5
  54. package/dist/git/utils/search.utils.js.map +1 -1
  55. package/dist/git/watching/watchService.d.ts +3 -0
  56. package/dist/git/watching/watchService.d.ts.map +1 -1
  57. package/dist/git/watching/watchService.js +8 -0
  58. package/dist/git/watching/watchService.js.map +1 -1
  59. package/dist/git-cli/cliGitProvider.d.ts +11 -0
  60. package/dist/git-cli/cliGitProvider.d.ts.map +1 -1
  61. package/dist/git-cli/cliGitProvider.js +12 -0
  62. package/dist/git-cli/cliGitProvider.js.map +1 -1
  63. package/dist/git-cli/exec/git.d.ts +23 -1
  64. package/dist/git-cli/exec/git.d.ts.map +1 -1
  65. package/dist/git-cli/exec/git.js +83 -8
  66. package/dist/git-cli/exec/git.js.map +1 -1
  67. package/dist/git-cli/exec/gitQueue.d.ts +2 -0
  68. package/dist/git-cli/exec/gitQueue.d.ts.map +1 -1
  69. package/dist/git-cli/exec/gitQueue.js +19 -17
  70. package/dist/git-cli/exec/gitQueue.js.map +1 -1
  71. package/dist/git-cli/parsers/logParser.d.ts.map +1 -1
  72. package/dist/git-cli/parsers/logParser.js +4 -1
  73. package/dist/git-cli/parsers/logParser.js.map +1 -1
  74. package/dist/git-cli/providers/blame.d.ts.map +1 -1
  75. package/dist/git-cli/providers/blame.js +4 -7
  76. package/dist/git-cli/providers/blame.js.map +1 -1
  77. package/dist/git-cli/providers/branches.d.ts.map +1 -1
  78. package/dist/git-cli/providers/branches.js +13 -3
  79. package/dist/git-cli/providers/branches.js.map +1 -1
  80. package/dist/git-cli/providers/commits.d.ts.map +1 -1
  81. package/dist/git-cli/providers/commits.js +34 -3
  82. package/dist/git-cli/providers/commits.js.map +1 -1
  83. package/dist/git-cli/providers/config.d.ts +27 -0
  84. package/dist/git-cli/providers/config.d.ts.map +1 -1
  85. package/dist/git-cli/providers/config.js +31 -6
  86. package/dist/git-cli/providers/config.js.map +1 -1
  87. package/dist/git-cli/providers/graph.d.ts +8 -0
  88. package/dist/git-cli/providers/graph.d.ts.map +1 -1
  89. package/dist/git-cli/providers/graph.js +85 -6
  90. package/dist/git-cli/providers/graph.js.map +1 -1
  91. package/dist/git-cli/providers/maintenance.d.ts +306 -0
  92. package/dist/git-cli/providers/maintenance.d.ts.map +1 -0
  93. package/dist/git-cli/providers/maintenance.js +2129 -0
  94. package/dist/git-cli/providers/maintenance.js.map +1 -0
  95. package/dist/git-cli/providers/operations.d.ts +1 -0
  96. package/dist/git-cli/providers/operations.d.ts.map +1 -1
  97. package/dist/git-cli/providers/operations.js +8 -0
  98. package/dist/git-cli/providers/operations.js.map +1 -1
  99. package/dist/git-cli/providers/pausedOperations.d.ts +1 -0
  100. package/dist/git-cli/providers/pausedOperations.d.ts.map +1 -1
  101. package/dist/git-cli/providers/pausedOperations.js +1 -1
  102. package/dist/git-cli/providers/pausedOperations.js.map +1 -1
  103. package/dist/git-cli/providers/tags.d.ts +3 -0
  104. package/dist/git-cli/providers/tags.d.ts.map +1 -1
  105. package/dist/git-cli/providers/tags.js +23 -1
  106. package/dist/git-cli/providers/tags.js.map +1 -1
  107. package/dist/plus/agents/agentCapabilities.d.ts +77 -0
  108. package/dist/plus/agents/agentCapabilities.d.ts.map +1 -0
  109. package/dist/plus/agents/agentCapabilities.js +197 -0
  110. package/dist/plus/agents/agentCapabilities.js.map +1 -0
  111. package/dist/plus/agents/providers/claudeCodeTranscript.d.ts +77 -13
  112. package/dist/plus/agents/providers/claudeCodeTranscript.d.ts.map +1 -1
  113. package/dist/plus/agents/providers/claudeCodeTranscript.js +263 -55
  114. package/dist/plus/agents/providers/claudeCodeTranscript.js.map +1 -1
  115. package/dist/plus/agents/providers/{claudeCodeProvider.d.ts → gkAgentProvider.d.ts} +52 -19
  116. package/dist/plus/agents/providers/gkAgentProvider.d.ts.map +1 -0
  117. package/dist/plus/agents/providers/{claudeCodeProvider.js → gkAgentProvider.js} +935 -262
  118. package/dist/plus/agents/providers/gkAgentProvider.js.map +1 -0
  119. package/dist/plus/agents/types.d.ts +113 -35
  120. package/dist/plus/agents/types.d.ts.map +1 -1
  121. package/dist/plus/agents/types.js +7 -4
  122. package/dist/plus/agents/types.js.map +1 -1
  123. package/dist/plus/ai/models/promptTemplates.d.ts +1 -1
  124. package/dist/plus/ai/models/promptTemplates.d.ts.map +1 -1
  125. package/dist/plus/ai/prompts.d.ts +4 -0
  126. package/dist/plus/ai/prompts.d.ts.map +1 -1
  127. package/dist/plus/ai/prompts.js +59 -4
  128. package/dist/plus/ai/prompts.js.map +1 -1
  129. package/dist/plus/git-github/api/github.d.ts +44 -1
  130. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  131. package/dist/plus/git-github/api/github.js +355 -62
  132. package/dist/plus/git-github/api/github.js.map +1 -1
  133. package/dist/plus/git-github/api/issueSearchQuery.d.ts +13 -0
  134. package/dist/plus/git-github/api/issueSearchQuery.d.ts.map +1 -1
  135. package/dist/plus/git-github/api/issueSearchQuery.js +41 -0
  136. package/dist/plus/git-github/api/issueSearchQuery.js.map +1 -1
  137. package/dist/plus/git-github/providers/github/blame.d.ts.map +1 -1
  138. package/dist/plus/git-github/providers/github/blame.js +2 -1
  139. package/dist/plus/git-github/providers/github/blame.js.map +1 -1
  140. package/dist/plus/integrations/collectionMetadata.d.ts +2 -0
  141. package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
  142. package/dist/plus/integrations/collectionMetadata.js +17 -1
  143. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  144. package/dist/plus/integrations/context.d.ts +4 -1
  145. package/dist/plus/integrations/context.d.ts.map +1 -1
  146. package/dist/plus/integrations/index.d.ts +1 -0
  147. package/dist/plus/integrations/index.d.ts.map +1 -1
  148. package/dist/plus/integrations/integrationService.d.ts +20 -1
  149. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  150. package/dist/plus/integrations/integrationService.js +12 -4
  151. package/dist/plus/integrations/integrationService.js.map +1 -1
  152. package/dist/plus/integrations/manager.d.ts +48 -0
  153. package/dist/plus/integrations/manager.d.ts.map +1 -1
  154. package/dist/plus/integrations/models/gitHostIntegration.d.ts +53 -26
  155. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  156. package/dist/plus/integrations/models/gitHostIntegration.js +123 -47
  157. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  158. package/dist/plus/integrations/models/integration.d.ts +10 -1
  159. package/dist/plus/integrations/models/integration.d.ts.map +1 -1
  160. package/dist/plus/integrations/models/integration.js +52 -19
  161. package/dist/plus/integrations/models/integration.js.map +1 -1
  162. package/dist/plus/integrations/models/issueCache.d.ts +16 -0
  163. package/dist/plus/integrations/models/issueCache.d.ts.map +1 -0
  164. package/dist/plus/integrations/models/issueCache.js +9 -0
  165. package/dist/plus/integrations/models/issueCache.js.map +1 -0
  166. package/dist/plus/integrations/models/issueReads.d.ts +13 -0
  167. package/dist/plus/integrations/models/issueReads.d.ts.map +1 -1
  168. package/dist/plus/integrations/models/issuesIntegration.d.ts +11 -13
  169. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
  170. package/dist/plus/integrations/models/issuesIntegration.js +15 -1
  171. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  172. package/dist/plus/integrations/providers/azureDevOps.d.ts +3 -5
  173. package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -1
  174. package/dist/plus/integrations/providers/azureDevOps.js +9 -8
  175. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  176. package/dist/plus/integrations/providers/bitbucket-server.d.ts +3 -5
  177. package/dist/plus/integrations/providers/bitbucket-server.d.ts.map +1 -1
  178. package/dist/plus/integrations/providers/bitbucket-server.js +2 -2
  179. package/dist/plus/integrations/providers/bitbucket-server.js.map +1 -1
  180. package/dist/plus/integrations/providers/bitbucket.d.ts +3 -5
  181. package/dist/plus/integrations/providers/bitbucket.d.ts.map +1 -1
  182. package/dist/plus/integrations/providers/bitbucket.js +2 -1
  183. package/dist/plus/integrations/providers/bitbucket.js.map +1 -1
  184. package/dist/plus/integrations/providers/github.d.ts +12 -5
  185. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  186. package/dist/plus/integrations/providers/github.js +10 -3
  187. package/dist/plus/integrations/providers/github.js.map +1 -1
  188. package/dist/plus/integrations/providers/gitlab.d.ts +3 -5
  189. package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
  190. package/dist/plus/integrations/providers/gitlab.js +2 -2
  191. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  192. package/dist/plus/integrations/providers/jira.d.ts +3 -7
  193. package/dist/plus/integrations/providers/jira.d.ts.map +1 -1
  194. package/dist/plus/integrations/providers/jira.js +35 -22
  195. package/dist/plus/integrations/providers/jira.js.map +1 -1
  196. package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +7 -0
  197. package/dist/plus/integrations/providers/jiraIssueByKey.d.ts.map +1 -0
  198. package/dist/plus/integrations/providers/jiraIssueByKey.js +102 -0
  199. package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -0
  200. package/dist/plus/integrations/providers/linear.d.ts +3 -7
  201. package/dist/plus/integrations/providers/linear.d.ts.map +1 -1
  202. package/dist/plus/integrations/providers/linear.js +23 -22
  203. package/dist/plus/integrations/providers/linear.js.map +1 -1
  204. package/dist/plus/integrations/providers/models.d.ts +3 -1
  205. package/dist/plus/integrations/providers/models.d.ts.map +1 -1
  206. package/dist/plus/integrations/providers/models.js +10 -4
  207. package/dist/plus/integrations/providers/models.js.map +1 -1
  208. package/dist/plus/integrations/providers/providerErrors.d.ts +7 -0
  209. package/dist/plus/integrations/providers/providerErrors.d.ts.map +1 -0
  210. package/dist/plus/integrations/providers/providerErrors.js +91 -0
  211. package/dist/plus/integrations/providers/providerErrors.js.map +1 -0
  212. package/dist/plus/integrations/providers/providersApi.d.ts +3 -2
  213. package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
  214. package/dist/plus/integrations/providers/providersApi.js +18 -52
  215. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  216. package/dist/plus/integrations/providers/trello.d.ts +2 -7
  217. package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
  218. package/dist/plus/integrations/providers/trello.js +3 -1
  219. package/dist/plus/integrations/providers/trello.js.map +1 -1
  220. package/dist/plus/integrations/providers/utils/providerPaging.d.ts +2 -0
  221. package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
  222. package/dist/plus/integrations/providers/utils/providerPaging.js +12 -7
  223. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  224. package/dist/plus/integrations/reads/broaden.d.ts +47 -10
  225. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
  226. package/dist/plus/integrations/reads/broaden.js +144 -33
  227. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  228. package/dist/plus/integrations/reads/filters.d.ts +18 -0
  229. package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
  230. package/dist/plus/integrations/reads/filters.js +37 -1
  231. package/dist/plus/integrations/reads/filters.js.map +1 -1
  232. package/dist/plus/integrations/reads/issueBatch.d.ts +51 -0
  233. package/dist/plus/integrations/reads/issueBatch.d.ts.map +1 -0
  234. package/dist/plus/integrations/reads/issueBatch.js +91 -0
  235. package/dist/plus/integrations/reads/issueBatch.js.map +1 -0
  236. package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
  237. package/dist/plus/integrations/reads/issueTracker.js +23 -17
  238. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  239. package/dist/plus/integrations/reads/sweeps.d.ts.map +1 -1
  240. package/dist/plus/integrations/reads/sweeps.js +5 -3
  241. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  242. package/dist/plus/integrations/reads/trackerIssue.d.ts +21 -0
  243. package/dist/plus/integrations/reads/trackerIssue.d.ts.map +1 -0
  244. package/dist/plus/integrations/reads/trackerIssue.js +51 -0
  245. package/dist/plus/integrations/reads/trackerIssue.js.map +1 -0
  246. package/dist/plus/integrations/reads/warnings.d.ts +2 -15
  247. package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
  248. package/dist/plus/integrations/reads/warnings.js +19 -1
  249. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  250. package/dist/plus/integrations/results.d.ts +4 -20
  251. package/dist/plus/integrations/results.d.ts.map +1 -1
  252. package/dist/plus/integrations/results.js.map +1 -1
  253. package/dist/utils/promise.d.ts +2 -0
  254. package/dist/utils/promise.d.ts.map +1 -1
  255. package/dist/utils/promise.js +11 -0
  256. package/dist/utils/promise.js.map +1 -1
  257. package/dist/utils/promiseCache.d.ts +19 -0
  258. package/dist/utils/promiseCache.d.ts.map +1 -1
  259. package/dist/utils/promiseCache.js +30 -0
  260. package/dist/utils/promiseCache.js.map +1 -1
  261. package/dist/utils/resourceUsage.d.ts +5 -0
  262. package/dist/utils/resourceUsage.d.ts.map +1 -0
  263. package/dist/utils/resourceUsage.js +2 -0
  264. package/dist/utils/resourceUsage.js.map +1 -0
  265. package/dist/utils/string.d.ts +12 -0
  266. package/dist/utils/string.d.ts.map +1 -1
  267. package/dist/utils/string.js +36 -1
  268. package/dist/utils/string.js.map +1 -1
  269. package/docs/integrations.md +48 -33
  270. package/docs/kepler-read-api-parity.md +25 -4
  271. package/package.json +12 -4
  272. package/src/git/cache.ts +36 -0
  273. package/src/git/context.ts +11 -0
  274. package/src/git/errors.ts +13 -1
  275. package/src/git/features.ts +51 -0
  276. package/src/git/gitHealth.ts +522 -0
  277. package/src/git/models/search.ts +19 -4
  278. package/src/git/parsers/diffParser.ts +27 -4
  279. package/src/git/providers/config.ts +42 -1
  280. package/src/git/providers/graph.ts +12 -0
  281. package/src/git/providers/maintenance.ts +224 -0
  282. package/src/git/providers/operations.ts +10 -0
  283. package/src/git/providers/pausedOperations.ts +3 -0
  284. package/src/git/providers/provider.ts +2 -0
  285. package/src/git/providers/tags.ts +1 -0
  286. package/src/git/repositoryService.ts +1 -0
  287. package/src/git/run.types.ts +8 -0
  288. package/src/git/service.ts +3 -0
  289. package/src/git/utils/search.utils.ts +129 -5
  290. package/src/git/watching/watchService.ts +10 -0
  291. package/src/git-cli/cliGitProvider.ts +25 -0
  292. package/src/git-cli/exec/git.ts +120 -6
  293. package/src/git-cli/exec/gitQueue.ts +19 -17
  294. package/src/git-cli/parsers/logParser.ts +4 -1
  295. package/src/git-cli/providers/blame.ts +3 -8
  296. package/src/git-cli/providers/branches.ts +28 -8
  297. package/src/git-cli/providers/commits.ts +34 -2
  298. package/src/git-cli/providers/config.ts +38 -6
  299. package/src/git-cli/providers/graph.ts +107 -7
  300. package/src/git-cli/providers/maintenance.ts +2599 -0
  301. package/src/git-cli/providers/operations.ts +11 -0
  302. package/src/git-cli/providers/pausedOperations.ts +7 -2
  303. package/src/git-cli/providers/tags.ts +36 -1
  304. package/src/plus/agents/agentCapabilities.ts +286 -0
  305. package/src/plus/agents/providers/claudeCodeTranscript.ts +313 -58
  306. package/src/plus/agents/providers/{claudeCodeProvider.ts → gkAgentProvider.ts} +1123 -277
  307. package/src/plus/agents/types.ts +137 -40
  308. package/src/plus/ai/models/promptTemplates.ts +1 -1
  309. package/src/plus/ai/prompts.ts +62 -4
  310. package/src/plus/git-github/api/github.ts +404 -82
  311. package/src/plus/git-github/api/issueSearchQuery.ts +49 -1
  312. package/src/plus/git-github/providers/github/blame.ts +2 -1
  313. package/src/plus/integrations/collectionMetadata.ts +23 -1
  314. package/src/plus/integrations/context.ts +1 -1
  315. package/src/plus/integrations/index.ts +1 -0
  316. package/src/plus/integrations/integrationService.ts +29 -4
  317. package/src/plus/integrations/manager.ts +48 -0
  318. package/src/plus/integrations/models/gitHostIntegration.ts +165 -129
  319. package/src/plus/integrations/models/integration.ts +57 -22
  320. package/src/plus/integrations/models/issueCache.ts +29 -0
  321. package/src/plus/integrations/models/issueReads.ts +15 -0
  322. package/src/plus/integrations/models/issuesIntegration.ts +38 -8
  323. package/src/plus/integrations/providers/azureDevOps.ts +16 -13
  324. package/src/plus/integrations/providers/bitbucket-server.ts +4 -6
  325. package/src/plus/integrations/providers/bitbucket.ts +3 -4
  326. package/src/plus/integrations/providers/github.ts +23 -7
  327. package/src/plus/integrations/providers/gitlab.ts +4 -6
  328. package/src/plus/integrations/providers/jira.ts +52 -26
  329. package/src/plus/integrations/providers/jiraIssueByKey.ts +155 -0
  330. package/src/plus/integrations/providers/linear.ts +38 -25
  331. package/src/plus/integrations/providers/models.ts +17 -5
  332. package/src/plus/integrations/providers/providerErrors.ts +115 -0
  333. package/src/plus/integrations/providers/providersApi.ts +26 -68
  334. package/src/plus/integrations/providers/trello.ts +5 -4
  335. package/src/plus/integrations/providers/utils/providerPaging.ts +14 -7
  336. package/src/plus/integrations/reads/broaden.ts +282 -70
  337. package/src/plus/integrations/reads/filters.ts +39 -1
  338. package/src/plus/integrations/reads/issueBatch.ts +197 -0
  339. package/src/plus/integrations/reads/issueTracker.ts +26 -24
  340. package/src/plus/integrations/reads/sweeps.ts +19 -12
  341. package/src/plus/integrations/reads/trackerIssue.ts +105 -0
  342. package/src/plus/integrations/reads/warnings.ts +23 -17
  343. package/src/plus/integrations/results.ts +4 -20
  344. package/src/utils/promise.ts +15 -0
  345. package/src/utils/promiseCache.ts +43 -0
  346. package/src/utils/resourceUsage.ts +5 -0
  347. package/src/utils/string.ts +38 -1
  348. package/dist/plus/agents/providers/claudeCodeProvider.d.ts.map +0 -1
  349. package/dist/plus/agents/providers/claudeCodeProvider.js.map +0 -1
@@ -80,6 +80,7 @@ import {
80
80
  gitHubIssueSearchRelationships,
81
81
  toGitHubIssueSearchQualifiers,
82
82
  toGitHubIssueSearchScopeQualifiers,
83
+ toGitHubIssueSearchSlideQualifier,
83
84
  toGitHubIssueSortQualifier,
84
85
  } from './issueSearchQuery.js';
85
86
  import { toGitHubPullRequestSearchFacets } from './pullRequestSearchQuery.js';
@@ -182,6 +183,52 @@ interface AliasedIssueSearch {
182
183
  query: string;
183
184
  }
184
185
 
186
+ /**
187
+ * The sort value of the last item on a page, which is the boundary a ceiling slide continues from.
188
+ *
189
+ * Reads the DATE off the issue rather than taking a caller-supplied position, so the boundary can only ever be a
190
+ * point this walk actually reached. Returns `undefined` for an empty page or a key carrying no date, which is the
191
+ * signal to report the ceiling instead of sliding.
192
+ */
193
+ function lastSortBoundary(issues: readonly IssueShape[], sort: IssueSorting): Date | undefined {
194
+ const last = issues.at(-1);
195
+ if (last == null) return undefined;
196
+
197
+ const value = sort.startsWith('created:')
198
+ ? last.createdDate
199
+ : sort.startsWith('updated:')
200
+ ? last.updatedDate
201
+ : undefined;
202
+ return value instanceof Date && !Number.isNaN(value.getTime()) ? value : undefined;
203
+ }
204
+
205
+ /**
206
+ * The per-alias ceiling-slide state, read defensively off a composite cursor.
207
+ *
208
+ * Keys are `<field>:<alias>`. The colon is what makes them collision-proof: a GraphQL alias is a name (a letter
209
+ * or underscore followed by letters, digits or underscores) and so can never contain one, which is why these need
210
+ * no entry in the reserved-alias check and that list does not have to grow with every field added here.
211
+ *
212
+ * Everything is validated to the shape this module emits, because a cursor is caller-supplied data on a round
213
+ * trip: an unrecognized value restarts the walk instead of reaching the query, which is the difference between a
214
+ * repeated page and an attacker-chosen qualifier re-scoping someone's search.
215
+ */
216
+ function cursorString(cursor: Record<string, unknown> | undefined, key: string): string | undefined {
217
+ const value = cursor?.[key];
218
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
219
+ }
220
+
221
+ function cursorNumber(cursor: Record<string, unknown> | undefined, key: string): number | undefined {
222
+ const value = cursor?.[key];
223
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
224
+ }
225
+
226
+ /** The urls a prior page already served at this alias's inclusive slide boundary. */
227
+ function slideSeenFor(cursor: Record<string, unknown> | undefined, alias: string): Set<string> | undefined {
228
+ const value = cursorString(cursor, `slideSeen:${alias}`);
229
+ return value != null ? new Set(value.split(' ')) : undefined;
230
+ }
231
+
185
232
  /** One page of a multi-search issue request, with its composite cursor across every alias. */
186
233
  export interface AliasedIssueSearchResult {
187
234
  values: IssueShape[];
@@ -3295,6 +3342,18 @@ export class GitHubApi {
3295
3342
  variables: RequestParameters,
3296
3343
  scope: ScopedLogger | undefined,
3297
3344
  cancellation?: AbortSignal | undefined,
3345
+ /**
3346
+ * Accept a response whose only failures are NOT_FOUND, returning its PARTIAL data instead of throwing.
3347
+ *
3348
+ * For a single-entity query a NOT_FOUND is the whole answer, so throwing is right. For an ALIASED batch it
3349
+ * is one slot's answer: GitHub replies 200 with every resolvable alias populated and a NOT_FOUND per
3350
+ * missing one, so throwing discards the results that did resolve. Since a missing coordinate is the common
3351
+ * case for a batch, that would make the read useless for the very question it answers.
3352
+ *
3353
+ * Narrow on purpose: any error that is NOT a NOT_FOUND still throws, so auth, rate-limit and query-cost
3354
+ * failures keep their existing handling rather than being silently reported as a batch of absences.
3355
+ */
3356
+ allowPartialNotFound?: boolean,
3298
3357
  ): Promise<T | undefined> {
3299
3358
  const { accessToken, ...tokenInfo } = token;
3300
3359
  // Only dedupe when no cancellation/request option is in play — sharing a promise that
@@ -3339,6 +3398,16 @@ export class GitHubApi {
3339
3398
  );
3340
3399
  } catch (ex) {
3341
3400
  if (ex instanceof GraphqlResponseError) {
3401
+ // `every`, not `[0]`: a batch can report several, and one non-NOT_FOUND among them is a real
3402
+ // failure that must not be reported as a set of absences.
3403
+ if (
3404
+ allowPartialNotFound &&
3405
+ ex.data != null &&
3406
+ (ex.errors?.every(e => e.type === 'NOT_FOUND') ?? false)
3407
+ ) {
3408
+ return ex.data as T;
3409
+ }
3410
+
3342
3411
  switch (ex.errors?.[0]?.type) {
3343
3412
  case 'NOT_FOUND':
3344
3413
  throw new RequestNotFoundError(ex);
@@ -3959,15 +4028,29 @@ export class GitHubApi {
3959
4028
  // - the ORDER is assigned → mentioned → authored, which is the order the union is emitted in and, because
3960
4029
  // the dedupe keeps the first occurrence of a url, also the precedence between the three. An issue that
3961
4030
  // is both assigned to and authored by the user surfaces as the assigned one.
4031
+ // The per-alias ceiling-slide bound a prior page sealed. Applied HERE because this method owns its query
4032
+ // text: without it the bound would be recorded and never honoured, so the continuation would re-issue the
4033
+ // unbounded query, serve the same first 1.000, and have every one of them filtered out as already-seen —
4034
+ // a page of nothing, repeated until the walk gave up (measured: 20 requests to do 10 requests' work).
4035
+ const bound = this.parseSlidesFromCursor(options?.cursor);
3962
4036
  const searches: AliasedIssueSearch[] = [];
3963
4037
  if (requested.assigned) {
3964
- searches.push({ alias: 'assigned', query: `${search} ${baseFilters} ${assignedQualifier}`.trim() });
4038
+ searches.push({
4039
+ alias: 'assigned',
4040
+ query: `${search} ${baseFilters} ${assignedQualifier} ${bound('assigned') ?? ''}`.trim(),
4041
+ });
3965
4042
  }
3966
4043
  if (requested.mentioned) {
3967
- searches.push({ alias: 'mentioned', query: `${search} ${baseFilters} mentions:@me`.trim() });
4044
+ searches.push({
4045
+ alias: 'mentioned',
4046
+ query: `${search} ${baseFilters} mentions:@me ${bound('mentioned') ?? ''}`.trim(),
4047
+ });
3968
4048
  }
3969
4049
  if (requested.authored) {
3970
- searches.push({ alias: 'authored', query: `${search} ${baseFilters} author:@me`.trim() });
4050
+ searches.push({
4051
+ alias: 'authored',
4052
+ query: `${search} ${baseFilters} author:@me ${bound('authored') ?? ''}`.trim(),
4053
+ });
3971
4054
  }
3972
4055
 
3973
4056
  // Field by field, like `searchIssuesPage`: `options` also carries `repos`/`includeAllAssignees`/`categories`,
@@ -4003,7 +4086,15 @@ export class GitHubApi {
4003
4086
  *
4004
4087
  * With more than one relationship the page is a UNION of several searches, each ordered by the provider; the
4005
4088
  * merged page is re-sorted here so the whole page honors the requested key. Across pages the order is still
4006
- * per-alias — see {@link searchIssuesByAlias}.
4089
+ * per-alias — see {@link searchIssuesByAlias}. Note a union can also serve the same issue on two pages when
4090
+ * its relationships OVERLAP (an issue both authored by and assigned to the user sits at different depths in
4091
+ * the two), which predates the ceiling slide and is a property of unioning independent walks: measured at 50
4092
+ * repeats over 450 issues with no ceiling involved. Dedupe by url if that matters to the surface.
4093
+ *
4094
+ * GitHub's per-query result ceiling is NOT terminal here: an alias that runs out of pages while capped
4095
+ * continues bounded to the far side of its own last item, which is a fresh query with a fresh budget. Bounded
4096
+ * per alias, since they exhaust independently. Only a ceiling an alias cannot slide past (a sort key with no
4097
+ * usable range qualifier) is reported as an omission.
4007
4098
  *
4008
4099
  * Each requested relationship becomes its own aliased search, unioned and deduped by url; with none, a single
4009
4100
  * search runs over the scope alone. `criteria.text` and the other free-form values are sanitized so user input
@@ -4032,17 +4123,22 @@ export class GitHubApi {
4032
4123
  ...toGitHubIssueSearchScopeQualifiers(options?.org, options?.repos),
4033
4124
  ...toGitHubIssueSearchQualifiers(options?.criteria, sort),
4034
4125
  ].join(' ');
4126
+ // A cursor from a walk that already slid past the ceiling carries the bound each alias slid to, and the
4127
+ // query has to be rebuilt with it or that alias would restart at the top and re-serve its first 1.000.
4128
+ // PER ALIAS, because they slide independently: one bound shared across them would sit below where an
4129
+ // early-finishing alias stopped and re-serve everything it had already delivered.
4130
+ const bound = this.parseSlidesFromCursor(options?.cursor);
4035
4131
 
4036
4132
  // One aliased search per relationship, OR-ed by union. They can't be one query: GitHub AND-s qualifiers,
4037
4133
  // so `author:@me assignee:@me` would return the intersection — issues the user both opened and is assigned
4038
4134
  // to — instead of either set. With no relationship the scope + criteria are already the whole query.
4039
4135
  const relationships = options?.criteria?.relationships;
4040
4136
  const searches: AliasedIssueSearch[] = relationships?.length
4041
- ? relationships.map(r => ({
4042
- alias: gitHubIssueSearchRelationships[r].alias,
4043
- query: `${base} ${gitHubIssueSearchRelationships[r].qualifier}`.trim(),
4044
- }))
4045
- : [{ alias: 'matched', query: base }];
4137
+ ? relationships.map(r => {
4138
+ const { alias, qualifier } = gitHubIssueSearchRelationships[r];
4139
+ return { alias: alias, query: `${base} ${qualifier} ${bound(alias) ?? ''}`.trim() };
4140
+ })
4141
+ : [{ alias: 'matched', query: `${base} ${bound('matched') ?? ''}`.trim() }];
4046
4142
 
4047
4143
  // Forwarded field by field rather than spread: `options` also carries `repos`/`org`/`criteria`, which are
4048
4144
  // already baked into `searches[].query` above and which the callee declares nothing about. `sort` is the
@@ -4067,6 +4163,32 @@ export class GitHubApi {
4067
4163
  );
4068
4164
  }
4069
4165
 
4166
+ /**
4167
+ * Reads back the per-alias ceiling-slide bounds a prior page sealed into its cursor.
4168
+ *
4169
+ * Each value is validated against the EXACT shape {@link toGitHubIssueSearchSlideQualifier} emits, not merely
4170
+ * checked for being a string. A cursor is caller-supplied data on a round trip, so an unvalidated value here
4171
+ * would be a qualifier of someone else's choosing appended to the search: `org:evil-corp` in a cursor would
4172
+ * re-scope the read. Anything unrecognized yields no bound, which restarts that alias at the top — a repeated
4173
+ * page, which is the safe failure.
4174
+ */
4175
+ private parseSlidesFromCursor(cursor: string | undefined): (alias: string) => string | undefined {
4176
+ let parsed: Record<string, unknown> | undefined;
4177
+ if (cursor != null) {
4178
+ try {
4179
+ parsed = JSON.parse(cursor) as Record<string, unknown>;
4180
+ } catch {}
4181
+ }
4182
+
4183
+ return alias => {
4184
+ const slide = parsed?.[`slide:${alias}`];
4185
+ return typeof slide === 'string' &&
4186
+ /^(?:created|updated):(?:<=|>=)\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/.test(slide)
4187
+ ? slide
4188
+ : undefined;
4189
+ };
4190
+ }
4191
+
4070
4192
  /**
4071
4193
  * Counts issues for several scopes in ONE request, transferring no issues at all — each scope is an aliased
4072
4194
  * `search` selecting only `issueCount`, with `first: 0` so no nodes are fetched.
@@ -4143,6 +4265,98 @@ export class GitHubApi {
4143
4265
  }
4144
4266
  }
4145
4267
 
4268
+ /**
4269
+ * Resolves several issues BY COORDINATE — `(owner, repo, number)` — in one request, via aliased `repository`
4270
+ * fields rather than aliased searches.
4271
+ *
4272
+ * Aliasing the point read rather than {@link searchIssuesByAlias} is the whole design, and each difference
4273
+ * matters to the caller this exists for (correlating a branch name to the issue it references):
4274
+ * - it resolves by EXACT NUMBER, where a search answers a relevance question;
4275
+ * - no result ceiling applies, so there is no partial window to reason about;
4276
+ * - and a null is a PROVEN ABSENCE rather than "not found within a page budget", which is what makes a miss
4277
+ * cacheable. That last one is the point: a caller that cannot prove absence re-walks its budget forever.
4278
+ *
4279
+ * Returns POSITIONALLY — one slot per input coordinate, in order — for the same reason {@link countIssues}
4280
+ * does: a caller's key is arbitrary text and would break the GraphQL document, so the aliases are generated
4281
+ * and the caller maps back by index. `undefined` in a slot means the issue does not exist (or is not visible
4282
+ * to this token), never that the read failed; a failure throws.
4283
+ */
4284
+ @trace({ args: (provider, token) => ({ provider: provider.name, token: `<token:${token.microHash}>` }) })
4285
+ async getIssuesBatch(
4286
+ provider: Provider,
4287
+ token: GitHubTokenInfo,
4288
+ coordinates: readonly { owner: string; repo: string; number: number }[],
4289
+ options?: { baseUrl?: string; avatarSize?: number; includeBody?: boolean },
4290
+ cancellation?: AbortSignal,
4291
+ ): Promise<(IssueShape | undefined)[]> {
4292
+ const scope = getScopedLogger();
4293
+ if (coordinates.length === 0) return [];
4294
+
4295
+ const params = coordinates
4296
+ .map((_, i) => `$o${i}: String!\n\t\t\t\t$n${i}: String!\n\t\t\t\t$k${i}: Int!`)
4297
+ .join('\n\t\t\t\t');
4298
+ const fields = coordinates
4299
+ .map(
4300
+ (_, i) => `i${i}: repository(owner: $o${i}, name: $n${i}) {
4301
+ issue(number: $k${i}) {
4302
+ ${gqIssueFragment}${options?.includeBody ? '\n\t\t\t\t\t\tbody' : ''}
4303
+ }
4304
+ }`,
4305
+ )
4306
+ .join('\n\t\t\t\t');
4307
+ const query = `query getIssuesBatch(
4308
+ ${params}
4309
+ $avatarSize: Int
4310
+ ) {
4311
+ ${fields}
4312
+ }`;
4313
+
4314
+ const variables: Record<string, unknown> = {
4315
+ baseUrl: options?.baseUrl,
4316
+ avatarSize: options?.avatarSize,
4317
+ };
4318
+ coordinates.forEach((c, i) => {
4319
+ variables[`o${i}`] = c.owner;
4320
+ variables[`n${i}`] = c.repo;
4321
+ variables[`k${i}`] = c.number;
4322
+ });
4323
+
4324
+ try {
4325
+ // `allowPartialNotFound`: a coordinate that does not exist is this read's ANSWER for that slot, not a
4326
+ // failure of the batch. GitHub replies 200 with the resolvable aliases populated and a NOT_FOUND per
4327
+ // missing one, so without this a single bad coordinate would discard every good result — and a miss
4328
+ // is the common outcome for the caller this read exists for.
4329
+ const rsp = await this.graphql<Record<string, { issue?: GitHubIssue | null } | null | undefined>>(
4330
+ provider,
4331
+ token,
4332
+ query,
4333
+ variables,
4334
+ scope,
4335
+ cancellation,
4336
+ true,
4337
+ );
4338
+ if (rsp == null) return coordinates.map(() => undefined);
4339
+
4340
+ // Mapped slot by slot so one unmappable issue can't discard the whole batch, matching the aliased
4341
+ // search's node-by-node mapping. An unmappable issue reads as absent, which is the safe direction
4342
+ // here only because the caller is asking "does this exist", and a false absent costs a re-read
4343
+ // rather than a wrong issue.
4344
+ return coordinates.map((c, i) => {
4345
+ const node = rsp[`i${i}`]?.issue;
4346
+ if (node == null) return undefined;
4347
+
4348
+ try {
4349
+ return fromGitHubIssue(node, provider);
4350
+ } catch (ex) {
4351
+ scope?.warn(`skipped unmappable issue; ${c.owner}/${c.repo}#${c.number}, ex=${ex}`);
4352
+ return undefined;
4353
+ }
4354
+ });
4355
+ } catch (ex) {
4356
+ throw this.handleException(ex, provider, scope);
4357
+ }
4358
+ }
4359
+
4146
4360
  /**
4147
4361
  * The PR twin of {@link countIssues}: counts pull requests for several scopes in ONE request via aliased
4148
4362
  * `search` fields selecting only `issueCount` with `first: 0`. Same positional/undefined contract as the issue
@@ -4281,6 +4495,15 @@ export class GitHubApi {
4281
4495
  interface SearchCursor {
4282
4496
  page?: number;
4283
4497
  truncated?: boolean;
4498
+ /**
4499
+ * Per-alias ceiling-slide state lives under `slide:<alias>`, `slideTs:<alias>` and
4500
+ * `slideSeen:<alias>`, which the index signature below already covers. Keyed with a colon so they
4501
+ * cannot collide with an alias slot, since a GraphQL alias cannot contain one — see the cursor
4502
+ * helpers above.
4503
+ *
4504
+ * `slideSeen:<alias>` is a space-delimited string rather than an array because this object's index
4505
+ * signature holds primitives, and a url cannot contain whitespace, so the separator is unambiguous.
4506
+ */
4284
4507
  /**
4285
4508
  * The order this cursor's pages were produced under: an `IssueSorting`, or `unsortedCursorSort` when
4286
4509
  * the caller asked for none. Written as a value rather than left absent in the no-order case
@@ -4419,19 +4642,31 @@ export class GitHubApi {
4419
4642
  );
4420
4643
  if (rsp == null) return { values: [], hasMore: false, page: page, truncated: false };
4421
4644
 
4422
- // Map node-by-node so one unmappable issue can't discard the whole result set
4423
- const issues: IssueShape[] = [];
4645
+ // Map node-by-node so one unmappable issue can't discard the whole result set. Kept PER ALIAS rather
4646
+ // than flattened straight away: a ceiling slide bounds each alias at its OWN last item, so the
4647
+ // boundary has to be read off that alias's own page and not off the merged one (see below).
4648
+ const issuesByAlias = new Map<string, IssueShape[]>();
4424
4649
  for (const s of active) {
4650
+ const mapped: IssueShape[] = [];
4651
+ // Drop what a previous page already served at this alias's INCLUSIVE slide boundary. Applied
4652
+ // before the merge so the dedupe's precedence rules only ever see genuinely new items.
4653
+ const seen = slideSeenFor(cursor, s.alias);
4425
4654
  for (const node of rsp[s.alias]?.nodes ?? []) {
4426
4655
  if (node?.id == null) continue;
4427
4656
 
4428
4657
  try {
4429
- issues.push(fromGitHubIssue(node, provider));
4658
+ const issue = fromGitHubIssue(node, provider);
4659
+ if (seen?.has(issue.url) !== true) {
4660
+ mapped.push(issue);
4661
+ }
4430
4662
  } catch (ex) {
4431
4663
  scope?.warn(`skipped unmappable issue; id=${node.id}, url=${node.url}, ex=${ex}`);
4432
4664
  }
4433
4665
  }
4666
+ issuesByAlias.set(s.alias, mapped);
4434
4667
  }
4668
+ // Flattened in `active` order, which is `searches` order, so the dedupe's precedence is unchanged.
4669
+ const issues = active.flatMap(s => issuesByAlias.get(s.alias) ?? []);
4435
4670
 
4436
4671
  // Dedupe by `url`, not `IssueShape.id`: for some providers `id` is a per-repository number, so an
4437
4672
  // id-keyed map would collapse distinct issues across repositories.
@@ -4470,30 +4705,124 @@ export class GitHubApi {
4470
4705
  let hasMore = false;
4471
4706
  let continuationMissing = false;
4472
4707
  let maxIssueCount = 0;
4708
+ // Whether ANY alias slid this round. A slide is forward progress, so it makes the read continuable
4709
+ // exactly as an ordinary next-cursor does.
4710
+ let slid = false;
4711
+ // Whether any alias ran out of pages against a capped query WITHOUT being able to slide, which is the
4712
+ // only remaining way this read leaves results unreachable.
4713
+ let strandedByCeiling = false;
4714
+ const sortKey = options.sort;
4473
4715
  for (const s of searches) {
4474
4716
  const category = rsp[s.alias];
4475
4717
  const endCursor =
4476
4718
  category?.pageInfo?.hasNextPage && category.pageInfo.endCursor ? category.pageInfo.endCursor : null;
4477
- next[s.alias] = endCursor;
4478
- if (endCursor != null) {
4479
- hasMore = true;
4480
- }
4481
4719
  if (category?.pageInfo?.hasNextPage === true && category.pageInfo.endCursor == null) {
4482
4720
  continuationMissing = true;
4483
4721
  }
4484
- maxIssueCount = Math.max(maxIssueCount, category?.issueCount ?? 0);
4722
+ const issueCount = category?.issueCount ?? 0;
4723
+ maxIssueCount = Math.max(maxIssueCount, issueCount);
4724
+
4725
+ // The urls already served at THIS alias's trailing timestamp, which its inclusive slide re-serves.
4726
+ // Accumulated across pages while that timestamp holds, and reset when it moves, so it carries one
4727
+ // second's worth of urls rather than the walk's — a tied block from a bulk edit can span several
4728
+ // pages, and rebuilding this from the last page alone would re-serve the earlier ones.
4729
+ const mapped = issuesByAlias.get(s.alias);
4730
+ const trailingTs =
4731
+ mapped != null ? lastSortBoundary(mapped, sortKey ?? defaultIssueSort)?.getTime() : undefined;
4732
+ const trailing = new Set<string>(
4733
+ trailingTs != null && cursorNumber(cursor, `slideTs:${s.alias}`) === trailingTs
4734
+ ? (slideSeenFor(cursor, s.alias) ?? [])
4735
+ : [],
4736
+ );
4737
+ // Accumulated only while this alias is CAPPED, since only a capped alias can reach a slide. Without
4738
+ // that test every page of every search would seal its trailing second's urls for a slide that
4739
+ // will never happen — a rounding error for an ordinary search, where a second holds one or two
4740
+ // issues, but this rides in a cursor `broadenIssues` nests once per org.
4741
+ //
4742
+ // The carry-forward below is NOT gated the same way: a slid query's remainder is often under the
4743
+ // cap, and its first page still needs the filter for the boundary second the inclusive bound
4744
+ // deliberately re-serves. Gating both re-served that block (measured: 100 duplicates).
4745
+ //
4746
+ // Bounded by the reachable window: one second's worth of urls drawn from at most
4747
+ // `githubSearchResultLimit` items, so ~45 KB per alias at the very worst, which takes a bulk edit
4748
+ // stamping a thousand issues with a single timestamp.
4749
+ const carryBoundary = issueCount > githubSearchResultLimit;
4750
+ if (trailingTs != null && carryBoundary) {
4751
+ for (const issue of mapped ?? []) {
4752
+ if (lastSortBoundary([issue], sortKey ?? defaultIssueSort)?.getTime() === trailingTs) {
4753
+ trailing.add(issue.url);
4754
+ }
4755
+ }
4756
+ next[`slideTs:${s.alias}`] = trailingTs;
4757
+ next[`slideSeen:${s.alias}`] = [...trailing].join(' ');
4758
+ } else {
4759
+ // A page can come back EMPTY right after a slide: the re-issued query restarts at the top of
4760
+ // the bounded range, so its first page can be entirely items this set already filtered out.
4761
+ // Dropping the set here would un-filter them on the page after, so carry it forward untouched.
4762
+ next[`slideTs:${s.alias}`] = cursorNumber(cursor, `slideTs:${s.alias}`);
4763
+ next[`slideSeen:${s.alias}`] = cursorString(cursor, `slideSeen:${s.alias}`);
4764
+ }
4765
+
4766
+ // A CEILING SLIDE turns the per-query cap from terminal into continuable: re-issuing THIS alias's
4767
+ // search bounded to the far side of its own last item gives its remainder a full budget. Bounded
4768
+ // PER ALIAS and not once for the page, because aliases exhaust independently: one bound taken
4769
+ // from the merged page would sit below where an early-finishing alias stopped and re-serve
4770
+ // everything it had already delivered.
4771
+ let aliasSlide: string | undefined;
4772
+ // Capped AND out of pages is where the ceiling actually bites. Evaluated on its own rather than
4773
+ // inside the slide attempt below: a read that cannot slide at all (no order requested, so no
4774
+ // boundary to slide from) must still REPORT the ceiling, and folding the two together silently
4775
+ // dropped that for `searchMyIssues`, whose ordering is opt-in.
4776
+ const strandedHere = endCursor == null && issueCount > githubSearchResultLimit;
4777
+ if (strandedHere && sortKey != null) {
4778
+ const boundary = mapped != null ? lastSortBoundary(mapped, sortKey) : undefined;
4779
+ aliasSlide = boundary != null ? toGitHubIssueSearchSlideQualifier(sortKey, boundary) : undefined;
4780
+ // A bound identical to the one this alias is ALREADY under would re-issue the same query
4781
+ // forever. NOT needed for correctness, and that is measured rather than assumed: against a
4782
+ // provider that honours the bound there is no walk that reaches it. The nearest case, a tied
4783
+ // block larger than the cap, terminates on its own instead — every page inside the block is
4784
+ // filtered away by `slideSeen` until the walk ends on an empty page with no boundary to slide
4785
+ // from, so the second slide is never computed (verified: 1.200 issues sharing one second serve
4786
+ // the reachable 1.000 and report the ceiling, with this guard never firing).
4787
+ //
4788
+ // It is here purely as defence in depth against a bound that silently does nothing, because
4789
+ // that failure mode is an unbounded request loop rather than a wrong answer, and this read
4790
+ // walks until a provider tells it to stop. Cheap insurance against the one bug class that
4791
+ // cannot be shrugged off.
4792
+ if (aliasSlide != null && aliasSlide === cursorString(cursor, `slide:${s.alias}`)) {
4793
+ aliasSlide = undefined;
4794
+ }
4795
+ }
4796
+ if (strandedHere && aliasSlide == null) {
4797
+ strandedByCeiling = true;
4798
+ }
4799
+
4800
+ if (aliasSlide != null) {
4801
+ // Restart this alias inside its new bound: a position from the old query is meaningless there.
4802
+ next[s.alias] = undefined;
4803
+ next[`slide:${s.alias}`] = aliasSlide;
4804
+ slid = true;
4805
+ } else {
4806
+ next[s.alias] = endCursor;
4807
+ // Carry the bound this alias is already walking under, or it would restart at the top.
4808
+ next[`slide:${s.alias}`] = cursorString(cursor, `slide:${s.alias}`);
4809
+ if (endCursor != null) {
4810
+ hasMore = true;
4811
+ }
4812
+ }
4485
4813
  }
4486
4814
 
4487
- // GitHub search exposes at most `githubSearchResultLimit` results PER SEARCH, so the ceiling is
4488
- // reached as soon as any one alias exceeds it. Paging removes the old 100-item truncation; only that
4489
- // upstream ceiling or an unusable continuation leaves the read incomplete.
4490
- const truncated =
4491
- cursor?.truncated === true || maxIssueCount > githubSearchResultLimit || continuationMissing;
4815
+ // Only the ceiling an alias could NOT slide past is incompleteness. A capped alias with pages left has
4816
+ // nothing to slide past yet, and one that slid has its remainder reachable through the cursor: marking
4817
+ // either would seal `truncated: true` and keep it there for the rest of the read, so a completed walk
4818
+ // would report results it went on to fetch as omitted.
4819
+ const truncated = cursor?.truncated === true || strandedByCeiling || continuationMissing;
4492
4820
  next.truncated = truncated || undefined;
4821
+ const continuable = hasMore || slid;
4493
4822
  return {
4494
4823
  values: deduped,
4495
- cursor: hasMore ? JSON.stringify(next) : undefined,
4496
- hasMore: hasMore,
4824
+ cursor: continuable ? JSON.stringify(next) : undefined,
4825
+ hasMore: continuable,
4497
4826
  page: page,
4498
4827
  truncated: truncated,
4499
4828
  totalCount: maxIssueCount,
@@ -4775,36 +5104,42 @@ export class GitHubApi {
4775
5104
  const scope = getScopedLogger();
4776
5105
  const pageSize = 10;
4777
5106
  const include = options?.include?.length ? options.include : undefined;
4778
- const requiresPagination = shouldPaginateGitHubSearchState(include);
4779
-
4780
- interface SearchResult {
4781
- search: {
4782
- pageInfo: {
4783
- endCursor?: string | null;
4784
- hasNextPage: boolean;
4785
- };
4786
- nodes: GitHubPullRequest[];
4787
- };
4788
- }
5107
+ const stateSearches = requiresGitHubPullRequestStateFacets(include)
5108
+ ? [
5109
+ { alias: 'opened', qualifier: 'is:open' },
5110
+ { alias: 'merged', qualifier: 'is:merged' },
5111
+ ]
5112
+ : [{ alias: 'search', qualifier: toGitHubSearchStateQualifier(include) }];
5113
+
5114
+ type SearchResult = Record<
5115
+ string,
5116
+ | {
5117
+ nodes: GitHubPullRequest[];
5118
+ }
5119
+ | undefined
5120
+ >;
4789
5121
 
4790
5122
  try {
4791
- const query = `query searchPullRequests(
4792
- $searchQuery: String!
4793
- $cursor: String
4794
- $avatarSize: Int
4795
- ) {
4796
- search(first: ${pageSize}, after: $cursor, query: $searchQuery, type: ISSUE) {
4797
- pageInfo {
4798
- endCursor
4799
- hasNextPage
4800
- }
5123
+ const params = stateSearches.map(({ alias }) =>
5124
+ alias === 'search' ? '$searchQuery: String!' : `$${alias}SearchQuery: String!`,
5125
+ );
5126
+ const fields = stateSearches.map(({ alias }) => {
5127
+ const fieldAlias = alias === 'search' ? '' : `${alias}: `;
5128
+ const variable = alias === 'search' ? 'searchQuery' : `${alias}SearchQuery`;
5129
+ return `${fieldAlias}search(first: ${pageSize}, query: $${variable}, type: ISSUE) {
4801
5130
  nodes {
4802
5131
  ...on PullRequest {
4803
5132
  ${gqlPullRequestFragment}
4804
5133
  ${gqlPullRequestStackFragmentFor(options)}
4805
5134
  }
4806
5135
  }
4807
- }
5136
+ }`;
5137
+ });
5138
+ const query = `query searchPullRequests(
5139
+ ${params.join('\n\t')}
5140
+ $avatarSize: Int
5141
+ ) {
5142
+ ${fields.join('\n\t')}
4808
5143
  }`;
4809
5144
 
4810
5145
  let search = options?.search?.trim() ?? '';
@@ -4818,44 +5153,32 @@ export class GitHubApi {
4818
5153
  search += `${repo}${options.repos.join(repo)}`;
4819
5154
  }
4820
5155
 
4821
- const searchQuery = ['is:pr', toGitHubSearchStateQualifier(include), 'archived:false', search.trim()]
4822
- .filter(Boolean)
4823
- .join(' ');
4824
-
4825
- // Bound the paginated case with a defensive page backstop like the other paged provider drains, so a
4826
- // large, low-match result set can't fan out into an unbounded request loop.
4827
- const maxSearchPages = 20;
4828
- let cursor: string | undefined;
4829
- const results: PullRequest[] = [];
4830
- for (let page = 0; page < maxSearchPages; page++) {
4831
- const rsp = await this.graphql<SearchResult>(
4832
- provider,
4833
- token,
4834
- query,
4835
- {
4836
- searchQuery: searchQuery,
4837
- cursor: cursor,
4838
- baseUrl: options?.baseUrl,
4839
- avatarSize: options?.avatarSize,
4840
- },
4841
- scope,
4842
- cancellation,
4843
- );
4844
- if (rsp == null) return results;
5156
+ const variables: Record<string, unknown> = {
5157
+ baseUrl: options?.baseUrl,
5158
+ avatarSize: options?.avatarSize,
5159
+ };
5160
+ for (const { alias, qualifier } of stateSearches) {
5161
+ const variable = alias === 'search' ? 'searchQuery' : `${alias}SearchQuery`;
5162
+ variables[variable] = ['is:pr', qualifier, 'archived:false', search.trim()].filter(Boolean).join(' ');
5163
+ }
4845
5164
 
4846
- const pageResults = filterPullRequestsBySearchState(
4847
- rsp.search.nodes.map(pr => fromGitHubPullRequest(pr, provider)),
4848
- include,
4849
- );
4850
- results.push(...pageResults);
5165
+ const rsp = await this.graphql<SearchResult>(provider, token, query, variables, scope, cancellation);
5166
+ if (rsp == null) return [];
4851
5167
 
4852
- cursor = rsp.search.pageInfo.endCursor ?? undefined;
4853
- if (!requiresPagination || results.length >= pageSize || !rsp.search.pageInfo.hasNextPage) {
4854
- break;
5168
+ const results: PullRequest[] = [];
5169
+ for (const { alias } of stateSearches) {
5170
+ for (const node of rsp[alias]?.nodes ?? []) {
5171
+ results.push(fromGitHubPullRequest(node, provider));
4855
5172
  }
4856
5173
  }
4857
5174
 
4858
- return results.slice(0, pageSize);
5175
+ return [
5176
+ ...uniqueBy(
5177
+ filterPullRequestsBySearchState(results, include),
5178
+ pr => pr.url,
5179
+ (original, _current) => original,
5180
+ ),
5181
+ ].slice(0, pageSize);
4859
5182
  } catch (ex) {
4860
5183
  throw this.handleException(ex, provider, scope);
4861
5184
  }
@@ -5110,8 +5433,7 @@ export function toGitHubSearchStateQualifier(include: PullRequestState[] | undef
5110
5433
  if (opened && closed && merged) return ''; // all states -> no qualifier
5111
5434
  if (opened && closed) return 'is:unmerged';
5112
5435
  if (closed && merged) return 'is:closed';
5113
- // `opened && merged` isn't expressible as a single AND qualifier; omit it here and post-filter.
5114
- if (opened && merged) return '';
5436
+ if (opened && merged) throw new Error('GitHub cannot express opened and merged as one search state qualifier');
5115
5437
  if (opened) return 'is:open';
5116
5438
  if (merged) return 'is:merged';
5117
5439
  if (closed) return 'is:closed is:unmerged';
@@ -5131,7 +5453,7 @@ export function filterPullRequestsBySearchState<T extends { state: PullRequestSt
5131
5453
  return pullRequests.filter(pr => allowedStates.has(pr.state));
5132
5454
  }
5133
5455
 
5134
- function shouldPaginateGitHubSearchState(include: PullRequestState[] | undefined): boolean {
5456
+ function requiresGitHubPullRequestStateFacets(include: PullRequestState[] | undefined): boolean {
5135
5457
  if (include == null || include.length === 0) return false;
5136
5458
 
5137
5459
  const uniqueStates = new Set<PullRequestState>(include);