@gitkraken/core-gitlens 0.5.107 → 0.5.109

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 (145) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/dist/git/models/issue.d.ts +70 -0
  3. package/dist/git/models/issue.d.ts.map +1 -1
  4. package/dist/git/models/issue.js +17 -0
  5. package/dist/git/models/issue.js.map +1 -1
  6. package/dist/git/utils/issue.utils.d.ts +41 -1
  7. package/dist/git/utils/issue.utils.d.ts.map +1 -1
  8. package/dist/git/utils/issue.utils.js +80 -0
  9. package/dist/git/utils/issue.utils.js.map +1 -1
  10. package/dist/plus/git-github/api/github.d.ts +33 -6
  11. package/dist/plus/git-github/api/github.d.ts.map +1 -1
  12. package/dist/plus/git-github/api/github.js +137 -15
  13. package/dist/plus/git-github/api/github.js.map +1 -1
  14. package/dist/plus/git-github/api/issueSearchQuery.d.ts +49 -5
  15. package/dist/plus/git-github/api/issueSearchQuery.d.ts.map +1 -1
  16. package/dist/plus/git-github/api/issueSearchQuery.js +64 -6
  17. package/dist/plus/git-github/api/issueSearchQuery.js.map +1 -1
  18. package/dist/plus/integrations/collectionMetadata.d.ts +18 -0
  19. package/dist/plus/integrations/collectionMetadata.d.ts.map +1 -1
  20. package/dist/plus/integrations/collectionMetadata.js +24 -0
  21. package/dist/plus/integrations/collectionMetadata.js.map +1 -1
  22. package/dist/plus/integrations/index.d.ts +1 -1
  23. package/dist/plus/integrations/index.d.ts.map +1 -1
  24. package/dist/plus/integrations/index.js.map +1 -1
  25. package/dist/plus/integrations/integrationService.d.ts +15 -9
  26. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  27. package/dist/plus/integrations/integrationService.js.map +1 -1
  28. package/dist/plus/integrations/manager.d.ts +115 -26
  29. package/dist/plus/integrations/manager.d.ts.map +1 -1
  30. package/dist/plus/integrations/models/gitHostIntegration.d.ts +6 -31
  31. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  32. package/dist/plus/integrations/models/gitHostIntegration.js +11 -1
  33. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  34. package/dist/plus/integrations/models/issueReads.d.ts +63 -1
  35. package/dist/plus/integrations/models/issueReads.d.ts.map +1 -1
  36. package/dist/plus/integrations/models/issuesIntegration.d.ts +7 -21
  37. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
  38. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  39. package/dist/plus/integrations/providerFilters.d.ts +1 -1
  40. package/dist/plus/integrations/providerFilters.d.ts.map +1 -1
  41. package/dist/plus/integrations/providers/azureDevOps.d.ts.map +1 -1
  42. package/dist/plus/integrations/providers/azureDevOps.js +1 -0
  43. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  44. package/dist/plus/integrations/providers/github.d.ts.map +1 -1
  45. package/dist/plus/integrations/providers/github.js +1 -0
  46. package/dist/plus/integrations/providers/github.js.map +1 -1
  47. package/dist/plus/integrations/providers/gitlab.d.ts.map +1 -1
  48. package/dist/plus/integrations/providers/gitlab.js +1 -0
  49. package/dist/plus/integrations/providers/gitlab.js.map +1 -1
  50. package/dist/plus/integrations/providers/issueSorts.d.ts +36 -0
  51. package/dist/plus/integrations/providers/issueSorts.d.ts.map +1 -0
  52. package/dist/plus/integrations/providers/issueSorts.js +76 -0
  53. package/dist/plus/integrations/providers/issueSorts.js.map +1 -0
  54. package/dist/plus/integrations/providers/jira.d.ts +3 -9
  55. package/dist/plus/integrations/providers/jira.d.ts.map +1 -1
  56. package/dist/plus/integrations/providers/jira.js +5 -1
  57. package/dist/plus/integrations/providers/jira.js.map +1 -1
  58. package/dist/plus/integrations/providers/linear.d.ts +24 -9
  59. package/dist/plus/integrations/providers/linear.d.ts.map +1 -1
  60. package/dist/plus/integrations/providers/linear.js +63 -5
  61. package/dist/plus/integrations/providers/linear.js.map +1 -1
  62. package/dist/plus/integrations/providers/models.d.ts +63 -7
  63. package/dist/plus/integrations/providers/models.d.ts.map +1 -1
  64. package/dist/plus/integrations/providers/models.js +55 -0
  65. package/dist/plus/integrations/providers/models.js.map +1 -1
  66. package/dist/plus/integrations/providers/providersApi.d.ts +18 -3
  67. package/dist/plus/integrations/providers/providersApi.d.ts.map +1 -1
  68. package/dist/plus/integrations/providers/providersApi.js +24 -7
  69. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  70. package/dist/plus/integrations/providers/trello.d.ts +3 -9
  71. package/dist/plus/integrations/providers/trello.d.ts.map +1 -1
  72. package/dist/plus/integrations/providers/trello.js +1 -0
  73. package/dist/plus/integrations/providers/trello.js.map +1 -1
  74. package/dist/plus/integrations/providers/utils/providerPaging.d.ts.map +1 -1
  75. package/dist/plus/integrations/providers/utils/providerPaging.js +5 -1
  76. package/dist/plus/integrations/providers/utils/providerPaging.js.map +1 -1
  77. package/dist/plus/integrations/reads/broaden.d.ts +6 -0
  78. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -1
  79. package/dist/plus/integrations/reads/broaden.js.map +1 -1
  80. package/dist/plus/integrations/reads/counts.d.ts +8 -1
  81. package/dist/plus/integrations/reads/counts.d.ts.map +1 -1
  82. package/dist/plus/integrations/reads/counts.js.map +1 -1
  83. package/dist/plus/integrations/reads/filters.d.ts +59 -9
  84. package/dist/plus/integrations/reads/filters.d.ts.map +1 -1
  85. package/dist/plus/integrations/reads/filters.js +17 -0
  86. package/dist/plus/integrations/reads/filters.js.map +1 -1
  87. package/dist/plus/integrations/reads/issueTracker.d.ts +18 -1
  88. package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
  89. package/dist/plus/integrations/reads/issueTracker.js +40 -3
  90. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  91. package/dist/plus/integrations/reads/issues.d.ts +27 -3
  92. package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
  93. package/dist/plus/integrations/reads/issues.js +223 -123
  94. package/dist/plus/integrations/reads/issues.js.map +1 -1
  95. package/dist/plus/integrations/reads/ordering.d.ts +101 -0
  96. package/dist/plus/integrations/reads/ordering.d.ts.map +1 -0
  97. package/dist/plus/integrations/reads/ordering.js +48 -0
  98. package/dist/plus/integrations/reads/ordering.js.map +1 -0
  99. package/dist/plus/integrations/reads/searchIssues.d.ts +7 -2
  100. package/dist/plus/integrations/reads/searchIssues.d.ts.map +1 -1
  101. package/dist/plus/integrations/reads/searchIssues.js +9 -3
  102. package/dist/plus/integrations/reads/searchIssues.js.map +1 -1
  103. package/dist/plus/integrations/reads/sweeps.d.ts.map +1 -1
  104. package/dist/plus/integrations/reads/sweeps.js +120 -56
  105. package/dist/plus/integrations/reads/sweeps.js.map +1 -1
  106. package/dist/plus/integrations/reads/warnings.d.ts +34 -2
  107. package/dist/plus/integrations/reads/warnings.d.ts.map +1 -1
  108. package/dist/plus/integrations/reads/warnings.js +47 -2
  109. package/dist/plus/integrations/reads/warnings.js.map +1 -1
  110. package/dist/plus/integrations/results.d.ts +10 -0
  111. package/dist/plus/integrations/results.d.ts.map +1 -1
  112. package/dist/plus/integrations/results.js.map +1 -1
  113. package/package.json +2 -2
  114. package/src/git/models/issue.ts +73 -0
  115. package/src/git/utils/issue.utils.ts +91 -1
  116. package/src/plus/git-github/api/github.ts +180 -20
  117. package/src/plus/git-github/api/issueSearchQuery.ts +67 -7
  118. package/src/plus/integrations/collectionMetadata.ts +24 -0
  119. package/src/plus/integrations/index.ts +1 -0
  120. package/src/plus/integrations/integrationService.ts +15 -10
  121. package/src/plus/integrations/manager.ts +116 -31
  122. package/src/plus/integrations/models/gitHostIntegration.ts +18 -31
  123. package/src/plus/integrations/models/issueReads.ts +65 -1
  124. package/src/plus/integrations/models/issuesIntegration.ts +7 -6
  125. package/src/plus/integrations/providerFilters.ts +2 -0
  126. package/src/plus/integrations/providers/azureDevOps.ts +1 -0
  127. package/src/plus/integrations/providers/github.ts +1 -0
  128. package/src/plus/integrations/providers/gitlab.ts +1 -0
  129. package/src/plus/integrations/providers/issueSorts.ts +89 -0
  130. package/src/plus/integrations/providers/jira.ts +9 -3
  131. package/src/plus/integrations/providers/linear.ts +77 -8
  132. package/src/plus/integrations/providers/models.ts +122 -5
  133. package/src/plus/integrations/providers/providersApi.ts +51 -13
  134. package/src/plus/integrations/providers/trello.ts +4 -3
  135. package/src/plus/integrations/providers/utils/providerPaging.ts +6 -1
  136. package/src/plus/integrations/reads/broaden.ts +6 -0
  137. package/src/plus/integrations/reads/counts.ts +8 -1
  138. package/src/plus/integrations/reads/filters.ts +70 -8
  139. package/src/plus/integrations/reads/issueTracker.ts +78 -4
  140. package/src/plus/integrations/reads/issues.ts +386 -194
  141. package/src/plus/integrations/reads/ordering.ts +124 -0
  142. package/src/plus/integrations/reads/searchIssues.ts +15 -3
  143. package/src/plus/integrations/reads/sweeps.ts +150 -76
  144. package/src/plus/integrations/reads/warnings.ts +72 -1
  145. package/src/plus/integrations/results.ts +10 -0
@@ -4,6 +4,7 @@ import type {
4
4
  IssueSearchCapabilities,
5
5
  IssueSearchCriteria,
6
6
  IssueSearchRelationship,
7
+ IssueSorting,
7
8
  PullRequestFilter,
8
9
  PullRequestSearchCapabilities,
9
10
  PullRequestSearchCriteria,
@@ -237,6 +238,12 @@ export function resolveIssueSearchCriteria(
237
238
  if (criteria.state != null && criteria.state !== 'open' && !supported.states) {
238
239
  unsupported.push('state');
239
240
  }
241
+ // Folded into the same `unsupported-criteria` rejection rather than given its own reason, so a caller asking
242
+ // for one inexpressible filter and one inexpressible sort learns about both at once instead of fixing them one
243
+ // refusal at a time. The `sorts` list is `supported`'s own, so this needs no branch on which surface it is.
244
+ if (criteria.sort != null && !supported.sorts.includes(criteria.sort)) {
245
+ unsupported.push(`sort:${criteria.sort}`);
246
+ }
240
247
 
241
248
  if (unsupported.length > 0) return { rejection: { reason: 'unsupported-criteria', criteria: unsupported } };
242
249
 
@@ -264,6 +271,7 @@ const unsupportedIssueSearchCapabilities: IssueSearchCapabilities = {
264
271
  createdAfter: false,
265
272
  withoutLinkedPullRequest: false,
266
273
  states: false,
274
+ sorts: [],
267
275
  };
268
276
 
269
277
  /** What a provider with no filtered pull-request search reports. */
@@ -313,6 +321,57 @@ export function resolveIssueSearchScope(
313
321
  return { rejection: 'unscoped' };
314
322
  }
315
323
 
324
+ /**
325
+ * What {@link getSupportedFilters} reports: every vocabulary a provider's reads can express, so a consumer can
326
+ * narrow to it BEFORE issuing a read. See that function for the capability table itself and why intersecting
327
+ * against this is what keeps a filtered read from being refused.
328
+ *
329
+ * Named rather than written inline, because the same shape is the declared return type of THREE declarations that
330
+ * all describe one value — this function, `IntegrationService.getSupportedFilters`, which only forwards to it, and
331
+ * `IntegrationManager.getSupportedFilters`, which publishes it. Spelled out three times, a member added here
332
+ * reached one of them and left the others a compile error away from the truth; that is exactly how the two sort
333
+ * members below came to be written out three times.
334
+ */
335
+ export type SupportedFilters = {
336
+ pullRequests: PullRequestFilter[];
337
+ pullRequestsAccountWide: PullRequestFilter[];
338
+ /**
339
+ * Criteria and scopes the filtered pull-request search can express. Always present; an empty relationship list
340
+ * means the provider exposes no filtered pull-request search.
341
+ */
342
+ pullRequestSearch: PullRequestSearchCapabilities;
343
+ /**
344
+ * Filters the repo-scoped issue read accepts — and, for an issue tracker (Jira/Linear/Trello), its
345
+ * project-scoped read, that being its only issue surface. A tracker therefore reports here and leaves
346
+ * `issuesAccountWide` empty, so intersecting a tracker against that field would read "cannot filter" for a
347
+ * provider that filters fine.
348
+ */
349
+ issues: IssueFilter[];
350
+ issuesAccountWide: IssueFilter[];
351
+ /**
352
+ * What the filtered issue search (and the count probe over the same criteria) can express. Always present: a
353
+ * provider with no filtered issue search reports an empty `relationships` and all-false flags, which is the
354
+ * signal to hide that surface rather than to hide individual chips.
355
+ */
356
+ issueSearch: IssueSearchCapabilities;
357
+ /**
358
+ * Sort keys the repo-scoped issue read can express — and, for an issue tracker, its project-scoped read. Empty
359
+ * means the read can't be ordered, so pass no `sort`.
360
+ *
361
+ * A key here is expressible on ONE provider query. A page spanning several repositories or projects is a merge,
362
+ * and a merge can only order by what a normalized issue carries, so `priority`/`dueDate`/`resolved` are refused
363
+ * there even where they are listed. Read one scope at a time to use them.
364
+ */
365
+ issueSorts: IssueSorting[];
366
+ /**
367
+ * Sort keys the account-wide issue read can express — a different vocabulary, not a subset: for GitLab the two
368
+ * reads are different APIs. Needs no mergeability caveat, because every account-wide read is a union of several
369
+ * queries, so only keys a merge can honor are listed at all. Empty for an issue tracker, which reports under
370
+ * `issueSorts`.
371
+ */
372
+ issueSortsAccountWide: IssueSorting[];
373
+ };
374
+
316
375
  /**
317
376
  * The filters `listPullRequestsPage`/`listIssuesPage` (and the sweeps) accept for a provider, so a caller can
318
377
  * narrow to what the provider can express BEFORE issuing the read.
@@ -345,19 +404,19 @@ export function resolveIssueSearchScope(
345
404
  * `pullRequestSearch` describes the separate filtered PR search. Its table declares each criteria vocabulary
346
405
  * plus repository/organization scope support; an empty relationship list means the search itself is absent.
347
406
  *
407
+ * `issueSorts` / `issueSortsAccountWide` / `issueSearch.sorts` are the ORDERING vocabulary of those same three
408
+ * issue reads, split the same way and for the same reason (GitLab's repo-scoped read is GraphQL and its
409
+ * account-wide read is REST, with genuinely different sort vocabularies). Empty means the read can't be ordered,
410
+ * so pass no `sort`; a key not listed refuses the whole read exactly like an inexpressible filter. Note a key
411
+ * listed here is expressible on ONE provider query: a read that fans out across projects can only honor a key
412
+ * derivable from a normalized issue, so it refuses `priority`/`dueDate`/`resolved` on top of this table.
413
+ *
348
414
  * Note this is a CAPABILITY table — "what the provider can express" — not a recommendation. A consumer
349
415
  * matching another tool's behavior may deliberately pass fewer filters than are listed here (or none, where an
350
416
  * already-scoped read would only be narrowed by them). Intersecting against this table is what keeps a
351
417
  * filtered read from being refused; it isn't a directive to use every filter in it.
352
418
  */
353
- export function getSupportedFilters(providerId: IntegrationIds): {
354
- pullRequests: PullRequestFilter[];
355
- pullRequestsAccountWide: PullRequestFilter[];
356
- pullRequestSearch: PullRequestSearchCapabilities;
357
- issues: IssueFilter[];
358
- issuesAccountWide: IssueFilter[];
359
- issueSearch: IssueSearchCapabilities;
360
- } {
419
+ export function getSupportedFilters(providerId: IntegrationIds): SupportedFilters {
361
420
  const metadata = providersMetadata[providerId];
362
421
  const issueSearch = metadata?.supportedIssueSearch;
363
422
  const pullRequestSearch = metadata?.supportedPullRequestSearch;
@@ -385,6 +444,9 @@ export function getSupportedFilters(providerId: IntegrationIds): {
385
444
  ...issueSearch,
386
445
  // Copied, so mutating the result can't corrupt the metadata table.
387
446
  relationships: [...(issueSearch?.relationships ?? [])],
447
+ sorts: [...(issueSearch?.sorts ?? [])],
388
448
  },
449
+ issueSorts: [...(metadata?.supportedIssueSorts ?? [])],
450
+ issueSortsAccountWide: [...(metadata?.supportedAccountWideIssueSorts ?? [])],
389
451
  };
390
452
  }
@@ -1,5 +1,5 @@
1
1
  import type { CollectionMetadata } from '@gitkraken/provider-apis';
2
- import type { IssueShape } from '../../../git/models/issue.js';
2
+ import type { IssueShape, IssueSorting } from '../../../git/models/issue.js';
3
3
  import type { ResourceDescriptor } from '../../../git/models/resourceDescriptor.js';
4
4
  import { mapBounded } from '../../../utils/promise.js';
5
5
  import { mergeAssessmentInto } from '../collectionMetadata.js';
@@ -15,7 +15,14 @@ import type { ProviderReadContext } from './context.js';
15
15
  import { parseIssueTrackerPageCursor, toIssueTrackerPageCursor } from './cursors.js';
16
16
  import { runCaptured } from './drains.js';
17
17
  import { projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg } from './hierarchy.utils.js';
18
- import { incompleteReadWarning, issueTrackerOnlySurfaceWarning, otherWarning } from './warnings.js';
18
+ import { resolveIssueSort, toIssueOrdering } from './ordering.js';
19
+ import {
20
+ incompleteReadWarning,
21
+ issueTrackerOnlySurfaceWarning,
22
+ otherWarning,
23
+ unmergeableIssueSortWarning,
24
+ unsupportedIssueSortWarning,
25
+ } from './warnings.js';
19
26
 
20
27
  export async function listIssueTrackerIssuesPage(
21
28
  ctx: ProviderReadContext,
@@ -26,6 +33,23 @@ export async function listIssueTrackerIssuesPage(
26
33
  filters?: IssueFilter[];
27
34
  /** Broadens the read to every assignee. Scopes to user-assigned issues when omitted. */
28
35
  includeAllAssignees?: boolean;
36
+ /**
37
+ * How to order the issues, as `field:direction`. Omitted orders most-recently-updated-first where the
38
+ * tracker can express it.
39
+ *
40
+ * Validated against `getSupportedFilters().issues`' sibling `issueSorts` — a tracker reports there, not
41
+ * under the account-wide table, because resource -> project IS its only issue surface. A key it can't
42
+ * express refuses the read rather than serving a differently-ordered list.
43
+ *
44
+ * One page can span SEVERAL projects, whose issues are merged here, so a key no normalized issue carries
45
+ * (`priority`, `dueDate`, `resolved`) is refused for a multi-project page even though the tracker orders by
46
+ * it perfectly well within one project.
47
+ *
48
+ * Unlike the git-host reads this one is safe to change mid-pagination: its cursor windows PROJECTS and
49
+ * drains each to exhaustion, so which projects a round covers doesn't depend on how their issues are
50
+ * ordered. Only the order within a page changes.
51
+ */
52
+ sort?: IssueSorting;
29
53
  forceSync?: boolean;
30
54
  page?: number;
31
55
  cursor?: string;
@@ -128,6 +152,19 @@ export async function listIssueTrackerIssuesPage(
128
152
 
129
153
  const domain = ctx.domainForRead(integration, options.providerId, options.connectionId);
130
154
 
155
+ // Before any upstream request: the discovery fan-outs below (resources, projects, per-resource accounts) are
156
+ // three round trips, and an order this tracker can't express refuses the read whatever they return. Placed after
157
+ // `domain` only because every warning carries it.
158
+ const resolvedSort = resolveIssueSort(providersMetadata[options.providerId]?.supportedIssueSorts, options.sort);
159
+ if (resolvedSort.rejection != null) {
160
+ warnings.push(
161
+ unsupportedIssueSortWarning(options.providerId, domain, options.connectionId, resolvedSort.rejection),
162
+ );
163
+ return emptyPage(true);
164
+ }
165
+
166
+ const sort = resolvedSort.sort;
167
+
131
168
  await ctx.forceRefreshIfRequested(integration, options.forceSync, options.connectionId);
132
169
 
133
170
  const { value: resources, warning: resourcesWarning } = await runCaptured(
@@ -362,6 +399,37 @@ export async function listIssueTrackerIssuesPage(
362
399
  });
363
400
  }
364
401
 
402
+ // A read spanning several projects is a merge, so it can only honor a key a normalized issue carries. Bound here
403
+ // rather than next to the key's validation above, because the project count is only known now — while still
404
+ // before any project read, so a refusal costs no request. Refused rather than served as concatenated
405
+ // per-project runs, which would look ordered without being so.
406
+ //
407
+ // Counted over every project the READ covers, not the `scopedProjects` window this page happens to hold: the
408
+ // window is `itemsPerPage` projects wide, so counting it would refuse a 20-project page and then serve the
409
+ // final 1-project page of the same paginated call — the same key answered two ways, with the earlier pages'
410
+ // issues missing from a read that reported success.
411
+ //
412
+ // Projects are not the only thing a tracker read merges: a user-scoped read asking for several relationships
413
+ // issues one provider query PER relationship and unions them within each project (Jira runs one JQL drain per
414
+ // filter and folds them into one map, in filter order), so a single-project page of two filters is just as
415
+ // concatenated as a two-project one. Counting only projects published those runs under the requested key.
416
+ // Not gated on the provider: the two trackers that don't fan out (Linear, Trello) declare `Assignee` as their
417
+ // only supported filter, so a multi-filter read is already refused above and can never reach this.
418
+ const mergesFilters = options.includeAllAssignees !== true && (options.filters?.length ?? 0) > 1;
419
+ const ordering = toIssueOrdering(sort, scopedProjectsWithUsers.length > 1 || mergesFilters);
420
+ if (ordering.unmergeable != null) {
421
+ warnings.push(
422
+ unmergeableIssueSortWarning(
423
+ options.providerId,
424
+ domain,
425
+ options.connectionId,
426
+ ordering.unmergeable,
427
+ scopedProjectsWithUsers.length > 1 ? 'projects' : 'filters',
428
+ ),
429
+ );
430
+ return emptyPage(true);
431
+ }
432
+
365
433
  const perProject = await mapBounded(scopedProjects, providerFanOutConcurrency, async project => ({
366
434
  project: project,
367
435
  ...(await runCaptured(options.providerId, domain, options.connectionId, () =>
@@ -370,6 +438,7 @@ export async function listIssueTrackerIssuesPage(
370
438
  {
371
439
  user: userForProject(project),
372
440
  filters: options.filters,
441
+ sort: sort,
373
442
  },
374
443
  options.connectionId,
375
444
  ),
@@ -417,6 +486,11 @@ export async function listIssueTrackerIssuesPage(
417
486
  }
418
487
  }
419
488
 
489
+ // Each provider query arrived ordered by the tracker; the union of them — across projects, and across the
490
+ // per-relationship queries a multi-filter read fans out into — is not, and the union is what this read
491
+ // publishes. A no-op when nothing merged, since the tracker already ordered that single run.
492
+ const orderedItems = ordering.order(items);
493
+
420
494
  const drainAssessment = mergeAssessmentInto(
421
495
  warnings,
422
496
  options.providerId,
@@ -463,9 +537,9 @@ export async function listIssueTrackerIssuesPage(
463
537
  : [],
464
538
  });
465
539
  return {
466
- items: items,
540
+ items: orderedItems,
467
541
  warnings: warnings,
468
- page: { currentPage: page, itemsPerPage: items.length, truncated: projectTruncated || undefined },
542
+ page: { currentPage: page, itemsPerPage: orderedItems.length, truncated: projectTruncated || undefined },
469
543
  // Failed-project retries alone are manual. Only an untouched project window is automatic progress.
470
544
  hasMore: nextPage != null && cursor != null,
471
545
  cursor: cursor,