@pratik7368patil/anchor-core 0.1.15 → 0.1.18

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.
package/dist/index.js CHANGED
@@ -551,6 +551,14 @@ CREATE TABLE IF NOT EXISTS sync_state (
551
551
  history_coverage TEXT,
552
552
  history_limit INTEGER,
553
553
  history_since TEXT,
554
+ graphql_cursor TEXT,
555
+ graphql_cursor_scope TEXT,
556
+ graphql_cursor_scanned_prs INTEGER,
557
+ graphql_cursor_matched_prs INTEGER,
558
+ graphql_cursor_page_size INTEGER,
559
+ graphql_cursor_reset_at TEXT,
560
+ graphql_cursor_reason TEXT,
561
+ graphql_cursor_updated_at TEXT,
554
562
  updated_at TEXT NOT NULL
555
563
  );
556
564
 
@@ -1418,6 +1426,14 @@ function initializeSchema(db) {
1418
1426
  ensureColumn(db, "sync_state", "history_coverage", "TEXT");
1419
1427
  ensureColumn(db, "sync_state", "history_limit", "INTEGER");
1420
1428
  ensureColumn(db, "sync_state", "history_since", "TEXT");
1429
+ ensureColumn(db, "sync_state", "graphql_cursor", "TEXT");
1430
+ ensureColumn(db, "sync_state", "graphql_cursor_scope", "TEXT");
1431
+ ensureColumn(db, "sync_state", "graphql_cursor_scanned_prs", "INTEGER");
1432
+ ensureColumn(db, "sync_state", "graphql_cursor_matched_prs", "INTEGER");
1433
+ ensureColumn(db, "sync_state", "graphql_cursor_page_size", "INTEGER");
1434
+ ensureColumn(db, "sync_state", "graphql_cursor_reset_at", "TEXT");
1435
+ ensureColumn(db, "sync_state", "graphql_cursor_reason", "TEXT");
1436
+ ensureColumn(db, "sync_state", "graphql_cursor_updated_at", "TEXT");
1421
1437
  }
1422
1438
  function ensureColumn(db, tableName, columnName, definition) {
1423
1439
  const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
@@ -1488,6 +1504,83 @@ function updateSyncState(db, repo, lastIndexedPr, metadata = {}) {
1488
1504
  now
1489
1505
  );
1490
1506
  }
1507
+ function graphQLFetchCheckpointScope(input) {
1508
+ const historyScope = input.all ? "all" : `limit:${input.limit ?? 200}`;
1509
+ return `${input.repo}|${historyScope}|since:${input.since ?? ""}`;
1510
+ }
1511
+ function getGraphQLFetchCheckpoint(db, repo, scope) {
1512
+ initializeSchema(db);
1513
+ const row = db.prepare(
1514
+ `SELECT graphql_cursor, graphql_cursor_scope, graphql_cursor_scanned_prs,
1515
+ graphql_cursor_matched_prs, graphql_cursor_page_size, graphql_cursor_reset_at,
1516
+ graphql_cursor_reason, graphql_cursor_updated_at
1517
+ FROM sync_state
1518
+ WHERE repo = ?`
1519
+ ).get(repo);
1520
+ if (!row?.graphql_cursor_scope || row.graphql_cursor_scope !== scope) return void 0;
1521
+ return {
1522
+ repo,
1523
+ scope,
1524
+ cursor: row.graphql_cursor ?? null,
1525
+ scannedPullRequests: row.graphql_cursor_scanned_prs ?? 0,
1526
+ matchedMergedPullRequests: row.graphql_cursor_matched_prs ?? 0,
1527
+ pageSize: row.graphql_cursor_page_size ?? 50,
1528
+ resetAt: row.graphql_cursor_reset_at ?? void 0,
1529
+ reason: row.graphql_cursor_reason ?? "GraphQL budget checkpoint",
1530
+ updatedAt: row.graphql_cursor_updated_at ?? (/* @__PURE__ */ new Date(0)).toISOString()
1531
+ };
1532
+ }
1533
+ function saveGraphQLFetchCheckpoint(db, checkpoint) {
1534
+ initializeSchema(db);
1535
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1536
+ db.prepare(
1537
+ `INSERT INTO sync_state
1538
+ (repo, last_sync_at, last_indexed_pr, history_coverage, history_limit, history_since,
1539
+ graphql_cursor, graphql_cursor_scope, graphql_cursor_scanned_prs,
1540
+ graphql_cursor_matched_prs, graphql_cursor_page_size, graphql_cursor_reset_at,
1541
+ graphql_cursor_reason, graphql_cursor_updated_at, updated_at)
1542
+ VALUES (?, NULL, NULL, 'unknown', NULL, NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1543
+ ON CONFLICT(repo) DO UPDATE SET
1544
+ graphql_cursor = excluded.graphql_cursor,
1545
+ graphql_cursor_scope = excluded.graphql_cursor_scope,
1546
+ graphql_cursor_scanned_prs = excluded.graphql_cursor_scanned_prs,
1547
+ graphql_cursor_matched_prs = excluded.graphql_cursor_matched_prs,
1548
+ graphql_cursor_page_size = excluded.graphql_cursor_page_size,
1549
+ graphql_cursor_reset_at = excluded.graphql_cursor_reset_at,
1550
+ graphql_cursor_reason = excluded.graphql_cursor_reason,
1551
+ graphql_cursor_updated_at = excluded.graphql_cursor_updated_at,
1552
+ updated_at = excluded.updated_at`
1553
+ ).run(
1554
+ checkpoint.repo,
1555
+ checkpoint.cursor ?? null,
1556
+ checkpoint.scope,
1557
+ checkpoint.scannedPullRequests,
1558
+ checkpoint.matchedMergedPullRequests,
1559
+ checkpoint.pageSize,
1560
+ checkpoint.resetAt ?? null,
1561
+ checkpoint.reason,
1562
+ checkpoint.updatedAt,
1563
+ now
1564
+ );
1565
+ }
1566
+ function clearGraphQLFetchCheckpoint(db, repo, scope) {
1567
+ initializeSchema(db);
1568
+ const row = db.prepare("SELECT graphql_cursor_scope FROM sync_state WHERE repo = ?").get(repo);
1569
+ if (scope && row?.graphql_cursor_scope && row.graphql_cursor_scope !== scope) return;
1570
+ db.prepare(
1571
+ `UPDATE sync_state SET
1572
+ graphql_cursor = NULL,
1573
+ graphql_cursor_scope = NULL,
1574
+ graphql_cursor_scanned_prs = NULL,
1575
+ graphql_cursor_matched_prs = NULL,
1576
+ graphql_cursor_page_size = NULL,
1577
+ graphql_cursor_reset_at = NULL,
1578
+ graphql_cursor_reason = NULL,
1579
+ graphql_cursor_updated_at = NULL,
1580
+ updated_at = ?
1581
+ WHERE repo = ?`
1582
+ ).run((/* @__PURE__ */ new Date()).toISOString(), repo);
1583
+ }
1491
1584
  function deleteExistingPrData(db, prId) {
1492
1585
  const unitRows = db.prepare("SELECT id FROM wisdom_units WHERE pr_id = ?").all(prId);
1493
1586
  const deleteFts = db.prepare("DELETE FROM wisdom_units_fts WHERE unitId = ?");
@@ -5942,6 +6035,25 @@ function getGitHubRateLimitDelayMs(error, attempt, now = Date.now()) {
5942
6035
  reason: `secondary rate limit backoff for ${backoffSeconds} seconds`
5943
6036
  };
5944
6037
  }
6038
+ function isGitHubGraphQLResourceLimitError(error) {
6039
+ const message = (error.message ?? "").toLowerCase();
6040
+ return message.includes("resource limit") || message.includes("timeout") || message.includes("timed out") || message.includes("couldn't respond") || message.includes("could not respond") || message.includes("exceeded") && message.includes("node");
6041
+ }
6042
+ function updateGitHubGraphQLRateLimitState(controller, rateLimit, requestName) {
6043
+ if (!rateLimit || rateLimit.remaining !== 0 || !rateLimit.resetAt) return;
6044
+ const resetAtMs = Date.parse(rateLimit.resetAt);
6045
+ if (!Number.isFinite(resetAtMs)) return;
6046
+ const now = controller.now?.() ?? Date.now();
6047
+ const retryAtMs = Math.max(resetAtMs + 2e3, now);
6048
+ controller.blockedUntilMs = Math.max(controller.blockedUntilMs ?? 0, retryAtMs);
6049
+ controller.onRateLimit?.({
6050
+ waitSeconds: Math.ceil(Math.max(0, retryAtMs - now) / 1e3),
6051
+ retryAt: new Date(retryAtMs).toISOString(),
6052
+ reason: `GraphQL rate limit exhausted${rateLimit.cost ? ` after query cost ${rateLimit.cost}` : ""}`,
6053
+ request: requestName,
6054
+ attempt: 1
6055
+ });
6056
+ }
5945
6057
  async function sleep(milliseconds) {
5946
6058
  await new Promise((resolve) => setTimeout(resolve, milliseconds));
5947
6059
  }
@@ -5986,7 +6098,8 @@ async function paginateWithGitHubRateLimit(requestPage, options) {
5986
6098
  for (let page = 1; ; page += 1) {
5987
6099
  const response = await requestWithGitHubRateLimit(() => requestPage(page), {
5988
6100
  controller: options.controller,
5989
- requestName: `${options.requestName} page ${page}`
6101
+ requestName: `${options.requestName} page ${page}`,
6102
+ maxRetries: options.maxRetries
5990
6103
  });
5991
6104
  results.push(...response.data);
5992
6105
  if (!hasNextPage(response.headers) && response.data.length < 100) break;
@@ -5996,6 +6109,84 @@ async function paginateWithGitHubRateLimit(requestPage, options) {
5996
6109
  return results;
5997
6110
  }
5998
6111
 
6112
+ // src/github/graphql-client.ts
6113
+ var GitHubGraphQLError = class extends Error {
6114
+ status;
6115
+ response;
6116
+ constructor(message, options) {
6117
+ super(message);
6118
+ this.name = "GitHubGraphQLError";
6119
+ this.status = options.status;
6120
+ this.response = { headers: options.headers };
6121
+ }
6122
+ };
6123
+ function headersToRecord(headers) {
6124
+ const result = {};
6125
+ headers.forEach((value, key) => {
6126
+ result[key.toLowerCase()] = value;
6127
+ });
6128
+ return result;
6129
+ }
6130
+ function errorStatus(status, errors) {
6131
+ if (status === 403 || status === 429) return status;
6132
+ const message = (errors ?? []).map((error) => error.message ?? "").join("\n").toLowerCase();
6133
+ if (message.includes("rate limit") || message.includes("secondary limit")) return 403;
6134
+ return status >= 400 ? status : 500;
6135
+ }
6136
+ function errorMessage(status, errors) {
6137
+ const messages = (errors ?? []).map((error) => error.message).filter((message) => Boolean(message?.trim()));
6138
+ if (messages.length > 0) return messages.join("; ");
6139
+ return `GitHub GraphQL request failed with status ${status}.`;
6140
+ }
6141
+ function createGitHubGraphQLRequester(options) {
6142
+ if (!options.token.trim()) {
6143
+ throw new Error("GitHub authentication is required. Run gh auth login, or export GITHUB_TOKEN/GH_TOKEN.");
6144
+ }
6145
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch;
6146
+ if (!fetchImpl) throw new Error("Global fetch is unavailable in this Node.js runtime.");
6147
+ return async function requestGitHubGraphQL(query, variables, requestOptions) {
6148
+ return requestWithGitHubRateLimit(
6149
+ async () => {
6150
+ const response = await fetchImpl("https://api.github.com/graphql", {
6151
+ method: "POST",
6152
+ headers: {
6153
+ accept: "application/vnd.github+json",
6154
+ authorization: `Bearer ${options.token}`,
6155
+ "content-type": "application/json",
6156
+ "user-agent": "anchor-local-mcp"
6157
+ },
6158
+ body: JSON.stringify({ query, variables })
6159
+ });
6160
+ const headers = headersToRecord(response.headers);
6161
+ const raw = await response.json();
6162
+ if (!response.ok || raw.errors?.length) {
6163
+ throw new GitHubGraphQLError(errorMessage(response.status, raw.errors), {
6164
+ status: errorStatus(response.status, raw.errors),
6165
+ headers
6166
+ });
6167
+ }
6168
+ if (!raw.data) {
6169
+ throw new GitHubGraphQLError("GitHub GraphQL response did not include data.", {
6170
+ status: response.status,
6171
+ headers
6172
+ });
6173
+ }
6174
+ updateGitHubGraphQLRateLimitState(
6175
+ requestOptions.controller,
6176
+ raw.data.rateLimit,
6177
+ requestOptions.requestName
6178
+ );
6179
+ return { data: raw.data, headers };
6180
+ },
6181
+ {
6182
+ controller: requestOptions.controller,
6183
+ requestName: requestOptions.requestName,
6184
+ maxRetries: requestOptions.maxRetries
6185
+ }
6186
+ );
6187
+ };
6188
+ }
6189
+
5999
6190
  // src/github/fetch-pr-details.ts
6000
6191
  async function fetchPullRequestDetails(octokit, repoFullName, pullNumber, controller = {}) {
6001
6192
  const [owner, repo] = repoFullName.split("/");
@@ -6116,6 +6307,756 @@ async function fetchPullRequestDetails(octokit, repoFullName, pullNumber, contro
6116
6307
  };
6117
6308
  }
6118
6309
 
6310
+ // src/github/fetch-prs-graphql.ts
6311
+ var MIN_PULL_REQUEST_PAGE_SIZE = 5;
6312
+ var INITIAL_PULL_REQUEST_PAGE_SIZE = 50;
6313
+ var MAX_PULL_REQUEST_PAGE_SIZE = 100;
6314
+ var REDUCED_PULL_REQUEST_PAGE_SIZES = [10, 5];
6315
+ var CONNECTION_PAGE_SIZE = 100;
6316
+ var GRAPHQL_RATE_LIMIT_RESERVE = 250;
6317
+ var GraphQLBudget = class {
6318
+ constructor(reserve) {
6319
+ this.reserve = reserve;
6320
+ }
6321
+ reserve;
6322
+ activePageCost = 0;
6323
+ averageCostPerPr;
6324
+ latestRateLimit;
6325
+ beginPage() {
6326
+ this.activePageCost = 0;
6327
+ }
6328
+ observe(rateLimit) {
6329
+ this.latestRateLimit = rateLimit ?? this.latestRateLimit;
6330
+ if (typeof rateLimit?.cost === "number" && Number.isFinite(rateLimit.cost)) {
6331
+ this.activePageCost += Math.max(0, rateLimit.cost);
6332
+ }
6333
+ }
6334
+ completePage(prCount) {
6335
+ if (prCount <= 0 || this.activePageCost <= 0) return;
6336
+ const pageCostPerPr = this.activePageCost / prCount;
6337
+ this.averageCostPerPr = this.averageCostPerPr === void 0 ? pageCostPerPr : this.averageCostPerPr * 0.65 + pageCostPerPr * 0.35;
6338
+ }
6339
+ shouldDefer() {
6340
+ const remaining = this.latestRateLimit?.remaining;
6341
+ return typeof remaining === "number" && remaining <= this.reserve;
6342
+ }
6343
+ rateLimit() {
6344
+ return this.latestRateLimit;
6345
+ }
6346
+ choosePageSize(currentPageSize, remainingPrs) {
6347
+ const remaining = this.latestRateLimit?.remaining;
6348
+ const averageCostPerPr = this.averageCostPerPr;
6349
+ if (typeof remaining !== "number" || remaining <= this.reserve || averageCostPerPr === void 0 || averageCostPerPr <= 0) {
6350
+ return { pageSize: currentPageSize, averageCostPerPr };
6351
+ }
6352
+ const safeBudget = Math.max(0, remaining - this.reserve);
6353
+ const budgetPageSize = Math.max(
6354
+ MIN_PULL_REQUEST_PAGE_SIZE,
6355
+ Math.min(MAX_PULL_REQUEST_PAGE_SIZE, Math.floor(safeBudget / averageCostPerPr))
6356
+ );
6357
+ const growthLimitedPageSize = budgetPageSize > currentPageSize ? Math.min(budgetPageSize, currentPageSize * 2) : budgetPageSize;
6358
+ const cappedPageSize = remainingPrs === void 0 ? growthLimitedPageSize : Math.min(growthLimitedPageSize, Math.max(MIN_PULL_REQUEST_PAGE_SIZE, remainingPrs));
6359
+ return {
6360
+ pageSize: Math.max(MIN_PULL_REQUEST_PAGE_SIZE, Math.min(MAX_PULL_REQUEST_PAGE_SIZE, cappedPageSize)),
6361
+ averageCostPerPr
6362
+ };
6363
+ }
6364
+ };
6365
+ var PULL_REQUEST_FIELDS = `
6366
+ number
6367
+ url
6368
+ title
6369
+ body
6370
+ createdAt
6371
+ mergedAt
6372
+ updatedAt
6373
+ author { login }
6374
+ labels(first: 100) {
6375
+ nodes { name }
6376
+ pageInfo { hasNextPage endCursor }
6377
+ }
6378
+ files(first: 100) {
6379
+ nodes { path additions deletions }
6380
+ pageInfo { hasNextPage endCursor }
6381
+ }
6382
+ comments(first: 100) {
6383
+ nodes { author { login } body createdAt }
6384
+ pageInfo { hasNextPage endCursor }
6385
+ }
6386
+ reviews(first: 100) {
6387
+ nodes {
6388
+ id
6389
+ author { login }
6390
+ body
6391
+ submittedAt
6392
+ comments(first: 100) {
6393
+ nodes { author { login } body path createdAt }
6394
+ pageInfo { hasNextPage endCursor }
6395
+ }
6396
+ }
6397
+ pageInfo { hasNextPage endCursor }
6398
+ }
6399
+ commits(first: 100) {
6400
+ nodes { commit { message } }
6401
+ pageInfo { hasNextPage endCursor }
6402
+ }
6403
+ `;
6404
+ var LIST_MERGED_PULL_REQUESTS_QUERY = `
6405
+ query AnchorMergedPullRequests($owner: String!, $name: String!, $first: Int!, $after: String) {
6406
+ repository(owner: $owner, name: $name) {
6407
+ pullRequests(states: MERGED, orderBy: { field: UPDATED_AT, direction: DESC }, first: $first, after: $after) {
6408
+ nodes {
6409
+ ${PULL_REQUEST_FIELDS}
6410
+ }
6411
+ pageInfo { hasNextPage endCursor }
6412
+ }
6413
+ }
6414
+ rateLimit { cost remaining resetAt }
6415
+ }
6416
+ `;
6417
+ var PULL_REQUEST_FILES_QUERY = `
6418
+ query AnchorPullRequestFiles($owner: String!, $name: String!, $number: Int!, $first: Int!, $after: String) {
6419
+ repository(owner: $owner, name: $name) {
6420
+ pullRequest(number: $number) {
6421
+ files(first: $first, after: $after) {
6422
+ nodes { path additions deletions }
6423
+ pageInfo { hasNextPage endCursor }
6424
+ }
6425
+ }
6426
+ }
6427
+ rateLimit { cost remaining resetAt }
6428
+ }
6429
+ `;
6430
+ var PULL_REQUEST_COMMENTS_QUERY = `
6431
+ query AnchorPullRequestComments($owner: String!, $name: String!, $number: Int!, $first: Int!, $after: String) {
6432
+ repository(owner: $owner, name: $name) {
6433
+ pullRequest(number: $number) {
6434
+ comments(first: $first, after: $after) {
6435
+ nodes { author { login } body createdAt }
6436
+ pageInfo { hasNextPage endCursor }
6437
+ }
6438
+ }
6439
+ }
6440
+ rateLimit { cost remaining resetAt }
6441
+ }
6442
+ `;
6443
+ var PULL_REQUEST_REVIEWS_QUERY = `
6444
+ query AnchorPullRequestReviews($owner: String!, $name: String!, $number: Int!, $first: Int!, $after: String) {
6445
+ repository(owner: $owner, name: $name) {
6446
+ pullRequest(number: $number) {
6447
+ reviews(first: $first, after: $after) {
6448
+ nodes {
6449
+ id
6450
+ author { login }
6451
+ body
6452
+ submittedAt
6453
+ comments(first: 100) {
6454
+ nodes { author { login } body path createdAt }
6455
+ pageInfo { hasNextPage endCursor }
6456
+ }
6457
+ }
6458
+ pageInfo { hasNextPage endCursor }
6459
+ }
6460
+ }
6461
+ }
6462
+ rateLimit { cost remaining resetAt }
6463
+ }
6464
+ `;
6465
+ var PULL_REQUEST_COMMITS_QUERY = `
6466
+ query AnchorPullRequestCommits($owner: String!, $name: String!, $number: Int!, $first: Int!, $after: String) {
6467
+ repository(owner: $owner, name: $name) {
6468
+ pullRequest(number: $number) {
6469
+ commits(first: $first, after: $after) {
6470
+ nodes { commit { message } }
6471
+ pageInfo { hasNextPage endCursor }
6472
+ }
6473
+ }
6474
+ }
6475
+ rateLimit { cost remaining resetAt }
6476
+ }
6477
+ `;
6478
+ var REVIEW_COMMENTS_QUERY = `
6479
+ query AnchorPullRequestReviewComments($reviewId: ID!, $first: Int!, $after: String) {
6480
+ node(id: $reviewId) {
6481
+ ... on PullRequestReview {
6482
+ comments(first: $first, after: $after) {
6483
+ nodes { author { login } body path createdAt }
6484
+ pageInfo { hasNextPage endCursor }
6485
+ }
6486
+ }
6487
+ }
6488
+ rateLimit { cost remaining resetAt }
6489
+ }
6490
+ `;
6491
+ var RATE_LIMIT_QUERY = `
6492
+ query AnchorGraphQLRateLimit {
6493
+ rateLimit { cost remaining resetAt }
6494
+ }
6495
+ `;
6496
+ function connectionNodes(connection) {
6497
+ return (connection?.nodes ?? []).filter((node) => Boolean(node));
6498
+ }
6499
+ async function requestGraphQLWithBudget(requestGraphQL, query, variables, options) {
6500
+ const response = await requestGraphQL(query, variables, {
6501
+ controller: options.controller,
6502
+ requestName: options.requestName
6503
+ });
6504
+ options.budget.observe(response.data.rateLimit);
6505
+ return response;
6506
+ }
6507
+ function pageInfo(connection) {
6508
+ return connection?.pageInfo ?? { hasNextPage: false, endCursor: null };
6509
+ }
6510
+ function labelName(label) {
6511
+ return label.name ? { name: label.name } : void 0;
6512
+ }
6513
+ function mapChangedFile(file) {
6514
+ if (!file.path) return void 0;
6515
+ return {
6516
+ filename: file.path,
6517
+ additions: file.additions ?? 0,
6518
+ deletions: file.deletions ?? 0
6519
+ };
6520
+ }
6521
+ function mapIssueComment(comment) {
6522
+ return {
6523
+ user: comment.author?.login ? { login: comment.author.login } : null,
6524
+ body: comment.body ?? "",
6525
+ created_at: comment.createdAt ?? void 0
6526
+ };
6527
+ }
6528
+ function mapReviewComment(comment) {
6529
+ return {
6530
+ user: comment.author?.login ? { login: comment.author.login } : null,
6531
+ body: comment.body ?? "",
6532
+ path: comment.path ?? void 0,
6533
+ created_at: comment.createdAt ?? void 0
6534
+ };
6535
+ }
6536
+ function mapReviewSummary(review) {
6537
+ return {
6538
+ user: review.author?.login ? { login: review.author.login } : null,
6539
+ body: review.body ?? "",
6540
+ created_at: review.submittedAt ?? void 0,
6541
+ submitted_at: review.submittedAt ?? void 0
6542
+ };
6543
+ }
6544
+ function mapPullRequest(repo, pull) {
6545
+ return {
6546
+ repo,
6547
+ number: pull.number,
6548
+ html_url: pull.url,
6549
+ title: pull.title,
6550
+ body: pull.body ?? "",
6551
+ user: pull.author?.login ? { login: pull.author.login } : null,
6552
+ labels: connectionNodes(pull.labels).map(labelName).filter((label) => Boolean(label)),
6553
+ created_at: pull.createdAt,
6554
+ merged_at: pull.mergedAt ?? void 0,
6555
+ updated_at: pull.updatedAt ?? pull.mergedAt ?? pull.createdAt,
6556
+ files: connectionNodes(pull.files).map(mapChangedFile).filter((file) => Boolean(file)),
6557
+ reviews: connectionNodes(pull.reviews).map(mapReviewSummary),
6558
+ reviewComments: connectionNodes(pull.reviews).flatMap(
6559
+ (review) => connectionNodes(review.comments).map(mapReviewComment)
6560
+ ),
6561
+ issueComments: connectionNodes(pull.comments).map(mapIssueComment),
6562
+ commits: connectionNodes(pull.commits).map((commit) => ({
6563
+ commit: { message: commit.commit?.message ?? "" }
6564
+ }))
6565
+ };
6566
+ }
6567
+ async function requestConnection(requestGraphQL, query, connectionName, variables, options) {
6568
+ const response = await requestGraphQLWithBudget(requestGraphQL, query, variables, options);
6569
+ return response.data.repository?.pullRequest?.[connectionName];
6570
+ }
6571
+ async function appendAdditionalFiles(requestGraphQL, record, initialConnection, options) {
6572
+ let info = pageInfo(initialConnection);
6573
+ while (info.hasNextPage && info.endCursor) {
6574
+ const connection = await requestConnection(
6575
+ requestGraphQL,
6576
+ PULL_REQUEST_FILES_QUERY,
6577
+ "files",
6578
+ {
6579
+ owner: options.owner,
6580
+ name: options.name,
6581
+ number: record.number,
6582
+ first: CONNECTION_PAGE_SIZE,
6583
+ after: info.endCursor
6584
+ },
6585
+ {
6586
+ controller: options.controller,
6587
+ requestName: `GraphQL /repos/${record.repo}/pulls/${record.number}/files`,
6588
+ budget: options.budget
6589
+ }
6590
+ );
6591
+ record.files.push(
6592
+ ...connectionNodes(connection).map(mapChangedFile).filter((file) => Boolean(file))
6593
+ );
6594
+ info = pageInfo(connection);
6595
+ }
6596
+ }
6597
+ async function appendAdditionalIssueComments(requestGraphQL, record, initialConnection, options) {
6598
+ let info = pageInfo(initialConnection);
6599
+ while (info.hasNextPage && info.endCursor) {
6600
+ const connection = await requestConnection(
6601
+ requestGraphQL,
6602
+ PULL_REQUEST_COMMENTS_QUERY,
6603
+ "comments",
6604
+ {
6605
+ owner: options.owner,
6606
+ name: options.name,
6607
+ number: record.number,
6608
+ first: CONNECTION_PAGE_SIZE,
6609
+ after: info.endCursor
6610
+ },
6611
+ {
6612
+ controller: options.controller,
6613
+ requestName: `GraphQL /repos/${record.repo}/issues/${record.number}/comments`,
6614
+ budget: options.budget
6615
+ }
6616
+ );
6617
+ record.issueComments?.push(...connectionNodes(connection).map(mapIssueComment));
6618
+ info = pageInfo(connection);
6619
+ }
6620
+ }
6621
+ async function appendAdditionalCommits(requestGraphQL, record, initialConnection, options) {
6622
+ let info = pageInfo(initialConnection);
6623
+ while (info.hasNextPage && info.endCursor) {
6624
+ const connection = await requestConnection(
6625
+ requestGraphQL,
6626
+ PULL_REQUEST_COMMITS_QUERY,
6627
+ "commits",
6628
+ {
6629
+ owner: options.owner,
6630
+ name: options.name,
6631
+ number: record.number,
6632
+ first: CONNECTION_PAGE_SIZE,
6633
+ after: info.endCursor
6634
+ },
6635
+ {
6636
+ controller: options.controller,
6637
+ requestName: `GraphQL /repos/${record.repo}/pulls/${record.number}/commits`,
6638
+ budget: options.budget
6639
+ }
6640
+ );
6641
+ record.commits?.push(
6642
+ ...connectionNodes(connection).map((commit) => ({
6643
+ commit: { message: commit.commit?.message ?? "" }
6644
+ }))
6645
+ );
6646
+ info = pageInfo(connection);
6647
+ }
6648
+ }
6649
+ async function appendAdditionalReviewComments(requestGraphQL, record, review, options) {
6650
+ let info = pageInfo(review.comments);
6651
+ while (info.hasNextPage && info.endCursor) {
6652
+ const response = await requestGraphQLWithBudget(
6653
+ requestGraphQL,
6654
+ REVIEW_COMMENTS_QUERY,
6655
+ {
6656
+ reviewId: review.id,
6657
+ first: CONNECTION_PAGE_SIZE,
6658
+ after: info.endCursor
6659
+ },
6660
+ {
6661
+ controller: options.controller,
6662
+ requestName: `GraphQL /pull-request-reviews/${review.id}/comments`,
6663
+ budget: options.budget
6664
+ }
6665
+ );
6666
+ const connection = response.data.node?.comments;
6667
+ record.reviewComments?.push(...connectionNodes(connection).map(mapReviewComment));
6668
+ info = pageInfo(connection);
6669
+ }
6670
+ }
6671
+ async function appendAdditionalReviews(requestGraphQL, record, initialConnection, options) {
6672
+ const reviewsToHydrate = [...connectionNodes(initialConnection)];
6673
+ let info = pageInfo(initialConnection);
6674
+ while (info.hasNextPage && info.endCursor) {
6675
+ const connection = await requestConnection(
6676
+ requestGraphQL,
6677
+ PULL_REQUEST_REVIEWS_QUERY,
6678
+ "reviews",
6679
+ {
6680
+ owner: options.owner,
6681
+ name: options.name,
6682
+ number: record.number,
6683
+ first: CONNECTION_PAGE_SIZE,
6684
+ after: info.endCursor
6685
+ },
6686
+ {
6687
+ controller: options.controller,
6688
+ requestName: `GraphQL /repos/${record.repo}/pulls/${record.number}/reviews`,
6689
+ budget: options.budget
6690
+ }
6691
+ );
6692
+ const reviewNodes = connectionNodes(connection);
6693
+ reviewsToHydrate.push(...reviewNodes);
6694
+ record.reviews?.push(...reviewNodes.map(mapReviewSummary));
6695
+ record.reviewComments?.push(
6696
+ ...reviewNodes.flatMap((review) => connectionNodes(review.comments).map(mapReviewComment))
6697
+ );
6698
+ info = pageInfo(connection);
6699
+ }
6700
+ for (const review of reviewsToHydrate) {
6701
+ await appendAdditionalReviewComments(requestGraphQL, record, review, {
6702
+ controller: options.controller,
6703
+ budget: options.budget
6704
+ });
6705
+ }
6706
+ }
6707
+ async function hydratePullRequestNestedConnections(requestGraphQL, record, pull, options) {
6708
+ await appendAdditionalFiles(requestGraphQL, record, pull.files, options);
6709
+ await appendAdditionalIssueComments(requestGraphQL, record, pull.comments, options);
6710
+ await appendAdditionalReviews(requestGraphQL, record, pull.reviews, options);
6711
+ await appendAdditionalCommits(requestGraphQL, record, pull.commits, options);
6712
+ }
6713
+ function mergePatchFiles(record, patchFiles) {
6714
+ const byFilename = new Map(patchFiles.map((file) => [file.filename, file]));
6715
+ let patches = 0;
6716
+ record.files = record.files.map((file) => {
6717
+ const patchFile = byFilename.get(file.filename);
6718
+ if (!patchFile) return file;
6719
+ if (patchFile.patch) patches += 1;
6720
+ return {
6721
+ ...file,
6722
+ additions: patchFile.additions ?? file.additions,
6723
+ deletions: patchFile.deletions ?? file.deletions,
6724
+ patch: patchFile.patch ?? file.patch
6725
+ };
6726
+ });
6727
+ const existing = new Set(record.files.map((file) => file.filename));
6728
+ for (const patchFile of patchFiles) {
6729
+ if (!existing.has(patchFile.filename)) {
6730
+ record.files.push(patchFile);
6731
+ if (patchFile.patch) patches += 1;
6732
+ }
6733
+ }
6734
+ return patches;
6735
+ }
6736
+ async function fetchPullRequestPatchFiles(octokit, repoFullName, pullNumber, controller) {
6737
+ const [owner, repo] = repoFullName.split("/");
6738
+ if (!owner || !repo) throw new Error(`Invalid repo '${repoFullName}'. Expected owner/name.`);
6739
+ const files = await paginateWithGitHubRateLimit(
6740
+ (page) => octokit.pulls.listFiles({
6741
+ owner,
6742
+ repo,
6743
+ pull_number: pullNumber,
6744
+ per_page: 100,
6745
+ page
6746
+ }),
6747
+ {
6748
+ controller,
6749
+ requestName: `GET /repos/${repoFullName}/pulls/${pullNumber}/files`,
6750
+ maxRetries: 0
6751
+ }
6752
+ );
6753
+ return files.map((file) => ({
6754
+ filename: file.filename,
6755
+ patch: "patch" in file ? file.patch : void 0,
6756
+ additions: file.additions,
6757
+ deletions: file.deletions
6758
+ }));
6759
+ }
6760
+ async function enrichPullRequestPatchesWithRest(options) {
6761
+ const octokit = options.restClient ?? createGitHubClient(options.token);
6762
+ let nextIndex = 0;
6763
+ let completed = 0;
6764
+ const workerCount = Math.min(options.detailConcurrency, options.records.length);
6765
+ async function worker() {
6766
+ while (nextIndex < options.records.length) {
6767
+ const index = nextIndex;
6768
+ nextIndex += 1;
6769
+ const record = options.records[index];
6770
+ if (!record) continue;
6771
+ options.onProgress?.({
6772
+ stage: "enriching_pull_request_patches",
6773
+ repo: options.repo,
6774
+ current: index + 1,
6775
+ total: options.records.length,
6776
+ prNumber: record.number,
6777
+ detailConcurrency: options.detailConcurrency
6778
+ });
6779
+ try {
6780
+ const patchFiles = await fetchPullRequestPatchFiles(
6781
+ octokit,
6782
+ options.repo,
6783
+ record.number,
6784
+ options.controller
6785
+ );
6786
+ const patches = mergePatchFiles(record, patchFiles);
6787
+ completed += 1;
6788
+ options.onProgress?.({
6789
+ stage: "enriched_pull_request_patches",
6790
+ repo: options.repo,
6791
+ current: completed,
6792
+ total: options.records.length,
6793
+ prNumber: record.number,
6794
+ detailConcurrency: options.detailConcurrency,
6795
+ patches
6796
+ });
6797
+ } catch (error) {
6798
+ completed += 1;
6799
+ if (!isGitHubRateLimitError(error)) {
6800
+ options.onProgress?.({
6801
+ stage: "skipped_pull_request_patch_enrichment",
6802
+ repo: options.repo,
6803
+ current: completed,
6804
+ total: options.records.length,
6805
+ prNumber: record.number,
6806
+ reason: error instanceof Error ? error.message : String(error)
6807
+ });
6808
+ continue;
6809
+ }
6810
+ options.onProgress?.({
6811
+ stage: "skipped_pull_request_patch_enrichment",
6812
+ repo: options.repo,
6813
+ current: completed,
6814
+ total: options.records.length,
6815
+ prNumber: record.number,
6816
+ reason: "GitHub REST rate limit reached during patch enrichment"
6817
+ });
6818
+ }
6819
+ }
6820
+ }
6821
+ if (workerCount > 0) await Promise.all(Array.from({ length: workerCount }, () => worker()));
6822
+ }
6823
+ function nextReducedPageSize(current) {
6824
+ return REDUCED_PULL_REQUEST_PAGE_SIZES.find((candidate) => candidate < current);
6825
+ }
6826
+ function checkpointFromState(options) {
6827
+ return {
6828
+ repo: options.repo,
6829
+ scope: options.scope,
6830
+ cursor: options.cursor ?? null,
6831
+ scannedPullRequests: options.scannedPullRequests,
6832
+ matchedMergedPullRequests: options.matchedMergedPullRequests,
6833
+ pageSize: options.pageSize,
6834
+ resetAt: options.rateLimit?.resetAt ?? void 0,
6835
+ reason: options.reason,
6836
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
6837
+ };
6838
+ }
6839
+ async function fetchMergedPullRequestsWithGraphQL(options) {
6840
+ const [owner, name] = options.repo.split("/");
6841
+ if (!owner || !name) throw new Error(`Invalid repo '${options.repo}'. Expected owner/name.`);
6842
+ const requestGraphQL = createGitHubGraphQLRequester({
6843
+ token: options.token,
6844
+ fetchImpl: options.fetchImpl
6845
+ });
6846
+ const sinceTime = options.since ? Date.parse(options.since) : void 0;
6847
+ const records = [];
6848
+ const checkpoint = options.graphQLCheckpoint;
6849
+ const baseScannedPullRequests = checkpoint?.scannedPullRequests ?? 0;
6850
+ const baseMatchedMergedPullRequests = checkpoint?.matchedMergedPullRequests ?? 0;
6851
+ let scannedPullRequests = baseScannedPullRequests;
6852
+ let reachedSinceBoundary = false;
6853
+ let cursor = checkpoint?.cursor ?? void 0;
6854
+ let pageSize = Math.min(
6855
+ MAX_PULL_REQUEST_PAGE_SIZE,
6856
+ checkpoint?.pageSize ?? Math.min(INITIAL_PULL_REQUEST_PAGE_SIZE, options.limit ?? INITIAL_PULL_REQUEST_PAGE_SIZE)
6857
+ );
6858
+ const budget = new GraphQLBudget(GRAPHQL_RATE_LIMIT_RESERVE);
6859
+ const checkpointScope = checkpoint?.scope ?? `${options.repo}|${options.limit === void 0 ? "all" : `limit:${options.limit}`}|since:${options.since ?? ""}`;
6860
+ options.onProgress?.({
6861
+ stage: "discovering_pull_requests",
6862
+ repo: options.repo,
6863
+ all: options.limit === void 0,
6864
+ limit: options.limit,
6865
+ since: options.since,
6866
+ backend: "graphql"
6867
+ });
6868
+ if (checkpoint) {
6869
+ options.onProgress?.({
6870
+ stage: "github_graphql_checkpoint_resumed",
6871
+ repo: options.repo,
6872
+ scannedPullRequests: checkpoint.scannedPullRequests,
6873
+ matchedMergedPullRequests: checkpoint.matchedMergedPullRequests,
6874
+ pageSize: checkpoint.pageSize,
6875
+ resetAt: checkpoint.resetAt
6876
+ });
6877
+ }
6878
+ await requestGraphQLWithBudget(
6879
+ requestGraphQL,
6880
+ RATE_LIMIT_QUERY,
6881
+ {},
6882
+ {
6883
+ controller: options.controller,
6884
+ requestName: "GraphQL rate limit preflight",
6885
+ budget
6886
+ }
6887
+ );
6888
+ const preflightRateLimit = budget.rateLimit();
6889
+ if (budget.shouldDefer()) {
6890
+ options.onGraphQLCheckpoint?.(
6891
+ checkpointFromState({
6892
+ repo: options.repo,
6893
+ scope: checkpointScope,
6894
+ cursor: cursor ?? null,
6895
+ scannedPullRequests,
6896
+ matchedMergedPullRequests: baseMatchedMergedPullRequests,
6897
+ pageSize,
6898
+ rateLimit: preflightRateLimit,
6899
+ reason: "GraphQL budget safety reserve reached before fetching another page"
6900
+ })
6901
+ );
6902
+ options.onProgress?.({
6903
+ stage: "github_graphql_budget_deferred",
6904
+ repo: options.repo,
6905
+ remaining: preflightRateLimit?.remaining,
6906
+ reserve: GRAPHQL_RATE_LIMIT_RESERVE,
6907
+ resetAt: preflightRateLimit?.resetAt,
6908
+ matchedMergedPullRequests: baseMatchedMergedPullRequests
6909
+ });
6910
+ return records;
6911
+ }
6912
+ if (typeof preflightRateLimit?.remaining === "number") {
6913
+ const preflightPageSize = Math.max(
6914
+ MIN_PULL_REQUEST_PAGE_SIZE,
6915
+ Math.min(
6916
+ pageSize,
6917
+ Math.floor((preflightRateLimit.remaining - GRAPHQL_RATE_LIMIT_RESERVE) / 4)
6918
+ )
6919
+ );
6920
+ if (preflightPageSize !== pageSize) {
6921
+ options.onProgress?.({
6922
+ stage: "github_graphql_page_size_selected",
6923
+ repo: options.repo,
6924
+ previousPageSize: pageSize,
6925
+ nextPageSize: preflightPageSize,
6926
+ remaining: preflightRateLimit.remaining
6927
+ });
6928
+ pageSize = preflightPageSize;
6929
+ }
6930
+ }
6931
+ while (true) {
6932
+ let response;
6933
+ budget.beginPage();
6934
+ try {
6935
+ response = await requestGraphQLWithBudget(
6936
+ requestGraphQL,
6937
+ LIST_MERGED_PULL_REQUESTS_QUERY,
6938
+ {
6939
+ owner,
6940
+ name,
6941
+ first: pageSize,
6942
+ after: cursor ?? null
6943
+ },
6944
+ {
6945
+ controller: options.controller,
6946
+ requestName: `GraphQL /repos/${options.repo}/pullRequests`,
6947
+ budget
6948
+ }
6949
+ );
6950
+ } catch (error) {
6951
+ const reducedPageSize = isGitHubGraphQLResourceLimitError(error) ? nextReducedPageSize(pageSize) : void 0;
6952
+ if (!reducedPageSize) throw error;
6953
+ options.onProgress?.({
6954
+ stage: "github_graphql_page_size_reduced",
6955
+ repo: options.repo,
6956
+ previousPageSize: pageSize,
6957
+ nextPageSize: reducedPageSize,
6958
+ reason: error instanceof Error ? error.message : String(error)
6959
+ });
6960
+ pageSize = reducedPageSize;
6961
+ continue;
6962
+ }
6963
+ const connection = response.data.repository?.pullRequests;
6964
+ const pullNodes = connectionNodes(connection);
6965
+ scannedPullRequests += pullNodes.length;
6966
+ const recordsBeforePage = records.length;
6967
+ for (const pull of pullNodes) {
6968
+ if (sinceTime && Date.parse(pull.updatedAt ?? pull.mergedAt ?? pull.createdAt) < sinceTime) {
6969
+ reachedSinceBoundary = true;
6970
+ break;
6971
+ }
6972
+ if (!pull.mergedAt) continue;
6973
+ const record = mapPullRequest(options.repo, pull);
6974
+ await hydratePullRequestNestedConnections(requestGraphQL, record, pull, {
6975
+ owner,
6976
+ name,
6977
+ controller: options.controller,
6978
+ budget
6979
+ });
6980
+ records.push(record);
6981
+ if (options.limit !== void 0 && records.length >= options.limit) break;
6982
+ }
6983
+ const pageMatchedPullRequests = records.length - recordsBeforePage;
6984
+ budget.completePage(pageMatchedPullRequests);
6985
+ options.onProgress?.({
6986
+ stage: "scanned_pull_request_page",
6987
+ repo: options.repo,
6988
+ all: options.limit === void 0,
6989
+ limit: options.limit,
6990
+ scannedPullRequests,
6991
+ matchedMergedPullRequests: baseMatchedMergedPullRequests + records.length,
6992
+ backend: "graphql",
6993
+ pageSize
6994
+ });
6995
+ const info = pageInfo(connection);
6996
+ const totalMatchedMergedPullRequests = baseMatchedMergedPullRequests + records.length;
6997
+ if (info.hasNextPage && info.endCursor && budget.shouldDefer()) {
6998
+ const rateLimit = budget.rateLimit();
6999
+ const checkpointToSave = checkpointFromState({
7000
+ repo: options.repo,
7001
+ scope: checkpointScope,
7002
+ cursor: info.endCursor,
7003
+ scannedPullRequests,
7004
+ matchedMergedPullRequests: totalMatchedMergedPullRequests,
7005
+ pageSize,
7006
+ rateLimit,
7007
+ reason: "GraphQL budget safety reserve reached"
7008
+ });
7009
+ options.onGraphQLCheckpoint?.(checkpointToSave);
7010
+ options.onProgress?.({
7011
+ stage: "github_graphql_budget_deferred",
7012
+ repo: options.repo,
7013
+ remaining: rateLimit?.remaining,
7014
+ reserve: GRAPHQL_RATE_LIMIT_RESERVE,
7015
+ resetAt: rateLimit?.resetAt,
7016
+ matchedMergedPullRequests: totalMatchedMergedPullRequests
7017
+ });
7018
+ break;
7019
+ }
7020
+ if (reachedSinceBoundary || options.limit !== void 0 && records.length >= options.limit || !info.hasNextPage || !info.endCursor) {
7021
+ options.onGraphQLCheckpoint?.(null);
7022
+ break;
7023
+ }
7024
+ cursor = info.endCursor;
7025
+ const remainingPrs = options.limit === void 0 ? void 0 : Math.max(0, options.limit - records.length);
7026
+ const decision = budget.choosePageSize(pageSize, remainingPrs);
7027
+ if (decision.pageSize !== pageSize) {
7028
+ options.onProgress?.({
7029
+ stage: "github_graphql_page_size_selected",
7030
+ repo: options.repo,
7031
+ previousPageSize: pageSize,
7032
+ nextPageSize: decision.pageSize,
7033
+ remaining: budget.rateLimit()?.remaining,
7034
+ averageCostPerPr: decision.averageCostPerPr
7035
+ });
7036
+ pageSize = decision.pageSize;
7037
+ }
7038
+ }
7039
+ options.onProgress?.({
7040
+ stage: "discovered_pull_requests",
7041
+ repo: options.repo,
7042
+ all: options.limit === void 0,
7043
+ total: records.length,
7044
+ limit: options.limit,
7045
+ detailConcurrency: options.detailConcurrency,
7046
+ backend: "graphql"
7047
+ });
7048
+ await enrichPullRequestPatchesWithRest({
7049
+ records,
7050
+ repo: options.repo,
7051
+ token: options.token,
7052
+ detailConcurrency: options.detailConcurrency,
7053
+ controller: options.restController ?? options.controller,
7054
+ onProgress: options.onProgress,
7055
+ restClient: options.restClient
7056
+ });
7057
+ return records;
7058
+ }
7059
+
6119
7060
  // src/github/fetch-prs.ts
6120
7061
  function resolvePullRequestFetchLimit(options) {
6121
7062
  return options.all ? void 0 : Math.max(1, Math.min(options.limit ?? 200, 1e3));
@@ -6125,6 +7066,18 @@ function resolvePullRequestDetailConcurrency(options) {
6125
7066
  if (!Number.isFinite(value)) return 5;
6126
7067
  return Math.max(1, Math.min(Math.trunc(value), 10));
6127
7068
  }
7069
+ function createProgressRateLimitController(repo, onProgress) {
7070
+ return {
7071
+ onRateLimit: (progress) => onProgress?.({
7072
+ stage: "github_rate_limited",
7073
+ repo,
7074
+ ...progress
7075
+ })
7076
+ };
7077
+ }
7078
+ function shouldFallbackToRestAfterGraphQLError(error) {
7079
+ return !isGitHubRateLimitError(error) && !isGitHubGraphQLResourceLimitError(error);
7080
+ }
6128
7081
  async function fetchPullRequestDetailsConcurrently(options) {
6129
7082
  const results = new Array(options.pullNumbers.length);
6130
7083
  let nextIndex = 0;
@@ -6169,19 +7122,12 @@ async function fetchPullRequestDetailsConcurrently(options) {
6169
7122
  return result;
6170
7123
  });
6171
7124
  }
6172
- async function fetchMergedPullRequests(options) {
7125
+ async function fetchMergedPullRequestsWithRest(options, rateLimitController) {
6173
7126
  const [owner, repo] = options.repo.split("/");
6174
7127
  if (!owner || !repo) throw new Error(`Invalid repo '${options.repo}'. Expected owner/name.`);
6175
- const octokit = createGitHubClient(options.token);
7128
+ const octokit = options.restClient ?? createGitHubClient(options.token);
6176
7129
  const limit = resolvePullRequestFetchLimit(options);
6177
7130
  const detailConcurrency = resolvePullRequestDetailConcurrency(options);
6178
- const rateLimitController = {
6179
- onRateLimit: (progress) => options.onProgress?.({
6180
- stage: "github_rate_limited",
6181
- repo: options.repo,
6182
- ...progress
6183
- })
6184
- };
6185
7131
  const sinceTime = options.since ? Date.parse(options.since) : void 0;
6186
7132
  const pullNumbers = [];
6187
7133
  let scannedPullRequests = 0;
@@ -6192,7 +7138,8 @@ async function fetchMergedPullRequests(options) {
6192
7138
  repo: options.repo,
6193
7139
  all: limit === void 0,
6194
7140
  limit,
6195
- since: options.since
7141
+ since: options.since,
7142
+ backend: "rest"
6196
7143
  });
6197
7144
  while (true) {
6198
7145
  const response = await requestWithGitHubRateLimit(
@@ -6226,7 +7173,8 @@ async function fetchMergedPullRequests(options) {
6226
7173
  all: limit === void 0,
6227
7174
  limit,
6228
7175
  scannedPullRequests,
6229
- matchedMergedPullRequests: pullNumbers.length
7176
+ matchedMergedPullRequests: pullNumbers.length,
7177
+ backend: "rest"
6230
7178
  });
6231
7179
  const hasNextPage2 = String(response.headers.link ?? "").includes('rel="next"');
6232
7180
  if (reachedSinceBoundary || limit !== void 0 && pullNumbers.length >= limit || !hasNextPage2) {
@@ -6240,7 +7188,8 @@ async function fetchMergedPullRequests(options) {
6240
7188
  all: limit === void 0,
6241
7189
  total: pullNumbers.length,
6242
7190
  limit,
6243
- detailConcurrency
7191
+ detailConcurrency,
7192
+ backend: "rest"
6244
7193
  });
6245
7194
  return fetchPullRequestDetailsConcurrently({
6246
7195
  octokit,
@@ -6251,6 +7200,42 @@ async function fetchMergedPullRequests(options) {
6251
7200
  onProgress: options.onProgress
6252
7201
  });
6253
7202
  }
7203
+ async function fetchMergedPullRequests(options) {
7204
+ const limit = resolvePullRequestFetchLimit(options);
7205
+ const detailConcurrency = resolvePullRequestDetailConcurrency(options);
7206
+ const graphqlRateLimitController = createProgressRateLimitController(
7207
+ options.repo,
7208
+ options.onProgress
7209
+ );
7210
+ const restRateLimitController = createProgressRateLimitController(options.repo, options.onProgress);
7211
+ try {
7212
+ return await fetchMergedPullRequestsWithGraphQL({
7213
+ token: options.token,
7214
+ repo: options.repo,
7215
+ limit,
7216
+ all: options.all,
7217
+ detailConcurrency,
7218
+ since: options.since,
7219
+ controller: graphqlRateLimitController,
7220
+ restController: restRateLimitController,
7221
+ graphQLCheckpoint: options.graphQLCheckpoint,
7222
+ onGraphQLCheckpoint: options.onGraphQLCheckpoint,
7223
+ onProgress: options.onProgress,
7224
+ fetchImpl: options.fetchImpl,
7225
+ restClient: options.restClient
7226
+ });
7227
+ } catch (error) {
7228
+ if (!shouldFallbackToRestAfterGraphQLError(error)) throw error;
7229
+ options.onProgress?.({
7230
+ stage: "github_fetch_backend_fallback",
7231
+ repo: options.repo,
7232
+ from: "graphql",
7233
+ to: "rest",
7234
+ reason: error instanceof Error ? error.message : String(error)
7235
+ });
7236
+ return fetchMergedPullRequestsWithRest(options, restRateLimitController);
7237
+ }
7238
+ }
6254
7239
 
6255
7240
  // src/doctor.ts
6256
7241
  import fs9 from "fs";
@@ -6315,6 +7300,49 @@ async function runDoctor(options) {
6315
7300
  )
6316
7301
  );
6317
7302
  }
7303
+ if (token) {
7304
+ try {
7305
+ const graphqlOk = options.githubGraphQLCheck !== void 0 ? Boolean(await options.githubGraphQLCheck(token)) : options.githubClientFactory !== void 0 ? true : Boolean(
7306
+ await createGitHubGraphQLRequester({ token })(
7307
+ `query AnchorDoctorGraphQL {
7308
+ viewer { login }
7309
+ rateLimit { cost remaining resetAt }
7310
+ }`,
7311
+ {},
7312
+ {
7313
+ controller: {},
7314
+ requestName: "GraphQL doctor reachability check"
7315
+ }
7316
+ )
7317
+ );
7318
+ checks.push(
7319
+ check(
7320
+ "GitHub GraphQL reachable",
7321
+ graphqlOk,
7322
+ graphqlOk ? "GitHub GraphQL API is reachable." : "GitHub GraphQL API check returned an unsuccessful result.",
7323
+ "Check token scope, network access, and GraphQL rate limits. Use read-only repo access."
7324
+ )
7325
+ );
7326
+ } catch (error) {
7327
+ checks.push(
7328
+ check(
7329
+ "GitHub GraphQL reachable",
7330
+ false,
7331
+ `GitHub GraphQL check failed: ${error instanceof Error ? error.message : String(error)}`,
7332
+ "Check token scope, network access, and GraphQL rate limits. Use read-only repo access."
7333
+ )
7334
+ );
7335
+ }
7336
+ } else {
7337
+ checks.push(
7338
+ check(
7339
+ "GitHub GraphQL reachable",
7340
+ false,
7341
+ "Skipped because token is missing.",
7342
+ githubAuthFixMessage()
7343
+ )
7344
+ );
7345
+ }
6318
7346
  const cursorConfigPath = path20.join(gitRoot ?? cwd, ".cursor", "mcp.json");
6319
7347
  let cursorConfig;
6320
7348
  let cursorConfigValid = false;
@@ -6444,6 +7472,7 @@ export {
6444
7472
  DEMO_CODE_FILES,
6445
7473
  DEMO_PULL_REQUESTS,
6446
7474
  DEMO_REPO,
7475
+ GitHubGraphQLError,
6447
7476
  SCHEMA_SQL,
6448
7477
  TEAM_RULES_FILE,
6449
7478
  addRetrievalEval,
@@ -6467,6 +7496,7 @@ export {
6467
7496
  claimKeyFor,
6468
7497
  clampMaxResults,
6469
7498
  classifyArchitectureArea,
7499
+ clearGraphQLFetchCheckpoint,
6470
7500
  clipSentence,
6471
7501
  confidenceAtLeast,
6472
7502
  confidenceLevelFor,
@@ -6474,6 +7504,7 @@ export {
6474
7504
  confidenceReasonsFor,
6475
7505
  countValidTeamRules,
6476
7506
  createGitHubClient,
7507
+ createGitHubGraphQLRequester,
6477
7508
  defaultDatabasePath,
6478
7509
  detectGitHubRepo,
6479
7510
  detectGitRoot,
@@ -6498,6 +7529,7 @@ export {
6498
7529
  extractWisdomUnits,
6499
7530
  feedbackAdjustedScore,
6500
7531
  fetchMergedPullRequests,
7532
+ fetchMergedPullRequestsWithGraphQL,
6501
7533
  fetchPullRequestDetails,
6502
7534
  filesFromDiff,
6503
7535
  formatAnchorContext,
@@ -6507,6 +7539,7 @@ export {
6507
7539
  getArchitectureContext,
6508
7540
  getArchitectureMapContext,
6509
7541
  getGitHubRateLimitDelayMs,
7542
+ getGraphQLFetchCheckpoint,
6510
7543
  getIndexStatus,
6511
7544
  getLastSyncTime,
6512
7545
  getPlaybook,
@@ -6515,6 +7548,7 @@ export {
6515
7548
  getSuggestedPrompts,
6516
7549
  getWisdomCategoryCounts,
6517
7550
  githubAuthFixMessage,
7551
+ graphQLFetchCheckpointScope,
6518
7552
  hasHighSignalLanguage,
6519
7553
  indexCodebase,
6520
7554
  indexPullRequests,
@@ -6522,6 +7556,7 @@ export {
6522
7556
  initPlaybooks,
6523
7557
  initRetrievalEvals,
6524
7558
  initializeSchema,
7559
+ isGitHubGraphQLResourceLimitError,
6525
7560
  isGitHubRateLimitError,
6526
7561
  isHardExcludedCodePath,
6527
7562
  isTestFilePath,
@@ -6557,6 +7592,8 @@ export {
6557
7592
  runDoctor,
6558
7593
  runRetrievalEvals,
6559
7594
  sanitizeHistoricalText,
7595
+ saveGraphQLFetchCheckpoint,
7596
+ shouldFallbackToRestAfterGraphQLError,
6560
7597
  shouldSyncSince,
6561
7598
  sourceTypeLabel,
6562
7599
  stripPromptInjection,
@@ -6566,6 +7603,7 @@ export {
6566
7603
  tokenizeSearchText,
6567
7604
  truncateText,
6568
7605
  uniqueStrings,
7606
+ updateGitHubGraphQLRateLimitState,
6569
7607
  updateSyncState,
6570
7608
  upsertPullRequest,
6571
7609
  validateTeamRulesFile,