@reviewflow/sdk 6.22.0 → 6.23.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.
@@ -2307,6 +2307,75 @@ const createGitHubTransport = (send) => ({
2307
2307
  }
2308
2308
  });
2309
2309
 
2310
+ const prStatuses = [
2311
+ "approved",
2312
+ "changesRequested",
2313
+ "closed",
2314
+ "draft",
2315
+ "merged",
2316
+ "noReviewers",
2317
+ "waitingReview"
2318
+ ];
2319
+ const reviewStatuses = {
2320
+ approved: "approved",
2321
+ changesRequested: "changesRequested",
2322
+ reviewRequired: "waitingReview"
2323
+ };
2324
+ const prStatus = ({
2325
+ state,
2326
+ reviewDecision
2327
+ }) => {
2328
+ if (state !== "open") return state;
2329
+ return reviewDecision === null ? "noReviewers" : reviewStatuses[reviewDecision];
2330
+ };
2331
+ const prStatusLabels = {
2332
+ approved: "approved",
2333
+ changesRequested: "changes requested",
2334
+ closed: "closed",
2335
+ draft: "draft",
2336
+ merged: "merged",
2337
+ noReviewers: "no reviewers",
2338
+ waitingReview: "waiting review"
2339
+ };
2340
+ const prHasConflicts = ({
2341
+ state,
2342
+ mergeable
2343
+ }) => (state === "open" || state === "draft") && mergeable === "conflicting";
2344
+ const prUrl = (ownerRepo, number) => `https://github.com/${ownerRepo}/pull/${String(number)}`;
2345
+ const prNumberPattern = /^#?(\d+)$/;
2346
+ const prUrlPattern = /^https?:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)(?:[/?#]\S*)?$/i;
2347
+ const prNumberOf = (input, ownerRepo) => {
2348
+ const text = input.trim();
2349
+ const url = prUrlPattern.exec(text);
2350
+ const [, digits] = url === null ? prNumberPattern.exec(text) ?? [] : [];
2351
+ if (url !== null && url[1]?.toLowerCase() !== ownerRepo.toLowerCase()) {
2352
+ return `that pull request is not in ${ownerRepo}`;
2353
+ }
2354
+ const number = Number(url?.[2] ?? digits);
2355
+ if (!Number.isInteger(number) || number < 1) {
2356
+ return "a pull request number or its GitHub link is expected";
2357
+ }
2358
+ return number;
2359
+ };
2360
+ const reviewflowReviewDecisions = {
2361
+ approved: "approved",
2362
+ changesRequested: "changesRequested",
2363
+ requested: "reviewRequired",
2364
+ none: null
2365
+ };
2366
+ const reviewflowPrStatus = ({
2367
+ state,
2368
+ reviewState
2369
+ }) => {
2370
+ if (state === null) return null;
2371
+ if (state !== "open") return state;
2372
+ if (reviewState === null) return null;
2373
+ return prStatus({
2374
+ state,
2375
+ reviewDecision: reviewflowReviewDecisions[reviewState]
2376
+ });
2377
+ };
2378
+
2310
2379
  const pullRequestParts = [
2311
2380
  "detail",
2312
2381
  "checks",
@@ -2391,6 +2460,32 @@ const pullRequestEditMovesBoard = (edit) => {
2391
2460
  }
2392
2461
  };
2393
2462
 
2463
+ const dayMs = 24 * 60 * 60 * 1e3;
2464
+ const weekdayReach = 6;
2465
+ const startOfDay = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
2466
+ const calendarDaysAgo = (date, now) => Math.round((startOfDay(now) - startOfDay(date)) / dayMs);
2467
+ const formatRelativeDate = (value, { now = /* @__PURE__ */ new Date(), locale } = {}) => {
2468
+ const date = typeof value === "string" ? new Date(value) : value;
2469
+ if (Number.isNaN(date.getTime())) return "unknown date";
2470
+ const time = date.toLocaleTimeString(locale, {
2471
+ hour: "2-digit",
2472
+ minute: "2-digit"
2473
+ });
2474
+ const days = calendarDaysAgo(date, now);
2475
+ if (days === 0) return `today ${time}`;
2476
+ if (days === 1) return `yesterday ${time}`;
2477
+ if (days > 1 && days <= weekdayReach) {
2478
+ const weekday = date.toLocaleDateString("en", { weekday: "long" });
2479
+ return `last ${weekday} ${time}`;
2480
+ }
2481
+ const day = date.toLocaleDateString(locale, {
2482
+ day: "numeric",
2483
+ month: "short",
2484
+ ...date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }
2485
+ });
2486
+ return `${day}, ${time}`;
2487
+ };
2488
+
2394
2489
  const partFields = {
2395
2490
  detail: (pr) => [
2396
2491
  pr.title,
@@ -3064,6 +3159,92 @@ const applyStreamEvent = (snapshot, event) => {
3064
3159
  };
3065
3160
  };
3066
3161
 
3162
+ const maxFailedCheckNames = 9;
3163
+ const bucketsSilentOnPassedChecks = /* @__PURE__ */ new Set(["changes-requested", "drafts"]);
3164
+ const reRequestedBucket = "re-requested-reviews";
3165
+ const requestingAttentionGroup = "requesting-attention";
3166
+ const counted = (count, one, many) => `${String(count)} ${count === 1 ? one : many}`;
3167
+ const people = (logins, viewer) => logins.map((login) => login === viewer ? "you" : `@${login}`).join(", ");
3168
+ const failedChecksText = ({
3169
+ checksConclusion,
3170
+ failedChecks
3171
+ }) => {
3172
+ if (failedChecks.length === 0) {
3173
+ return checksConclusion === "failed" ? "checks failed" : null;
3174
+ }
3175
+ const names = failedChecks.slice(0, maxFailedCheckNames).join(", ");
3176
+ const remaining = failedChecks.length - maxFailedCheckNames;
3177
+ const more = remaining > 0 ? ` +${String(remaining)}` : "";
3178
+ return `${failedChecks.length === 1 ? "check" : "checks"} failed: ${names}${more}`;
3179
+ };
3180
+ const reviewflowPrFailure = (pr) => {
3181
+ const parts = [
3182
+ failedChecksText(pr),
3183
+ pr.lintFailed ? "pr lint failed" : null,
3184
+ pr.mergeable === "conflicting" ? "conflicts" : null
3185
+ ].filter((part) => part !== null);
3186
+ return parts.length === 0 ? null : parts.join(" \xB7 ");
3187
+ };
3188
+ const reviewflowPrChangesRequested = ({ changesRequestedBy }, viewer) => changesRequestedBy.length === 0 ? null : `changes requested by ${people(changesRequestedBy, viewer)}`;
3189
+ const checksProgressText = ({
3190
+ checksConclusion,
3191
+ checksRunning
3192
+ }, bucketKey) => {
3193
+ if (checksRunning > 0) {
3194
+ return `${counted(checksRunning, "check", "checks")} running`;
3195
+ }
3196
+ return checksConclusion === "passed" && !bucketsSilentOnPassedChecks.has(bucketKey) ? "checks passed" : null;
3197
+ };
3198
+ const awaitedText = ({
3199
+ requestedReviewers,
3200
+ reRequestedReviewers,
3201
+ requestedTeams
3202
+ }, { bucketKey, groupKey, viewer }) => {
3203
+ const reviewers = [
3204
+ ...requestedReviewers,
3205
+ ...bucketKey === reRequestedBucket ? [] : reRequestedReviewers
3206
+ ];
3207
+ if (reviewers.length === 0 && requestedTeams.length === 0) return null;
3208
+ const verb = groupKey === requestingAttentionGroup ? "requested" : "awaiting";
3209
+ const awaited = [
3210
+ ...reviewers.length === 0 ? [] : [people(reviewers, viewer)],
3211
+ ...requestedTeams.map((team) => `#${team}`)
3212
+ ];
3213
+ return `${verb} ${awaited.join(", ")}`;
3214
+ };
3215
+ const reviewflowPrProgress = (pr, context) => {
3216
+ const parts = [
3217
+ checksProgressText(pr, context.bucketKey),
3218
+ pr.approvedBy.length === 0 ? null : `approved by ${people(pr.approvedBy, context.viewer)}`,
3219
+ pr.unresolvedThreads ? counted(pr.unresolvedThreads, "unresolved thread", "unresolved threads") : null,
3220
+ awaitedText(pr, context)
3221
+ ].filter((part) => part !== null);
3222
+ return parts.length === 0 ? null : parts.join(" \xB7 ");
3223
+ };
3224
+ const reviewflowPrOwners = ({ author, assignees }, viewer) => {
3225
+ const named = [...author === null ? [] : [author], ...assignees];
3226
+ if (named.length === 0) return null;
3227
+ if (viewer !== null && named.every((login) => login === viewer)) return null;
3228
+ const byline = author === null ? null : `by ${people([author], viewer)}`;
3229
+ if (assignees.length === 0) return byline;
3230
+ const assignment = `assigned to ${people(assignees, viewer)}`;
3231
+ if (author === null || !assignees.includes(author)) {
3232
+ return byline === null ? assignment : `${byline} \xB7 ${assignment}`;
3233
+ }
3234
+ return assignees.length === 1 ? byline : assignment;
3235
+ };
3236
+ const reviewflowPrFlowDate = ({ openedAt, approvedAt }, options) => {
3237
+ if (approvedAt !== null) {
3238
+ return `approved ${formatRelativeDate(approvedAt, options)}`;
3239
+ }
3240
+ return openedAt === null ? null : `opened ${formatRelativeDate(openedAt, options)}`;
3241
+ };
3242
+ const formatPrChanges = ({
3243
+ changedFiles,
3244
+ additions,
3245
+ deletions
3246
+ }) => `${counted(changedFiles, "file", "files")} +${String(additions)} \u2212${String(deletions)}`;
3247
+
3067
3248
  const lingerMs = 5 * 6e4;
3068
3249
  const boardReadDelayMs = 3e3;
3069
3250
  const reconnectDelayMs = 1e3;
@@ -3324,5 +3505,5 @@ const insertCheckoutEntry = (source, ownerRepo) => {
3324
3505
  return { source: text, cursor: value };
3325
3506
  };
3326
3507
 
3327
- export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, diffLines, diffPaths, diffThemeNames, diffThemeOptions, diffThemes, editPullRequest, fail, fetchCompareDiff, fetchPullRequestCandidates, fetchPullRequestHead, fetchPullRequestPart, fetchPullRequestRevisions, insertCheckoutEntry, interpretGraphql, isAttachablePath, isDiffTheme, isDiffWhitespace, isPullRequestOnBoard, isPullRequestPart, isPullRequestReactionContent, isReviewflowFailure, lingerMs, maxExtraPagesPerBucket, maxLookupTargets, maxRetryMs, parsePrQueryResponse, parsePullRequestCandidates, parsePullRequestChecks, parsePullRequestCommits, parsePullRequestDetail, parsePullRequestEdit, parsePullRequestFiles, parsePullRequestRevisions, parsePullRequestTimeline, parseSuggestions, partFingerprint, planSuggestions, prFields, pullRequestCandidatesQuery, pullRequestChecksQuery, pullRequestCommitsQuery, pullRequestDetailQuery, pullRequestEditMovesBoard, pullRequestFilesQuery, pullRequestHeadQuery, pullRequestParts, pullRequestPartsStaleAfter, pullRequestReactionContents, pullRequestRevisionsQuery, pullRequestTimelineQuery, readAccountBoards, readPrDetail, readReviewflowStream, reconnectDelayMs, responseFailure, restErrorMessage, reviewflowErrorCode, splitOwnerRepo, streamClosedWithinDays, streamIdleTimeoutMs, subjectsOf, suggestionCommitRefusal, suggestionOriginalLines, suggestionRangesOverlap, toBoardPr, toFetchedPr, withAccountBoard, withFileViewed };
3508
+ export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, diffLines, diffPaths, diffThemeNames, diffThemeOptions, diffThemes, editPullRequest, fail, fetchCompareDiff, fetchPullRequestCandidates, fetchPullRequestHead, fetchPullRequestPart, fetchPullRequestRevisions, formatPrChanges, formatRelativeDate, insertCheckoutEntry, interpretGraphql, isAttachablePath, isDiffTheme, isDiffWhitespace, isPullRequestOnBoard, isPullRequestPart, isPullRequestReactionContent, isReviewflowFailure, lingerMs, maxExtraPagesPerBucket, maxFailedCheckNames, maxLookupTargets, maxRetryMs, parsePrQueryResponse, parsePullRequestCandidates, parsePullRequestChecks, parsePullRequestCommits, parsePullRequestDetail, parsePullRequestEdit, parsePullRequestFiles, parsePullRequestRevisions, parsePullRequestTimeline, parseSuggestions, partFingerprint, planSuggestions, prFields, prHasConflicts, prNumberOf, prStatus, prStatusLabels, prStatuses, prUrl, pullRequestCandidatesQuery, pullRequestChecksQuery, pullRequestCommitsQuery, pullRequestDetailQuery, pullRequestEditMovesBoard, pullRequestFilesQuery, pullRequestHeadQuery, pullRequestParts, pullRequestPartsStaleAfter, pullRequestReactionContents, pullRequestRevisionsQuery, pullRequestTimelineQuery, readAccountBoards, readPrDetail, readReviewflowStream, reconnectDelayMs, responseFailure, restErrorMessage, reviewflowErrorCode, reviewflowPrChangesRequested, reviewflowPrFailure, reviewflowPrFlowDate, reviewflowPrOwners, reviewflowPrProgress, reviewflowPrStatus, reviewflowReviewDecisions, splitOwnerRepo, streamClosedWithinDays, streamIdleTimeoutMs, subjectsOf, suggestionCommitRefusal, suggestionOriginalLines, suggestionRangesOverlap, toBoardPr, toFetchedPr, withAccountBoard, withFileViewed };
3328
3509
  //# sourceMappingURL=index-browser.es.js.map
@@ -2307,6 +2307,75 @@ const createGitHubTransport = (send) => ({
2307
2307
  }
2308
2308
  });
2309
2309
 
2310
+ const prStatuses = [
2311
+ "approved",
2312
+ "changesRequested",
2313
+ "closed",
2314
+ "draft",
2315
+ "merged",
2316
+ "noReviewers",
2317
+ "waitingReview"
2318
+ ];
2319
+ const reviewStatuses = {
2320
+ approved: "approved",
2321
+ changesRequested: "changesRequested",
2322
+ reviewRequired: "waitingReview"
2323
+ };
2324
+ const prStatus = ({
2325
+ state,
2326
+ reviewDecision
2327
+ }) => {
2328
+ if (state !== "open") return state;
2329
+ return reviewDecision === null ? "noReviewers" : reviewStatuses[reviewDecision];
2330
+ };
2331
+ const prStatusLabels = {
2332
+ approved: "approved",
2333
+ changesRequested: "changes requested",
2334
+ closed: "closed",
2335
+ draft: "draft",
2336
+ merged: "merged",
2337
+ noReviewers: "no reviewers",
2338
+ waitingReview: "waiting review"
2339
+ };
2340
+ const prHasConflicts = ({
2341
+ state,
2342
+ mergeable
2343
+ }) => (state === "open" || state === "draft") && mergeable === "conflicting";
2344
+ const prUrl = (ownerRepo, number) => `https://github.com/${ownerRepo}/pull/${String(number)}`;
2345
+ const prNumberPattern = /^#?(\d+)$/;
2346
+ const prUrlPattern = /^https?:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)(?:[/?#]\S*)?$/i;
2347
+ const prNumberOf = (input, ownerRepo) => {
2348
+ const text = input.trim();
2349
+ const url = prUrlPattern.exec(text);
2350
+ const [, digits] = url === null ? prNumberPattern.exec(text) ?? [] : [];
2351
+ if (url !== null && url[1]?.toLowerCase() !== ownerRepo.toLowerCase()) {
2352
+ return `that pull request is not in ${ownerRepo}`;
2353
+ }
2354
+ const number = Number(url?.[2] ?? digits);
2355
+ if (!Number.isInteger(number) || number < 1) {
2356
+ return "a pull request number or its GitHub link is expected";
2357
+ }
2358
+ return number;
2359
+ };
2360
+ const reviewflowReviewDecisions = {
2361
+ approved: "approved",
2362
+ changesRequested: "changesRequested",
2363
+ requested: "reviewRequired",
2364
+ none: null
2365
+ };
2366
+ const reviewflowPrStatus = ({
2367
+ state,
2368
+ reviewState
2369
+ }) => {
2370
+ if (state === null) return null;
2371
+ if (state !== "open") return state;
2372
+ if (reviewState === null) return null;
2373
+ return prStatus({
2374
+ state,
2375
+ reviewDecision: reviewflowReviewDecisions[reviewState]
2376
+ });
2377
+ };
2378
+
2310
2379
  const pullRequestParts = [
2311
2380
  "detail",
2312
2381
  "checks",
@@ -2391,6 +2460,32 @@ const pullRequestEditMovesBoard = (edit) => {
2391
2460
  }
2392
2461
  };
2393
2462
 
2463
+ const dayMs = 24 * 60 * 60 * 1e3;
2464
+ const weekdayReach = 6;
2465
+ const startOfDay = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
2466
+ const calendarDaysAgo = (date, now) => Math.round((startOfDay(now) - startOfDay(date)) / dayMs);
2467
+ const formatRelativeDate = (value, { now = /* @__PURE__ */ new Date(), locale } = {}) => {
2468
+ const date = typeof value === "string" ? new Date(value) : value;
2469
+ if (Number.isNaN(date.getTime())) return "unknown date";
2470
+ const time = date.toLocaleTimeString(locale, {
2471
+ hour: "2-digit",
2472
+ minute: "2-digit"
2473
+ });
2474
+ const days = calendarDaysAgo(date, now);
2475
+ if (days === 0) return `today ${time}`;
2476
+ if (days === 1) return `yesterday ${time}`;
2477
+ if (days > 1 && days <= weekdayReach) {
2478
+ const weekday = date.toLocaleDateString("en", { weekday: "long" });
2479
+ return `last ${weekday} ${time}`;
2480
+ }
2481
+ const day = date.toLocaleDateString(locale, {
2482
+ day: "numeric",
2483
+ month: "short",
2484
+ ...date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }
2485
+ });
2486
+ return `${day}, ${time}`;
2487
+ };
2488
+
2394
2489
  const partFields = {
2395
2490
  detail: (pr) => [
2396
2491
  pr.title,
@@ -3064,6 +3159,92 @@ const applyStreamEvent = (snapshot, event) => {
3064
3159
  };
3065
3160
  };
3066
3161
 
3162
+ const maxFailedCheckNames = 9;
3163
+ const bucketsSilentOnPassedChecks = /* @__PURE__ */ new Set(["changes-requested", "drafts"]);
3164
+ const reRequestedBucket = "re-requested-reviews";
3165
+ const requestingAttentionGroup = "requesting-attention";
3166
+ const counted = (count, one, many) => `${String(count)} ${count === 1 ? one : many}`;
3167
+ const people = (logins, viewer) => logins.map((login) => login === viewer ? "you" : `@${login}`).join(", ");
3168
+ const failedChecksText = ({
3169
+ checksConclusion,
3170
+ failedChecks
3171
+ }) => {
3172
+ if (failedChecks.length === 0) {
3173
+ return checksConclusion === "failed" ? "checks failed" : null;
3174
+ }
3175
+ const names = failedChecks.slice(0, maxFailedCheckNames).join(", ");
3176
+ const remaining = failedChecks.length - maxFailedCheckNames;
3177
+ const more = remaining > 0 ? ` +${String(remaining)}` : "";
3178
+ return `${failedChecks.length === 1 ? "check" : "checks"} failed: ${names}${more}`;
3179
+ };
3180
+ const reviewflowPrFailure = (pr) => {
3181
+ const parts = [
3182
+ failedChecksText(pr),
3183
+ pr.lintFailed ? "pr lint failed" : null,
3184
+ pr.mergeable === "conflicting" ? "conflicts" : null
3185
+ ].filter((part) => part !== null);
3186
+ return parts.length === 0 ? null : parts.join(" \xB7 ");
3187
+ };
3188
+ const reviewflowPrChangesRequested = ({ changesRequestedBy }, viewer) => changesRequestedBy.length === 0 ? null : `changes requested by ${people(changesRequestedBy, viewer)}`;
3189
+ const checksProgressText = ({
3190
+ checksConclusion,
3191
+ checksRunning
3192
+ }, bucketKey) => {
3193
+ if (checksRunning > 0) {
3194
+ return `${counted(checksRunning, "check", "checks")} running`;
3195
+ }
3196
+ return checksConclusion === "passed" && !bucketsSilentOnPassedChecks.has(bucketKey) ? "checks passed" : null;
3197
+ };
3198
+ const awaitedText = ({
3199
+ requestedReviewers,
3200
+ reRequestedReviewers,
3201
+ requestedTeams
3202
+ }, { bucketKey, groupKey, viewer }) => {
3203
+ const reviewers = [
3204
+ ...requestedReviewers,
3205
+ ...bucketKey === reRequestedBucket ? [] : reRequestedReviewers
3206
+ ];
3207
+ if (reviewers.length === 0 && requestedTeams.length === 0) return null;
3208
+ const verb = groupKey === requestingAttentionGroup ? "requested" : "awaiting";
3209
+ const awaited = [
3210
+ ...reviewers.length === 0 ? [] : [people(reviewers, viewer)],
3211
+ ...requestedTeams.map((team) => `#${team}`)
3212
+ ];
3213
+ return `${verb} ${awaited.join(", ")}`;
3214
+ };
3215
+ const reviewflowPrProgress = (pr, context) => {
3216
+ const parts = [
3217
+ checksProgressText(pr, context.bucketKey),
3218
+ pr.approvedBy.length === 0 ? null : `approved by ${people(pr.approvedBy, context.viewer)}`,
3219
+ pr.unresolvedThreads ? counted(pr.unresolvedThreads, "unresolved thread", "unresolved threads") : null,
3220
+ awaitedText(pr, context)
3221
+ ].filter((part) => part !== null);
3222
+ return parts.length === 0 ? null : parts.join(" \xB7 ");
3223
+ };
3224
+ const reviewflowPrOwners = ({ author, assignees }, viewer) => {
3225
+ const named = [...author === null ? [] : [author], ...assignees];
3226
+ if (named.length === 0) return null;
3227
+ if (viewer !== null && named.every((login) => login === viewer)) return null;
3228
+ const byline = author === null ? null : `by ${people([author], viewer)}`;
3229
+ if (assignees.length === 0) return byline;
3230
+ const assignment = `assigned to ${people(assignees, viewer)}`;
3231
+ if (author === null || !assignees.includes(author)) {
3232
+ return byline === null ? assignment : `${byline} \xB7 ${assignment}`;
3233
+ }
3234
+ return assignees.length === 1 ? byline : assignment;
3235
+ };
3236
+ const reviewflowPrFlowDate = ({ openedAt, approvedAt }, options) => {
3237
+ if (approvedAt !== null) {
3238
+ return `approved ${formatRelativeDate(approvedAt, options)}`;
3239
+ }
3240
+ return openedAt === null ? null : `opened ${formatRelativeDate(openedAt, options)}`;
3241
+ };
3242
+ const formatPrChanges = ({
3243
+ changedFiles,
3244
+ additions,
3245
+ deletions
3246
+ }) => `${counted(changedFiles, "file", "files")} +${String(additions)} \u2212${String(deletions)}`;
3247
+
3067
3248
  const lingerMs = 5 * 6e4;
3068
3249
  const boardReadDelayMs = 3e3;
3069
3250
  const reconnectDelayMs = 1e3;
@@ -3324,5 +3505,5 @@ const insertCheckoutEntry = (source, ownerRepo) => {
3324
3505
  return { source: text, cursor: value };
3325
3506
  };
3326
3507
 
3327
- export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, diffLines, diffPaths, diffThemeNames, diffThemeOptions, diffThemes, editPullRequest, fail, fetchCompareDiff, fetchPullRequestCandidates, fetchPullRequestHead, fetchPullRequestPart, fetchPullRequestRevisions, insertCheckoutEntry, interpretGraphql, isAttachablePath, isDiffTheme, isDiffWhitespace, isPullRequestOnBoard, isPullRequestPart, isPullRequestReactionContent, isReviewflowFailure, lingerMs, maxExtraPagesPerBucket, maxLookupTargets, maxRetryMs, parsePrQueryResponse, parsePullRequestCandidates, parsePullRequestChecks, parsePullRequestCommits, parsePullRequestDetail, parsePullRequestEdit, parsePullRequestFiles, parsePullRequestRevisions, parsePullRequestTimeline, parseSuggestions, partFingerprint, planSuggestions, prFields, pullRequestCandidatesQuery, pullRequestChecksQuery, pullRequestCommitsQuery, pullRequestDetailQuery, pullRequestEditMovesBoard, pullRequestFilesQuery, pullRequestHeadQuery, pullRequestParts, pullRequestPartsStaleAfter, pullRequestReactionContents, pullRequestRevisionsQuery, pullRequestTimelineQuery, readAccountBoards, readPrDetail, readReviewflowStream, reconnectDelayMs, responseFailure, restErrorMessage, reviewflowErrorCode, splitOwnerRepo, streamClosedWithinDays, streamIdleTimeoutMs, subjectsOf, suggestionCommitRefusal, suggestionOriginalLines, suggestionRangesOverlap, toBoardPr, toFetchedPr, withAccountBoard, withFileViewed };
3508
+ export { accountOfOwnerRepo, applyPullRequestEdit, applyStreamEvent, applySuggestionsToText, assembleBoard, boardReadDelayMs, buildPrQuery, changedPullRequestParts, checkOutcomes, checksFromReviewflow, coAuthorTrailers, commitSuggestions, createGitHubTransport, createReviewflowClient, createReviewflowFollower, diffLines, diffPaths, diffThemeNames, diffThemeOptions, diffThemes, editPullRequest, fail, fetchCompareDiff, fetchPullRequestCandidates, fetchPullRequestHead, fetchPullRequestPart, fetchPullRequestRevisions, formatPrChanges, formatRelativeDate, insertCheckoutEntry, interpretGraphql, isAttachablePath, isDiffTheme, isDiffWhitespace, isPullRequestOnBoard, isPullRequestPart, isPullRequestReactionContent, isReviewflowFailure, lingerMs, maxExtraPagesPerBucket, maxFailedCheckNames, maxLookupTargets, maxRetryMs, parsePrQueryResponse, parsePullRequestCandidates, parsePullRequestChecks, parsePullRequestCommits, parsePullRequestDetail, parsePullRequestEdit, parsePullRequestFiles, parsePullRequestRevisions, parsePullRequestTimeline, parseSuggestions, partFingerprint, planSuggestions, prFields, prHasConflicts, prNumberOf, prStatus, prStatusLabels, prStatuses, prUrl, pullRequestCandidatesQuery, pullRequestChecksQuery, pullRequestCommitsQuery, pullRequestDetailQuery, pullRequestEditMovesBoard, pullRequestFilesQuery, pullRequestHeadQuery, pullRequestParts, pullRequestPartsStaleAfter, pullRequestReactionContents, pullRequestRevisionsQuery, pullRequestTimelineQuery, readAccountBoards, readPrDetail, readReviewflowStream, reconnectDelayMs, responseFailure, restErrorMessage, reviewflowErrorCode, reviewflowPrChangesRequested, reviewflowPrFailure, reviewflowPrFlowDate, reviewflowPrOwners, reviewflowPrProgress, reviewflowPrStatus, reviewflowReviewDecisions, splitOwnerRepo, streamClosedWithinDays, streamIdleTimeoutMs, subjectsOf, suggestionCommitRefusal, suggestionOriginalLines, suggestionRangesOverlap, toBoardPr, toFetchedPr, withAccountBoard, withFileViewed };
3328
3509
  //# sourceMappingURL=index-node.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reviewflow/sdk",
3
- "version": "6.22.0",
3
+ "version": "6.23.0",
4
4
  "description": "pull request model shared by the reviewflow app server and its ui: diff and api types, pure utils, the GitHub layer behind an injected transport, and (node only) reading a diff from a local checkout (@reviewflow/sdk/git) and editing a text file in place (@reviewflow/sdk/files)",
5
5
  "keywords": [
6
6
  "code-review",
package/src/index.ts CHANGED
@@ -20,15 +20,18 @@ export type * from "./api/pullRequest.ts";
20
20
  export type * from "./types/entities.ts";
21
21
  export type * from "./types/ids.ts";
22
22
  export type * from "./types/reviewflow.ts";
23
+ export * from "./utils/prStatus.ts";
23
24
  export * from "./utils/pullRequestEdit.ts";
24
25
  export * from "./utils/pullRequestPart.ts";
25
26
  export * from "./utils/pullRequestReaction.ts";
26
27
  export * from "./utils/pullRequestSuggestion.ts";
28
+ export * from "./utils/relativeDate.ts";
27
29
  export * from "./reviewflow/changedParts.ts";
28
30
  export * from "./reviewflow/checksFromReviewflow.ts";
29
31
  export * from "./reviewflow/client.ts";
30
32
  export * from "./reviewflow/stream.ts";
31
33
  export * from "./reviewflow/board.ts";
34
+ export * from "./reviewflow/boardFacts.ts";
32
35
  export * from "./reviewflow/follow.ts";
33
36
  export type * from "./reviewflow/status.ts";
34
37
  export { insertCheckoutEntry } from "./config/insertCheckoutEntry.ts";
@@ -0,0 +1,184 @@
1
+ import type { ReviewflowPrChanges } from "../types/reviewflow.ts";
2
+ import type { FormatRelativeDateOptions } from "../utils/relativeDate.ts";
3
+ import { formatRelativeDate } from "../utils/relativeDate.ts";
4
+ import type { ReviewflowBoardPr } from "./board.ts";
5
+
6
+ /**
7
+ * What reviewflow's own board row says about a pull request, in its wording so
8
+ * the two boards read the same. The status line comes in three parts by how
9
+ * loudly each is shown: what blocks the merge, changes requested, the rest.
10
+ */
11
+
12
+ export const maxFailedCheckNames = 9;
13
+
14
+ /** Where a row is shown, which decides what it leaves to its bucket's title. */
15
+ export interface ReviewflowRowContext {
16
+ bucketKey: string;
17
+ groupKey: string;
18
+ /** the token's login, named "you" */
19
+ viewer: string | null;
20
+ }
21
+
22
+ /** the buckets whose title already says whether the checks pass */
23
+ const bucketsSilentOnPassedChecks = new Set(["changes-requested", "drafts"]);
24
+ /** the bucket whose title already says every reviewer was asked again */
25
+ const reRequestedBucket = "re-requested-reviews";
26
+ /** someone else's pull request requests a review, the operator's awaits one */
27
+ const requestingAttentionGroup = "requesting-attention";
28
+
29
+ const counted = (count: number, one: string, many: string): string =>
30
+ `${String(count)} ${count === 1 ? one : many}`;
31
+
32
+ const people = (logins: string[], viewer: string | null): string =>
33
+ logins.map((login) => (login === viewer ? "you" : `@${login}`)).join(", ");
34
+
35
+ const failedChecksText = ({
36
+ checksConclusion,
37
+ failedChecks,
38
+ }: Pick<ReviewflowBoardPr, "checksConclusion" | "failedChecks">):
39
+ | string
40
+ | null => {
41
+ if (failedChecks.length === 0) {
42
+ return checksConclusion === "failed" ? "checks failed" : null;
43
+ }
44
+ const names = failedChecks.slice(0, maxFailedCheckNames).join(", ");
45
+ const remaining = failedChecks.length - maxFailedCheckNames;
46
+ const more = remaining > 0 ? ` +${String(remaining)}` : "";
47
+ return `${failedChecks.length === 1 ? "check" : "checks"} failed: ${names}${more}`;
48
+ };
49
+
50
+ /**
51
+ * Failing checks (after reviewflow's allowed-to-fail rules, by name), a failed
52
+ * pr lint and a merge conflict; null when nothing blocks.
53
+ */
54
+ export const reviewflowPrFailure = (
55
+ pr: Pick<
56
+ ReviewflowBoardPr,
57
+ "checksConclusion" | "failedChecks" | "lintFailed" | "mergeable"
58
+ >,
59
+ ): string | null => {
60
+ const parts = [
61
+ failedChecksText(pr),
62
+ pr.lintFailed ? "pr lint failed" : null,
63
+ pr.mergeable === "conflicting" ? "conflicts" : null,
64
+ ].filter((part) => part !== null);
65
+ return parts.length === 0 ? null : parts.join(" · ");
66
+ };
67
+
68
+ export const reviewflowPrChangesRequested = (
69
+ { changesRequestedBy }: Pick<ReviewflowBoardPr, "changesRequestedBy">,
70
+ viewer: string | null,
71
+ ): string | null =>
72
+ changesRequestedBy.length === 0
73
+ ? null
74
+ : `changes requested by ${people(changesRequestedBy, viewer)}`;
75
+
76
+ const checksProgressText = (
77
+ {
78
+ checksConclusion,
79
+ checksRunning,
80
+ }: Pick<ReviewflowBoardPr, "checksConclusion" | "checksRunning">,
81
+ bucketKey: string,
82
+ ): string | null => {
83
+ if (checksRunning > 0) {
84
+ return `${counted(checksRunning, "check", "checks")} running`;
85
+ }
86
+ return checksConclusion === "passed" &&
87
+ !bucketsSilentOnPassedChecks.has(bucketKey)
88
+ ? "checks passed"
89
+ : null;
90
+ };
91
+
92
+ const awaitedText = (
93
+ {
94
+ requestedReviewers,
95
+ reRequestedReviewers,
96
+ requestedTeams,
97
+ }: Pick<
98
+ ReviewflowBoardPr,
99
+ "requestedReviewers" | "requestedTeams" | "reRequestedReviewers"
100
+ >,
101
+ { bucketKey, groupKey, viewer }: ReviewflowRowContext,
102
+ ): string | null => {
103
+ const reviewers = [
104
+ ...requestedReviewers,
105
+ ...(bucketKey === reRequestedBucket ? [] : reRequestedReviewers),
106
+ ];
107
+ if (reviewers.length === 0 && requestedTeams.length === 0) return null;
108
+ const verb = groupKey === requestingAttentionGroup ? "requested" : "awaiting";
109
+ const awaited = [
110
+ ...(reviewers.length === 0 ? [] : [people(reviewers, viewer)]),
111
+ ...requestedTeams.map((team) => `#${team}`),
112
+ ];
113
+ return `${verb} ${awaited.join(", ")}`;
114
+ };
115
+
116
+ /** running or passed checks, approvals, open threads, awaited reviewers */
117
+ export const reviewflowPrProgress = (
118
+ pr: Pick<
119
+ ReviewflowBoardPr,
120
+ | "approvedBy"
121
+ | "checksConclusion"
122
+ | "checksRunning"
123
+ | "requestedReviewers"
124
+ | "requestedTeams"
125
+ | "reRequestedReviewers"
126
+ | "unresolvedThreads"
127
+ >,
128
+ context: ReviewflowRowContext,
129
+ ): string | null => {
130
+ const parts = [
131
+ checksProgressText(pr, context.bucketKey),
132
+ pr.approvedBy.length === 0
133
+ ? null
134
+ : `approved by ${people(pr.approvedBy, context.viewer)}`,
135
+ pr.unresolvedThreads
136
+ ? counted(pr.unresolvedThreads, "unresolved thread", "unresolved threads")
137
+ : null,
138
+ awaitedText(pr, context),
139
+ ].filter((part) => part !== null);
140
+ return parts.length === 0 ? null : parts.join(" · ");
141
+ };
142
+
143
+ /**
144
+ * "by @author", "assigned to @a", or both. An author who is their own only
145
+ * assignee is a byline, not an assignment; a pull request whose only person is
146
+ * the viewer names nobody.
147
+ */
148
+ export const reviewflowPrOwners = (
149
+ { author, assignees }: Pick<ReviewflowBoardPr, "assignees" | "author">,
150
+ viewer: string | null,
151
+ ): string | null => {
152
+ const named = [...(author === null ? [] : [author]), ...assignees];
153
+ if (named.length === 0) return null;
154
+ if (viewer !== null && named.every((login) => login === viewer)) return null;
155
+
156
+ const byline = author === null ? null : `by ${people([author], viewer)}`;
157
+ if (assignees.length === 0) return byline;
158
+ const assignment = `assigned to ${people(assignees, viewer)}`;
159
+
160
+ if (author === null || !assignees.includes(author)) {
161
+ return byline === null ? assignment : `${byline} · ${assignment}`;
162
+ }
163
+ // naming every assignee already names the author among them
164
+ return assignees.length === 1 ? byline : assignment;
165
+ };
166
+
167
+ export const reviewflowPrFlowDate = (
168
+ { openedAt, approvedAt }: Pick<ReviewflowBoardPr, "approvedAt" | "openedAt">,
169
+ options?: FormatRelativeDateOptions,
170
+ ): string | null => {
171
+ if (approvedAt !== null) {
172
+ return `approved ${formatRelativeDate(approvedAt, options)}`;
173
+ }
174
+ return openedAt === null
175
+ ? null
176
+ : `opened ${formatRelativeDate(openedAt, options)}`;
177
+ };
178
+
179
+ export const formatPrChanges = ({
180
+ changedFiles,
181
+ additions,
182
+ deletions,
183
+ }: ReviewflowPrChanges): string =>
184
+ `${counted(changedFiles, "file", "files")} +${String(additions)} −${String(deletions)}`;
@@ -0,0 +1,152 @@
1
+ import type { ReviewflowBoardPr } from "../reviewflow/board.ts";
2
+ import type {
3
+ PrMergeable,
4
+ PrReviewDecision,
5
+ PrState,
6
+ } from "../types/entities.ts";
7
+ import type { ReviewflowReviewState } from "../types/reviewflow.ts";
8
+
9
+ /**
10
+ * What the operator is told about a pull request, in one word or two.
11
+ *
12
+ * `pr.state` and `pr.reviewDecision` are two columns because GitHub reports two
13
+ * fields, but they are never two answers: an open PR is only ever interesting
14
+ * for where its review stands, and "open" on its own says nothing the branch did
15
+ * not already say. So the two collapse into one status, and every surface
16
+ * reads it from here rather than
17
+ * formatting the columns itself. They had drifted into three different renderings
18
+ * of the same row.
19
+ *
20
+ * `noReviewers` and `waitingReview` are a real distinction, not a null check:
21
+ * GitHub leaves `reviewDecision` null when nothing is required and nobody was
22
+ * asked, and answers `REVIEW_REQUIRED` when a review is required and missing.
23
+ * The first is the operator's own omission — nobody is coming — and the second
24
+ * is somebody else's turn.
25
+ */
26
+ export const prStatuses = [
27
+ "approved",
28
+ "changesRequested",
29
+ "closed",
30
+ "draft",
31
+ "merged",
32
+ "noReviewers",
33
+ "waitingReview",
34
+ ] as const;
35
+
36
+ export type PrStatus = (typeof prStatuses)[number];
37
+
38
+ const reviewStatuses: Record<
39
+ NonNullable<PrReviewDecision>,
40
+ Extract<PrStatus, "approved" | "changesRequested" | "waitingReview">
41
+ > = {
42
+ approved: "approved",
43
+ changesRequested: "changesRequested",
44
+ reviewRequired: "waitingReview",
45
+ };
46
+
47
+ /**
48
+ * A closed or merged PR keeps its own status whatever the last review said: an
49
+ * approved PR that was closed is closed, and saying "approved" would describe a
50
+ * decision nobody is going to act on.
51
+ */
52
+ export const prStatus = ({
53
+ state,
54
+ reviewDecision,
55
+ }: {
56
+ state: PrState;
57
+ reviewDecision: PrReviewDecision;
58
+ }): PrStatus => {
59
+ if (state !== "open") return state;
60
+ return reviewDecision === null
61
+ ? "noReviewers"
62
+ : reviewStatuses[reviewDecision];
63
+ };
64
+
65
+ export const prStatusLabels: Record<PrStatus, string> = {
66
+ approved: "approved",
67
+ changesRequested: "changes requested",
68
+ closed: "closed",
69
+ draft: "draft",
70
+ merged: "merged",
71
+ noReviewers: "no reviewers",
72
+ waitingReview: "waiting review",
73
+ };
74
+
75
+ /**
76
+ * Only while it can still be merged: GitHub keeps answering for a closed one,
77
+ * and a conflict nobody will resolve is not something to look at.
78
+ */
79
+ export const prHasConflicts = ({
80
+ state,
81
+ mergeable,
82
+ }: {
83
+ state: PrState;
84
+ mergeable: PrMergeable;
85
+ }): boolean =>
86
+ (state === "open" || state === "draft") && mergeable === "conflicting";
87
+
88
+ /**
89
+ * Built rather than stored: the number and `owner/repo` are kept apart,
90
+ * and a url column would be a third copy of both that goes stale when either
91
+ * moves. GitHub redirects `/pull/<n>` to the right kind of page, so nothing here
92
+ * has to know whether the number belongs to a PR or an issue.
93
+ */
94
+ export const prUrl = (ownerRepo: string, number: number): string =>
95
+ `https://github.com/${ownerRepo}/pull/${String(number)}`;
96
+
97
+ const prNumberPattern = /^#?(\d+)$/;
98
+ const prUrlPattern =
99
+ /^https?:\/\/github\.com\/([^/\s]+\/[^/\s]+)\/pull\/(\d+)(?:[/?#]\S*)?$/i;
100
+
101
+ /**
102
+ * A pull request of `ownerRepo` as the operator names one — `123`, `#123` or
103
+ * its GitHub url, a tab of it included — or why the text names none. A url of
104
+ * another repository is refused rather than reduced to its number, which would
105
+ * name a different pull request.
106
+ */
107
+ export const prNumberOf = (
108
+ input: string,
109
+ ownerRepo: string,
110
+ ): number | string => {
111
+ const text = input.trim();
112
+ const url = prUrlPattern.exec(text);
113
+ const [, digits] = url === null ? (prNumberPattern.exec(text) ?? []) : [];
114
+ if (url !== null && url[1]?.toLowerCase() !== ownerRepo.toLowerCase()) {
115
+ return `that pull request is not in ${ownerRepo}`;
116
+ }
117
+ const number = Number(url?.[2] ?? digits);
118
+ if (!Number.isInteger(number) || number < 1) {
119
+ return "a pull request number or its GitHub link is expected";
120
+ }
121
+ return number;
122
+ };
123
+
124
+ /**
125
+ * reviewflow's review state in GitHub's vocabulary, which `pr` stores. `none` is
126
+ * what GitHub cannot say on a repository that requires a review: it answers
127
+ * `REVIEW_REQUIRED` whether or not anyone was asked, where reviewflow tells the
128
+ * two apart.
129
+ */
130
+ export const reviewflowReviewDecisions: Record<
131
+ ReviewflowReviewState,
132
+ PrReviewDecision
133
+ > = {
134
+ approved: "approved",
135
+ changesRequested: "changesRequested",
136
+ requested: "reviewRequired",
137
+ none: null,
138
+ };
139
+
140
+ /** null when reviewflow does not know enough to say */
141
+ export const reviewflowPrStatus = ({
142
+ state,
143
+ reviewState,
144
+ }: Pick<ReviewflowBoardPr, "reviewState" | "state">): PrStatus | null => {
145
+ if (state === null) return null;
146
+ if (state !== "open") return state;
147
+ if (reviewState === null) return null;
148
+ return prStatus({
149
+ state,
150
+ reviewDecision: reviewflowReviewDecisions[reviewState],
151
+ });
152
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A past date the way the operator says it: "today 14:32", "yesterday 09:10",
3
+ * "last Monday 16:05", then "12 Oct, 14:32" — and the year once it is not this
4
+ * one. The boards read dates to answer "is this from this morning or last
5
+ * week", which a full timestamp makes them work out.
6
+ *
7
+ * Calendar days in the machine's own time zone, not 24-hour spans: 23:50
8
+ * yesterday is "yesterday" at 00:10. The time and the month follow the
9
+ * locale; the words are the app's, which is in English.
10
+ */
11
+
12
+ const dayMs = 24 * 60 * 60 * 1000;
13
+
14
+ /** how many days back the weekday alone still names the day without doubt */
15
+ const weekdayReach = 6;
16
+
17
+ const startOfDay = (date: Date): number =>
18
+ new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
19
+
20
+ /** Whole calendar days from `date` to `now`; negative for a future date. */
21
+ const calendarDaysAgo = (date: Date, now: Date): number =>
22
+ Math.round((startOfDay(now) - startOfDay(date)) / dayMs);
23
+
24
+ export interface FormatRelativeDateOptions {
25
+ now?: Date;
26
+ /** a BCP 47 tag; the runtime's own when absent */
27
+ locale?: string;
28
+ }
29
+
30
+ export const formatRelativeDate = (
31
+ value: Date | string,
32
+ { now = new Date(), locale }: FormatRelativeDateOptions = {},
33
+ ): string => {
34
+ const date = typeof value === "string" ? new Date(value) : value;
35
+ if (Number.isNaN(date.getTime())) return "unknown date";
36
+
37
+ const time = date.toLocaleTimeString(locale, {
38
+ hour: "2-digit",
39
+ minute: "2-digit",
40
+ });
41
+ const days = calendarDaysAgo(date, now);
42
+
43
+ if (days === 0) return `today ${time}`;
44
+ if (days === 1) return `yesterday ${time}`;
45
+ if (days > 1 && days <= weekdayReach) {
46
+ const weekday = date.toLocaleDateString("en", { weekday: "long" });
47
+ return `last ${weekday} ${time}`;
48
+ }
49
+
50
+ const day = date.toLocaleDateString(locale, {
51
+ day: "numeric",
52
+ month: "short",
53
+ ...(date.getFullYear() === now.getFullYear() ? {} : { year: "numeric" }),
54
+ });
55
+ return `${day}, ${time}`;
56
+ };