pr-shepherd 1.0.0 → 1.1.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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/cli/poll-summary-formatter.mjs +3 -0
- package/bin/cli/rule-auto-resolve-lines.d.mts +3 -0
- package/bin/cli/rule-auto-resolve-lines.mjs +12 -0
- package/bin/cli/stack-layer-view.d.mts +2 -1
- package/bin/cli/stack-overview.mjs +3 -0
- package/bin/commands/check-activity.d.mts +8 -0
- package/bin/commands/check-activity.mjs +26 -0
- package/bin/commands/check-annotations.mjs +2 -1
- package/bin/commands/check-wait-detectors.mjs +11 -0
- package/bin/commands/check.mjs +6 -1
- package/bin/commands/poll-summary-signature.d.mts +6 -0
- package/bin/commands/poll-summary-signature.mjs +19 -2
- package/bin/commands/poll-summary.mjs +7 -0
- package/bin/commands/ready-delay.d.mts +1 -0
- package/bin/commands/ready-delay.mjs +8 -3
- package/bin/commands/rule-auto-resolve-format.d.mts +7 -0
- package/bin/commands/rule-auto-resolve-format.mjs +11 -4
- package/bin/commands/rule-auto-resolve.d.mts +14 -2
- package/bin/comments/thread-visibility.d.mts +6 -0
- package/bin/comments/thread-visibility.mjs +30 -15
- package/bin/github/batch-parse-suites.d.mts +14 -0
- package/bin/github/batch-parse-suites.mjs +4 -1
- package/bin/github/batch-parser-helpers.mjs +3 -0
- package/bin/github/check-annotation-cache.d.mts +5 -0
- package/bin/github/check-annotation-cache.mjs +6 -3
- package/bin/github/fingerprint.mjs +4 -0
- package/bin/github/poll-summary-annotation-probe.d.mts +5 -0
- package/bin/github/poll-summary-annotation-probe.mjs +17 -3
- package/bin/github/poll-summary-annotations.d.mts +14 -0
- package/bin/github/poll-summary-annotations.mjs +75 -0
- package/bin/github/poll-summary-blocking-reviewer.d.mts +2 -0
- package/bin/github/poll-summary-blocking-reviewer.mjs +9 -0
- package/bin/github/poll-summary-checks.d.mts +1 -0
- package/bin/github/poll-summary-checks.mjs +20 -2
- package/bin/github/poll-summary-fingerprint.d.mts +7 -0
- package/bin/github/poll-summary-fingerprint.mjs +7 -0
- package/bin/github/poll-summary-projector.d.mts +3 -1
- package/bin/github/poll-summary-projector.mjs +13 -24
- package/bin/github/poll-summary-queue-removal.d.mts +2 -0
- package/bin/github/poll-summary-queue-removal.mjs +11 -0
- package/bin/github/poll-summary-readiness.mjs +1 -0
- package/bin/github/poll-summary-review-parity.d.mts +24 -0
- package/bin/github/poll-summary-review-parity.mjs +82 -0
- package/bin/github/poll-summary-review.d.mts +11 -2
- package/bin/github/poll-summary-review.mjs +87 -59
- package/bin/github/poll-summary-row-error.d.mts +16 -0
- package/bin/github/poll-summary-row-error.mjs +74 -0
- package/bin/github/poll-summary-rule-auto-resolve.d.mts +32 -0
- package/bin/github/poll-summary-rule-auto-resolve.mjs +119 -0
- package/bin/github/poll-summary-rule-handoff.d.mts +7 -0
- package/bin/github/poll-summary-rule-handoff.mjs +20 -0
- package/bin/github/poll-summary-rule-items.d.mts +20 -0
- package/bin/github/poll-summary-rule-items.mjs +28 -0
- package/bin/github/poll-summary.mjs +15 -7
- package/bin/github/rest-reader-core.mjs +13 -2
- package/bin/github/rest-summary-read.mjs +12 -1
- package/bin/types/github.d.mts +2 -0
- package/bin/types/poll-summary.d.mts +23 -0
- package/package.json +2 -2
- package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
- package/plugins/pr-shepherd/.codex.mcp.json +1 -1
- package/plugins/pr-shepherd/.mcp.json +1 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { pollRateLimitRetryAfterMs } from "../commands/poll-quota.mjs";
|
|
2
|
+
import { fetchCheckRunAnnotationsBatch } from "./check-annotations-batch.mjs";
|
|
3
|
+
import { hydrateAnnotationTotals } from "./poll-summary-annotation-probe.mjs";
|
|
4
|
+
import { classifiedSummaryChecks } from "./poll-summary-checks.mjs";
|
|
5
|
+
/** Routes whose action claims the row has nothing left to do. */
|
|
6
|
+
const READY_CLAIMS = new Set([
|
|
7
|
+
"appears-ready",
|
|
8
|
+
"draft-appears-ready",
|
|
9
|
+
"already-in-merge-queue",
|
|
10
|
+
"already-auto-merging",
|
|
11
|
+
]);
|
|
12
|
+
/**
|
|
13
|
+
* The one-PR check treats unseen annotations on any non-SUCCESS check (failing, skipped,
|
|
14
|
+
* filtered, ignored or neutral) as actionable work. The compact summary only has annotation
|
|
15
|
+
* totals, and only for a row that otherwise claims readiness, so it loads them then and reads
|
|
16
|
+
* the annotations of just the non-SUCCESS checks that have any. A row with unseen annotations,
|
|
17
|
+
* or whose annotations cannot be read, goes to its one-PR `pollCommand` instead of
|
|
18
|
+
* claiming `appears-ready`, `cancel`, `mark_ready` or `merge`.
|
|
19
|
+
*/
|
|
20
|
+
export async function applyUnseenCheckAnnotations(raw, repo, routed, checks, seen, required) {
|
|
21
|
+
if (!routed.reasons.some((reason) => READY_CLAIMS.has(reason)))
|
|
22
|
+
return routed;
|
|
23
|
+
// Successful checks never carry actionable annotations: skip the probe when none can.
|
|
24
|
+
if (nonSuccessCheckTotals(raw, required, false).size === 0)
|
|
25
|
+
return routed;
|
|
26
|
+
const totalsKnown = await hydrateAnnotationTotals(raw, repo);
|
|
27
|
+
const candidates = nonSuccessCheckTotals(raw, required, totalsKnown);
|
|
28
|
+
if (candidates.size === 0)
|
|
29
|
+
return routed;
|
|
30
|
+
const count = totalsKnown
|
|
31
|
+
? await countChecksWithUnseen(raw, repo, candidates, seen)
|
|
32
|
+
: candidates.size;
|
|
33
|
+
if (count === 0)
|
|
34
|
+
return routed;
|
|
35
|
+
checks.annotated = count;
|
|
36
|
+
return { action: "triage", reasons: ["check-annotations"] };
|
|
37
|
+
}
|
|
38
|
+
/** Completed, non-SUCCESS head checks with their annotation totals (all of them when totals are unknown). */
|
|
39
|
+
function nonSuccessCheckTotals(raw, required, totalsKnown) {
|
|
40
|
+
const head = new Map((raw.commits.nodes[0]?.commit.statusCheckRollup?.contexts.nodes ?? []).flatMap((node) => node.__typename === "CheckRun" && node.id
|
|
41
|
+
? [[node.id, node.annotations?.totalCount]]
|
|
42
|
+
: []));
|
|
43
|
+
const totals = new Map();
|
|
44
|
+
for (const check of classifiedSummaryChecks(raw, required)) {
|
|
45
|
+
if (check.scope || check.id == null || !head.has(check.id))
|
|
46
|
+
continue;
|
|
47
|
+
if (check.category === "in_progress" || check.category === "superseded")
|
|
48
|
+
continue;
|
|
49
|
+
if (check.status !== "COMPLETED" || check.conclusion === "SUCCESS")
|
|
50
|
+
continue;
|
|
51
|
+
const total = head.get(check.id) ?? 0;
|
|
52
|
+
if (!totalsKnown || total > 0)
|
|
53
|
+
totals.set(check.id, total);
|
|
54
|
+
}
|
|
55
|
+
return totals;
|
|
56
|
+
}
|
|
57
|
+
async function countChecksWithUnseen(raw, repo, totals, seen) {
|
|
58
|
+
const ids = [...totals.keys()];
|
|
59
|
+
try {
|
|
60
|
+
const batch = await fetchCheckRunAnnotationsBatch(ids, {
|
|
61
|
+
stateKey: { owner: repo.owner, repo: repo.name, pr: raw.number },
|
|
62
|
+
headSha: raw.headRefOid,
|
|
63
|
+
expectedCounts: totals,
|
|
64
|
+
});
|
|
65
|
+
// A check whose annotations could not be read may still hold unseen work.
|
|
66
|
+
const unread = ids.filter((id) => !batch.annotations.has(id)).length;
|
|
67
|
+
const unseen = [...batch.annotations.values()].filter((list) => list.some((annotation) => !seen.has(annotation.id))).length;
|
|
68
|
+
return unread + unseen;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (pollRateLimitRetryAfterMs(error) !== null)
|
|
72
|
+
throw error;
|
|
73
|
+
return ids.length;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { loadConfig } from "../config/load.mjs";
|
|
2
|
+
export function detectBlockingReviewer(raw) {
|
|
3
|
+
const prefixes = loadConfig()["mark-ready"]["blocking-reviewer-logins"].map((login) => login.toLowerCase());
|
|
4
|
+
// A team reviewer has a name, not a login, so it never matches a login prefix.
|
|
5
|
+
const matches = (author) => typeof author?.login === "string" &&
|
|
6
|
+
prefixes.some((prefix) => author.login.toLowerCase().startsWith(prefix));
|
|
7
|
+
return ((raw.reviewRequests?.nodes ?? []).some((request) => matches(request.requestedReviewer)) ||
|
|
8
|
+
(raw.latestReviews?.nodes ?? []).some((review) => review.state === "PENDING" && matches(review.author)));
|
|
9
|
+
}
|
|
@@ -3,3 +3,4 @@ import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
|
3
3
|
export declare function summarizePollSummaryChecks(raw: RawSummaryPr, requiredContexts?: readonly string[]): PollSummaryChecks;
|
|
4
4
|
/** Names of failing checks in the loaded summary rollup. */
|
|
5
5
|
export declare function failingSummaryCheckNames(raw: RawSummaryPr, requiredContexts?: readonly string[]): string[];
|
|
6
|
+
export declare function classifiedSummaryChecks(raw: RawSummaryPr, requiredContexts?: readonly string[]): import("../types.mts").ClassifiedCheck[];
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { classifyChecks } from "../checks/classify.mjs";
|
|
2
|
+
import { mergeStartupFailureChecks } from "../checks/startup-failures.mjs";
|
|
3
|
+
import { startupFailureChecksFromSuites } from "./batch-parse-suites.mjs";
|
|
2
4
|
import { parseCreatedAt } from "./batch-parser-helpers.mjs";
|
|
3
5
|
export function summarizePollSummaryChecks(raw, requiredContexts) {
|
|
4
6
|
const rollups = summaryRollups(raw);
|
|
@@ -19,6 +21,11 @@ export function summarizePollSummaryChecks(raw, requiredContexts) {
|
|
|
19
21
|
if (rollups.some(({ rollup }) => rollup.contexts.pageInfo.hasPreviousPage)) {
|
|
20
22
|
summary.incomplete = true;
|
|
21
23
|
}
|
|
24
|
+
// The first 50 check suites are all the summary reads. The one-PR check lists the
|
|
25
|
+
// remaining workflow runs for startup failures; here an unread suite stays unverified.
|
|
26
|
+
if (raw.commits.nodes[0]?.commit.checkSuites?.pageInfo?.hasNextPage === true) {
|
|
27
|
+
summary.incomplete = true;
|
|
28
|
+
}
|
|
22
29
|
return summary;
|
|
23
30
|
}
|
|
24
31
|
/** Names of failing checks in the loaded summary rollup. */
|
|
@@ -36,13 +43,24 @@ function summaryRollups(raw) {
|
|
|
36
43
|
},
|
|
37
44
|
].flatMap(({ rollup, scope }) => (rollup ? [{ rollup, scope }] : []));
|
|
38
45
|
}
|
|
39
|
-
function classifiedSummaryChecks(raw, requiredContexts) {
|
|
46
|
+
export function classifiedSummaryChecks(raw, requiredContexts) {
|
|
40
47
|
// A successful check on the queue commit cannot cover a PR-head cancellation, or vice versa.
|
|
41
|
-
|
|
48
|
+
const groups = summaryRollups(raw).map(({ rollup, scope }) => ({
|
|
49
|
+
runs: checkRuns(rollup, scope),
|
|
50
|
+
scope,
|
|
51
|
+
}));
|
|
52
|
+
if (!groups.some(({ scope }) => scope === undefined))
|
|
53
|
+
groups.unshift({ runs: [], scope: undefined });
|
|
54
|
+
return groups.flatMap(({ runs, scope }) => classifyChecks(scope ? runs : withStartupFailures(raw, runs), {
|
|
42
55
|
...(scope === "merge_group" && { additionalRelevantEvents: ["merge_group"] }),
|
|
43
56
|
...(requiredContexts && { requiredContexts }),
|
|
44
57
|
}));
|
|
45
58
|
}
|
|
59
|
+
/** A workflow run that never started has no check run; the one-PR check synthesizes it as failing. */
|
|
60
|
+
function withStartupFailures(raw, runs) {
|
|
61
|
+
const suites = raw.commits.nodes[0]?.commit.checkSuites;
|
|
62
|
+
return mergeStartupFailureChecks(runs, startupFailureChecksFromSuites(suites));
|
|
63
|
+
}
|
|
46
64
|
function checkRuns(rollup, scope) {
|
|
47
65
|
return rollup.contexts.nodes.map((context) => {
|
|
48
66
|
if (context.__typename === "StatusContext") {
|
|
@@ -4,5 +4,12 @@ import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
|
4
4
|
* completion and stack reconciliation. Queue entry, queue checks, and GitHub's
|
|
5
5
|
* computed merge state are checked separately: including them here would
|
|
6
6
|
* invalidate a READY receipt merely because a ready PR entered the queue.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately conservative: every comment, review and thread node and the ignored
|
|
9
|
+
* checks are hashed without applying classification rules. A rule-suppressed bot
|
|
10
|
+
* comment or an ignored-check change therefore invalidates a receipt, but a miss only
|
|
11
|
+
* costs one more one-PR session (`no-ready-receipt`), whereas a hash that skipped
|
|
12
|
+
* evidence could certify a state the one-PR check never examined. Do not narrow this
|
|
13
|
+
* to post-rule state unless the one-PR check hashes the same projection.
|
|
7
14
|
*/
|
|
8
15
|
export declare function fingerprintRawSummaryPr(summary: RawSummaryPr): string | null;
|
|
@@ -5,6 +5,13 @@ import { withoutHandoffFields } from "./poll-summary-handoff-fields.mjs";
|
|
|
5
5
|
* completion and stack reconciliation. Queue entry, queue checks, and GitHub's
|
|
6
6
|
* computed merge state are checked separately: including them here would
|
|
7
7
|
* invalidate a READY receipt merely because a ready PR entered the queue.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately conservative: every comment, review and thread node and the ignored
|
|
10
|
+
* checks are hashed without applying classification rules. A rule-suppressed bot
|
|
11
|
+
* comment or an ignored-check change therefore invalidates a receipt, but a miss only
|
|
12
|
+
* costs one more one-PR session (`no-ready-receipt`), whereas a hash that skipped
|
|
13
|
+
* evidence could certify a state the one-PR check never examined. Do not narrow this
|
|
14
|
+
* to post-rule state unless the one-PR check hashes the same projection.
|
|
8
15
|
*/
|
|
9
16
|
export function fingerprintRawSummaryPr(summary) {
|
|
10
17
|
const raw = withoutHandoffFields(summary);
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { PollSummaryCommandOptions, PollSummaryItem } from "../types.mts";
|
|
2
2
|
import type { RepoInfo } from "./client.mts";
|
|
3
3
|
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
4
|
-
export declare function summarizePollSummaryPr(raw: RawSummaryPr, repo: RepoInfo, opts: PollSummaryCommandOptions, viewerCanAdminister?: boolean | undefined, mergeTargetContexts?: readonly string[], viewerLogin?: string | null
|
|
4
|
+
export declare function summarizePollSummaryPr(raw: RawSummaryPr, repo: RepoInfo, opts: PollSummaryCommandOptions, viewerCanAdminister?: boolean | undefined, mergeTargetContexts?: readonly string[], viewerLogin?: string | null,
|
|
5
|
+
/** Aggregate polls only: apply rule-driven auto-resolve. Receipt and merge validation reads never mutate. */
|
|
6
|
+
applyRuleAutoResolve?: boolean): Promise<PollSummaryItem>;
|
|
@@ -5,8 +5,9 @@ import { formatPrUrl } from "../pr-reference.mjs";
|
|
|
5
5
|
import { loadSeenMap } from "../state/seen-comments.mjs";
|
|
6
6
|
import { isReadyReceiptCurrent, readReadyReceipt } from "../state/ready-receipts.mjs";
|
|
7
7
|
import { summarizePollSummaryChecks } from "./poll-summary-checks.mjs";
|
|
8
|
-
import {
|
|
9
|
-
import { currentQueueRemovalEvent } from "./poll-summary-queue-removal.mjs";
|
|
8
|
+
import { evaluatePollSummaryReview } from "./poll-summary-review.mjs";
|
|
9
|
+
import { currentQueueRemovalEvent, projectQueueRemoval } from "./poll-summary-queue-removal.mjs";
|
|
10
|
+
import { applyUnseenCheckAnnotations } from "./poll-summary-annotations.mjs";
|
|
10
11
|
import { isCurrentSummaryReady } from "./poll-summary-readiness.mjs";
|
|
11
12
|
import { applyOpenCheckBlockers } from "./poll-summary-check-blockers.mjs";
|
|
12
13
|
import { normalizePollSummaryState, routePollSummary } from "./poll-summary-route.mjs";
|
|
@@ -14,14 +15,19 @@ import { hydrateReadyAnnotationProbe, readyFingerprint } from "./poll-summary-an
|
|
|
14
15
|
import { applyUnreportedRequiredChecks, summaryRequiredContexts, } from "./poll-summary-unreported.mjs";
|
|
15
16
|
import { aliasThreadSeenMarkers } from "./rest-identities.mjs";
|
|
16
17
|
import { restQueueRequirement } from "./poll-summary-rest-queue.mjs";
|
|
17
|
-
|
|
18
|
+
import { detectBlockingReviewer } from "./poll-summary-blocking-reviewer.mjs";
|
|
19
|
+
export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdminister = undefined, mergeTargetContexts, viewerLogin = null,
|
|
20
|
+
/** Aggregate polls only: apply rule-driven auto-resolve. Receipt and merge validation reads never mutate. */
|
|
21
|
+
applyRuleAutoResolve = false) {
|
|
18
22
|
const repoName = `${repo.owner}/${repo.name}`;
|
|
19
23
|
const seen = await loadSeenMap({ owner: repo.owner, repo: repo.name, pr: raw.number });
|
|
20
24
|
await aliasThreadSeenMarkers(repo, raw.number, raw.reviewThreads.nodes.map(({ id }) => id), seen);
|
|
21
25
|
const required = summaryRequiredContexts(raw, mergeTargetContexts);
|
|
22
26
|
const checks = summarizePollSummaryChecks(raw, required);
|
|
23
27
|
applyUnreportedRequiredChecks(raw, checks, required);
|
|
24
|
-
const review = await
|
|
28
|
+
const { review, ruleAutoResolve } = await evaluatePollSummaryReview(raw, seen, viewerCanAdminister, applyRuleAutoResolve && normalizePollSummaryState(raw.state) === "OPEN"
|
|
29
|
+
? { owner: repo.owner, repo: repo.name, pr: raw.number }
|
|
30
|
+
: undefined);
|
|
25
31
|
const blockingReviewerInProgress = detectBlockingReviewer(raw);
|
|
26
32
|
const removalEvent = currentQueueRemovalEvent(raw);
|
|
27
33
|
let { action, reasons } = routePollSummary(raw, checks, review, opts);
|
|
@@ -34,10 +40,11 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
|
|
|
34
40
|
action = "wait";
|
|
35
41
|
reasons = ["blocking-reviewer-in-progress"];
|
|
36
42
|
}
|
|
43
|
+
({ action, reasons } = await applyUnseenCheckAnnotations(raw, repo, { action, reasons }, checks, seen, required));
|
|
37
44
|
const appearsReady = reasons.includes("appears-ready");
|
|
38
45
|
if (opts.stackPrNumber === undefined) {
|
|
39
46
|
const readyDelaySeconds = opts.readyDelaySeconds ?? loadConfig().iterate["ready-delay-minutes"] * 60;
|
|
40
|
-
const readyState = await updateReadyDelay(raw.number, appearsReady, readyDelaySeconds, repo.owner, repo.name, { headSha: raw.headRefOid });
|
|
47
|
+
const readyState = await updateReadyDelay(raw.number, appearsReady, readyDelaySeconds, repo.owner, repo.name, { headSha: raw.headRefOid, summary: true });
|
|
41
48
|
if (appearsReady && !readyState.shouldCancel) {
|
|
42
49
|
action = "wait";
|
|
43
50
|
reasons = ["ready-delay"];
|
|
@@ -112,6 +119,7 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
|
|
|
112
119
|
...(remainingSeconds !== undefined && { remainingSeconds }),
|
|
113
120
|
...(Object.keys(checks).length > 0 && { checks }),
|
|
114
121
|
...(Object.keys(review).length > 0 && { review }),
|
|
122
|
+
...(ruleAutoResolve && { ruleAutoResolve }),
|
|
115
123
|
...(stack && { stack }),
|
|
116
124
|
...(readyReceipt && { readyReceipt: true }),
|
|
117
125
|
...((opts.stackPrNumber !== undefined && raw.state === "OPEN") ||
|
|
@@ -122,22 +130,3 @@ export async function summarizePollSummaryPr(raw, repo, opts, viewerCanAdministe
|
|
|
122
130
|
...(blocked.pollProbe ? { pollProbe: true } : {}),
|
|
123
131
|
};
|
|
124
132
|
}
|
|
125
|
-
function projectQueueRemoval(removal) {
|
|
126
|
-
const removalTime = Date.parse(removal.createdAt);
|
|
127
|
-
const parents = removal.beforeCommit?.parents?.nodes.map((parent) => parent.oid) ?? [];
|
|
128
|
-
return {
|
|
129
|
-
reason: removal.reason,
|
|
130
|
-
createdAtUnix: Math.floor(removalTime / 1000),
|
|
131
|
-
...(removal.actor?.login && { actor: removal.actor.login }),
|
|
132
|
-
...(removal.beforeCommit?.oid && { beforeCommitOid: removal.beforeCommit.oid }),
|
|
133
|
-
beforeCommitParentOids: parents,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
function detectBlockingReviewer(raw) {
|
|
137
|
-
const prefixes = loadConfig()["mark-ready"]["blocking-reviewer-logins"].map((login) => login.toLowerCase());
|
|
138
|
-
// A team reviewer has a name, not a login, so it never matches a login prefix.
|
|
139
|
-
const matches = (author) => typeof author?.login === "string" &&
|
|
140
|
-
prefixes.some((prefix) => author.login.toLowerCase().startsWith(prefix));
|
|
141
|
-
return ((raw.reviewRequests?.nodes ?? []).some((request) => matches(request.requestedReviewer)) ||
|
|
142
|
-
(raw.latestReviews?.nodes ?? []).some((review) => review.state === "PENDING" && matches(review.author)));
|
|
143
|
-
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { PollSummaryItem } from "../types.mts";
|
|
1
2
|
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
2
3
|
type QueueRemovalEvent = NonNullable<RawSummaryPr["mergeQueueRemovals"]>["nodes"][number];
|
|
3
4
|
/** Latest queue removal that still applies to this PR head, not an older attempt. */
|
|
4
5
|
export declare function currentQueueRemovalEvent(raw: RawSummaryPr): QueueRemovalEvent | null;
|
|
6
|
+
export declare function projectQueueRemoval(removal: QueueRemovalEvent): PollSummaryItem["queueRemoval"];
|
|
5
7
|
export {};
|
|
@@ -28,3 +28,14 @@ export function currentQueueRemovalEvent(raw) {
|
|
|
28
28
|
}
|
|
29
29
|
return removal;
|
|
30
30
|
}
|
|
31
|
+
export function projectQueueRemoval(removal) {
|
|
32
|
+
const removalTime = Date.parse(removal.createdAt);
|
|
33
|
+
const parents = removal.beforeCommit?.parents?.nodes.map((parent) => parent.oid) ?? [];
|
|
34
|
+
return {
|
|
35
|
+
reason: removal.reason,
|
|
36
|
+
createdAtUnix: Math.floor(removalTime / 1000),
|
|
37
|
+
...(removal.actor?.login && { actor: removal.actor.login }),
|
|
38
|
+
...(removal.beforeCommit?.oid && { beforeCommitOid: removal.beforeCommit.oid }),
|
|
39
|
+
beforeCommitParentOids: parents,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -38,6 +38,7 @@ export function isCurrentSummaryReady(raw, checks, review, options = {}) {
|
|
|
38
38
|
(queued || raw.mergeable === "MERGEABLE") &&
|
|
39
39
|
(queued || !blockedStates.includes(raw.mergeStateStatus)) &&
|
|
40
40
|
(checks.failing ?? 0) === 0 &&
|
|
41
|
+
(checks.annotated ?? 0) === 0 &&
|
|
41
42
|
(sourceChecks.failing ?? 0) === 0 &&
|
|
42
43
|
(sourceChecks.inProgress ?? 0) === 0 &&
|
|
43
44
|
(checks.unreportedRequired?.length ?? 0) === 0 &&
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type NormalizedBotUsernames } from "../comments/authors.mts";
|
|
2
|
+
import type { MinimizeCommentsPolicy, ResolveOtherHumanThreads } from "../config/load.mts";
|
|
3
|
+
import { type SeenMarker } from "../state/seen-comments.mts";
|
|
4
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
5
|
+
type RawComment = RawSummaryPr["comments"]["nodes"][number];
|
|
6
|
+
type RawThread = RawSummaryPr["reviewThreads"]["nodes"][number];
|
|
7
|
+
/**
|
|
8
|
+
* Count PR comments the full check would route to work, through the same classifier it uses: a new
|
|
9
|
+
* comment, plus any seen comment eligible for minimization under `iterate.minimize-comments`.
|
|
10
|
+
*/
|
|
11
|
+
export declare function countCommentWork(eligible: RawComment[], unconfirmed: ReadonlySet<string>, seen: Map<string, SeenMarker>, minimizeComments: MinimizeCommentsPolicy | undefined, bots: NormalizedBotUsernames): number;
|
|
12
|
+
export interface SummaryThreadView {
|
|
13
|
+
thread: RawThread;
|
|
14
|
+
root: RawThread["comments"]["nodes"][number] | undefined;
|
|
15
|
+
truncated: boolean;
|
|
16
|
+
transcript: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Unresolved thread ids that keep returning every tick once seen. This is the full check's
|
|
20
|
+
* `classifyThreadVisibility` repeat rule, including `iterate.resolve-other-human-threads`,
|
|
21
|
+
* shared rather than re-derived.
|
|
22
|
+
*/
|
|
23
|
+
export declare function repeatingThreadIds(views: SummaryThreadView[], seen: Map<string, SeenMarker>, bots: NormalizedBotUsernames, policy: ResolveOtherHumanThreads): Set<string>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { buildThreadMutationRouting, threadHasAuthorizedMutation, } from "../commands/iterate/thread-mutation-routing.mjs";
|
|
2
|
+
import { normalizeAuthorType } from "../comments/authors.mjs";
|
|
3
|
+
import { threadEndedByShepherd } from "../comments/marker.mjs";
|
|
4
|
+
import { unresolvedThreadRepeats } from "../comments/thread-visibility.mjs";
|
|
5
|
+
import { classifyVisibleComments } from "../comments/visible-comments.mjs";
|
|
6
|
+
import { mutationWasDenied } from "../state/seen-comments.mjs";
|
|
7
|
+
const asPrComment = (c) => ({
|
|
8
|
+
id: c.id,
|
|
9
|
+
isMinimized: c.isMinimized === true,
|
|
10
|
+
...(c.viewerCanMinimize !== undefined && { viewerCanMinimize: c.viewerCanMinimize }),
|
|
11
|
+
author: c.author?.login ?? "unknown",
|
|
12
|
+
authorType: normalizeAuthorType(c.author?.__typename, c.author?.login),
|
|
13
|
+
body: c.body,
|
|
14
|
+
url: c.url ?? "",
|
|
15
|
+
createdAtUnix: 0,
|
|
16
|
+
});
|
|
17
|
+
/**
|
|
18
|
+
* Count PR comments the full check would route to work, through the same classifier it uses: a new
|
|
19
|
+
* comment, plus any seen comment eligible for minimization under `iterate.minimize-comments`.
|
|
20
|
+
*/
|
|
21
|
+
export function countCommentWork(eligible, unconfirmed, seen, minimizeComments, bots) {
|
|
22
|
+
const denied = new Set(eligible.filter((c) => mutationWasDenied(c.id, c.body, seen)).map((c) => c.id));
|
|
23
|
+
const work = new Set(classifyVisibleComments(eligible.map(asPrComment), seen, minimizeComments, bots, denied).actionable.map((c) => c.id));
|
|
24
|
+
// An edited PR comment is shown once by iterate but never blocks readiness.
|
|
25
|
+
return eligible.filter((c) => work.has(c.id) || unconfirmed.has(c.id)).length;
|
|
26
|
+
}
|
|
27
|
+
const asReviewThread = ({ thread, root, truncated }) => {
|
|
28
|
+
const author = root?.author?.login ?? "unknown";
|
|
29
|
+
const authorType = normalizeAuthorType(root?.author?.__typename, root?.author?.login);
|
|
30
|
+
return {
|
|
31
|
+
id: thread.id,
|
|
32
|
+
...(thread.isResolved !== undefined && { isResolved: thread.isResolved }),
|
|
33
|
+
...(thread.isOutdated !== undefined && { isOutdated: thread.isOutdated }),
|
|
34
|
+
isMinimized: root?.isMinimized === true,
|
|
35
|
+
...(thread.viewerCanReply !== undefined && { viewerCanReply: thread.viewerCanReply }),
|
|
36
|
+
...(thread.viewerCanResolve !== undefined && { viewerCanResolve: thread.viewerCanResolve }),
|
|
37
|
+
path: thread.path,
|
|
38
|
+
line: null,
|
|
39
|
+
startLine: null,
|
|
40
|
+
author,
|
|
41
|
+
authorType,
|
|
42
|
+
...(root?.viewerDidAuthor === true && { viewerDidAuthor: true }),
|
|
43
|
+
body: root?.body ?? "",
|
|
44
|
+
url: "",
|
|
45
|
+
// A window never shows a truncated thread's last comment, so it cannot prove the thread ended.
|
|
46
|
+
comments: truncated
|
|
47
|
+
? []
|
|
48
|
+
: thread.comments.nodes.map((c) => ({
|
|
49
|
+
id: c.id,
|
|
50
|
+
author: c.author?.login ?? "unknown",
|
|
51
|
+
authorType: normalizeAuthorType(c.author?.__typename, c.author?.login),
|
|
52
|
+
body: c.body,
|
|
53
|
+
url: "",
|
|
54
|
+
createdAtUnix: 0,
|
|
55
|
+
})),
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Unresolved thread ids that keep returning every tick once seen. This is the full check's
|
|
60
|
+
* `classifyThreadVisibility` repeat rule, including `iterate.resolve-other-human-threads`,
|
|
61
|
+
* shared rather than re-derived.
|
|
62
|
+
*/
|
|
63
|
+
export function repeatingThreadIds(views, seen, bots, policy) {
|
|
64
|
+
const threads = views.map((view) => asReviewThread(view));
|
|
65
|
+
const routing = buildThreadMutationRouting(threads, bots, [], policy);
|
|
66
|
+
const replyIds = new Set(routing.replyThreadIds);
|
|
67
|
+
const resolveIds = new Set(routing.resolveThreadIds);
|
|
68
|
+
const repeating = new Set();
|
|
69
|
+
views.forEach((view, index) => {
|
|
70
|
+
const thread = threads[index];
|
|
71
|
+
if (thread.isResolved !== false || thread.isOutdated === undefined)
|
|
72
|
+
return;
|
|
73
|
+
if (mutationWasDenied(thread.id, view.transcript, seen))
|
|
74
|
+
return;
|
|
75
|
+
if (!threadHasAuthorizedMutation(thread, replyIds, resolveIds))
|
|
76
|
+
return;
|
|
77
|
+
const ended = !view.truncated && threadEndedByShepherd(thread);
|
|
78
|
+
if (unresolvedThreadRepeats(thread, ended, bots, policy))
|
|
79
|
+
repeating.add(thread.id);
|
|
80
|
+
});
|
|
81
|
+
return repeating;
|
|
82
|
+
}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { type SeenMarker } from "../state/seen-comments.mts";
|
|
2
|
-
import type { PollSummaryReview } from "../types.mts";
|
|
2
|
+
import type { PollSummaryReview, PollSummaryRuleAutoResolve } from "../types.mts";
|
|
3
3
|
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
4
|
-
|
|
4
|
+
import { type SummaryStateKey } from "./poll-summary-rule-auto-resolve.mts";
|
|
5
|
+
/**
|
|
6
|
+
* Count the review work a compact row routes to the full check. With `autoResolveKey` and
|
|
7
|
+
* `actions.auto-minimize-suppressed` on, rule-driven auto-resolve runs first, exactly as in the
|
|
8
|
+
* one-PR path, so a handled bot notice never sends the row to triage.
|
|
9
|
+
*/
|
|
10
|
+
export declare function evaluatePollSummaryReview(raw: RawSummaryPr, seen: Map<string, SeenMarker>, viewerCanAdminister: boolean | undefined, autoResolveKey?: SummaryStateKey): Promise<{
|
|
11
|
+
review: PollSummaryReview;
|
|
12
|
+
ruleAutoResolve?: PollSummaryRuleAutoResolve;
|
|
13
|
+
}>;
|
|
@@ -1,15 +1,24 @@
|
|
|
1
|
-
import { normalizeBotUsernames } from "../comments/authors.mjs";
|
|
2
|
-
import { applyRules } from "../classify/apply.mjs";
|
|
3
|
-
import { normalizeAuthorType } from "../comments/authors.mjs";
|
|
1
|
+
import { isConfiguredBotAuthor, isHumanAuthor, normalizeAuthorType, normalizeBotUsernames, } from "../comments/authors.mjs";
|
|
4
2
|
import { loadClassifyRules } from "../classify/loader.mjs";
|
|
5
3
|
import { loadConfig } from "../config/load.mjs";
|
|
6
4
|
import { getEffectiveCwd } from "../execution-context.mjs";
|
|
7
|
-
import { classifyItem } from "../state/seen-comments.mjs";
|
|
8
|
-
import { mutationWasDenied } from "../state/seen-comments.mjs";
|
|
9
|
-
import { canGenerateGithubMutation } from "./mutation-policy.mjs";
|
|
5
|
+
import { classifyItem, mutationWasDenied } from "../state/seen-comments.mjs";
|
|
10
6
|
import { threadEndedByShepherd } from "../comments/marker.mjs";
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
import { threadTranscriptBodies } from "../threads/transcript.mjs";
|
|
8
|
+
import { canGenerateGithubMutation } from "./mutation-policy.mjs";
|
|
9
|
+
import { applySummaryRuleAutoResolve, } from "./poll-summary-rule-auto-resolve.mjs";
|
|
10
|
+
import { countCommentWork, repeatingThreadIds } from "./poll-summary-review-parity.mjs";
|
|
11
|
+
import { ruleHides, summaryRuleAction } from "./poll-summary-rule-items.mjs";
|
|
12
|
+
const authorOf = (author) => ({
|
|
13
|
+
login: author?.login ?? "",
|
|
14
|
+
authorType: normalizeAuthorType(author?.__typename, author?.login),
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Count the review work a compact row routes to the full check. With `autoResolveKey` and
|
|
18
|
+
* `actions.auto-minimize-suppressed` on, rule-driven auto-resolve runs first, exactly as in the
|
|
19
|
+
* one-PR path, so a handled bot notice never sends the row to triage.
|
|
20
|
+
*/
|
|
21
|
+
export async function evaluatePollSummaryReview(raw, seen, viewerCanAdminister, autoResolveKey) {
|
|
13
22
|
const config = loadConfig();
|
|
14
23
|
const bots = normalizeBotUsernames(config["bot-usernames"]);
|
|
15
24
|
const rules = await loadClassifyRules(getEffectiveCwd(), config.standardRules);
|
|
@@ -17,18 +26,54 @@ export async function summarizePollSummaryReview(raw, seen, viewerCanAdminister)
|
|
|
17
26
|
let incomplete = raw.comments.pageInfo.hasPreviousPage ||
|
|
18
27
|
raw.reviews.pageInfo.hasPreviousPage ||
|
|
19
28
|
raw.reviewThreads.pageInfo.hasPreviousPage;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
29
|
+
// A pre-minimized comment is a first-look acknowledgement, never readiness work.
|
|
30
|
+
const actions = {
|
|
31
|
+
comments: new Map(),
|
|
32
|
+
summaries: new Map(),
|
|
33
|
+
threads: new Map(),
|
|
34
|
+
};
|
|
35
|
+
for (const comment of raw.comments.nodes)
|
|
36
|
+
if (!comment.isMinimized)
|
|
37
|
+
actions.comments.set(comment.id, summaryRuleAction(rules, "pr-comment", comment));
|
|
38
|
+
for (const review of raw.reviews.nodes)
|
|
39
|
+
if (review.state === "COMMENTED" && review.body.trim() !== "" && !review.isMinimized)
|
|
40
|
+
actions.summaries.set(review.id, summaryRuleAction(rules, "review-summary", review));
|
|
41
|
+
const threadView = raw.reviewThreads.nodes.map((thread) => {
|
|
42
|
+
const truncated = thread.comments.pageInfo.hasPreviousPage;
|
|
43
|
+
if (truncated)
|
|
44
|
+
incomplete = true;
|
|
45
|
+
const root = thread.comments.nodes[0];
|
|
46
|
+
const transcript = threadTranscriptBodies(thread.comments.nodes.map((c) => c.body));
|
|
47
|
+
// The window's last comment is the thread's last only when nothing lies beyond it.
|
|
48
|
+
const ended = thread.isResolved === true &&
|
|
49
|
+
!truncated &&
|
|
50
|
+
threadEndedByShepherd({ body: root?.body ?? "", comments: thread.comments.nodes });
|
|
51
|
+
if (!ended)
|
|
52
|
+
actions.threads.set(thread.id, summaryRuleAction(rules, "review-thread", root, thread.id, thread.path ?? root?.path ?? null, root?.body ?? ""));
|
|
53
|
+
return { thread, truncated, root, transcript, ended };
|
|
54
|
+
});
|
|
55
|
+
const handled = autoResolveKey && config.actions["auto-minimize-suppressed"]
|
|
56
|
+
? await applySummaryRuleAutoResolve({ raw, actions, seen, key: autoResolveKey })
|
|
57
|
+
: { confirmed: new Set(), unconfirmed: new Set() };
|
|
58
|
+
const { confirmed, unconfirmed } = handled;
|
|
59
|
+
actionable += countCommentWork(raw.comments.nodes.filter((comment) => {
|
|
60
|
+
const action = actions.comments.get(comment.id);
|
|
61
|
+
return action !== undefined && !ruleHides(action, "pr-comment", confirmed.has(comment.id));
|
|
62
|
+
}), unconfirmed, seen, config.iterate["minimize-comments"], bots);
|
|
26
63
|
const latestStateByAuthor = new Map((raw.latestReviews?.nodes ?? []).flatMap((review) => review.author ? [[review.author.login.toLowerCase(), review.state]] : []));
|
|
27
64
|
for (const review of raw.reviews.nodes) {
|
|
28
65
|
if (review.state === "COMMENTED") {
|
|
29
|
-
|
|
66
|
+
const action = actions.summaries.get(review.id);
|
|
67
|
+
if (!action || ruleHides(action, "review-summary", confirmed.has(review.id)))
|
|
68
|
+
continue;
|
|
69
|
+
if (unconfirmed.has(review.id) || classifyItem(review.id, review.body, seen) !== "unchanged")
|
|
70
|
+
actionable += 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// An approval is surfaced once, as work, when iterate.minimize-approvals is on.
|
|
74
|
+
if (review.state === "APPROVED") {
|
|
75
|
+
if (config.iterate["minimize-approvals"] &&
|
|
30
76
|
!review.isMinimized &&
|
|
31
|
-
!isSuppressed(rules, "review-summary", review) &&
|
|
32
77
|
classifyItem(review.id, review.body, seen) !== "unchanged")
|
|
33
78
|
actionable += 1;
|
|
34
79
|
continue;
|
|
@@ -38,63 +83,46 @@ export async function summarizePollSummaryReview(raw, seen, viewerCanAdminister)
|
|
|
38
83
|
const login = review.author?.login.toLowerCase();
|
|
39
84
|
if (login && latestStateByAuthor.get(login) === "APPROVED")
|
|
40
85
|
continue;
|
|
41
|
-
if (
|
|
86
|
+
if (ruleHides(summaryRuleAction(rules, "changes-requested", review), "changes-requested", false))
|
|
42
87
|
continue;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
88
|
+
const author = authorOf(review.author);
|
|
89
|
+
const isBot = !isHumanAuthor(author) || isConfiguredBotAuthor(author, bots);
|
|
90
|
+
// The full check counts a human changes-requested review whether or not it was seen.
|
|
91
|
+
if (!isBot ||
|
|
92
|
+
(canGenerateGithubMutation(viewerCanAdminister, "dismiss") &&
|
|
93
|
+
!mutationWasDenied(review.id, review.body, seen)) ||
|
|
47
94
|
classifyItem(review.id, review.body, seen) !== "unchanged")
|
|
48
95
|
actionable += 1;
|
|
49
96
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (truncated)
|
|
55
|
-
incomplete = true;
|
|
56
|
-
const root = thread.comments.nodes[0];
|
|
57
|
-
const transcript = thread.comments.nodes
|
|
58
|
-
.map((comment) => comment.body)
|
|
59
|
-
.join(THREAD_COMMENT_SEPARATOR);
|
|
60
|
-
const completeResolved = thread.isResolved === true && !truncated;
|
|
61
|
-
if (completeResolved &&
|
|
62
|
-
threadEndedByShepherd({ body: root?.body ?? "", comments: thread.comments.nodes }))
|
|
97
|
+
const repeating = repeatingThreadIds(threadView, seen, bots, config.iterate["resolve-other-human-threads"]);
|
|
98
|
+
for (const { thread, truncated, transcript, ended } of threadView) {
|
|
99
|
+
const action = actions.threads.get(thread.id);
|
|
100
|
+
if (ended || !action)
|
|
63
101
|
continue;
|
|
64
|
-
|
|
102
|
+
// Rules read the root, so a resolved or outdated thread satisfies its auto-resolve match.
|
|
103
|
+
const satisfied = confirmed.has(thread.id) || thread.isResolved === true || thread.isOutdated === true;
|
|
104
|
+
if (ruleHides(action, "review-thread", satisfied))
|
|
65
105
|
continue;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
106
|
+
if (unconfirmed.has(thread.id)) {
|
|
107
|
+
actionable += 1;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// The marker hashes the full transcript, which a window never reproduces. A long resolved
|
|
111
|
+
// thread is seen once any marker exists; any other long thread cannot prove it was seen.
|
|
112
|
+
const unseen = truncated
|
|
113
|
+
? !(thread.isResolved === true && seen.has(thread.id))
|
|
114
|
+
: classifyItem(thread.id, transcript, seen) !== "unchanged";
|
|
115
|
+
if ((thread.isResolved === false && (repeating.has(thread.id) || unseen)) ||
|
|
72
116
|
(thread.isResolved === undefined && unseen) ||
|
|
73
117
|
((thread.isResolved || thread.isOutdated) && unseen))
|
|
74
118
|
actionable += 1;
|
|
75
119
|
}
|
|
76
|
-
|
|
120
|
+
const review = {
|
|
77
121
|
...(raw.comments.totalCount > 0 && { comments: raw.comments.totalCount }),
|
|
78
122
|
...(raw.reviews.totalCount > 0 && { reviews: raw.reviews.totalCount }),
|
|
79
123
|
...(raw.reviewThreads.totalCount > 0 && { threads: raw.reviewThreads.totalCount }),
|
|
80
124
|
...(actionable > 0 && { actionable }),
|
|
81
125
|
...(incomplete && { incomplete: true }),
|
|
82
126
|
};
|
|
83
|
-
}
|
|
84
|
-
function isSuppressed(rules, kind, raw, id = raw?.id ?? "unknown", path, body = raw?.body ?? "", autoResolveSatisfied = false) {
|
|
85
|
-
const item = {
|
|
86
|
-
kind,
|
|
87
|
-
id,
|
|
88
|
-
author: raw?.author?.login ?? "unknown",
|
|
89
|
-
authorType: normalizeAuthorType(raw?.author?.__typename, raw?.author?.login),
|
|
90
|
-
...(raw?.authorAssociation !== undefined && { authorAssociation: raw.authorAssociation }),
|
|
91
|
-
body,
|
|
92
|
-
...(kind === "review-thread" ? { url: raw?.url ?? "" } : raw?.url ? { url: raw.url } : {}),
|
|
93
|
-
...(kind === "review-thread" && { path }),
|
|
94
|
-
};
|
|
95
|
-
const action = applyRules(rules, item);
|
|
96
|
-
// An auto-resolve match is hidden only once the full check minimizes or resolves it, or
|
|
97
|
-
// surfaces a denied mutation once; until then it stays actionable so that check runs.
|
|
98
|
-
return (action.suppress === true &&
|
|
99
|
-
(action.autoResolve !== true || kind === "changes-requested" || autoResolveSatisfied));
|
|
127
|
+
return { review, ...(handled.report && { ruleAutoResolve: handled.report }) };
|
|
100
128
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { PollSummaryCommandOptions, PollSummaryItem } from "../types.mts";
|
|
2
|
+
import type { RepoInfo } from "./client.mts";
|
|
3
|
+
import type { RawSummaryPr } from "./poll-summary-raw.mts";
|
|
4
|
+
/** `failures` maps a PR to the isolatable error that stopped its chunk-time hydration. */
|
|
5
|
+
export interface ExplicitChunk {
|
|
6
|
+
prs: RawSummaryPr[];
|
|
7
|
+
failures?: Map<number, unknown>;
|
|
8
|
+
}
|
|
9
|
+
/** Hydrate each PR's checks and mergeability; one PR's unreadable checks must not fail its siblings. */
|
|
10
|
+
export declare function hydrateRowsIsolated(raws: RawSummaryPr[], repo: RepoInfo): Promise<Map<number, unknown>>;
|
|
11
|
+
/**
|
|
12
|
+
* Summarize each PR, turning an isolatable failure into a row. `failures` holds
|
|
13
|
+
* PRs whose chunk-time hydration already failed. A fatal error, or no readable
|
|
14
|
+
* row at all, rejects the tick with the first such error.
|
|
15
|
+
*/
|
|
16
|
+
export declare function summarizeRowsIsolated(raws: RawSummaryPr[], failures: Map<number, unknown>, repo: RepoInfo, opts: PollSummaryCommandOptions, summarize: (raw: RawSummaryPr) => Promise<PollSummaryItem>): Promise<PollSummaryItem[]>;
|