@gitkraken/core-gitlens 0.4.100 → 0.4.101

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 (65) hide show
  1. package/CHANGELOG.md +4 -1
  2. package/dist/plus/integrations/errors.d.ts.map +1 -1
  3. package/dist/plus/integrations/errors.js +6 -2
  4. package/dist/plus/integrations/errors.js.map +1 -1
  5. package/dist/plus/integrations/integrationService.d.ts +8 -5
  6. package/dist/plus/integrations/integrationService.d.ts.map +1 -1
  7. package/dist/plus/integrations/integrationService.js +20 -652
  8. package/dist/plus/integrations/integrationService.js.map +1 -1
  9. package/dist/plus/integrations/models/gitHostIntegration.d.ts +10 -17
  10. package/dist/plus/integrations/models/gitHostIntegration.d.ts.map +1 -1
  11. package/dist/plus/integrations/models/gitHostIntegration.js +38 -88
  12. package/dist/plus/integrations/models/gitHostIntegration.js.map +1 -1
  13. package/dist/plus/integrations/models/issuesIntegration.d.ts +4 -21
  14. package/dist/plus/integrations/models/issuesIntegration.d.ts.map +1 -1
  15. package/dist/plus/integrations/models/issuesIntegration.js +5 -47
  16. package/dist/plus/integrations/models/issuesIntegration.js.map +1 -1
  17. package/dist/plus/integrations/providers/azureDevOps.js.map +1 -1
  18. package/dist/plus/integrations/reads/broaden.d.ts +20 -0
  19. package/dist/plus/integrations/reads/broaden.d.ts.map +1 -0
  20. package/dist/plus/integrations/reads/broaden.js +326 -0
  21. package/dist/plus/integrations/reads/broaden.js.map +1 -0
  22. package/dist/plus/integrations/reads/context.d.ts +22 -0
  23. package/dist/plus/integrations/reads/context.d.ts.map +1 -1
  24. package/dist/plus/integrations/reads/hierarchy.d.ts +1 -1
  25. package/dist/plus/integrations/reads/hierarchy.d.ts.map +1 -1
  26. package/dist/plus/integrations/reads/hierarchy.js +109 -137
  27. package/dist/plus/integrations/reads/hierarchy.js.map +1 -1
  28. package/dist/plus/integrations/reads/hierarchy.utils.d.ts +25 -1
  29. package/dist/plus/integrations/reads/hierarchy.utils.d.ts.map +1 -1
  30. package/dist/plus/integrations/reads/hierarchy.utils.js +43 -0
  31. package/dist/plus/integrations/reads/hierarchy.utils.js.map +1 -1
  32. package/dist/plus/integrations/reads/issueTracker.d.ts +1 -1
  33. package/dist/plus/integrations/reads/issueTracker.d.ts.map +1 -1
  34. package/dist/plus/integrations/reads/issueTracker.js +13 -25
  35. package/dist/plus/integrations/reads/issueTracker.js.map +1 -1
  36. package/dist/plus/integrations/reads/issues.d.ts +2 -2
  37. package/dist/plus/integrations/reads/issues.d.ts.map +1 -1
  38. package/dist/plus/integrations/reads/issues.js.map +1 -1
  39. package/dist/plus/integrations/reads/pullRequests.d.ts +1 -1
  40. package/dist/plus/integrations/reads/pullRequests.d.ts.map +1 -1
  41. package/dist/plus/integrations/reads/pullRequests.js +1 -1
  42. package/dist/plus/integrations/reads/pullRequests.js.map +1 -1
  43. package/dist/plus/integrations/reads/resolveRepository.d.ts +25 -0
  44. package/dist/plus/integrations/reads/resolveRepository.d.ts.map +1 -0
  45. package/dist/plus/integrations/reads/resolveRepository.js +235 -0
  46. package/dist/plus/integrations/reads/resolveRepository.js.map +1 -0
  47. package/dist/plus/integrations/reads/sweeps.d.ts +11 -0
  48. package/dist/plus/integrations/reads/sweeps.d.ts.map +1 -0
  49. package/dist/plus/integrations/reads/sweeps.js +125 -0
  50. package/dist/plus/integrations/reads/sweeps.js.map +1 -0
  51. package/package.json +1 -1
  52. package/src/plus/integrations/errors.ts +6 -2
  53. package/src/plus/integrations/integrationService.ts +27 -801
  54. package/src/plus/integrations/models/gitHostIntegration.ts +71 -93
  55. package/src/plus/integrations/models/issuesIntegration.ts +5 -52
  56. package/src/plus/integrations/providers/azureDevOps.ts +1 -1
  57. package/src/plus/integrations/reads/broaden.ts +452 -0
  58. package/src/plus/integrations/reads/context.ts +24 -0
  59. package/src/plus/integrations/reads/hierarchy.ts +133 -153
  60. package/src/plus/integrations/reads/hierarchy.utils.ts +50 -1
  61. package/src/plus/integrations/reads/issueTracker.ts +15 -27
  62. package/src/plus/integrations/reads/issues.ts +4 -2
  63. package/src/plus/integrations/reads/pullRequests.ts +2 -9
  64. package/src/plus/integrations/reads/resolveRepository.ts +272 -0
  65. package/src/plus/integrations/reads/sweeps.ts +185 -0
@@ -0,0 +1,452 @@
1
+ import type { IssueShape } from '../../../git/models/issue.js';
2
+ import { mapBounded } from '../../../utils/promise.js';
3
+ import { mergeAssessmentInto } from '../collectionMetadata.js';
4
+ import type { IntegrationIds } from '../constants.js';
5
+ import { providerFanOutConcurrency } from '../constants.js';
6
+ import type { ProviderBroadenOrg } from '../manager.js';
7
+ import type { ProviderReposInput } from '../providers/models.js';
8
+ import type { ProviderBroadenResult, ProviderWarning } from '../results.js';
9
+ import { appendDedupedWarning } from '../results.js';
10
+ import { hostFromDomain } from '../utils/domain.utils.js';
11
+ import { isGitHostIntegration, isIssuesHostIntegrationId } from '../utils/integration.utils.js';
12
+ import type { ProviderReadContext } from './context.js';
13
+ import type { BroadenIssuesExhaustedOrg, BroadenIssuesOrgCursor } from './cursors.js';
14
+ import { getBroadenIssuesCursor, isBroadenIssuesOrgExhausted, toBroadenIssuesCursor } from './cursors.js';
15
+ import { drainRepositories, runCaptured } from './drains.js';
16
+ import { resolveContinuation, toProviderPageInfo } from './paging.js';
17
+ import { gitHostOnlySurfaceWarning, issuesUnsupportedWarning, noConnectionWarning, otherWarning } from './warnings.js';
18
+
19
+ /**
20
+ * The issue-broadening fan-out: per org, drain that org's repositories, then read every visible issue in them.
21
+ *
22
+ * What makes this its own read rather than a variant of `listIssuesPage` is that one logical page spans several
23
+ * provider positions — one per org — so its continuation is a per-org cursor BUNDLE (see `cursors.ts`) and its
24
+ * failure attribution is per provider across those orgs. Both are the reason a page number alone can't address a
25
+ * later page, and why {@link broadenIssues} walks prior pages itself when given only `page`.
26
+ */
27
+
28
+ export interface BroadenIssuesOptions {
29
+ orgs: ProviderBroadenOrg[];
30
+ page?: number;
31
+ cursor?: string;
32
+ forceSync?: boolean;
33
+ }
34
+
35
+ /** One org slice of the fan-out: its issues plus the position and health the aggregation keys the bundle by. */
36
+ interface BroadenSlice {
37
+ items: IssueShape[];
38
+ warnings: ProviderWarning[];
39
+ broadenedProviderIds: IntegrationIds[];
40
+ providerId: IntegrationIds;
41
+ org: string;
42
+ connectionId?: string;
43
+ domain?: string;
44
+ nextCursor?: string;
45
+ retryPage?: number;
46
+ hasMore: boolean;
47
+ exhausted: boolean;
48
+ fetchFailed: boolean;
49
+ truncated: boolean;
50
+ }
51
+
52
+ /**
53
+ * Accumulates provider attribution across the rounds of a page-number traversal.
54
+ *
55
+ * The three sets are mutually exclusive per provider and the ordering rules are what keep them so: a provider
56
+ * that failed in one round but broadened in another is INCOMPLETE, not failed, whichever round reported first.
57
+ */
58
+ class ProviderAttribution {
59
+ private readonly broadened = new Set<IntegrationIds>();
60
+ private readonly failed = new Set<IntegrationIds>();
61
+ private readonly incomplete = new Set<IntegrationIds>();
62
+
63
+ merge(result: ProviderBroadenResult<IssueShape>): void {
64
+ for (const providerId of result.failedProviderIds) {
65
+ if (this.broadened.has(providerId) || result.broadenedProviderIds.includes(providerId)) {
66
+ this.incomplete.add(providerId);
67
+ } else {
68
+ this.failed.add(providerId);
69
+ }
70
+ }
71
+ for (const providerId of result.incompleteProviderIds) {
72
+ this.failed.delete(providerId);
73
+ this.incomplete.add(providerId);
74
+ }
75
+ for (const providerId of result.broadenedProviderIds) {
76
+ this.broadened.add(providerId);
77
+ if (this.failed.delete(providerId)) {
78
+ this.incomplete.add(providerId);
79
+ }
80
+ }
81
+ }
82
+
83
+ toResultFields(): Pick<
84
+ ProviderBroadenResult<IssueShape>,
85
+ 'broadenedProviderIds' | 'failedProviderIds' | 'incompleteProviderIds'
86
+ > {
87
+ return {
88
+ broadenedProviderIds: [...this.broadened],
89
+ failedProviderIds: [...this.failed],
90
+ incompleteProviderIds: [...this.incomplete],
91
+ };
92
+ }
93
+ }
94
+
95
+ export async function broadenIssues(
96
+ ctx: ProviderReadContext,
97
+ options: BroadenIssuesOptions,
98
+ ): Promise<ProviderBroadenResult<IssueShape>> {
99
+ const page = Math.max(1, Math.trunc(options.page ?? 1));
100
+
101
+ // Kepler's existing contract persists only a page number. When no opaque continuation was supplied,
102
+ // advance through prior pages internally so cursor-only providers still return the requested page.
103
+ // Each recursive call below carries a cursor, so it bypasses this block and performs exactly one round.
104
+ if (options.cursor == null && page > 1) {
105
+ return traverseToRequestedPage(ctx, options, page);
106
+ }
107
+
108
+ const results = await mapBounded(options.orgs, providerFanOutConcurrency, org =>
109
+ readOrgSlice(ctx, options, org, page),
110
+ );
111
+
112
+ const items: IssueShape[] = [];
113
+ const warnings: ProviderWarning[] = [];
114
+ const broadenedProviderIds = new Set<IntegrationIds>();
115
+ const problemProviderIds = new Set<IntegrationIds>();
116
+ const cursors: BroadenIssuesOrgCursor[] = [];
117
+ const exhausted: BroadenIssuesExhaustedOrg[] = [];
118
+ let hasMore = false;
119
+ let fetchFailed = false;
120
+ let truncated = false;
121
+ for (const result of results) {
122
+ if (result == null) {
123
+ continue;
124
+ }
125
+
126
+ items.push(...result.items);
127
+ warnings.push(...result.warnings);
128
+ for (const id of result.broadenedProviderIds) {
129
+ broadenedProviderIds.add(id);
130
+ }
131
+ if (result.nextCursor != null || result.retryPage != null) {
132
+ cursors.push({
133
+ providerId: result.providerId,
134
+ org: result.org,
135
+ connectionId: result.connectionId,
136
+ domain: result.domain,
137
+ cursor: result.nextCursor,
138
+ retryPage: result.retryPage,
139
+ });
140
+ }
141
+ if (result.exhausted) {
142
+ exhausted.push({
143
+ providerId: result.providerId,
144
+ org: result.org,
145
+ connectionId: result.connectionId,
146
+ domain: result.domain,
147
+ });
148
+ }
149
+ if (result.hasMore) {
150
+ hasMore = true;
151
+ }
152
+ if (result.fetchFailed) {
153
+ fetchFailed = true;
154
+ }
155
+ if (result.truncated) {
156
+ truncated = true;
157
+ }
158
+ if (result.fetchFailed || result.truncated) {
159
+ problemProviderIds.add(result.providerId);
160
+ }
161
+ }
162
+
163
+ const cursor = toBroadenIssuesCursor(cursors, exhausted, options.orgs.length);
164
+ const failedProviderIds: IntegrationIds[] = [];
165
+ const incompleteProviderIds: IntegrationIds[] = [];
166
+ for (const providerId of problemProviderIds) {
167
+ if (broadenedProviderIds.has(providerId)) {
168
+ incompleteProviderIds.push(providerId);
169
+ } else {
170
+ failedProviderIds.push(providerId);
171
+ }
172
+ }
173
+ return {
174
+ items: items,
175
+ warnings: warnings,
176
+ // `currentPage` is positional, per ProviderPageInfo.currentPage: this fan-out has no provider-reported
177
+ // page of its own (its cursor is a per-org bundle, not a page), so the position is the one the caller
178
+ // addressed — the `page` it supplied, or the page the internal traversal advanced to.
179
+ page: { currentPage: page, itemsPerPage: items.length, truncated: truncated || undefined },
180
+ // `hasMore` promises a resumable continuation, so it must be true ONLY when a real cursor was
181
+ // produced. Repo-drain truncation (a backstop hit with no persisted repo cursor) can't be resumed —
182
+ // re-invoking would re-drain the same repos and repeat issues — so it is surfaced as the terminal
183
+ // `page.truncated` incompleteness signal instead of `hasMore`, matching listRepos. Guard `hasMore`
184
+ // against a missing cursor so we never advertise a continuation the caller can't make.
185
+ hasMore: hasMore && cursor != null,
186
+ cursor: cursor,
187
+ fetchFailed: fetchFailed || undefined,
188
+ broadenedProviderIds: [...broadenedProviderIds],
189
+ failedProviderIds: failedProviderIds,
190
+ incompleteProviderIds: incompleteProviderIds,
191
+ fanOutCount: options.orgs.length,
192
+ };
193
+ }
194
+
195
+ /**
196
+ * Advances to page N by re-running the fan-out for each prior page, threading each round's cursor bundle into the
197
+ * next. Only the requested page's items are returned; warnings and provider attribution accumulate across the
198
+ * whole walked prefix, since a failure in an earlier round still means this result is not authoritative.
199
+ */
200
+ async function traverseToRequestedPage(
201
+ ctx: ProviderReadContext,
202
+ options: BroadenIssuesOptions,
203
+ page: number,
204
+ ): Promise<ProviderBroadenResult<IssueShape>> {
205
+ let cursor: string | undefined;
206
+ const traversalWarnings: ProviderWarning[] = [];
207
+ const attribution = new ProviderAttribution();
208
+ let traversalFetchFailed = false;
209
+ let traversalTruncated = false;
210
+
211
+ for (let currentPage = 1; currentPage < page; currentPage++) {
212
+ const previous = await broadenIssues(ctx, {
213
+ ...options,
214
+ page: currentPage,
215
+ cursor: cursor,
216
+ // A forced refresh belongs to the logical read, not every cursor-advancement round.
217
+ forceSync: currentPage === 1 ? options.forceSync : false,
218
+ });
219
+ for (const warning of previous.warnings) {
220
+ appendDedupedWarning(traversalWarnings, warning);
221
+ }
222
+ attribution.merge(previous);
223
+ traversalFetchFailed ||= previous.fetchFailed === true;
224
+ traversalTruncated ||= previous.page.truncated === true;
225
+ if (!previous.hasMore || previous.cursor == null) {
226
+ // The requested page lies past the last continuation: an empty page N, never the last page relabeled.
227
+ return {
228
+ items: [],
229
+ warnings: traversalWarnings,
230
+ page: { currentPage: page, itemsPerPage: 0, truncated: traversalTruncated || undefined },
231
+ hasMore: false,
232
+ fetchFailed: traversalFetchFailed || undefined,
233
+ ...attribution.toResultFields(),
234
+ fanOutCount: options.orgs.length,
235
+ };
236
+ }
237
+
238
+ cursor = previous.cursor;
239
+ }
240
+
241
+ const requested = await broadenIssues(ctx, { ...options, page: page, cursor: cursor, forceSync: false });
242
+ for (const warning of requested.warnings) {
243
+ appendDedupedWarning(traversalWarnings, warning);
244
+ }
245
+ attribution.merge(requested);
246
+ return {
247
+ ...requested,
248
+ warnings: traversalWarnings,
249
+ page: {
250
+ ...requested.page,
251
+ truncated: traversalTruncated || requested.page.truncated === true || undefined,
252
+ },
253
+ fetchFailed: traversalFetchFailed || requested.fetchFailed === true || undefined,
254
+ ...attribution.toResultFields(),
255
+ };
256
+ }
257
+
258
+ /** Reads one org's visible issues: drain the org's repos, then read the issues across them. */
259
+ async function readOrgSlice(
260
+ ctx: ProviderReadContext,
261
+ options: BroadenIssuesOptions,
262
+ org: ProviderBroadenOrg,
263
+ page: number,
264
+ ): Promise<BroadenSlice> {
265
+ const connectionId = org.connectionId;
266
+ const requestedDomain = org.domain;
267
+ const cursorDomain = hostFromDomain(requestedDomain) ?? requestedDomain;
268
+ // An org slice that yielded no issues and has nothing to continue: the org's identity (which the
269
+ // aggregation above keys the per-org cursor bundle by) plus why it produced nothing. `exhausted` marks
270
+ // an org a prior round already drained, so it stays skipped rather than being re-read from page 1.
271
+ const barrenSlice = (
272
+ warnings: ProviderWarning[],
273
+ flags?: { exhausted?: boolean; fetchFailed?: boolean; truncated?: boolean },
274
+ ): BroadenSlice => ({
275
+ items: [],
276
+ warnings: warnings,
277
+ broadenedProviderIds: [],
278
+ providerId: org.providerId,
279
+ org: org.name,
280
+ connectionId: connectionId,
281
+ domain: cursorDomain,
282
+ nextCursor: undefined,
283
+ hasMore: false,
284
+ exhausted: flags?.exhausted ?? false,
285
+ fetchFailed: flags?.fetchFailed ?? false,
286
+ truncated: flags?.truncated ?? false,
287
+ });
288
+
289
+ if (isIssuesHostIntegrationId(org.providerId)) {
290
+ return barrenSlice(
291
+ [gitHostOnlySurfaceWarning(org.providerId, requestedDomain, connectionId, 'issue broadening')],
292
+ {
293
+ fetchFailed: true,
294
+ },
295
+ );
296
+ }
297
+
298
+ const integration = await ctx.getIntegrationForRead(org.providerId, connectionId, requestedDomain);
299
+ if (integration == null) {
300
+ // A requested connection or domain that can't be resolved is a broken target — surface it as a
301
+ // warning + fetchFailed rather than dropping the org silently.
302
+ const early = ctx.earlyReturnConnectionWarnings(org.providerId, connectionId, requestedDomain);
303
+ return barrenSlice(
304
+ early.warnings.length > 0
305
+ ? early.warnings
306
+ : [noConnectionWarning(org.providerId, requestedDomain, connectionId)],
307
+ { fetchFailed: true },
308
+ );
309
+ }
310
+ if (!isGitHostIntegration(integration)) {
311
+ return barrenSlice(
312
+ [gitHostOnlySurfaceWarning(org.providerId, requestedDomain, connectionId, 'issue broadening')],
313
+ {
314
+ fetchFailed: true,
315
+ },
316
+ );
317
+ }
318
+ // A git host whose issue tracker is deprecated (Bitbucket) exposes no issues here — surface a
319
+ // warning + fetchFailed and skip it (no repo drain), so broadening never serves a legacy source.
320
+ if (!integration.supportsIssues) {
321
+ return barrenSlice(
322
+ [
323
+ issuesUnsupportedWarning(
324
+ org.providerId,
325
+ ctx.domainForRead(integration, org.providerId, connectionId, requestedDomain),
326
+ connectionId,
327
+ ),
328
+ ],
329
+ { fetchFailed: true },
330
+ );
331
+ }
332
+
333
+ // An org a prior round already drained must not be re-read: cursor-only providers would answer a
334
+ // fresh page-1 request with their first page again, duplicating issues across rounds. Skip it
335
+ // before any work (including the repo drain) and keep it marked exhausted so it stays skipped
336
+ // for the rest of the fan-out.
337
+ if (isBroadenIssuesOrgExhausted(options.cursor, org, options.orgs.length)) {
338
+ return barrenSlice([], { exhausted: true });
339
+ }
340
+
341
+ await ctx.forceRefreshIfRequested(integration, options.forceSync, connectionId);
342
+
343
+ const domain = ctx.domainForRead(integration, org.providerId, connectionId, requestedDomain);
344
+ const reposDrain = await drainRepositories(
345
+ integration,
346
+ org.providerId,
347
+ domain,
348
+ org.name,
349
+ undefined,
350
+ connectionId,
351
+ 100,
352
+ );
353
+ const warnings: ProviderWarning[] = [...reposDrain.warnings];
354
+ const fetchFailed = reposDrain.fetchFailed;
355
+ const truncated = reposDrain.truncated;
356
+
357
+ const repos: ProviderReposInput = reposDrain.repos.map(r => ({ ...r }));
358
+ if (repos.length === 0) {
359
+ return barrenSlice(warnings, { fetchFailed: fetchFailed, truncated: truncated });
360
+ }
361
+
362
+ // Broaden = "all visible": drop the assigned-to-me filter so unassigned issues are included.
363
+ const cursor = getBroadenIssuesCursor(options.cursor, org, page, options.orgs.length);
364
+ const issuesCaptured = await runCaptured(
365
+ org.providerId,
366
+ domain,
367
+ connectionId,
368
+ () =>
369
+ // Normalized shapes seam, uniform with listIssuesPage.
370
+ integration.getMyIssuesForReposAsShapesResult(
371
+ repos,
372
+ { includeAllAssignees: true, cursor: cursor },
373
+ connectionId,
374
+ ),
375
+ { warnOnMissingSession: true },
376
+ );
377
+ if (issuesCaptured.warning != null) {
378
+ warnings.push(issuesCaptured.warning);
379
+ }
380
+ const issuesAssessment = mergeAssessmentInto(
381
+ warnings,
382
+ org.providerId,
383
+ domain,
384
+ connectionId,
385
+ issuesCaptured.value?.metadata,
386
+ );
387
+ let issuesFetchFailed =
388
+ issuesAssessment.fetchFailed || (issuesCaptured.warning != null && issuesCaptured.value == null);
389
+ const items: IssueShape[] = [];
390
+ let hasMore = false;
391
+ let nextCursor: string | undefined;
392
+ let retryPage: number | undefined;
393
+ // Carry a truncation signal from the issue read too: a provider that couldn't confirm it drained
394
+ // a repo (`paging.truncated`) means this org's issues may be incomplete, on top of any repo-drain
395
+ // truncation already captured above.
396
+ let issuesTruncated = false;
397
+ if (issuesCaptured.value != null) {
398
+ items.push(...issuesCaptured.value.values);
399
+ const paged = toProviderPageInfo(issuesCaptured.value.values.length, issuesCaptured.value.paging);
400
+ // An org that reports another page but no usable cursor can't be resumed: it would neither be
401
+ // recorded in the composite cursor nor marked exhausted, so the next round would re-read its
402
+ // page 1 and repeat every issue. Treat it as terminal-but-incomplete (which also marks the org
403
+ // exhausted below, since `exhausted` keys off `!hasMore`).
404
+ const continuation = resolveContinuation(paged, undefined);
405
+ hasMore = continuation.hasMore;
406
+ nextCursor = continuation.cursor;
407
+ issuesTruncated = continuation.truncated || issuesAssessment.truncated;
408
+ } else if (issuesCaptured.warning != null || cursor != null) {
409
+ // Keep the exact position that failed. Without this retry cursor a multi-org continuation would
410
+ // omit this org from the bundle, then synthesize the next numbered page and silently skip the
411
+ // failed page. The synthesized page cursor is also actionable for a first-page cursor-only read:
412
+ // that provider ignores the page marker and retries its first page. A retry slot alone is NOT
413
+ // forward progress, though: advertising `hasMore` for a persistent failure would make an infinite
414
+ // query request it forever. Healthy sibling continuations set `hasMore` independently.
415
+ if (cursor != null) {
416
+ nextCursor = cursor;
417
+ if (issuesCaptured.warning == null) {
418
+ appendDedupedWarning(
419
+ warnings,
420
+ otherWarning(
421
+ org.providerId,
422
+ domain,
423
+ connectionId,
424
+ 'Issue continuation returned no result and must be retried',
425
+ ),
426
+ );
427
+ issuesFetchFailed = true;
428
+ issuesTruncated = true;
429
+ }
430
+ } else {
431
+ retryPage = page;
432
+ }
433
+ }
434
+
435
+ return {
436
+ items: items,
437
+ warnings: warnings,
438
+ broadenedProviderIds: issuesCaptured.value != null ? [org.providerId] : [],
439
+ providerId: org.providerId,
440
+ org: org.name,
441
+ connectionId: connectionId,
442
+ domain: cursorDomain,
443
+ nextCursor: nextCursor,
444
+ retryPage: retryPage,
445
+ hasMore: hasMore,
446
+ // Exhausted once a successful read reports no more pages — recorded in the cursor so later
447
+ // rounds skip it while other orgs keep paging.
448
+ exhausted: issuesCaptured.value != null && !hasMore,
449
+ fetchFailed: fetchFailed || issuesFetchFailed,
450
+ truncated: truncated || issuesTruncated,
451
+ };
452
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ConfiguredIntegrationDescriptor } from '../authentication/models.js';
2
2
  import type { IntegrationIds } from '../constants.js';
3
+ import type { IntegrationsRemoteConfig } from '../context.js';
3
4
  import type { Integration } from '../models/integration.js';
4
5
  import type { ProviderWarning } from '../results.js';
5
6
 
@@ -73,3 +74,26 @@ export interface ProviderReadContext {
73
74
  options?: { cloud?: boolean; domain?: string },
74
75
  ): ConfiguredIntegrationDescriptor[];
75
76
  }
77
+
78
+ /**
79
+ * What {@link resolveRepository} needs on top of {@link ProviderReadContext}.
80
+ *
81
+ * It is the one read whose input is a remote URL rather than a resolved target, so it needs two things the others
82
+ * don't: the user's remote configs (to map a custom domain onto a provider for path parsing) and instance lookup
83
+ * by DOMAIN for a cloud host — `getIntegrationForRead` selects by configured connection, which a cloud provider
84
+ * reached through its canonical host has no need of.
85
+ *
86
+ * Kept as an extension rather than folded into the base contract so the other reads still can't reach either one.
87
+ */
88
+ export interface RepositoryResolutionContext extends ProviderReadContext {
89
+ /** Per-remote-host configurations from `remotes`, as {@link ConfigProvider.getRemoteConfigs} exposes them. */
90
+ getRemoteConfigs(): readonly IntegrationsRemoteConfig[];
91
+
92
+ /**
93
+ * The integration instance for a provider on a given host, bypassing connection selection. Only for the
94
+ * cloud-host branch of the resolution, whose host is canonical by construction (the guards above rejected
95
+ * anything else); every self-managed lookup still goes through {@link ProviderReadContext.getIntegrationForRead}
96
+ * so repository data can never select a host.
97
+ */
98
+ getIntegrationForDomain(id: IntegrationIds, domain: string | undefined): Promise<Integration | undefined>;
99
+ }