@gitkraken/core-gitlens 0.11.1 → 0.12.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 (57) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/dist/git/models/issue.d.ts +3 -1
  3. package/dist/git/models/issue.js.map +1 -1
  4. package/dist/git/utils/issue.utils.d.ts +3 -1
  5. package/dist/git/utils/issue.utils.js +5 -1
  6. package/dist/git/utils/issue.utils.js.map +1 -1
  7. package/dist/plus/integrations/models/gitHostIntegration.js +3 -1
  8. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  9. package/dist/plus/integrations/models/integration.js +1 -1
  10. package/dist/plus/integrations/providers/azure/azure.js +3 -2
  11. package/dist/plus/integrations/providers/azure/azure.js.map +1 -1
  12. package/dist/plus/integrations/providers/azure/models.d.ts +27 -10
  13. package/dist/plus/integrations/providers/azure/models.js +39 -19
  14. package/dist/plus/integrations/providers/azure/models.js.map +1 -1
  15. package/dist/plus/integrations/providers/azure/workItemStateCategories.d.ts +19 -0
  16. package/dist/plus/integrations/providers/azure/workItemStateCategories.js +32 -0
  17. package/dist/plus/integrations/providers/azure/workItemStateCategories.js.map +1 -0
  18. package/dist/plus/integrations/providers/azureDevOps.js +5 -5
  19. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  20. package/dist/plus/integrations/providers/issueSorts.d.ts +0 -1
  21. package/dist/plus/integrations/providers/jira-server.js +11 -5
  22. package/dist/plus/integrations/providers/jira-server.js.map +1 -1
  23. package/dist/plus/integrations/providers/jira.d.ts +5 -2
  24. package/dist/plus/integrations/providers/jira.js +45 -30
  25. package/dist/plus/integrations/providers/jira.js.map +1 -1
  26. package/dist/plus/integrations/providers/jiraIssueByKey.d.ts +3 -4
  27. package/dist/plus/integrations/providers/jiraIssueByKey.js +3 -4
  28. package/dist/plus/integrations/providers/jiraIssueByKey.js.map +1 -1
  29. package/dist/plus/integrations/providers/linearIssuesEtag.d.ts +5 -5
  30. package/dist/plus/integrations/providers/linearIssuesEtag.js +6 -6
  31. package/dist/plus/integrations/providers/linearIssuesEtag.js.map +1 -1
  32. package/dist/plus/integrations/providers/models.d.ts +11 -1
  33. package/dist/plus/integrations/providers/models.js +9 -9
  34. package/dist/plus/integrations/providers/models.js.map +1 -1
  35. package/dist/plus/integrations/providers/providersApi.d.ts +30 -32
  36. package/dist/plus/integrations/providers/providersApi.js +149 -46
  37. package/dist/plus/integrations/providers/providersApi.js.map +1 -1
  38. package/dist/plus/integrations/providers/utils/jiraIssueDrains.d.ts +27 -15
  39. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js +72 -10
  40. package/dist/plus/integrations/providers/utils/jiraIssueDrains.js.map +1 -1
  41. package/dist/plus/integrations/reads/cursors.js +1 -1
  42. package/dist/plus/integrations/reads/drains.d.ts +0 -1
  43. package/dist/plus/integrations/reads/hierarchy.js +2 -2
  44. package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
  45. package/dist/plus/integrations/reads/hierarchy.utils.d.ts +13 -0
  46. package/dist/plus/integrations/reads/hierarchy.utils.js +26 -1
  47. package/dist/plus/integrations/reads/hierarchy.utils.js.map +1 -1
  48. package/dist/plus/integrations/reads/issueTracker.js +4 -4
  49. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  50. package/dist/plus/integrations/reads/issues.js +1 -1
  51. package/dist/plus/integrations/results.d.ts +12 -2
  52. package/dist/plus/integrations/results.js.map +1 -1
  53. package/dist/plus/integrations/utils/integration.utils.d.ts +6 -1
  54. package/dist/plus/integrations/utils/integration.utils.js +8 -1
  55. package/dist/plus/integrations/utils/integration.utils.js.map +1 -1
  56. package/docs/integrations.md +49 -19
  57. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import { ProviderReference } from "../../../../git/models/remoteProvider.js";
2
2
  import { IssueShape } from "../../../../git/models/issue.js";
3
3
  import { IssueFilter } from "../../providerFilters.js";
4
- import { ProviderIssue } from "../models.js";
4
+ import { JiraIssuePage, ProviderIssue } from "../models.js";
5
5
  import { CollectionMetadata, CollectionScopeFailure } from "@gitkraken/provider-apis";
6
6
  //#region ../plus/integrations/src/providers/utils/jiraIssueDrains.d.ts
7
7
  /**
@@ -14,16 +14,10 @@ export type JiraUserScope = {
14
14
  assigneeLogins?: string[];
15
15
  mentionLogin?: string;
16
16
  };
17
- /** One page of a Jira issue search, whether it names one project or several. */
18
- export type JiraIssuePage = {
19
- data: ProviderIssue[];
20
- hasMore: boolean;
21
- nextCursor: string | undefined;
22
- } | undefined;
23
17
  export type JiraIssuePageFetcher = (scope: JiraUserScope, cursor: string | undefined) => Promise<JiraIssuePage>;
24
18
  /**
25
- * Three states rather than a boolean: the backstop is recoverable by narrowing the scope, a page error or a stalled
26
- * cursor is not, and `ProjectIssuesDrain` makes the caller say which.
19
+ * Three states rather than a boolean: the backstop is recoverable by narrowing the scope, a page error, a stalled
20
+ * cursor or a page that dropped issues is not, and `ProjectIssuesDrain` makes the caller say which.
27
21
  */
28
22
  export type JiraIssuesDrain = {
29
23
  issues: ProviderIssue[];
@@ -37,7 +31,7 @@ export type JiraUserScopedRead = {
37
31
  };
38
32
  export type JiraDrainOptions = {
39
33
  maxPages: number;
40
- /** Where a failure that stops the drain is recorded. */
34
+ /** Where a failure that stops the drain, or that a page reported, is recorded. */
41
35
  failureScope: CollectionScopeFailure['scope'];
42
36
  /** Names the provider in the failures this drain synthesizes. */
43
37
  providerName: string;
@@ -47,10 +41,27 @@ export type JiraDrainOptions = {
47
41
  */
48
42
  isEmptyFailure?: (ex: unknown) => boolean;
49
43
  };
44
+ /**
45
+ * A search page's metadata with its failures recorded under `scope`. provider-apis scopes a page failure to what it
46
+ * searched (a Cloud site, or nothing below the provider on Data Center, which would publish it as a failure of the
47
+ * whole connection); the caller knows what it read, e.g. one project. Omissions keep their own scope.
48
+ */
49
+ export declare function withFailureScope(metadata: CollectionMetadata, scope: CollectionScopeFailure['scope']): CollectionMetadata;
50
+ /**
51
+ * Folds what a drain's pages reported into one metadata, with one failure per scope and kind however many pages
52
+ * reported one. provider-apis reports the issues a page dropped per page, naming that page's keys, so the failures
53
+ * of a drain over many unreadable pages never dedupe and would publish one warning per page. The first page's
54
+ * failure is kept, naming its keys, and says how many more pages dropped issues too.
55
+ */
56
+ export declare function summarizePageMetadata(pages: readonly CollectionMetadata[]): CollectionMetadata | undefined;
50
57
  /**
51
58
  * Follows a search's cursor up to `maxPages`. A page failure after the first page leaves the already-drained prefix
52
59
  * intact and records the failure instead of re-throwing and discarding the prefix; if nothing was fetched yet, the
53
60
  * throw propagates so the caller sees a hard error rather than an empty partial success.
61
+ *
62
+ * A page that dropped issues it could not read (its metadata says so) makes the drain `incomplete` without stopping
63
+ * it: the issues it did read and the pages after it are kept, and its failures are recorded under `failureScope`,
64
+ * which names what this drain reads (e.g. one project) more narrowly than the site the SDK searched.
54
65
  */
55
66
  export declare function drainJiraIssues(fetchPage: JiraIssuePageFetcher, scope: JiraUserScope, options: JiraDrainOptions): Promise<JiraIssuesDrain>;
56
67
  /**
@@ -72,11 +83,12 @@ export declare function readUserScopedJiraIssues(fetchPage: JiraIssuePageFetcher
72
83
  export declare function toUniqueJiraIssueShapes(issues: readonly ProviderIssue[], provider: ProviderReference): IssueShape[];
73
84
  /**
74
85
  * Reads the current user's issues of several projects with one search per relationship, split back to the projects
75
- * in `projectIds` order by each issue's `project.id`. Undefined when the search did not complete or an issue belongs
76
- * to no requested project, so the caller reads them one by one: a search's pages are global to its project set, and
77
- * an incomplete one says nothing about which of its projects it covered. Its failures are therefore never
78
- * published, and the waste is bounded by one project's page budget, paid only by a search that needed more than
79
- * that or failed partway.
86
+ * in `projectIds` order by each issue's `project.id`. Undefined when the search did not complete (including a page
87
+ * that dropped an issue it could not read) or an issue belongs to no requested project, so the caller reads them one
88
+ * by one: a search's pages are global to its project set, and an incomplete one says nothing about which of its
89
+ * projects it covered. Its failures are therefore never published, and the waste is bounded by one project's page
90
+ * budget, paid by a search that needed more than that or did not read cleanly. An issue that stays unreadable makes
91
+ * every such search pay it, as Linear's does for an incomplete search.
80
92
  */
81
93
  export declare function searchJiraProjectIssues(fetchPage: JiraIssuePageFetcher, projectIds: readonly string[], filters: readonly IssueFilter[] | undefined, identities: {
82
94
  userField: string;
@@ -1,20 +1,76 @@
1
1
  import { Logger as _logger } from "../../../../utils/logger.js";
2
- import { throwIfCallerContractError, toCollectionScopeFailure } from "../../collectionMetadata.js";
2
+ import { collectionScopeKey } from "../../results.js";
3
+ import { isIncompleteCollection, throwIfCallerContractError, toCollectionScopeFailure } from "../../collectionMetadata.js";
3
4
  import "../../providerFilters.js";
4
5
  import { mergeCollectionMetadata } from "./providerPaging.js";
5
6
  import { toIssueShape } from "../models.js";
6
7
  import { splitProjectIssuesSearch } from "../../models/issuesIntegration.js";
7
8
  //#region ../plus/integrations/src/providers/utils/jiraIssueDrains.ts
8
9
  /**
10
+ * A search page's metadata with its failures recorded under `scope`. provider-apis scopes a page failure to what it
11
+ * searched (a Cloud site, or nothing below the provider on Data Center, which would publish it as a failure of the
12
+ * whole connection); the caller knows what it read, e.g. one project. Omissions keep their own scope.
13
+ */
14
+ function withFailureScope(metadata, scope) {
15
+ if (metadata.failures == null) return metadata;
16
+ return {
17
+ ...metadata,
18
+ failures: metadata.failures.map((failure) => ({
19
+ ...failure,
20
+ scope
21
+ }))
22
+ };
23
+ }
24
+ /**
25
+ * Folds what a drain's pages reported into one metadata, with one failure per scope and kind however many pages
26
+ * reported one. provider-apis reports the issues a page dropped per page, naming that page's keys, so the failures
27
+ * of a drain over many unreadable pages never dedupe and would publish one warning per page. The first page's
28
+ * failure is kept, naming its keys, and says how many more pages dropped issues too.
29
+ */
30
+ function summarizePageMetadata(pages) {
31
+ let summary;
32
+ const failures = /* @__PURE__ */ new Map();
33
+ for (const { failures: pageFailures, ...page } of pages) {
34
+ summary = mergeCollectionMetadata(summary, page);
35
+ for (const failure of pageFailures ?? []) {
36
+ const key = `${collectionScopeKey(failure.scope)}\0${failure.kind}`;
37
+ const group = failures.get(key);
38
+ if (group == null) failures.set(key, {
39
+ failure,
40
+ pages: 1
41
+ });
42
+ else group.pages++;
43
+ }
44
+ }
45
+ if (failures.size === 0) return summary;
46
+ return mergeCollectionMetadata(summary, {
47
+ completeness: "partial",
48
+ failures: Array.from(failures.values(), ({ failure, pages: count }) => count === 1 ? failure : {
49
+ ...failure,
50
+ message: `${failure.message != null ? `${failure.message} ` : ""}Issues on ${count - 1} more ${count === 2 ? "page" : "pages"} could not be read either.`
51
+ })
52
+ });
53
+ }
54
+ /**
9
55
  * Follows a search's cursor up to `maxPages`. A page failure after the first page leaves the already-drained prefix
10
56
  * intact and records the failure instead of re-throwing and discarding the prefix; if nothing was fetched yet, the
11
57
  * throw propagates so the caller sees a hard error rather than an empty partial success.
58
+ *
59
+ * A page that dropped issues it could not read (its metadata says so) makes the drain `incomplete` without stopping
60
+ * it: the issues it did read and the pages after it are kept, and its failures are recorded under `failureScope`,
61
+ * which names what this drain reads (e.g. one project) more narrowly than the site the SDK searched.
12
62
  */
13
63
  async function drainJiraIssues(fetchPage, scope, options) {
14
64
  const issues = [];
15
65
  let cursor;
16
66
  let status = "complete";
17
67
  let metadata;
68
+ const pages = [];
69
+ const recordPage = (pageMetadata) => {
70
+ if (pageMetadata == null || !isIncompleteCollection(pageMetadata) && !pageMetadata.failures?.length) return;
71
+ status = "incomplete";
72
+ pages.push(withFailureScope(pageMetadata, options.failureScope));
73
+ };
18
74
  const stopIncomplete = (ex) => {
19
75
  status = "incomplete";
20
76
  metadata = mergeCollectionMetadata(metadata, {
@@ -27,13 +83,14 @@ async function drainJiraIssues(fetchPage, scope, options) {
27
83
  try {
28
84
  result = await fetchPage(scope, cursor);
29
85
  } catch (ex) {
30
- if (issues.length === 0) {
86
+ if (issues.length === 0 && pages.length === 0) {
31
87
  if (options.isEmptyFailure?.(ex)) return {
32
88
  issues: [],
33
89
  status: "complete"
34
90
  };
35
91
  throw ex;
36
92
  }
93
+ if (issues.length === 0 && options.isEmptyFailure?.(ex)) break;
37
94
  stopIncomplete(ex);
38
95
  break;
39
96
  }
@@ -43,18 +100,22 @@ async function drainJiraIssues(fetchPage, scope, options) {
43
100
  break;
44
101
  }
45
102
  issues.push(...result.data);
103
+ recordPage(result.metadata);
46
104
  if (!result.hasMore) break;
47
105
  if (result.nextCursor == null || result.nextCursor === cursor) {
48
106
  stopIncomplete(/* @__PURE__ */ new Error(`${options.providerName} returned no advancing issue continuation`));
49
107
  break;
50
108
  }
51
109
  cursor = result.nextCursor;
52
- if (i === options.maxPages - 1) status = "backstop";
110
+ if (i === options.maxPages - 1) {
111
+ if (status === "complete") status = "backstop";
112
+ else stopIncomplete(/* @__PURE__ */ new Error(`${options.providerName} issue drain stopped at the ${options.maxPages}-page backstop`));
113
+ }
53
114
  }
54
115
  return {
55
116
  issues,
56
117
  status,
57
- metadata
118
+ metadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata)
58
119
  };
59
120
  }
60
121
  /**
@@ -122,11 +183,12 @@ function toUniqueJiraIssueShapes(issues, provider) {
122
183
  }
123
184
  /**
124
185
  * Reads the current user's issues of several projects with one search per relationship, split back to the projects
125
- * in `projectIds` order by each issue's `project.id`. Undefined when the search did not complete or an issue belongs
126
- * to no requested project, so the caller reads them one by one: a search's pages are global to its project set, and
127
- * an incomplete one says nothing about which of its projects it covered. Its failures are therefore never
128
- * published, and the waste is bounded by one project's page budget, paid only by a search that needed more than
129
- * that or failed partway.
186
+ * in `projectIds` order by each issue's `project.id`. Undefined when the search did not complete (including a page
187
+ * that dropped an issue it could not read) or an issue belongs to no requested project, so the caller reads them one
188
+ * by one: a search's pages are global to its project set, and an incomplete one says nothing about which of its
189
+ * projects it covered. Its failures are therefore never published, and the waste is bounded by one project's page
190
+ * budget, paid by a search that needed more than that or did not read cleanly. An issue that stays unreadable makes
191
+ * every such search pay it, as Linear's does for an incomplete search.
130
192
  */
131
193
  async function searchJiraProjectIssues(fetchPage, projectIds, filters, identities, options, provider) {
132
194
  const read = await readUserScopedJiraIssues(fetchPage, filters, identities, options);
@@ -134,6 +196,6 @@ async function searchJiraProjectIssues(fetchPage, projectIds, filters, identitie
134
196
  return splitProjectIssuesSearch(projectIds, read.issues, (issue) => issue.project?.id ?? void 0)?.map((issues) => toUniqueJiraIssueShapes(issues, provider));
135
197
  }
136
198
  //#endregion
137
- export { drainJiraIssues, readUserScopedJiraIssues, searchJiraProjectIssues, toUniqueJiraIssueShapes };
199
+ export { drainJiraIssues, readUserScopedJiraIssues, searchJiraProjectIssues, summarizePageMetadata, toUniqueJiraIssueShapes, withFailureScope };
138
200
 
139
201
  //# sourceMappingURL=jiraIssueDrains.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"jiraIssueDrains.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/utils/jiraIssueDrains.ts"],"sourcesContent":["import type { CollectionMetadata, CollectionScopeFailure } from '@gitkraken/provider-apis';\nimport type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { ProviderReference } from '@gitlens/git/models/remoteProvider.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport { throwIfCallerContractError, toCollectionScopeFailure } from '../../collectionMetadata.js';\nimport { splitProjectIssuesSearch } from '../../models/issuesIntegration.js';\nimport { IssueFilter } from '../../providerFilters.js';\nimport type { ProviderIssue } from '../models.js';\nimport { toIssueShape } from '../models.js';\nimport { mergeCollectionMetadata } from './providerPaging.js';\n\n/**\n * The issue drains Jira Cloud and Jira Server share. Their reads differ only in how a page is addressed (a site's\n * `resourceId` vs an instance's `baseUrl`) and in which identity a user field takes, so the page fetch and the\n * identities come in as arguments and the bookkeeping is written once.\n */\n\nexport type JiraUserScope = { authorLogin?: string; assigneeLogins?: string[]; mentionLogin?: string };\n\n/** One page of a Jira issue search, whether it names one project or several. */\nexport type JiraIssuePage = { data: ProviderIssue[]; hasMore: boolean; nextCursor: string | undefined } | undefined;\nexport type JiraIssuePageFetcher = (scope: JiraUserScope, cursor: string | undefined) => Promise<JiraIssuePage>;\n\n/**\n * Three states rather than a boolean: the backstop is recoverable by narrowing the scope, a page error or a stalled\n * cursor is not, and `ProjectIssuesDrain` makes the caller say which.\n */\nexport type JiraIssuesDrain = {\n\tissues: ProviderIssue[];\n\tstatus: 'complete' | 'backstop' | 'incomplete';\n\tmetadata?: CollectionMetadata;\n};\n\nexport type JiraUserScopedRead = { issues: ProviderIssue[]; truncated: boolean; metadata?: CollectionMetadata };\n\nexport type JiraDrainOptions = {\n\tmaxPages: number;\n\t/** Where a failure that stops the drain is recorded. */\n\tfailureScope: CollectionScopeFailure['scope'];\n\t/** Names the provider in the failures this drain synthesizes. */\n\tproviderName: string;\n\t/**\n\t * A failure before anything was read that means \"no issues here\" rather than a failed read, e.g. a project this\n\t * connection can no longer see (#5907). Returning true serves the drain as complete and empty.\n\t */\n\tisEmptyFailure?: (ex: unknown) => boolean;\n};\n\n/**\n * Follows a search's cursor up to `maxPages`. A page failure after the first page leaves the already-drained prefix\n * intact and records the failure instead of re-throwing and discarding the prefix; if nothing was fetched yet, the\n * throw propagates so the caller sees a hard error rather than an empty partial success.\n */\nexport async function drainJiraIssues(\n\tfetchPage: JiraIssuePageFetcher,\n\tscope: JiraUserScope,\n\toptions: JiraDrainOptions,\n): Promise<JiraIssuesDrain> {\n\tconst issues: ProviderIssue[] = [];\n\tlet cursor: string | undefined;\n\tlet status: JiraIssuesDrain['status'] = 'complete';\n\tlet metadata: CollectionMetadata | undefined;\n\tconst stopIncomplete = (ex: unknown): void => {\n\t\tstatus = 'incomplete';\n\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\tcompleteness: 'partial',\n\t\t\tfailures: [toCollectionScopeFailure(options.failureScope, ex)],\n\t\t});\n\t};\n\tfor (let i = 0; i < options.maxPages; i++) {\n\t\tlet result: JiraIssuePage;\n\t\ttry {\n\t\t\tresult = await fetchPage(scope, cursor);\n\t\t} catch (ex) {\n\t\t\tif (issues.length === 0) {\n\t\t\t\tif (options.isEmptyFailure?.(ex)) return { issues: [], status: 'complete' };\n\n\t\t\t\tthrow ex;\n\t\t\t}\n\n\t\t\tstopIncomplete(ex);\n\t\t\tbreak;\n\t\t}\n\t\tif (result == null) {\n\t\t\tif (cursor == null) break;\n\n\t\t\tstopIncomplete(new Error(`${options.providerName} returned no page after advertising a continuation`));\n\t\t\tbreak;\n\t\t}\n\n\t\tissues.push(...result.data);\n\t\tif (!result.hasMore) break;\n\n\t\t// The provider claims more pages but gave no advancing cursor: we can't continue, so the drain\n\t\t// is incomplete — flag it rather than silently stopping (matches drainPullRequests/Repositories).\n\t\tif (result.nextCursor == null || result.nextCursor === cursor) {\n\t\t\tstopIncomplete(new Error(`${options.providerName} returned no advancing issue continuation`));\n\t\t\tbreak;\n\t\t}\n\n\t\tcursor = result.nextCursor;\n\t\t// Pages remain but this was the last allowed iteration: the backstop stopped the drain, which a\n\t\t// narrower scope would avoid.\n\t\tif (i === options.maxPages - 1) {\n\t\t\tstatus = 'backstop';\n\t\t}\n\t}\n\treturn { issues: issues, status: status, metadata: metadata };\n}\n\n/**\n * Runs one drain per requested relationship and keeps whatever succeeded. Throws only when every relationship failed,\n * so the caller sees a hard error rather than an empty success.\n *\n * `identities` are the values each kind of clause takes, which the providers resolve differently: `userField` scopes\n * `assignee`/`creator`, which are user fields, and `mention` scopes `comment ~ \"...\"`, a free-text search over comment\n * bodies, which only a name can match.\n */\nexport async function readUserScopedJiraIssues(\n\tfetchPage: JiraIssuePageFetcher,\n\trequestedFilters: readonly IssueFilter[] | undefined,\n\tidentities: { userField: string; mention: string },\n\toptions: JiraDrainOptions,\n): Promise<JiraUserScopedRead> {\n\t// A resolved user always scopes the read. Default to the assignee filter (\"my issues\") when no explicit filters\n\t// are given — otherwise a caller that scopes by user but omits filters would fall through to an unscoped fetch\n\t// and get every issue in the project instead of the user's (#5438).\n\tconst filters = requestedFilters?.length ? requestedFilters : [IssueFilter.Assignee];\n\tconst settled = await Promise.allSettled(\n\t\tfilters.map(filter =>\n\t\t\tdrainJiraIssues(\n\t\t\t\tfetchPage,\n\t\t\t\t{\n\t\t\t\t\tauthorLogin: filter === IssueFilter.Author ? identities.userField : undefined,\n\t\t\t\t\tassigneeLogins: filter === IssueFilter.Assignee ? [identities.userField] : undefined,\n\t\t\t\t\tmentionLogin: filter === IssueFilter.Mention ? identities.mention : undefined,\n\t\t\t\t},\n\t\t\t\toptions,\n\t\t\t),\n\t\t),\n\t);\n\n\t// If every filter branch rejected, the read failed outright — propagate the first rejection instead\n\t// of returning an empty list, which the facade (getIssuesForProjectResult → runCaptured) would\n\t// otherwise surface as a successful \"no issues\" rather than a warning + fetchFailed. The first\n\t// reason is re-thrown as-is (not wrapped in an AggregateError) so the facade can still classify it\n\t// by type (auth/rate-limit) — wrapping would collapse every failure to a generic 'other'. The\n\t// remaining reasons would otherwise be discarded, so log them here to keep them diagnosable.\n\tif (settled.every(r => r.status === 'rejected')) {\n\t\tfor (let i = 1; i < settled.length; i++) {\n\t\t\tconst outcome = settled[i];\n\t\t\tif (outcome.status === 'rejected') {\n\t\t\t\tLogger.error(outcome.reason, `readUserScopedJiraIssues: filter '${filters[i]}' failed`);\n\t\t\t}\n\t\t}\n\t\tthrow settled[0].status === 'rejected'\n\t\t\t? settled[0].reason\n\t\t\t: new Error(`${options.providerName} issue read failed`);\n\t}\n\n\tlet truncated = false;\n\tlet metadata: CollectionMetadata | undefined;\n\tconst issues: ProviderIssue[] = [];\n\tfor (let i = 0; i < settled.length; i++) {\n\t\tconst outcome = settled[i];\n\t\tconst filter = filters[i];\n\t\t// A rejected filter branch (with at least one sibling succeeding) means these issues are incomplete:\n\t\t// keep the sibling results but record a structured failure so the facade can warn on the specific\n\t\t// filter (auth/rate-limit) instead of just a generic truncation flag.\n\t\tif (outcome.status !== 'fulfilled') {\n\t\t\t// Identical for every filter branch, so degrading it would report one failure per branch for a\n\t\t\t// single invalid call — see `throwIfCallerContractError`.\n\t\t\tthrowIfCallerContractError(outcome.reason);\n\n\t\t\ttruncated = true;\n\t\t\tconst failure = toCollectionScopeFailure(options.failureScope, outcome.reason);\n\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\tcompleteness: 'partial',\n\t\t\t\tfailures: [\n\t\t\t\t\t{\n\t\t\t\t\t\t...failure,\n\t\t\t\t\t\tmessage: `Issue filter '${filter}' could not be read${\n\t\t\t\t\t\t\tfailure.message != null ? `: ${failure.message}` : ''\n\t\t\t\t\t\t}`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (outcome.value.status !== 'complete') {\n\t\t\ttruncated = true;\n\t\t}\n\t\tif (outcome.value.metadata != null) {\n\t\t\tmetadata = mergeCollectionMetadata(metadata, outcome.value.metadata);\n\t\t}\n\t\tissues.push(...outcome.value.issues);\n\t}\n\treturn { issues: issues, truncated: truncated, metadata: metadata };\n}\n\n/**\n * Each relationship is its own search, so an issue matching two of them arrives twice. Every caller is a tracker\n * project read, so the rows carry the `project` projection.\n */\nexport function toUniqueJiraIssueShapes(issues: readonly ProviderIssue[], provider: ProviderReference): IssueShape[] {\n\tconst resultsById = new Map<string, IssueShape>();\n\tfor (const issue of issues) {\n\t\tconst shape = toIssueShape(issue, provider, { projection: 'project' });\n\t\tif (shape != null && !resultsById.has(shape.id)) {\n\t\t\tresultsById.set(shape.id, shape);\n\t\t}\n\t}\n\treturn [...resultsById.values()];\n}\n\n/**\n * Reads the current user's issues of several projects with one search per relationship, split back to the projects\n * in `projectIds` order by each issue's `project.id`. Undefined when the search did not complete or an issue belongs\n * to no requested project, so the caller reads them one by one: a search's pages are global to its project set, and\n * an incomplete one says nothing about which of its projects it covered. Its failures are therefore never\n * published, and the waste is bounded by one project's page budget, paid only by a search that needed more than\n * that or failed partway.\n */\nexport async function searchJiraProjectIssues(\n\tfetchPage: JiraIssuePageFetcher,\n\tprojectIds: readonly string[],\n\tfilters: readonly IssueFilter[] | undefined,\n\tidentities: { userField: string; mention: string },\n\toptions: JiraDrainOptions,\n\tprovider: ProviderReference,\n): Promise<IssueShape[][] | undefined> {\n\tconst read = await readUserScopedJiraIssues(fetchPage, filters, identities, options);\n\tif (read.truncated) return undefined;\n\n\tconst split = splitProjectIssuesSearch(projectIds, read.issues, issue => issue.project?.id ?? undefined);\n\treturn split?.map(issues => toUniqueJiraIssueShapes(issues, provider));\n}\n"],"mappings":";;;;;;;;;;;;AAqDA,eAAsB,gBACrB,WACA,OACA,SAC2B;CAC3B,MAAM,SAA0B,CAAC;CACjC,IAAI;CACJ,IAAI,SAAoC;CACxC,IAAI;CACJ,MAAM,kBAAkB,OAAsB;EAC7C,SAAS;EACT,WAAW,wBAAwB,UAAU;GAC5C,cAAc;GACd,UAAU,CAAC,yBAAyB,QAAQ,cAAc,EAAE,CAAC;EAC9D,CAAC;CACF;CACA,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,UAAU,KAAK;EAC1C,IAAI;EACJ,IAAI;GACH,SAAS,MAAM,UAAU,OAAO,MAAM;EACvC,SAAS,IAAI;GACZ,IAAI,OAAO,WAAW,GAAG;IACxB,IAAI,QAAQ,iBAAiB,EAAE,GAAG,OAAO;KAAE,QAAQ,CAAC;KAAG,QAAQ;IAAW;IAE1E,MAAM;GACP;GAEA,eAAe,EAAE;GACjB;EACD;EACA,IAAI,UAAU,MAAM;GACnB,IAAI,UAAU,MAAM;GAEpB,+BAAe,IAAI,MAAM,GAAG,QAAQ,aAAa,mDAAmD,CAAC;GACrG;EACD;EAEA,OAAO,KAAK,GAAG,OAAO,IAAI;EAC1B,IAAI,CAAC,OAAO,SAAS;EAIrB,IAAI,OAAO,cAAc,QAAQ,OAAO,eAAe,QAAQ;GAC9D,+BAAe,IAAI,MAAM,GAAG,QAAQ,aAAa,0CAA0C,CAAC;GAC5F;EACD;EAEA,SAAS,OAAO;EAGhB,IAAI,MAAM,QAAQ,WAAW,GAC5B,SAAS;CAEX;CACA,OAAO;EAAU;EAAgB;EAAkB;CAAS;AAC7D;;;;;;;;;AAUA,eAAsB,yBACrB,WACA,kBACA,YACA,SAC8B;CAI9B,MAAM,UAAU,kBAAkB,SAAS,mBAAmB,CAAA,UAAqB;CACnF,MAAM,UAAU,MAAM,QAAQ,WAC7B,QAAQ,KAAI,WACX,gBACC,WACA;EACC,aAAa,WAAA,WAAgC,WAAW,YAAY,KAAA;EACpE,gBAAgB,WAAA,aAAkC,CAAC,WAAW,SAAS,IAAI,KAAA;EAC3E,cAAc,WAAA,YAAiC,WAAW,UAAU,KAAA;CACrE,GACA,OACD,CACD,CACD;CAQA,IAAI,QAAQ,OAAM,MAAK,EAAE,WAAW,UAAU,GAAG;EAChD,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACxC,MAAM,UAAU,QAAQ;GACxB,IAAI,QAAQ,WAAW,YACtB,QAAO,MAAM,QAAQ,QAAQ,qCAAqC,QAAQ,GAAG,SAAS;EAExF;EACA,MAAM,QAAQ,EAAE,CAAC,WAAW,aACzB,QAAQ,EAAE,CAAC,yBACX,IAAI,MAAM,GAAG,QAAQ,aAAa,mBAAmB;CACzD;CAEA,IAAI,YAAY;CAChB,IAAI;CACJ,MAAM,SAA0B,CAAC;CACjC,KAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACxC,MAAM,UAAU,QAAQ;EACxB,MAAM,SAAS,QAAQ;EAIvB,IAAI,QAAQ,WAAW,aAAa;GAGnC,2BAA2B,QAAQ,MAAM;GAEzC,YAAY;GACZ,MAAM,UAAU,yBAAyB,QAAQ,cAAc,QAAQ,MAAM;GAC7E,WAAW,wBAAwB,UAAU;IAC5C,cAAc;IACd,UAAU,CACT;KACC,GAAG;KACH,SAAS,iBAAiB,OAAO,qBAChC,QAAQ,WAAW,OAAO,KAAK,QAAQ,YAAY;IAErD,CACD;GACD,CAAC;GACD;EACD;EAEA,IAAI,QAAQ,MAAM,WAAW,YAC5B,YAAY;EAEb,IAAI,QAAQ,MAAM,YAAY,MAC7B,WAAW,wBAAwB,UAAU,QAAQ,MAAM,QAAQ;EAEpE,OAAO,KAAK,GAAG,QAAQ,MAAM,MAAM;CACpC;CACA,OAAO;EAAU;EAAmB;EAAqB;CAAS;AACnE;;;;;AAMA,SAAgB,wBAAwB,QAAkC,UAA2C;CACpH,MAAM,8BAAc,IAAI,IAAwB;CAChD,KAAK,MAAM,SAAS,QAAQ;EAC3B,MAAM,QAAQ,aAAa,OAAO,UAAU,EAAE,YAAY,UAAU,CAAC;EACrE,IAAI,SAAS,QAAQ,CAAC,YAAY,IAAI,MAAM,EAAE,GAC7C,YAAY,IAAI,MAAM,IAAI,KAAK;CAEjC;CACA,OAAO,CAAC,GAAG,YAAY,OAAO,CAAC;AAChC;;;;;;;;;AAUA,eAAsB,wBACrB,WACA,YACA,SACA,YACA,SACA,UACsC;CACtC,MAAM,OAAO,MAAM,yBAAyB,WAAW,SAAS,YAAY,OAAO;CACnF,IAAI,KAAK,WAAW,OAAO,KAAA;CAG3B,OADc,yBAAyB,YAAY,KAAK,SAAQ,UAAS,MAAM,SAAS,MAAM,KAAA,CACnF,CAAC,EAAE,KAAI,WAAU,wBAAwB,QAAQ,QAAQ,CAAC;AACtE"}
1
+ {"version":3,"file":"jiraIssueDrains.js","names":[],"sources":["../../../../../../plus/integrations/src/providers/utils/jiraIssueDrains.ts"],"sourcesContent":["import type { CollectionMetadata, CollectionScopeFailure } from '@gitkraken/provider-apis';\nimport type { IssueShape } from '@gitlens/git/models/issue.js';\nimport type { ProviderReference } from '@gitlens/git/models/remoteProvider.js';\nimport { Logger } from '@gitlens/utils/logger.js';\nimport {\n\tisIncompleteCollection,\n\tthrowIfCallerContractError,\n\ttoCollectionScopeFailure,\n} from '../../collectionMetadata.js';\nimport { splitProjectIssuesSearch } from '../../models/issuesIntegration.js';\nimport { IssueFilter } from '../../providerFilters.js';\nimport { collectionScopeKey } from '../../results.js';\nimport type { JiraIssuePage, ProviderIssue } from '../models.js';\nimport { toIssueShape } from '../models.js';\nimport { mergeCollectionMetadata } from './providerPaging.js';\n\n/**\n * The issue drains Jira Cloud and Jira Server share. Their reads differ only in how a page is addressed (a site's\n * `resourceId` vs an instance's `baseUrl`) and in which identity a user field takes, so the page fetch and the\n * identities come in as arguments and the bookkeeping is written once.\n */\n\nexport type JiraUserScope = { authorLogin?: string; assigneeLogins?: string[]; mentionLogin?: string };\n\nexport type JiraIssuePageFetcher = (scope: JiraUserScope, cursor: string | undefined) => Promise<JiraIssuePage>;\n\n/**\n * Three states rather than a boolean: the backstop is recoverable by narrowing the scope, a page error, a stalled\n * cursor or a page that dropped issues is not, and `ProjectIssuesDrain` makes the caller say which.\n */\nexport type JiraIssuesDrain = {\n\tissues: ProviderIssue[];\n\tstatus: 'complete' | 'backstop' | 'incomplete';\n\tmetadata?: CollectionMetadata;\n};\n\nexport type JiraUserScopedRead = { issues: ProviderIssue[]; truncated: boolean; metadata?: CollectionMetadata };\n\nexport type JiraDrainOptions = {\n\tmaxPages: number;\n\t/** Where a failure that stops the drain, or that a page reported, is recorded. */\n\tfailureScope: CollectionScopeFailure['scope'];\n\t/** Names the provider in the failures this drain synthesizes. */\n\tproviderName: string;\n\t/**\n\t * A failure before anything was read that means \"no issues here\" rather than a failed read, e.g. a project this\n\t * connection can no longer see (#5907). Returning true serves the drain as complete and empty.\n\t */\n\tisEmptyFailure?: (ex: unknown) => boolean;\n};\n\n/**\n * A search page's metadata with its failures recorded under `scope`. provider-apis scopes a page failure to what it\n * searched (a Cloud site, or nothing below the provider on Data Center, which would publish it as a failure of the\n * whole connection); the caller knows what it read, e.g. one project. Omissions keep their own scope.\n */\nexport function withFailureScope(\n\tmetadata: CollectionMetadata,\n\tscope: CollectionScopeFailure['scope'],\n): CollectionMetadata {\n\tif (metadata.failures == null) return metadata;\n\n\treturn { ...metadata, failures: metadata.failures.map(failure => ({ ...failure, scope: scope })) };\n}\n\n/**\n * Folds what a drain's pages reported into one metadata, with one failure per scope and kind however many pages\n * reported one. provider-apis reports the issues a page dropped per page, naming that page's keys, so the failures\n * of a drain over many unreadable pages never dedupe and would publish one warning per page. The first page's\n * failure is kept, naming its keys, and says how many more pages dropped issues too.\n */\nexport function summarizePageMetadata(pages: readonly CollectionMetadata[]): CollectionMetadata | undefined {\n\tlet summary: CollectionMetadata | undefined;\n\tconst failures = new Map<string, { failure: CollectionScopeFailure; pages: number }>();\n\tfor (const { failures: pageFailures, ...page } of pages) {\n\t\tsummary = mergeCollectionMetadata(summary, page);\n\t\tfor (const failure of pageFailures ?? []) {\n\t\t\tconst key = `${collectionScopeKey(failure.scope)}\\0${failure.kind}`;\n\t\t\tconst group = failures.get(key);\n\t\t\tif (group == null) {\n\t\t\t\tfailures.set(key, { failure: failure, pages: 1 });\n\t\t\t} else {\n\t\t\t\tgroup.pages++;\n\t\t\t}\n\t\t}\n\t}\n\tif (failures.size === 0) return summary;\n\n\treturn mergeCollectionMetadata(summary, {\n\t\tcompleteness: 'partial',\n\t\tfailures: Array.from(failures.values(), ({ failure, pages: count }) =>\n\t\t\tcount === 1\n\t\t\t\t? failure\n\t\t\t\t: {\n\t\t\t\t\t\t...failure,\n\t\t\t\t\t\tmessage: `${failure.message != null ? `${failure.message} ` : ''}Issues on ${count - 1} more ${count === 2 ? 'page' : 'pages'} could not be read either.`,\n\t\t\t\t\t},\n\t\t),\n\t});\n}\n\n/**\n * Follows a search's cursor up to `maxPages`. A page failure after the first page leaves the already-drained prefix\n * intact and records the failure instead of re-throwing and discarding the prefix; if nothing was fetched yet, the\n * throw propagates so the caller sees a hard error rather than an empty partial success.\n *\n * A page that dropped issues it could not read (its metadata says so) makes the drain `incomplete` without stopping\n * it: the issues it did read and the pages after it are kept, and its failures are recorded under `failureScope`,\n * which names what this drain reads (e.g. one project) more narrowly than the site the SDK searched.\n */\nexport async function drainJiraIssues(\n\tfetchPage: JiraIssuePageFetcher,\n\tscope: JiraUserScope,\n\toptions: JiraDrainOptions,\n): Promise<JiraIssuesDrain> {\n\tconst issues: ProviderIssue[] = [];\n\tlet cursor: string | undefined;\n\tlet status: JiraIssuesDrain['status'] = 'complete';\n\tlet metadata: CollectionMetadata | undefined;\n\tconst pages: CollectionMetadata[] = [];\n\tconst recordPage = (pageMetadata: CollectionMetadata | undefined): void => {\n\t\tif (pageMetadata == null || (!isIncompleteCollection(pageMetadata) && !pageMetadata.failures?.length)) return;\n\n\t\tstatus = 'incomplete';\n\t\tpages.push(withFailureScope(pageMetadata, options.failureScope));\n\t};\n\tconst stopIncomplete = (ex: unknown): void => {\n\t\tstatus = 'incomplete';\n\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\tcompleteness: 'partial',\n\t\t\tfailures: [toCollectionScopeFailure(options.failureScope, ex)],\n\t\t});\n\t};\n\tfor (let i = 0; i < options.maxPages; i++) {\n\t\tlet result: JiraIssuePage;\n\t\ttry {\n\t\t\tresult = await fetchPage(scope, cursor);\n\t\t} catch (ex) {\n\t\t\t// A page that read no issue but reported dropping some (every issue on it was unreadable) is not\n\t\t\t// \"nothing fetched\": its failure is kept rather than lost to the throw or to an empty success.\n\t\t\tconst nothingRead = issues.length === 0 && pages.length === 0;\n\t\t\tif (nothingRead) {\n\t\t\t\tif (options.isEmptyFailure?.(ex)) return { issues: [], status: 'complete' };\n\n\t\t\t\tthrow ex;\n\t\t\t}\n\t\t\t// The project is gone, so it has nothing more to report; what its pages dropped already is.\n\t\t\tif (issues.length === 0 && options.isEmptyFailure?.(ex)) break;\n\n\t\t\tstopIncomplete(ex);\n\t\t\tbreak;\n\t\t}\n\t\tif (result == null) {\n\t\t\tif (cursor == null) break;\n\n\t\t\tstopIncomplete(new Error(`${options.providerName} returned no page after advertising a continuation`));\n\t\t\tbreak;\n\t\t}\n\n\t\tissues.push(...result.data);\n\t\trecordPage(result.metadata);\n\t\tif (!result.hasMore) break;\n\n\t\t// The provider claims more pages but gave no advancing cursor: we can't continue, so the drain\n\t\t// is incomplete — flag it rather than silently stopping (matches drainPullRequests/Repositories).\n\t\tif (result.nextCursor == null || result.nextCursor === cursor) {\n\t\t\tstopIncomplete(new Error(`${options.providerName} returned no advancing issue continuation`));\n\t\t\tbreak;\n\t\t}\n\n\t\tcursor = result.nextCursor;\n\t\t// Pages remain but this was the last allowed iteration: the backstop stopped the drain, which a\n\t\t// narrower scope would avoid. A drain already incomplete stays so (narrowing can't recover a dropped\n\t\t// issue), so its metadata says the backstop also stopped it rather than only naming the dropped issues.\n\t\tif (i === options.maxPages - 1) {\n\t\t\tif (status === 'complete') {\n\t\t\t\tstatus = 'backstop';\n\t\t\t} else {\n\t\t\t\tstopIncomplete(\n\t\t\t\t\tnew Error(`${options.providerName} issue drain stopped at the ${options.maxPages}-page backstop`),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\treturn {\n\t\tissues: issues,\n\t\tstatus: status,\n\t\tmetadata: mergeCollectionMetadata(summarizePageMetadata(pages), metadata),\n\t};\n}\n\n/**\n * Runs one drain per requested relationship and keeps whatever succeeded. Throws only when every relationship failed,\n * so the caller sees a hard error rather than an empty success.\n *\n * `identities` are the values each kind of clause takes, which the providers resolve differently: `userField` scopes\n * `assignee`/`creator`, which are user fields, and `mention` scopes `comment ~ \"...\"`, a free-text search over comment\n * bodies, which only a name can match.\n */\nexport async function readUserScopedJiraIssues(\n\tfetchPage: JiraIssuePageFetcher,\n\trequestedFilters: readonly IssueFilter[] | undefined,\n\tidentities: { userField: string; mention: string },\n\toptions: JiraDrainOptions,\n): Promise<JiraUserScopedRead> {\n\t// A resolved user always scopes the read. Default to the assignee filter (\"my issues\") when no explicit filters\n\t// are given — otherwise a caller that scopes by user but omits filters would fall through to an unscoped fetch\n\t// and get every issue in the project instead of the user's (#5438).\n\tconst filters = requestedFilters?.length ? requestedFilters : [IssueFilter.Assignee];\n\tconst settled = await Promise.allSettled(\n\t\tfilters.map(filter =>\n\t\t\tdrainJiraIssues(\n\t\t\t\tfetchPage,\n\t\t\t\t{\n\t\t\t\t\tauthorLogin: filter === IssueFilter.Author ? identities.userField : undefined,\n\t\t\t\t\tassigneeLogins: filter === IssueFilter.Assignee ? [identities.userField] : undefined,\n\t\t\t\t\tmentionLogin: filter === IssueFilter.Mention ? identities.mention : undefined,\n\t\t\t\t},\n\t\t\t\toptions,\n\t\t\t),\n\t\t),\n\t);\n\n\t// If every filter branch rejected, the read failed outright — propagate the first rejection instead\n\t// of returning an empty list, which the facade (getIssuesForProjectResult → runCaptured) would\n\t// otherwise surface as a successful \"no issues\" rather than a warning + fetchFailed. The first\n\t// reason is re-thrown as-is (not wrapped in an AggregateError) so the facade can still classify it\n\t// by type (auth/rate-limit) — wrapping would collapse every failure to a generic 'other'. The\n\t// remaining reasons would otherwise be discarded, so log them here to keep them diagnosable.\n\tif (settled.every(r => r.status === 'rejected')) {\n\t\tfor (let i = 1; i < settled.length; i++) {\n\t\t\tconst outcome = settled[i];\n\t\t\tif (outcome.status === 'rejected') {\n\t\t\t\tLogger.error(outcome.reason, `readUserScopedJiraIssues: filter '${filters[i]}' failed`);\n\t\t\t}\n\t\t}\n\t\tthrow settled[0].status === 'rejected'\n\t\t\t? settled[0].reason\n\t\t\t: new Error(`${options.providerName} issue read failed`);\n\t}\n\n\tlet truncated = false;\n\tlet metadata: CollectionMetadata | undefined;\n\tconst issues: ProviderIssue[] = [];\n\tfor (let i = 0; i < settled.length; i++) {\n\t\tconst outcome = settled[i];\n\t\tconst filter = filters[i];\n\t\t// A rejected filter branch (with at least one sibling succeeding) means these issues are incomplete:\n\t\t// keep the sibling results but record a structured failure so the facade can warn on the specific\n\t\t// filter (auth/rate-limit) instead of just a generic truncation flag.\n\t\tif (outcome.status !== 'fulfilled') {\n\t\t\t// Identical for every filter branch, so degrading it would report one failure per branch for a\n\t\t\t// single invalid call — see `throwIfCallerContractError`.\n\t\t\tthrowIfCallerContractError(outcome.reason);\n\n\t\t\ttruncated = true;\n\t\t\tconst failure = toCollectionScopeFailure(options.failureScope, outcome.reason);\n\t\t\tmetadata = mergeCollectionMetadata(metadata, {\n\t\t\t\tcompleteness: 'partial',\n\t\t\t\tfailures: [\n\t\t\t\t\t{\n\t\t\t\t\t\t...failure,\n\t\t\t\t\t\tmessage: `Issue filter '${filter}' could not be read${\n\t\t\t\t\t\t\tfailure.message != null ? `: ${failure.message}` : ''\n\t\t\t\t\t\t}`,\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (outcome.value.status !== 'complete') {\n\t\t\ttruncated = true;\n\t\t}\n\t\tif (outcome.value.metadata != null) {\n\t\t\tmetadata = mergeCollectionMetadata(metadata, outcome.value.metadata);\n\t\t}\n\t\tissues.push(...outcome.value.issues);\n\t}\n\treturn { issues: issues, truncated: truncated, metadata: metadata };\n}\n\n/**\n * Each relationship is its own search, so an issue matching two of them arrives twice. Every caller is a tracker\n * project read, so the rows carry the `project` projection.\n */\nexport function toUniqueJiraIssueShapes(issues: readonly ProviderIssue[], provider: ProviderReference): IssueShape[] {\n\tconst resultsById = new Map<string, IssueShape>();\n\tfor (const issue of issues) {\n\t\tconst shape = toIssueShape(issue, provider, { projection: 'project' });\n\t\tif (shape != null && !resultsById.has(shape.id)) {\n\t\t\tresultsById.set(shape.id, shape);\n\t\t}\n\t}\n\treturn [...resultsById.values()];\n}\n\n/**\n * Reads the current user's issues of several projects with one search per relationship, split back to the projects\n * in `projectIds` order by each issue's `project.id`. Undefined when the search did not complete (including a page\n * that dropped an issue it could not read) or an issue belongs to no requested project, so the caller reads them one\n * by one: a search's pages are global to its project set, and an incomplete one says nothing about which of its\n * projects it covered. Its failures are therefore never published, and the waste is bounded by one project's page\n * budget, paid by a search that needed more than that or did not read cleanly. An issue that stays unreadable makes\n * every such search pay it, as Linear's does for an incomplete search.\n */\nexport async function searchJiraProjectIssues(\n\tfetchPage: JiraIssuePageFetcher,\n\tprojectIds: readonly string[],\n\tfilters: readonly IssueFilter[] | undefined,\n\tidentities: { userField: string; mention: string },\n\toptions: JiraDrainOptions,\n\tprovider: ProviderReference,\n): Promise<IssueShape[][] | undefined> {\n\tconst read = await readUserScopedJiraIssues(fetchPage, filters, identities, options);\n\tif (read.truncated) return undefined;\n\n\tconst split = splitProjectIssuesSearch(projectIds, read.issues, issue => issue.project?.id ?? undefined);\n\treturn split?.map(issues => toUniqueJiraIssueShapes(issues, provider));\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { toPageCursor } from "../providers/utils/providerPaging.js";
2
1
  import { hostFromDomain } from "../utils/domain.utils.js";
2
+ import { toPageCursor } from "../providers/utils/providerPaging.js";
3
3
  import { pageToCursor } from "./paging.js";
4
4
  //#region ../plus/integrations/src/reads/cursors.ts
5
5
  /** Positive safe integers only, deduped; `undefined` when nothing survives (so the field is omitted). */
@@ -1,7 +1,6 @@
1
1
  import { PullRequest, PullRequestFilter, PullRequestStateFilter } from "../../../git/models/pullRequest.js";
2
2
  import { IntegrationIds } from "../constants.js";
3
3
  import { ProviderWarning } from "../results.js";
4
- import "../providerFilters.js";
5
4
  import { ProviderPullRequest, ProviderReposInput, ProviderRepository } from "../providers/models.js";
6
5
  import { GitHostIntegration } from "../models/gitHostIntegration.js";
7
6
  import { IntegrationResult } from "../models/integration.js";
@@ -8,7 +8,7 @@ import { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toP
8
8
  import { gitHostOnlySurfaceWarning, truncationWarning, unsupportedWarning } from "./warnings.js";
9
9
  import { runCaptured } from "./drains.js";
10
10
  import { isIssuesIntegration } from "../models/issuesIntegration.js";
11
- import { assertHierarchyReadTarget, mergeHierarchyResults, orgForProject, resourceMatchesOrg, resourceToOrg, withProviderContext } from "./hierarchy.utils.js";
11
+ import { assertHierarchyReadTarget, mergeHierarchyResults, projectToOrg, resourceMatchesOrg, resourceToOrg, withProviderContext } from "./hierarchy.utils.js";
12
12
  //#region ../plus/integrations/src/reads/hierarchy.ts
13
13
  /**
14
14
  * Folds a flat hierarchy result's incompleteness into `warnings`, returning whether it leaves the read
@@ -145,7 +145,7 @@ async function listProjects(ctx, options) {
145
145
  warnings.push(projectsWarning);
146
146
  if (projects == null) fetchFailed = true;
147
147
  }
148
- if (projects != null) items.push(...projects.values.map((project) => resourceToOrg(id, project, orgForProject(id, project, scopedResources))));
148
+ if (projects != null) items.push(...projects.values.map((project) => projectToOrg(id, project, scopedResources)));
149
149
  const assessment = mergeAssessmentInto(warnings, id, domain, connectionId, projects?.metadata);
150
150
  if (assessment.fetchFailed || assessment.truncated) fetchFailed = true;
151
151
  }
@@ -1 +1 @@
1
- {"version":3,"file":"hierarchy.js","names":[],"sources":["../../../../../plus/integrations/src/reads/hierarchy.ts"],"sourcesContent":["import { mapBounded } from '@gitlens/utils/promise.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tproviderFanOutConcurrency,\n\tsupportedOrderedCloudIntegrationIds,\n\tsupportedOrderedCloudIssuesIntegrationIds,\n} from '../constants.js';\nimport type { ListOrgsOptions, ListProjectsOptions } from '../manager.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport type { ProviderHierarchyResult } from '../providers/models.js';\nimport { toProviderRepositoryShape } from '../providers/models.js';\nimport type {\n\tProviderOrganization,\n\tProviderPagedResult,\n\tProviderRepositoryShape,\n\tProviderResult,\n\tProviderWarning,\n} from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tassertHierarchyReadTarget,\n\tmergeHierarchyResults,\n\torgForProject,\n\tresourceMatchesOrg,\n\tresourceToOrg,\n\twithProviderContext,\n} from './hierarchy.utils.js';\nimport { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from './paging.js';\nimport { gitHostOnlySurfaceWarning, truncationWarning, unsupportedWarning } from './warnings.js';\n\n/**\n * Folds a flat hierarchy result's incompleteness into `warnings`, returning whether it leaves the read\n * non-authoritative. The single funnel for both hierarchy reads, so neither can drift from the other on any of\n * the four decisions it makes:\n *\n * - The metadata is assessed FIRST, so the truncation warning can tell whether it already explained this gap.\n * Ordered deliberately: emitting first meant an expired credential produced BOTH a typed `auth` warning and\n * an unclassifiable one for the same cause, and a consumer routing on classification showed the second as\n * \"this provider failed to load\", with a remedy that doesn't exist, next to the reconnect prompt that was the\n * actual fix.\n * - The read states the truncation itself only when the metadata reported nothing — the rule\n * `assessCollectionMetadata` applies to its own generic fallback. Whatever the metadata reported already names\n * this provider and says something stronger, so restating it costs a false verdict and adds no information.\n * - `'exhausted'`, never `'page-budget'`: `truncated` is one boolean here, so a drain that stopped at its page\n * budget is indistinguishable from one whose cursor stalled, and `IncompleteReadCause` makes `'exhausted'`\n * the default whenever a raisable budget is not demonstrably the cause.\n * - A truncation counts as non-authoritative even though the omission above asserts the read succeeded. Unlike\n * paged repository reads, a flattened hierarchy result has no page object on which to carry incompleteness,\n * so `fetchFailed` is the only signal a consumer has that the list is short.\n */\nfunction mergeHierarchyIncompleteness(\n\twarnings: ProviderWarning[],\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tresult: Pick<ProviderHierarchyResult<unknown>, 'truncated' | 'metadata'>,\n\treadKind: 'Organization' | 'Project',\n): boolean {\n\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, result.metadata);\n\tif (!result.truncated) return assessment.fetchFailed || assessment.truncated;\n\n\tif (!assessment.reported) {\n\t\t// Deduped like every other `truncationWarning` emission (see `drains.ts`), not plain-pushed: no caller can\n\t\t// collide with it today, but nothing about this funnel guarantees a third read won't.\n\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, readKind, 'exhausted'));\n\t}\n\treturn true;\n}\n\nexport async function listOrgs(\n\tctx: ProviderReadContext,\n\toptions?: ListOrgsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listOrgs', options);\n\tconst ids = options?.providerId != null ? [options.providerId] : supportedOrderedCloudIntegrationIds;\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A specifically requested connection that can't be resolved is a broken connection, not a\n\t\t\t// provider with no orgs — surface it (warning + fetchFailed) instead of dropping the id\n\t\t\t// silently, so a caller can tell it apart from an account that genuinely has no orgs.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\t\tif (isIssuesIntegration(integration)) {\n\t\t\t// Issue trackers expose \"resources\" (Jira sites, Linear orgs, …) as their org analogue.\n\t\t\tconst { value: resources, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getResourcesForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (resources != null) {\n\t\t\t\titems.push(...resources.map(r => resourceToOrg(id, r)));\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\t// A warning with no value is a hard read failure, not an empty account.\n\t\t\t\tif (resources == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (!integration.supportsOrganizationDiscovery) {\n\t\t\t// The provider registers no org-discovery hook. Report it as\n\t\t\t// explicitly unsupported rather than contributing a silent empty list that a caller can't\n\t\t\t// tell apart from \"this account has no orgs\".\n\t\t\tfetchFailed = true;\n\t\t\twarnings.push(\n\t\t\t\tunsupportedWarning(id, domain, connectionId, `Organization discovery is not supported by '${id}'.`),\n\t\t\t);\n\t\t} else {\n\t\t\tconst { value, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getOrganizationsForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (value != null) {\n\t\t\t\titems.push(...value.values.map(org => withProviderContext(id, org)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, value, 'Organization')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (value == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listProjects(\n\tctx: ProviderReadContext,\n\toptions?: ListProjectsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listProjects', options);\n\tconst ids =\n\t\toptions?.providerId != null\n\t\t\t? [options.providerId]\n\t\t\t: [\n\t\t\t\t\t...supportedOrderedCloudIssuesIntegrationIds,\n\t\t\t\t\tGitCloudHostIntegrationId.AzureDevOps,\n\t\t\t\t\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n\t\t\t\t];\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A requested connection that can't be resolved is a broken connection, not a provider with\n\t\t\t// no projects — surface it (warning + fetchFailed) instead of dropping the id silently.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst org = options?.org;\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\n\t\t// Git hosts with a project tier (Azure DevOps) read projects through their own hierarchy hook,\n\t\t// scoped to `org` when given. Non-Azure git hosts have no project tier and return undefined.\n\t\tif (!isIssuesIntegration(integration)) {\n\t\t\t// Check the capability first, like listOrgs/listRepos/listIssuesPage do. Without it, a host\n\t\t\t// with no project tier returns `undefined` from a successful read, which `runCaptured` can't\n\t\t\t// tell apart from an unresolvable session — turning a healthy provider into a `no-connection`\n\t\t\t// warning + `fetchFailed` (and, in Kepler, a spurious reconnect prompt).\n\t\t\tif (!integration.supportsProjectDiscovery) {\n\t\t\t\treturn { items: items, warnings: warnings, fetchFailed: false };\n\t\t\t}\n\n\t\t\tconst { value: projects, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getProjectsForOrgResult(org, connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => withProviderContext(id, project)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, projects, 'Project')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t\t}\n\n\t\tconst { value: resources, warning: resourcesWarning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() => integration.getResourcesForUserResult(connectionId),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\t\tif (resourcesWarning != null) {\n\t\t\twarnings.push(resourcesWarning);\n\t\t\tif (resources == null) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\tconst scopedResources =\n\t\t\torg != null ? resources?.filter(resource => resourceMatchesOrg(resource, org)) : resources;\n\t\tif (scopedResources != null && scopedResources.length !== 0) {\n\t\t\tconst { value: projects, warning: projectsWarning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\t\tintegration.getProjectsForResourcesWithMetadataResult(scopedResources, connectionId),\n\t\t\t);\n\t\t\tif (projectsWarning != null) {\n\t\t\t\twarnings.push(projectsWarning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(\n\t\t\t\t\t...projects.values.map(project =>\n\t\t\t\t\t\tresourceToOrg(id, project, orgForProject(id, project, scopedResources)),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, projects?.metadata);\n\t\t\tif (assessment.fetchFailed || assessment.truncated) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listRepos(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<ProviderRepositoryShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and bounds the cursor walk below, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tconst accountWide = options.org == null;\n\tconst supported = accountWide\n\t\t? integration.supportsUserRepositoryDiscovery\n\t\t: integration.supportsRepositoryDiscovery;\n\tif (!supported) {\n\t\t// No matching repo-discovery hook — org-scoped (e.g. Bitbucket Data Center) or account-wide (e.g.\n\t\t// Bitbucket/Azure, whose repos can only be walked per workspace/org). Report unsupported rather than\n\t\t// a silent empty page indistinguishable from \"no repos\"; for the account-wide case the caller should\n\t\t// fan out per org from listOrgs instead.\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunsupportedWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\taccountWide\n\t\t\t\t\t\t? `Account-wide repository discovery is not supported by '${options.providerId}'; list repositories per org instead.`\n\t\t\t\t\t\t: `Repository discovery is not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst org = options.org;\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\torg == null\n\t\t\t\t\t? integration.getRepositoriesForUserResult({ cursor: cursor, connectionId: options.connectionId })\n\t\t\t\t\t: integration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\t\t\tproject: options.project,\n\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\tconnectionId: options.connectionId,\n\t\t\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\t/**\n\t * Builds one page's result from what the provider returned. Shared by the initial read and by every round of\n\t * the cursor walk below, so a walked page N is described by exactly the same rules as a directly-requested one\n\t * (position, continuation, truncation) rather than by a second copy of them.\n\t */\n\tconst toResult = (\n\t\t{ value, warning }: Awaited<ReturnType<typeof readPage>>,\n\t\trequestedPage: number,\n\t\tsuppliedCursor: string | undefined,\n\t): ProviderPagedResult<ProviderRepositoryShape> => {\n\t\tconst items = value?.values ?? [];\n\t\tconst warnings = warning != null ? [warning] : [];\n\t\t// The repos read core is cursor-only and can't accept a page size, so don't echo the requested\n\t\t// `itemsPerPage` as if it were applied — report what the provider returned (its own pageSize when\n\t\t// available, else the actual item count).\n\t\tconst paged = toProviderPageInfo(items.length, value?.paging);\n\t\t// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending\n\t\t// them to any captured thrown-error warning without discarding the partial result's items.\n\t\tconst assessment = mergeAssessmentInto(\n\t\t\twarnings,\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\tvalue?.metadata,\n\t\t);\n\t\t// Whether this read honors a page NUMBER, decided from what the provider actually reported rather than\n\t\t// guessed from the absence of a cursor. A numbered-page repos host reports its position (`paging.page`)\n\t\t// and/or its successor (`paging.nextPage`) — Bitbucket's workspace walk consumes `page` as a real 1-based\n\t\t// page and reports `nextPage` without echoing `currentPage`, so both signals have to count. Every wired\n\t\t// cursor-based repos read (GitHub's org/user walks, GitLab's user walk, Bitbucket's cursor read) reports\n\t\t// neither. Keying off `paged.cursor == null` instead read the paging layer's \"reported another page,\n\t\t// handed back no continuation\" sentinel as evidence of a numbered host, which is exactly backwards.\n\t\tconst pageAdvanceable = value?.paging?.page != null || value?.paging?.nextPage != null;\n\t\t// Numbered-page hosts that don't echo `currentPage` may still be advanced by the requested `page` (initial\n\t\t// read) or by the cursor the caller threaded back. A cursor-only host ignores an UNTHREADED `page`\n\t\t// request, so its page-less first page is page 1.\n\t\tconst currentPage = resolveCurrentPage({\n\t\t\tproviderPage: paged.page.currentPage,\n\t\t\trequestedPage: requestedPage,\n\t\t\tsuppliedCursor: suppliedCursor,\n\t\t\tpageAdvanceable: pageAdvanceable,\n\t\t});\n\t\t// Never advertise `hasMore` without a continuation the caller can act on — the same contract\n\t\t// `listPullRequestsPage`, `listIssuesPage` and `broadenIssues` hold, via the same helper. A provider that\n\t\t// reports another page but hands back neither `endCursor` nor `nextPage` (surfaced as the `'{}'` sentinel,\n\t\t// which `toProviderPageInfo` drops) is terminal-but-incomplete: `hasMore: false` + `page.truncated`.\n\t\t// Synthesizing a page-number cursor for a cursor-only host instead — which is what this read used to do\n\t\t// unconditionally — handed back a continuation the provider ignores, so it answered with its FIRST page\n\t\t// again while still reporting `hasMore: true`. A consumer draining until `hasMore` clears (Kepler's repo\n\t\t// drain does exactly that) then looped to its own page cap, accumulating a duplicate copy of every repo\n\t\t// per round, and never saw a truncation signal.\n\t\tconst continuation = resolveContinuation(paged, pageAdvanceable ? currentPage + 1 : undefined);\n\t\t// The org-hierarchy read can also stop at a defensive backstop with more repos unlisted and no cursor to\n\t\t// resume (top-level `truncated`, or `paging.truncated` on a single-page read). Metadata incompleteness and\n\t\t// a demoted continuation are two further, independent sources of the same signal.\n\t\tconst truncated =\n\t\t\t(value?.truncated ?? false) ||\n\t\t\t(value?.paging?.truncated ?? false) ||\n\t\t\tassessment.truncated ||\n\t\t\tcontinuation.truncated;\n\t\treturn {\n\t\t\t// Normalize the raw provider-apis repos to the GitLens-owned shape at the surface boundary.\n\t\t\titems: items.map(toProviderRepositoryShape),\n\t\t\twarnings: warnings,\n\t\t\tpage: { ...paged.page, currentPage: currentPage, truncated: truncated || undefined },\n\t\t\thasMore: continuation.hasMore,\n\t\t\tcursor: continuation.cursor,\n\t\t\tfetchFailed: assessment.fetchFailed || (warning != null && value == null) || undefined,\n\t\t};\n\t};\n\n\tconst first = await readPage(\n\t\toptions.cursor ??\n\t\t\t(integration.id === GitSelfManagedHostIntegrationId.BitbucketServer ? undefined : pageToCursor(page)),\n\t);\n\tconst result = toResult(first, page, options.cursor);\n\t// A numbered-page host honored the synthesized cursor, so this IS page N; a cursor-only host ignored it and\n\t// answered with page 1, which the walk below advances. Decided from the provider's own paging metadata rather\n\t// than assumed, hence after the read.\n\tconst pageAdvanceable = first.value?.paging?.page != null || first.value?.paging?.nextPage != null;\n\tif (options.cursor != null || page === 1 || pageAdvanceable) return result;\n\n\t// Walk the provider's real continuations to the requested page. Only the last page's items are kept — pages\n\t// 1..N returned as \"page N\" would duplicate items for a normal paged consumer — while warnings, truncation,\n\t// and failure accumulate across the whole walked prefix, since an earlier round's problem still means this\n\t// result isn't authoritative. The provider read is re-issued per round, but the integration is resolved once:\n\t// re-entering this function per page would repeat its auth resolution N times for page N.\n\tconst walkedWarnings = [...result.warnings];\n\tlet walkedFetchFailed = result.fetchFailed === true;\n\tlet walkedTruncated = result.page.truncated === true;\n\tlet previous = result;\n\tfor (let currentPage = 2; currentPage <= page; currentPage++) {\n\t\tif (!previous.hasMore || previous.cursor == null) {\n\t\t\t// The requested page lies past the provider's terminal cursor: an EMPTY page N, never the last\n\t\t\t// available page relabeled (see ProviderPageInfo.currentPage).\n\t\t\treturn {\n\t\t\t\titems: [],\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { currentPage: page, itemsPerPage: 0, truncated: walkedTruncated || undefined },\n\t\t\t\thasMore: false,\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst suppliedCursor = previous.cursor;\n\t\tconst requested = toResult(await readPage(suppliedCursor), currentPage, suppliedCursor);\n\t\tfor (const warning of requested.warnings) {\n\t\t\tappendDedupedWarning(walkedWarnings, warning);\n\t\t}\n\t\twalkedFetchFailed ||= requested.fetchFailed === true;\n\t\twalkedTruncated ||= requested.page.truncated === true;\n\t\tif (currentPage === page) {\n\t\t\treturn {\n\t\t\t\t...requested,\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { ...requested.page, truncated: walkedTruncated || undefined },\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tprevious = requested;\n\t}\n\n\treturn result;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"hierarchy.js","names":[],"sources":["../../../../../plus/integrations/src/reads/hierarchy.ts"],"sourcesContent":["import { mapBounded } from '@gitlens/utils/promise.js';\nimport { mergeAssessmentInto } from '../collectionMetadata.js';\nimport type { IntegrationIds } from '../constants.js';\nimport {\n\tGitCloudHostIntegrationId,\n\tGitSelfManagedHostIntegrationId,\n\tproviderFanOutConcurrency,\n\tsupportedOrderedCloudIntegrationIds,\n\tsupportedOrderedCloudIssuesIntegrationIds,\n} from '../constants.js';\nimport type { ListOrgsOptions, ListProjectsOptions } from '../manager.js';\nimport { isIssuesIntegration } from '../models/issuesIntegration.js';\nimport type { ProviderHierarchyResult } from '../providers/models.js';\nimport { toProviderRepositoryShape } from '../providers/models.js';\nimport type {\n\tProviderOrganization,\n\tProviderPagedResult,\n\tProviderRepositoryShape,\n\tProviderResult,\n\tProviderWarning,\n} from '../results.js';\nimport { appendDedupedWarning } from '../results.js';\nimport {\n\tisGitHostIntegration,\n\tisIssuesHostIntegrationId,\n\twarnOnMissingSessionForDomain,\n} from '../utils/integration.utils.js';\nimport type { ProviderReadContext } from './context.js';\nimport { runCaptured } from './drains.js';\nimport {\n\tassertHierarchyReadTarget,\n\tmergeHierarchyResults,\n\tprojectToOrg,\n\tresourceMatchesOrg,\n\tresourceToOrg,\n\twithProviderContext,\n} from './hierarchy.utils.js';\nimport { pageToCursor, refusedPage, resolveContinuation, resolveCurrentPage, toProviderPageInfo } from './paging.js';\nimport { gitHostOnlySurfaceWarning, truncationWarning, unsupportedWarning } from './warnings.js';\n\n/**\n * Folds a flat hierarchy result's incompleteness into `warnings`, returning whether it leaves the read\n * non-authoritative. The single funnel for both hierarchy reads, so neither can drift from the other on any of\n * the four decisions it makes:\n *\n * - The metadata is assessed FIRST, so the truncation warning can tell whether it already explained this gap.\n * Ordered deliberately: emitting first meant an expired credential produced BOTH a typed `auth` warning and\n * an unclassifiable one for the same cause, and a consumer routing on classification showed the second as\n * \"this provider failed to load\", with a remedy that doesn't exist, next to the reconnect prompt that was the\n * actual fix.\n * - The read states the truncation itself only when the metadata reported nothing — the rule\n * `assessCollectionMetadata` applies to its own generic fallback. Whatever the metadata reported already names\n * this provider and says something stronger, so restating it costs a false verdict and adds no information.\n * - `'exhausted'`, never `'page-budget'`: `truncated` is one boolean here, so a drain that stopped at its page\n * budget is indistinguishable from one whose cursor stalled, and `IncompleteReadCause` makes `'exhausted'`\n * the default whenever a raisable budget is not demonstrably the cause.\n * - A truncation counts as non-authoritative even though the omission above asserts the read succeeded. Unlike\n * paged repository reads, a flattened hierarchy result has no page object on which to carry incompleteness,\n * so `fetchFailed` is the only signal a consumer has that the list is short.\n */\nfunction mergeHierarchyIncompleteness(\n\twarnings: ProviderWarning[],\n\tid: IntegrationIds,\n\tdomain: string | undefined,\n\tconnectionId: string | undefined,\n\tresult: Pick<ProviderHierarchyResult<unknown>, 'truncated' | 'metadata'>,\n\treadKind: 'Organization' | 'Project',\n): boolean {\n\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, result.metadata);\n\tif (!result.truncated) return assessment.fetchFailed || assessment.truncated;\n\n\tif (!assessment.reported) {\n\t\t// Deduped like every other `truncationWarning` emission (see `drains.ts`), not plain-pushed: no caller can\n\t\t// collide with it today, but nothing about this funnel guarantees a third read won't.\n\t\tappendDedupedWarning(warnings, truncationWarning(id, domain, connectionId, readKind, 'exhausted'));\n\t}\n\treturn true;\n}\n\nexport async function listOrgs(\n\tctx: ProviderReadContext,\n\toptions?: ListOrgsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listOrgs', options);\n\tconst ids = options?.providerId != null ? [options.providerId] : supportedOrderedCloudIntegrationIds;\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A specifically requested connection that can't be resolved is a broken connection, not a\n\t\t\t// provider with no orgs — surface it (warning + fetchFailed) instead of dropping the id\n\t\t\t// silently, so a caller can tell it apart from an account that genuinely has no orgs.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\t\tif (isIssuesIntegration(integration)) {\n\t\t\t// Issue trackers expose \"resources\" (Jira sites, Linear orgs, …) as their org analogue.\n\t\t\tconst { value: resources, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getResourcesForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (resources != null) {\n\t\t\t\titems.push(...resources.map(r => resourceToOrg(id, r)));\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\t// A warning with no value is a hard read failure, not an empty account.\n\t\t\t\tif (resources == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (!integration.supportsOrganizationDiscovery) {\n\t\t\t// The provider registers no org-discovery hook. Report it as\n\t\t\t// explicitly unsupported rather than contributing a silent empty list that a caller can't\n\t\t\t// tell apart from \"this account has no orgs\".\n\t\t\tfetchFailed = true;\n\t\t\twarnings.push(\n\t\t\t\tunsupportedWarning(id, domain, connectionId, `Organization discovery is not supported by '${id}'.`),\n\t\t\t);\n\t\t} else {\n\t\t\tconst { value, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getOrganizationsForUserResult(connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (value != null) {\n\t\t\t\titems.push(...value.values.map(org => withProviderContext(id, org)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, value, 'Organization')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (value == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listProjects(\n\tctx: ProviderReadContext,\n\toptions?: ListProjectsOptions,\n): Promise<ProviderResult<ProviderOrganization>> {\n\tassertHierarchyReadTarget('listProjects', options);\n\tconst ids =\n\t\toptions?.providerId != null\n\t\t\t? [options.providerId]\n\t\t\t: [\n\t\t\t\t\t...supportedOrderedCloudIssuesIntegrationIds,\n\t\t\t\t\tGitCloudHostIntegrationId.AzureDevOps,\n\t\t\t\t\tGitSelfManagedHostIntegrationId.AzureDevOpsServer,\n\t\t\t\t];\n\tconst singleProvider = ids.length === 1;\n\tconst connectionId = singleProvider ? options?.connectionId : undefined;\n\tconst requestedDomain = singleProvider ? options?.domain : undefined;\n\n\tconst results = await mapBounded(ids, providerFanOutConcurrency, async id => {\n\t\tconst integration = await ctx.getIntegrationForRead(id, connectionId, requestedDomain);\n\t\tif (integration == null) {\n\t\t\t// A requested connection that can't be resolved is a broken connection, not a provider with\n\t\t\t// no projects — surface it (warning + fetchFailed) instead of dropping the id silently.\n\t\t\tconst early = ctx.earlyReturnConnectionWarnings(id, connectionId, requestedDomain);\n\t\t\treturn {\n\t\t\t\titems: [] as ProviderOrganization[],\n\t\t\t\twarnings: early.warnings,\n\t\t\t\tfetchFailed: early.fetchFailed,\n\t\t\t};\n\t\t}\n\n\t\tconst items: ProviderOrganization[] = [];\n\t\tconst warnings: ProviderWarning[] = [];\n\t\tlet fetchFailed = false;\n\t\tconst domain = ctx.domainForRead(integration, id, connectionId, requestedDomain);\n\t\tconst org = options?.org;\n\t\tconst warnOnMissingSession = warnOnMissingSessionForDomain(id, requestedDomain);\n\n\t\t// Git hosts with a project tier (Azure DevOps) read projects through their own hierarchy hook,\n\t\t// scoped to `org` when given. Non-Azure git hosts have no project tier and return undefined.\n\t\tif (!isIssuesIntegration(integration)) {\n\t\t\t// Check the capability first, like listOrgs/listRepos/listIssuesPage do. Without it, a host\n\t\t\t// with no project tier returns `undefined` from a successful read, which `runCaptured` can't\n\t\t\t// tell apart from an unresolvable session — turning a healthy provider into a `no-connection`\n\t\t\t// warning + `fetchFailed` (and, in Kepler, a spurious reconnect prompt).\n\t\t\tif (!integration.supportsProjectDiscovery) {\n\t\t\t\treturn { items: items, warnings: warnings, fetchFailed: false };\n\t\t\t}\n\n\t\t\tconst { value: projects, warning } = await runCaptured(\n\t\t\t\tid,\n\t\t\t\tdomain,\n\t\t\t\tconnectionId,\n\t\t\t\t() => integration.getProjectsForOrgResult(org, connectionId),\n\t\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t\t);\n\t\t\tif (warning != null) {\n\t\t\t\twarnings.push(warning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => withProviderContext(id, project)));\n\n\t\t\t\tif (mergeHierarchyIncompleteness(warnings, id, domain, connectionId, projects, 'Project')) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t\t}\n\n\t\tconst { value: resources, warning: resourcesWarning } = await runCaptured(\n\t\t\tid,\n\t\t\tdomain,\n\t\t\tconnectionId,\n\t\t\t() => integration.getResourcesForUserResult(connectionId),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\t\tif (resourcesWarning != null) {\n\t\t\twarnings.push(resourcesWarning);\n\t\t\tif (resources == null) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\t// By any identifier, unlike the issue reads' `selectorMatches`: this lists what the caller can then select, so a\n\t\t// match through a name shows the user those projects rather than silently reading another backlog.\n\t\tconst scopedResources =\n\t\t\torg != null ? resources?.filter(resource => resourceMatchesOrg(resource, org)) : resources;\n\t\tif (scopedResources != null && scopedResources.length !== 0) {\n\t\t\tconst { value: projects, warning: projectsWarning } = await runCaptured(id, domain, connectionId, () =>\n\t\t\t\tintegration.getProjectsForResourcesWithMetadataResult(scopedResources, connectionId),\n\t\t\t);\n\t\t\tif (projectsWarning != null) {\n\t\t\t\twarnings.push(projectsWarning);\n\t\t\t\tif (projects == null) {\n\t\t\t\t\tfetchFailed = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (projects != null) {\n\t\t\t\titems.push(...projects.values.map(project => projectToOrg(id, project, scopedResources)));\n\t\t\t}\n\t\t\tconst assessment = mergeAssessmentInto(warnings, id, domain, connectionId, projects?.metadata);\n\t\t\tif (assessment.fetchFailed || assessment.truncated) {\n\t\t\t\tfetchFailed = true;\n\t\t\t}\n\t\t}\n\n\t\treturn { items: items, warnings: warnings, fetchFailed: fetchFailed };\n\t});\n\n\treturn mergeHierarchyResults(results);\n}\n\nexport async function listRepos(\n\tctx: ProviderReadContext,\n\toptions: {\n\t\tproviderId: IntegrationIds;\n\t\torg?: string;\n\t\tproject?: string;\n\t\tpage?: number;\n\t\tcursor?: string;\n\t\titemsPerPage?: number;\n\t\tconnectionId?: string;\n\t\tdomain?: string;\n\t},\n): Promise<ProviderPagedResult<ProviderRepositoryShape>> {\n\t// Truncated as well as floored, like every other paged read here: `page` is reported back as\n\t// `page.currentPage` and bounds the cursor walk below, and neither can be fractional.\n\tconst page = Math.max(1, Math.trunc(options.page ?? 1));\n\tif (isIssuesHostIntegrationId(options.providerId)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst integration = await ctx.getIntegrationForRead(options.providerId, options.connectionId, options.domain);\n\tif (integration == null) {\n\t\t// A supplied connection or domain that no longer resolves is a broken target, not an empty account —\n\t\t// surface a no-connection warning + fetchFailed rather than a silent empty page.\n\t\tconst early = ctx.earlyReturnConnectionWarnings(options.providerId, options.connectionId, options.domain);\n\t\treturn refusedPage(page, early.warnings, early.fetchFailed);\n\t}\n\tif (!isGitHostIntegration(integration)) {\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[gitHostOnlySurfaceWarning(options.providerId, undefined, options.connectionId, 'repository discovery')],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst domain = ctx.domainForRead(integration, options.providerId, options.connectionId, options.domain);\n\n\tconst accountWide = options.org == null;\n\tconst supported = accountWide\n\t\t? integration.supportsUserRepositoryDiscovery\n\t\t: integration.supportsRepositoryDiscovery;\n\tif (!supported) {\n\t\t// No matching repo-discovery hook — org-scoped (e.g. Bitbucket Data Center) or account-wide (e.g.\n\t\t// Bitbucket/Azure, whose repos can only be walked per workspace/org). Report unsupported rather than\n\t\t// a silent empty page indistinguishable from \"no repos\"; for the account-wide case the caller should\n\t\t// fan out per org from listOrgs instead.\n\t\treturn refusedPage(\n\t\t\tpage,\n\t\t\t[\n\t\t\t\tunsupportedWarning(\n\t\t\t\t\toptions.providerId,\n\t\t\t\t\tdomain,\n\t\t\t\t\toptions.connectionId,\n\t\t\t\t\taccountWide\n\t\t\t\t\t\t? `Account-wide repository discovery is not supported by '${options.providerId}'; list repositories per org instead.`\n\t\t\t\t\t\t: `Repository discovery is not supported by '${options.providerId}'.`,\n\t\t\t\t),\n\t\t\t],\n\t\t\ttrue,\n\t\t);\n\t}\n\n\tconst org = options.org;\n\tconst warnOnMissingSession = warnOnMissingSessionForDomain(options.providerId, options.domain);\n\tconst readPage = (cursor: string | undefined) =>\n\t\trunCaptured(\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\t() =>\n\t\t\t\torg == null\n\t\t\t\t\t? integration.getRepositoriesForUserResult({ cursor: cursor, connectionId: options.connectionId })\n\t\t\t\t\t: integration.getRepositoriesForOrgResult(org, {\n\t\t\t\t\t\t\tproject: options.project,\n\t\t\t\t\t\t\tcursor: cursor,\n\t\t\t\t\t\t\tconnectionId: options.connectionId,\n\t\t\t\t\t\t}),\n\t\t\t{ warnOnMissingSession: warnOnMissingSession },\n\t\t);\n\n\t/**\n\t * Builds one page's result from what the provider returned. Shared by the initial read and by every round of\n\t * the cursor walk below, so a walked page N is described by exactly the same rules as a directly-requested one\n\t * (position, continuation, truncation) rather than by a second copy of them.\n\t */\n\tconst toResult = (\n\t\t{ value, warning }: Awaited<ReturnType<typeof readPage>>,\n\t\trequestedPage: number,\n\t\tsuppliedCursor: string | undefined,\n\t): ProviderPagedResult<ProviderRepositoryShape> => {\n\t\tconst items = value?.values ?? [];\n\t\tconst warnings = warning != null ? [warning] : [];\n\t\t// The repos read core is cursor-only and can't accept a page size, so don't echo the requested\n\t\t// `itemsPerPage` as if it were applied — report what the provider returned (its own pageSize when\n\t\t// available, else the actual item count).\n\t\tconst paged = toProviderPageInfo(items.length, value?.paging);\n\t\t// Convert the SDK collection metadata into scope-aware warnings + failure/truncation flags, appending\n\t\t// them to any captured thrown-error warning without discarding the partial result's items.\n\t\tconst assessment = mergeAssessmentInto(\n\t\t\twarnings,\n\t\t\toptions.providerId,\n\t\t\tdomain,\n\t\t\toptions.connectionId,\n\t\t\tvalue?.metadata,\n\t\t);\n\t\t// Whether this read honors a page NUMBER, decided from what the provider actually reported rather than\n\t\t// guessed from the absence of a cursor. A numbered-page repos host reports its position (`paging.page`)\n\t\t// and/or its successor (`paging.nextPage`) — Bitbucket's workspace walk consumes `page` as a real 1-based\n\t\t// page and reports `nextPage` without echoing `currentPage`, so both signals have to count. Every wired\n\t\t// cursor-based repos read (GitHub's org/user walks, GitLab's user walk, Bitbucket's cursor read) reports\n\t\t// neither. Keying off `paged.cursor == null` instead read the paging layer's \"reported another page,\n\t\t// handed back no continuation\" sentinel as evidence of a numbered host, which is exactly backwards.\n\t\tconst pageAdvanceable = value?.paging?.page != null || value?.paging?.nextPage != null;\n\t\t// Numbered-page hosts that don't echo `currentPage` may still be advanced by the requested `page` (initial\n\t\t// read) or by the cursor the caller threaded back. A cursor-only host ignores an UNTHREADED `page`\n\t\t// request, so its page-less first page is page 1.\n\t\tconst currentPage = resolveCurrentPage({\n\t\t\tproviderPage: paged.page.currentPage,\n\t\t\trequestedPage: requestedPage,\n\t\t\tsuppliedCursor: suppliedCursor,\n\t\t\tpageAdvanceable: pageAdvanceable,\n\t\t});\n\t\t// Never advertise `hasMore` without a continuation the caller can act on — the same contract\n\t\t// `listPullRequestsPage`, `listIssuesPage` and `broadenIssues` hold, via the same helper. A provider that\n\t\t// reports another page but hands back neither `endCursor` nor `nextPage` (surfaced as the `'{}'` sentinel,\n\t\t// which `toProviderPageInfo` drops) is terminal-but-incomplete: `hasMore: false` + `page.truncated`.\n\t\t// Synthesizing a page-number cursor for a cursor-only host instead — which is what this read used to do\n\t\t// unconditionally — handed back a continuation the provider ignores, so it answered with its FIRST page\n\t\t// again while still reporting `hasMore: true`. A consumer draining until `hasMore` clears (Kepler's repo\n\t\t// drain does exactly that) then looped to its own page cap, accumulating a duplicate copy of every repo\n\t\t// per round, and never saw a truncation signal.\n\t\tconst continuation = resolveContinuation(paged, pageAdvanceable ? currentPage + 1 : undefined);\n\t\t// The org-hierarchy read can also stop at a defensive backstop with more repos unlisted and no cursor to\n\t\t// resume (top-level `truncated`, or `paging.truncated` on a single-page read). Metadata incompleteness and\n\t\t// a demoted continuation are two further, independent sources of the same signal.\n\t\tconst truncated =\n\t\t\t(value?.truncated ?? false) ||\n\t\t\t(value?.paging?.truncated ?? false) ||\n\t\t\tassessment.truncated ||\n\t\t\tcontinuation.truncated;\n\t\treturn {\n\t\t\t// Normalize the raw provider-apis repos to the GitLens-owned shape at the surface boundary.\n\t\t\titems: items.map(toProviderRepositoryShape),\n\t\t\twarnings: warnings,\n\t\t\tpage: { ...paged.page, currentPage: currentPage, truncated: truncated || undefined },\n\t\t\thasMore: continuation.hasMore,\n\t\t\tcursor: continuation.cursor,\n\t\t\tfetchFailed: assessment.fetchFailed || (warning != null && value == null) || undefined,\n\t\t};\n\t};\n\n\tconst first = await readPage(\n\t\toptions.cursor ??\n\t\t\t(integration.id === GitSelfManagedHostIntegrationId.BitbucketServer ? undefined : pageToCursor(page)),\n\t);\n\tconst result = toResult(first, page, options.cursor);\n\t// A numbered-page host honored the synthesized cursor, so this IS page N; a cursor-only host ignored it and\n\t// answered with page 1, which the walk below advances. Decided from the provider's own paging metadata rather\n\t// than assumed, hence after the read.\n\tconst pageAdvanceable = first.value?.paging?.page != null || first.value?.paging?.nextPage != null;\n\tif (options.cursor != null || page === 1 || pageAdvanceable) return result;\n\n\t// Walk the provider's real continuations to the requested page. Only the last page's items are kept — pages\n\t// 1..N returned as \"page N\" would duplicate items for a normal paged consumer — while warnings, truncation,\n\t// and failure accumulate across the whole walked prefix, since an earlier round's problem still means this\n\t// result isn't authoritative. The provider read is re-issued per round, but the integration is resolved once:\n\t// re-entering this function per page would repeat its auth resolution N times for page N.\n\tconst walkedWarnings = [...result.warnings];\n\tlet walkedFetchFailed = result.fetchFailed === true;\n\tlet walkedTruncated = result.page.truncated === true;\n\tlet previous = result;\n\tfor (let currentPage = 2; currentPage <= page; currentPage++) {\n\t\tif (!previous.hasMore || previous.cursor == null) {\n\t\t\t// The requested page lies past the provider's terminal cursor: an EMPTY page N, never the last\n\t\t\t// available page relabeled (see ProviderPageInfo.currentPage).\n\t\t\treturn {\n\t\t\t\titems: [],\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { currentPage: page, itemsPerPage: 0, truncated: walkedTruncated || undefined },\n\t\t\t\thasMore: false,\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tconst suppliedCursor = previous.cursor;\n\t\tconst requested = toResult(await readPage(suppliedCursor), currentPage, suppliedCursor);\n\t\tfor (const warning of requested.warnings) {\n\t\t\tappendDedupedWarning(walkedWarnings, warning);\n\t\t}\n\t\twalkedFetchFailed ||= requested.fetchFailed === true;\n\t\twalkedTruncated ||= requested.page.truncated === true;\n\t\tif (currentPage === page) {\n\t\t\treturn {\n\t\t\t\t...requested,\n\t\t\t\twarnings: walkedWarnings,\n\t\t\t\tpage: { ...requested.page, truncated: walkedTruncated || undefined },\n\t\t\t\tfetchFailed: walkedFetchFailed || undefined,\n\t\t\t};\n\t\t}\n\n\t\tprevious = requested;\n\t}\n\n\treturn result;\n}\n"],"mappings":"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"}
@@ -20,6 +20,13 @@ import { ProviderOrganization, ProviderResult, ProviderWarning } from "../result
20
20
  export declare function resourceLabel(resource: ResourceDescriptor): string;
21
21
  /** Whether `org` addresses this resource by any of the identifiers a caller could reasonably have. */
22
22
  export declare function resourceMatchesOrg(resource: ResourceDescriptor, org: string): boolean;
23
+ /**
24
+ * Whether a caller's `org`/`project` selector addresses this resource or project, for the reads that act on a
25
+ * remembered selection. Jira's only by its canonical key (a site's id, a project's key): a site or project that was
26
+ * removed must select nothing, not resolve through its old name or id to another one that happens to carry it as a
27
+ * name, id or key. Every other provider by any identifier, as {@link resourceMatchesOrg}.
28
+ */
29
+ export declare function selectorMatches(providerId: IntegrationIds, resource: ResourceDescriptor, selector: string): boolean;
23
30
  /**
24
31
  * The id of the RESOURCE a project belongs to, used to look up which account (and therefore which user) that
25
32
  * project's issues should be scoped to. Falls back through the project's own identifiers because some providers
@@ -38,6 +45,12 @@ export declare function projectKey(project: ResourceDescriptor): string;
38
45
  * optional for the one provider family that may omit it.
39
46
  */
40
47
  export declare function resourceToOrg(providerId: IntegrationIds, resource: ResourceDescriptor, org?: string): ProviderOrganization;
48
+ /**
49
+ * Maps an issue-tracker project descriptor to the unified org shape, as {@link resourceToOrg} does with its parent's
50
+ * label as `org`. A Jira project is published by what its reads take: `name` is its key (its display name can collide
51
+ * with another project's key), and `orgId` its site's id, since the label in `org` can't address the site.
52
+ */
53
+ export declare function projectToOrg(providerId: IntegrationIds, project: ResourceDescriptor, resources: ResourceDescriptor[]): ProviderOrganization;
41
54
  /**
42
55
  * The resource a project belongs under, as a display label — the `org` of the project's unified org shape.
43
56
  *
@@ -1,5 +1,6 @@
1
1
  import "../constants.js";
2
2
  import { appendDedupedWarning } from "../results.js";
3
+ import { isJiraIntegrationId } from "../utils/integration.utils.js";
3
4
  //#region ../plus/integrations/src/reads/hierarchy.utils.ts
4
5
  /**
5
6
  * Mapping of a provider's own hierarchy vocabulary onto the single {@link ProviderOrganization} shape the facade
@@ -26,6 +27,15 @@ function resourceMatchesOrg(resource, org) {
26
27
  return resource.key === org || typed.id === org || typed.name === org;
27
28
  }
28
29
  /**
30
+ * Whether a caller's `org`/`project` selector addresses this resource or project, for the reads that act on a
31
+ * remembered selection. Jira's only by its canonical key (a site's id, a project's key): a site or project that was
32
+ * removed must select nothing, not resolve through its old name or id to another one that happens to carry it as a
33
+ * name, id or key. Every other provider by any identifier, as {@link resourceMatchesOrg}.
34
+ */
35
+ function selectorMatches(providerId, resource, selector) {
36
+ return isJiraIntegrationId(providerId) ? resource.key === selector : resourceMatchesOrg(resource, selector);
37
+ }
38
+ /**
29
39
  * The id of the RESOURCE a project belongs to, used to look up which account (and therefore which user) that
30
40
  * project's issues should be scoped to. Falls back through the project's own identifiers because some providers
31
41
  * (and test doubles) return project descriptors without naming their parent.
@@ -63,6 +73,21 @@ function resourceToOrg(providerId, resource, org) {
63
73
  };
64
74
  }
65
75
  /**
76
+ * Maps an issue-tracker project descriptor to the unified org shape, as {@link resourceToOrg} does with its parent's
77
+ * label as `org`. A Jira project is published by what its reads take: `name` is its key (its display name can collide
78
+ * with another project's key), and `orgId` its site's id, since the label in `org` can't address the site.
79
+ */
80
+ function projectToOrg(providerId, project, resources) {
81
+ const item = resourceToOrg(providerId, project, orgForProject(providerId, project, resources));
82
+ if (!isJiraIntegrationId(providerId)) return item;
83
+ const { resourceId } = project;
84
+ return {
85
+ ...item,
86
+ name: project.key,
87
+ ...typeof resourceId === "string" ? { orgId: resourceId } : {}
88
+ };
89
+ }
90
+ /**
66
91
  * The resource a project belongs under, as a display label — the `org` of the project's unified org shape.
67
92
  *
68
93
  * A project doesn't reliably name its parent, so try each identifier it does carry against the resources that
@@ -124,6 +149,6 @@ function assertHierarchyReadTarget(method, options) {
124
149
  if (options?.providerId == null && (options?.connectionId != null || options?.domain != null)) throw new TypeError(`'${method}' requires 'providerId' when 'connectionId' or 'domain' is supplied`);
125
150
  }
126
151
  //#endregion
127
- export { assertHierarchyReadTarget, mergeHierarchyResults, orgForProject, projectKey, resourceIdForProject, resourceLabel, resourceMatchesOrg, resourceToOrg, withProviderContext };
152
+ export { assertHierarchyReadTarget, mergeHierarchyResults, orgForProject, projectKey, projectToOrg, resourceIdForProject, resourceLabel, resourceMatchesOrg, resourceToOrg, selectorMatches, withProviderContext };
128
153
 
129
154
  //# sourceMappingURL=hierarchy.utils.js.map