pr-shepherd 0.54.1 → 0.55.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/README.md +3 -3
- package/bin/checks/unreported-required.d.mts +29 -0
- package/bin/checks/unreported-required.mjs +52 -0
- package/bin/classify/apply.d.mts +2 -0
- package/bin/classify/apply.mjs +31 -31
- package/bin/classify/rule-action.d.mts +8 -0
- package/bin/classify/rule-action.mjs +29 -0
- package/bin/classify/types.d.mts +1 -1
- package/bin/cli/help-command-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.d.mts +2 -2
- package/bin/cli/help-iterate-poll-pages.mjs +6 -5
- package/bin/cli/help-top-page.d.mts +1 -1
- package/bin/cli/help-top-page.mjs +2 -2
- package/bin/cli/help.d.mts +3 -3
- package/bin/cli/iterate-formatter.mjs +29 -9
- package/bin/cli/iterate-instructions.mjs +6 -0
- package/bin/cli/iterate-lean.mjs +15 -0
- package/bin/cli/poll-summary-emitter.mjs +4 -1
- package/bin/cli/poll-summary-formatter.mjs +10 -3
- package/bin/cli/stack-overview.d.mts +48 -0
- package/bin/cli/stack-overview.mjs +138 -0
- package/bin/commands/check-fingerprint.mjs +2 -1
- package/bin/commands/check-status.d.mts +1 -1
- package/bin/commands/check-status.mjs +8 -3
- package/bin/commands/check-unreported.d.mts +26 -0
- package/bin/commands/check-unreported.mjs +73 -0
- package/bin/commands/check.mjs +44 -38
- package/bin/commands/iterate/base.mjs +9 -0
- package/bin/commands/iterate/escalate.mjs +4 -0
- package/bin/commands/iterate/index.mjs +69 -3
- package/bin/commands/iterate/merge-state.mjs +12 -12
- package/bin/commands/iterate/merge.d.mts +12 -2
- package/bin/commands/iterate/merge.mjs +32 -2
- package/bin/commands/iterate/unreported-required.d.mts +18 -0
- package/bin/commands/iterate/unreported-required.mjs +122 -0
- package/bin/commands/poll-summary.mjs +1 -0
- package/bin/commands/poll.mjs +9 -2
- package/bin/commands/ready-mergeability.d.mts +1 -1
- package/bin/commands/ready-mergeability.mjs +2 -2
- package/bin/commands/rule-auto-resolve-format.d.mts +22 -0
- package/bin/commands/rule-auto-resolve-format.mjs +151 -0
- package/bin/commands/rule-auto-resolve.d.mts +21 -0
- package/bin/commands/rule-auto-resolve.mjs +113 -0
- package/bin/commands/stack-drain.d.mts +0 -5
- package/bin/commands/stack-drain.mjs +26 -16
- package/bin/commands/stack-merge-flag.d.mts +7 -0
- package/bin/commands/stack-merge-flag.mjs +6 -0
- package/bin/config/load.d.mts +3 -0
- package/bin/config/load.mjs +9 -2
- package/bin/config/merge-method.d.mts +27 -0
- package/bin/config/merge-method.mjs +56 -0
- package/bin/exit-codes.d.mts +1 -1
- package/bin/exit-codes.mjs +2 -1
- package/bin/github/batch-parse-suites.d.mts +3 -0
- package/bin/github/batch-parse-suites.mjs +15 -0
- package/bin/github/batch-parsers.d.mts +1 -1
- package/bin/github/batch-parsers.mjs +7 -0
- package/bin/github/batch-raw-types.d.mts +5 -0
- package/bin/github/batch.d.mts +3 -0
- package/bin/github/batch.mjs +9 -1
- package/bin/github/errors.d.mts +6 -0
- package/bin/github/errors.mjs +23 -5
- package/bin/github/fingerprint-fields.d.mts +1 -0
- package/bin/github/fingerprint-fields.mjs +1 -1
- package/bin/github/gql/batch-pr-page.gql +2 -0
- package/bin/github/gql/batch-pr.gql +5 -0
- package/bin/github/gql/commit-check-suites.gql +1 -0
- package/bin/github/gql/poll-stack-summary.gql +6 -0
- package/bin/github/gql/poll-summary-annotation-probe.gql +32 -0
- package/bin/github/gql/poll-summary-check-contexts.gql +2 -3
- package/bin/github/gql/poll-summary-fragment.gql +16 -0
- package/bin/github/gql/pr-merge-policy.gql +1 -44
- package/bin/github/gql/ref-rules-query.gql +18 -0
- package/bin/github/gql/ref-rules.gql +46 -0
- package/bin/github/graphql-internal-retry.d.mts +1 -1
- package/bin/github/graphql-internal-retry.mjs +4 -3
- package/bin/github/merge-queue-checks.mjs +7 -3
- package/bin/github/merge-target-rules.d.mts +20 -0
- package/bin/github/merge-target-rules.mjs +54 -0
- package/bin/github/poll-summary-annotation-probe.d.mts +19 -0
- package/bin/github/poll-summary-annotation-probe.mjs +88 -0
- package/bin/github/poll-summary-fingerprint.mjs +1 -1
- package/bin/github/poll-summary-projector.d.mts +1 -1
- package/bin/github/poll-summary-projector.mjs +19 -12
- package/bin/github/poll-summary-queue-removal.mjs +7 -3
- package/bin/github/poll-summary-raw.d.mts +16 -0
- package/bin/github/poll-summary-readiness.mjs +1 -0
- package/bin/github/poll-summary-route.mjs +5 -0
- package/bin/github/poll-summary-unreported.d.mts +6 -0
- package/bin/github/poll-summary-unreported.mjs +51 -0
- package/bin/github/poll-summary.d.mts +1 -0
- package/bin/github/poll-summary.mjs +28 -3
- package/bin/github/queries.d.mts +7 -0
- package/bin/github/queries.mjs +9 -2
- package/bin/github/queue-removal-freshness.d.mts +20 -5
- package/bin/github/queue-removal-freshness.mjs +37 -10
- package/bin/github/stack-read.d.mts +4 -0
- package/bin/github/stack-read.mjs +13 -1
- package/bin/mcp/server.mjs +6 -1
- package/bin/state/ci-retrigger.d.mts +25 -0
- package/bin/state/ci-retrigger.mjs +48 -0
- package/bin/types/escalate.d.mts +1 -1
- package/bin/types/github.d.mts +11 -0
- package/bin/types/iterate.d.mts +14 -3
- package/bin/types/poll-summary.d.mts +13 -0
- package/bin/types/report.d.mts +32 -1
- package/package.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
- package/plugins/pr-shepherd/skills/pr-shepherd/SKILL.md +2 -2
- package/plugins/pr-shepherd/skills/reduce-pr-noise/references/classifiers.md +1 -1
- package/src/classify/types.mts +1 -1
|
@@ -18,6 +18,7 @@ import { markReadyIfAuthorized } from "./mark-ready.mjs";
|
|
|
18
18
|
import { withIterateApiUsage } from "./run.mjs";
|
|
19
19
|
import { fetchRawSummaryPr } from "../../github/poll-summary.mjs";
|
|
20
20
|
import { summarizePollSummaryPr } from "../../github/poll-summary-projector.mjs";
|
|
21
|
+
import { annotationProbeUnavailable } from "../../github/poll-summary-annotation-probe.mjs";
|
|
21
22
|
import { fingerprintRawSummaryPr } from "../../github/poll-summary-fingerprint.mjs";
|
|
22
23
|
import { currentQueueRemovalEvent } from "../../github/poll-summary-queue-removal.mjs";
|
|
23
24
|
import { isCurrentSummaryReady } from "../../github/poll-summary-readiness.mjs";
|
|
@@ -25,6 +26,7 @@ import { clearReadyReceipt, isReadyReceiptCurrent, readReadyReceipt, writeReadyR
|
|
|
25
26
|
import { stackDraftHold } from "./parent-first.mjs";
|
|
26
27
|
import { findStaleNativeStackAncestry } from "./stale-ancestry.mjs";
|
|
27
28
|
import { annotateBlockedWait, resolveCheckBlockerGate } from "./check-blocker-gate.mjs";
|
|
29
|
+
import { buildUnreportedFixResult, planUnreportedRequired } from "./unreported-required.mjs";
|
|
28
30
|
export function runIterate(opts) {
|
|
29
31
|
return withIterateApiUsage(opts, () => runIterateCore(opts));
|
|
30
32
|
}
|
|
@@ -122,6 +124,16 @@ async function runIterateCore(opts) {
|
|
|
122
124
|
// readiness evidence, so a rerun (e.g. with --merge) does not wait again.
|
|
123
125
|
const readyState = await updateReadyDelay(report.pr, isCleanReadyState, readyDelaySeconds, repoOwner, repoName, { headSha, alreadyElapsed: receiptCurrent });
|
|
124
126
|
const base = buildIterateBase(reportForWork, readyState);
|
|
127
|
+
const unreportedPlan = await planUnreportedRequired({
|
|
128
|
+
report,
|
|
129
|
+
base,
|
|
130
|
+
stateKey: stallKey,
|
|
131
|
+
headSha,
|
|
132
|
+
otherAutonomousWork: hasActionableWork || staleAncestry !== null,
|
|
133
|
+
});
|
|
134
|
+
if (unreportedPlan.escalate)
|
|
135
|
+
return unreportedPlan.escalate;
|
|
136
|
+
const unreportedRepair = unreportedPlan.repairInstructions ?? [];
|
|
125
137
|
// Checks (including merge-queue synthetic-commit checks) and hard conflicts are signals
|
|
126
138
|
// GitHub itself is already acting on — the queue will eject the PR for these regardless of
|
|
127
139
|
// what Shepherd does, so they always surface immediately. Only review threads/comments/
|
|
@@ -148,9 +160,36 @@ async function runIterateCore(opts) {
|
|
|
148
160
|
surfacedApprovals,
|
|
149
161
|
botUsernames,
|
|
150
162
|
releasedCheckNames: blockerGate?.releasedNames,
|
|
163
|
+
...((unreportedRepair.length > 0 || staleAncestry !== null) && {
|
|
164
|
+
repairInstructions: [...unreportedRepair, ...(staleAncestry?.instructions ?? [])],
|
|
165
|
+
}),
|
|
166
|
+
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (unreportedRepair.length > 0 && staleAncestry) {
|
|
170
|
+
return handleFixCode({
|
|
171
|
+
base,
|
|
172
|
+
report: reportForWork,
|
|
173
|
+
opts: { ...opts, prNumber, neverCancelRuns },
|
|
174
|
+
headSha,
|
|
175
|
+
stallKey,
|
|
176
|
+
prNumber,
|
|
177
|
+
stallTimeoutSeconds,
|
|
178
|
+
repoOwner,
|
|
179
|
+
repoName,
|
|
180
|
+
reviewSummaryIds,
|
|
181
|
+
firstLookSummaries,
|
|
182
|
+
editedSummaries,
|
|
183
|
+
surfacedApprovals,
|
|
184
|
+
botUsernames,
|
|
185
|
+
releasedCheckNames: blockerGate?.releasedNames,
|
|
186
|
+
repairInstructions: [...unreportedRepair, ...staleAncestry.instructions],
|
|
151
187
|
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
152
188
|
});
|
|
153
189
|
}
|
|
190
|
+
if (unreportedRepair.length > 0) {
|
|
191
|
+
return annotateBlockedWait(await applyStallGuard(stallKey, stallTimeoutSeconds, headSha, base, prNumber, buildUnreportedFixResult(base, report, unreportedRepair), reportForWork, reviewSummaryIds), blockerGate);
|
|
192
|
+
}
|
|
154
193
|
const mergeStateResult = await handleActiveMergeState({
|
|
155
194
|
enabled: opts.merge,
|
|
156
195
|
active: activeMerge,
|
|
@@ -182,7 +221,7 @@ async function runIterateCore(opts) {
|
|
|
182
221
|
surfacedApprovals,
|
|
183
222
|
botUsernames,
|
|
184
223
|
releasedCheckNames: blockerGate?.releasedNames,
|
|
185
|
-
repairInstructions: staleAncestry.instructions,
|
|
224
|
+
repairInstructions: [...unreportedRepair, ...staleAncestry.instructions],
|
|
186
225
|
ruleAutoResolveThreadIds: report.threads.ruleAutoResolveIds,
|
|
187
226
|
});
|
|
188
227
|
}
|
|
@@ -220,6 +259,17 @@ async function runIterateCore(opts) {
|
|
|
220
259
|
log: `CANCEL: PR #${base.pr} ${cancelNote} — ready-delay elapsed, stopping`,
|
|
221
260
|
};
|
|
222
261
|
}
|
|
262
|
+
if (isCleanReadyState &&
|
|
263
|
+
readyState.isReady &&
|
|
264
|
+
!readyState.shouldCancel &&
|
|
265
|
+
readyState.remainingSeconds > 0) {
|
|
266
|
+
await clearStallState(stallKey);
|
|
267
|
+
return {
|
|
268
|
+
...base,
|
|
269
|
+
action: "ready",
|
|
270
|
+
log: `READY: PR #${base.pr} is ready — ${readyState.remainingSeconds}s of ready-delay remaining — ${buildWaitLog(base).slice("WAIT: ".length)}`,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
223
273
|
const hold = stackDraftHold(report, autoMarkReady);
|
|
224
274
|
const wait = {
|
|
225
275
|
...base,
|
|
@@ -238,14 +288,19 @@ async function recordReadyReceipt(key, report) {
|
|
|
238
288
|
return false;
|
|
239
289
|
try {
|
|
240
290
|
const raw = await fetchRawSummaryPr(report.pr, { owner: key.owner, name: key.repo });
|
|
241
|
-
|
|
242
|
-
if (fingerprint === null ||
|
|
291
|
+
if (fingerprintRawSummaryPr(raw) === null ||
|
|
243
292
|
raw.state !== "OPEN" ||
|
|
244
293
|
raw.isDraft ||
|
|
245
294
|
raw.headRefOid !== report.headSha ||
|
|
246
295
|
raw.baseRefOid !== report.baseRefOid)
|
|
247
296
|
return false;
|
|
297
|
+
// Summarize hydrates annotation totals onto the snapshot before the stored hash.
|
|
248
298
|
const summary = await summarizePollSummaryPr(raw, { owner: key.owner, name: key.repo }, { stackPrNumber: report.pr });
|
|
299
|
+
if (annotationProbeUnavailable(raw))
|
|
300
|
+
return false;
|
|
301
|
+
const fingerprint = fingerprintRawSummaryPr(raw);
|
|
302
|
+
if (fingerprint === null)
|
|
303
|
+
return false;
|
|
249
304
|
if (!isCurrentSummaryReady(raw, summary.checks ?? {}, summary.review ?? {}))
|
|
250
305
|
return false;
|
|
251
306
|
const removalEvent = currentQueueRemovalEvent(raw);
|
|
@@ -290,6 +345,17 @@ async function revalidateReadyReceipt(key, report, hasReadinessWork) {
|
|
|
290
345
|
}
|
|
291
346
|
try {
|
|
292
347
|
const raw = await fetchRawSummaryPr(report.pr, { owner: key.owner, name: key.repo });
|
|
348
|
+
const summary = await summarizePollSummaryPr(raw, { owner: key.owner, name: key.repo }, { stackPrNumber: report.pr });
|
|
349
|
+
if (annotationProbeUnavailable(raw)) {
|
|
350
|
+
const sameHead = raw.headRefOid === report.headSha;
|
|
351
|
+
const sameBase = retainQueuedReceipt || raw.baseRefOid === report.baseRefOid;
|
|
352
|
+
const stillReady = isCurrentSummaryReady(raw, summary.checks ?? {}, summary.review ?? {});
|
|
353
|
+
if (!sameHead || !sameBase || !stillReady) {
|
|
354
|
+
await clearReadyReceipt(key);
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
293
359
|
// Queue predecessors can advance the target branch without changing this
|
|
294
360
|
// PR's source. Compare against the receipt's base only while both fresh
|
|
295
361
|
// views still place the PR in the queue.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clearStallState } from "../../state/iterate-stall.mjs";
|
|
2
2
|
import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
|
|
3
|
-
import { buildMergeCommandPlan } from "./merge.mjs";
|
|
3
|
+
import { buildMergeCommandPlan, unavailableMergeResult } from "./merge.mjs";
|
|
4
4
|
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
5
5
|
import { buildPrShepherdCommand } from "../../cli/runner.mjs";
|
|
6
6
|
import { inlineCode } from "../../util/markdown.mjs";
|
|
@@ -49,17 +49,17 @@ export function buildReadyMergeOutcome(enabled, readyElapsed, base, report) {
|
|
|
49
49
|
return buildStackedRouteResult(base, report, stack);
|
|
50
50
|
const queue = Boolean(report.mergeStatus.mergeRequirements?.mergeQueue?.required ||
|
|
51
51
|
report.mergeStatus.mergeRequirements?.mergeQueue?.enabled);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
};
|
|
52
|
+
const plan = buildMergeCommandPlan({
|
|
53
|
+
pr: report.pr,
|
|
54
|
+
repo: report.repo,
|
|
55
|
+
nodeId: report.nodeId,
|
|
56
|
+
headSha: report.headSha ?? "unknown",
|
|
57
|
+
queue,
|
|
58
|
+
...(report.allowedMergeMethods && { allowedMergeMethods: report.allowedMergeMethods }),
|
|
59
|
+
});
|
|
60
|
+
if ("unavailable" in plan)
|
|
61
|
+
return unavailableMergeResult(base, report, plan.unavailable);
|
|
62
|
+
return { ...base, action: "merge", merge: plan };
|
|
63
63
|
}
|
|
64
64
|
/** Raw counts of non-CI actionable work held back for one queued-PR wait tick. Omitted (all zero) when empty. */
|
|
65
65
|
function buildDeferredWork(input) {
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MergeMethod } from "../../config/merge-method.mts";
|
|
2
|
+
import type { IterateResult, IterateResultBase, MergeCommandPlan } from "../../types.mts";
|
|
2
3
|
interface MergePlanInput {
|
|
3
4
|
pr: number;
|
|
4
5
|
repo: string;
|
|
5
6
|
nodeId: string;
|
|
6
7
|
headSha: string;
|
|
7
8
|
queue: boolean;
|
|
9
|
+
/** Omitted when the batch did not select repository merge settings. */
|
|
10
|
+
allowedMergeMethods?: readonly MergeMethod[];
|
|
8
11
|
}
|
|
9
|
-
export
|
|
12
|
+
export interface MergeMethodUnavailable {
|
|
13
|
+
unavailable: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildMergeCommandPlan(input: MergePlanInput): MergeCommandPlan | MergeMethodUnavailable;
|
|
10
16
|
export declare function renderMergeCommand(command: {
|
|
11
17
|
argv: string[];
|
|
12
18
|
}): string;
|
|
19
|
+
export declare function unavailableMergeResult(base: IterateResultBase, report: {
|
|
20
|
+
pr: number;
|
|
21
|
+
repo: string;
|
|
22
|
+
}, unavailable: string): IterateResult;
|
|
13
23
|
export {};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { loadConfig } from "../../config/load.mjs";
|
|
2
2
|
import { findMergeStrategies } from "../../config/merge-command-args.mjs";
|
|
3
|
+
import { chooseMergeMethod, configuredMergeMethod, } from "../../config/merge-method.mjs";
|
|
4
|
+
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
3
5
|
import { renderShellCommand } from "../../cli/runner.mjs";
|
|
6
|
+
import { buildEscalateHumanMessage } from "./escalate.mjs";
|
|
4
7
|
const ENQUEUE_MUTATION = "mutation EnqueuePullRequest($pullRequestId: ID!, $expectedHeadOid: GitObjectID!) { enqueuePullRequest(input: { pullRequestId: $pullRequestId, expectedHeadOid: $expectedHeadOid }) { mergeQueueEntry { id } } }";
|
|
5
8
|
export function buildMergeCommandPlan(input) {
|
|
6
9
|
const base = [
|
|
@@ -32,9 +35,17 @@ export function buildMergeCommandPlan(input) {
|
|
|
32
35
|
},
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
|
-
const
|
|
38
|
+
const mergeConfig = loadConfig().merge;
|
|
39
|
+
const configuredArgs = mergeConfig?.commandArgs ?? [];
|
|
40
|
+
const decision = chooseMergeMethod({
|
|
41
|
+
allowed: input.allowedMergeMethods,
|
|
42
|
+
configured: configuredMergeMethod(mergeConfig ?? {}),
|
|
43
|
+
fallback: "merge",
|
|
44
|
+
});
|
|
45
|
+
if ("unavailable" in decision)
|
|
46
|
+
return decision;
|
|
36
47
|
const hasStrategy = findMergeStrategies(configuredArgs).length > 0;
|
|
37
|
-
const commandArgs = hasStrategy ? configuredArgs : [...configuredArgs,
|
|
48
|
+
const commandArgs = hasStrategy ? configuredArgs : [...configuredArgs, `--${decision.method}`];
|
|
38
49
|
return {
|
|
39
50
|
mode: "auto",
|
|
40
51
|
command: { argv: [...base, "--auto", ...commandArgs] },
|
|
@@ -44,3 +55,22 @@ export function buildMergeCommandPlan(input) {
|
|
|
44
55
|
export function renderMergeCommand(command) {
|
|
45
56
|
return renderShellCommand(command.argv);
|
|
46
57
|
}
|
|
58
|
+
export function unavailableMergeResult(base, report, unavailable) {
|
|
59
|
+
const escalateBase = {
|
|
60
|
+
triggers: ["merge-method-unavailable"],
|
|
61
|
+
unresolvedThreads: [],
|
|
62
|
+
ambiguousComments: [],
|
|
63
|
+
changesRequestedReviews: [],
|
|
64
|
+
suggestion: unavailable,
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
action: "escalate",
|
|
69
|
+
escalate: {
|
|
70
|
+
...escalateBase,
|
|
71
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, formatPrUrl(report.repo, report.pr), {
|
|
72
|
+
merge: true,
|
|
73
|
+
}),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IterateResult, IterateResultBase, ShepherdReport } from "../../types.mts";
|
|
2
|
+
interface UnreportedPlan {
|
|
3
|
+
escalate?: IterateResult;
|
|
4
|
+
repairInstructions?: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function planUnreportedRequired(input: {
|
|
7
|
+
report: ShepherdReport;
|
|
8
|
+
base: IterateResultBase;
|
|
9
|
+
stateKey: {
|
|
10
|
+
owner: string;
|
|
11
|
+
repo: string;
|
|
12
|
+
pr: number;
|
|
13
|
+
};
|
|
14
|
+
headSha: string;
|
|
15
|
+
otherAutonomousWork: boolean;
|
|
16
|
+
}): Promise<UnreportedPlan>;
|
|
17
|
+
export declare function buildUnreportedFixResult(base: IterateResultBase, report: ShepherdReport, instructions: string[]): IterateResult;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { buildPrShepherdCommand } from "../../cli/runner.mjs";
|
|
2
|
+
import { formatPrUrl } from "../../pr-reference.mjs";
|
|
3
|
+
import { readCiRetrigger, sameCiRetrigger, writeCiRetrigger } from "../../state/ci-retrigger.mjs";
|
|
4
|
+
import { buildFixCompletionInstruction } from "./check-instructions.mjs";
|
|
5
|
+
import { buildEscalateHumanMessage, buildEscalateSuggestion } from "./escalate.mjs";
|
|
6
|
+
import { buildNativeStackRebaseInstruction } from "./native-stack-rebase.mjs";
|
|
7
|
+
/** Rebase when the trunk or the PR base is behind; otherwise close and reopen once. */
|
|
8
|
+
function decideUnreportedRequired(input) {
|
|
9
|
+
if (input.behind)
|
|
10
|
+
return "rebase";
|
|
11
|
+
if (input.alreadyRetriggered)
|
|
12
|
+
return input.otherAutonomousWork ? "none" : "escalate";
|
|
13
|
+
return "reopen";
|
|
14
|
+
}
|
|
15
|
+
function buildUnreportedRequiredInstruction(input) {
|
|
16
|
+
const listed = input.names.map((name) => `\`${name}\``).join(", ");
|
|
17
|
+
const observed = input.trunkBehindBy !== undefined && input.trunkBehindBy > 0
|
|
18
|
+
? `The stack trunk compare is behind by ${input.trunkBehindBy}.`
|
|
19
|
+
: input.behind
|
|
20
|
+
? "This PR's derived merge status is BEHIND."
|
|
21
|
+
: "The stack trunk compare is not behind and this PR's derived merge status is not BEHIND.";
|
|
22
|
+
const update = input.stackRebase
|
|
23
|
+
? `${input.stackRebase} Then push the rewritten stack with \`gh stack push\`.`
|
|
24
|
+
: `Rebase or otherwise update the PR branch from \`${input.baseBranch}\` according to repository conventions, then push.`;
|
|
25
|
+
return [
|
|
26
|
+
`Required status checks have no check run and no status context on this head, and no Actions workflow is running: ${listed}.`,
|
|
27
|
+
observed,
|
|
28
|
+
`If the stack trunk is behind or this PR is behind its base, update the branch so a pull_request synchronize event runs: ${update}`,
|
|
29
|
+
`Otherwise retrigger workflows once for this head with \`gh pr close ${input.pr} -R ${input.repo}\` then \`gh pr reopen ${input.pr} -R ${input.repo}\`.`,
|
|
30
|
+
"Do not close the PR again if those checks are still missing on the next poll.",
|
|
31
|
+
].join(" ");
|
|
32
|
+
}
|
|
33
|
+
export async function planUnreportedRequired(input) {
|
|
34
|
+
const names = input.report.unreportedRequiredChecks ?? [];
|
|
35
|
+
const actionsRunning = input.report.actionsWorkflowInProgress === true || input.report.checks.inProgress.length > 0;
|
|
36
|
+
const failing = input.report.checks.failing.length > 0;
|
|
37
|
+
const conflicts = input.report.mergeStatus.status === "CONFLICTS";
|
|
38
|
+
const inQueue = input.report.mergeQueue?.inQueue === true;
|
|
39
|
+
if (names.length === 0 || actionsRunning || failing || conflicts || inQueue)
|
|
40
|
+
return {};
|
|
41
|
+
const behind = (input.report.trunkBehindBy ?? 0) > 0 || input.report.mergeStatus.status === "BEHIND";
|
|
42
|
+
const decision = decideUnreportedRequired({
|
|
43
|
+
behind,
|
|
44
|
+
alreadyRetriggered: sameCiRetrigger(await readCiRetrigger(input.stateKey), input.headSha, names),
|
|
45
|
+
otherAutonomousWork: input.otherAutonomousWork,
|
|
46
|
+
});
|
|
47
|
+
if (decision === "none")
|
|
48
|
+
return {};
|
|
49
|
+
if (decision === "escalate")
|
|
50
|
+
return { escalate: escalateUnreported(input.base, input.report, names) };
|
|
51
|
+
const rebase = stackRebase(input.report);
|
|
52
|
+
const instruction = buildUnreportedRequiredInstruction({
|
|
53
|
+
names,
|
|
54
|
+
repo: input.report.repo,
|
|
55
|
+
pr: input.report.pr,
|
|
56
|
+
baseBranch: input.report.baseBranch,
|
|
57
|
+
behind,
|
|
58
|
+
...(input.report.trunkBehindBy !== undefined && { trunkBehindBy: input.report.trunkBehindBy }),
|
|
59
|
+
...(rebase && { stackRebase: rebase }),
|
|
60
|
+
});
|
|
61
|
+
if (decision === "reopen") {
|
|
62
|
+
await writeCiRetrigger(input.stateKey, { headSha: input.headSha, contexts: names });
|
|
63
|
+
}
|
|
64
|
+
return { repairInstructions: [instruction] };
|
|
65
|
+
}
|
|
66
|
+
export function buildUnreportedFixResult(base, report, instructions) {
|
|
67
|
+
const prUrl = formatPrUrl(report.repo, report.pr);
|
|
68
|
+
return {
|
|
69
|
+
...base,
|
|
70
|
+
action: "fix_code",
|
|
71
|
+
fix: {
|
|
72
|
+
threads: [],
|
|
73
|
+
resolutionOnlyThreads: [],
|
|
74
|
+
actionableComments: [],
|
|
75
|
+
reviewSummaryIds: [],
|
|
76
|
+
firstLookSummaries: [],
|
|
77
|
+
editedSummaries: [],
|
|
78
|
+
surfacedApprovals: [],
|
|
79
|
+
checks: [],
|
|
80
|
+
changesRequestedReviews: [],
|
|
81
|
+
resolveCommand: {
|
|
82
|
+
argv: buildPrShepherdCommand(["apply", "review", prUrl]).argv,
|
|
83
|
+
requiresHeadSha: false,
|
|
84
|
+
requiresDismissMessage: false,
|
|
85
|
+
hasMutations: false,
|
|
86
|
+
},
|
|
87
|
+
instructions: [...instructions, buildFixCompletionInstruction([])],
|
|
88
|
+
inProgressRunIds: [],
|
|
89
|
+
protectedRuns: [],
|
|
90
|
+
firstLookThreads: [],
|
|
91
|
+
firstLookComments: [],
|
|
92
|
+
},
|
|
93
|
+
cancelled: [],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function stackRebase(report) {
|
|
97
|
+
const stack = report.mergeStatus.mergeRequirements?.stack;
|
|
98
|
+
if (!stack)
|
|
99
|
+
return undefined;
|
|
100
|
+
const start = report.stackBottomPr !== undefined
|
|
101
|
+
? { bottomPr: report.stackBottomPr }
|
|
102
|
+
: { trunk: stack.baseRefName };
|
|
103
|
+
return buildNativeStackRebaseInstruction(report.repo, stack.number, start);
|
|
104
|
+
}
|
|
105
|
+
function escalateUnreported(base, report, names) {
|
|
106
|
+
const detail = names.map((name) => `\`${name}\``).join(", ");
|
|
107
|
+
const escalateBase = {
|
|
108
|
+
triggers: ["required-checks-unreported"],
|
|
109
|
+
unresolvedThreads: [],
|
|
110
|
+
ambiguousComments: [],
|
|
111
|
+
changesRequestedReviews: [],
|
|
112
|
+
suggestion: buildEscalateSuggestion(["required-checks-unreported"], detail),
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
...base,
|
|
116
|
+
action: "escalate",
|
|
117
|
+
escalate: {
|
|
118
|
+
...escalateBase,
|
|
119
|
+
humanMessage: buildEscalateHumanMessage(escalateBase, report.pr),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -31,6 +31,7 @@ async function runPollSummaryCore(opts) {
|
|
|
31
31
|
reason: allTerminal ? "all_terminal" : actionable ? "actionable" : "waiting",
|
|
32
32
|
prs: fetched.prs,
|
|
33
33
|
...(fetched.stackAncestry?.length && { stackAncestry: fetched.stackAncestry }),
|
|
34
|
+
...(fetched.allowedMergeMethods && { allowedMergeMethods: fetched.allowedMergeMethods }),
|
|
34
35
|
}, opts.merge === true);
|
|
35
36
|
return applyStackStallGuard(planned, repo, opts.stallTimeoutSeconds ?? loadConfig().iterate.stallTimeoutMinutes * 60);
|
|
36
37
|
}
|
package/bin/commands/poll.mjs
CHANGED
|
@@ -6,6 +6,10 @@ import { quotaPollIntervalMs } from "./poll-quota.mjs";
|
|
|
6
6
|
import { onePrCancelFromPulls, onePrRateLimitTargets } from "./poll-rate-limit-cancel.mjs";
|
|
7
7
|
import { createUntilTerminalRateLimitRetry } from "./poll-rate-limit-wait.mjs";
|
|
8
8
|
import { writeDebounceProgress, writeWaitProgress } from "./poll-progress.mjs";
|
|
9
|
+
function noteRuleAutoResolve(result) {
|
|
10
|
+
if (result.ruleAutoResolve?.summary)
|
|
11
|
+
process.stderr.write(`${result.ruleAutoResolve.summary}\n`);
|
|
12
|
+
}
|
|
9
13
|
const DEFAULT_POLL_DEBOUNCE_SECONDS = 60;
|
|
10
14
|
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
11
15
|
const TIMER_DRIFT_TOLERANCE_MS = 500;
|
|
@@ -38,8 +42,7 @@ async function runPollCore(opts) {
|
|
|
38
42
|
const remainingBefore = untilTerminal
|
|
39
43
|
? Number.POSITIVE_INFINITY
|
|
40
44
|
: timeoutMs - (Date.now() - start);
|
|
41
|
-
// Cache only internal continuation ticks
|
|
42
|
-
// and any tick we return to the caller must fetch BatchPr.
|
|
45
|
+
// Cache only internal continuation ticks; returned ticks must fetch BatchPr.
|
|
43
46
|
const allowCache = debounceUntil === null && remainingBefore + TIMER_DRIFT_TOLERANCE_MS >= intervalMs;
|
|
44
47
|
const iterateTick = (fingerprintCache) => runIterate({
|
|
45
48
|
...iterateOpts,
|
|
@@ -89,6 +92,7 @@ async function runPollCore(opts) {
|
|
|
89
92
|
if (lastResult.action === "fix_code" && debounceSeconds > 0 && !pastDebounce) {
|
|
90
93
|
debounceUntil ??= Date.now() + debounceMs;
|
|
91
94
|
const remainingMs = Math.max(debounceUntil - Date.now(), 0);
|
|
95
|
+
noteRuleAutoResolve(lastResult);
|
|
92
96
|
writeDebounceProgress(tick, Date.now() - start, remainingMs);
|
|
93
97
|
await sleep(Math.min(intervalMs, remainingMs));
|
|
94
98
|
continue;
|
|
@@ -123,6 +127,7 @@ async function runPollCore(opts) {
|
|
|
123
127
|
verbose,
|
|
124
128
|
lastWaitSignature,
|
|
125
129
|
});
|
|
130
|
+
noteRuleAutoResolve(lastResult);
|
|
126
131
|
await sleep(sleepMs);
|
|
127
132
|
continue;
|
|
128
133
|
}
|
|
@@ -139,10 +144,12 @@ async function runPollCore(opts) {
|
|
|
139
144
|
break;
|
|
140
145
|
}
|
|
141
146
|
}
|
|
147
|
+
noteRuleAutoResolve(lastResult);
|
|
142
148
|
await sleep(sleepMs);
|
|
143
149
|
continue;
|
|
144
150
|
}
|
|
145
151
|
if (lastResult.action === "fix_code" && debounceSeconds > 0 && !pastDebounce) {
|
|
152
|
+
noteRuleAutoResolve(lastResult);
|
|
146
153
|
debounceUntil ??= Date.now() + debounceMs;
|
|
147
154
|
const remainingMs = debounceUntil - Date.now();
|
|
148
155
|
if (remainingMs > 0) {
|
|
@@ -10,6 +10,6 @@ export declare function refreshUnknownMergeability(prNumber: number, repo: RepoI
|
|
|
10
10
|
batchData: BatchPrData;
|
|
11
11
|
didRefresh: boolean;
|
|
12
12
|
}>;
|
|
13
|
-
export declare function refreshReadyMergeability(prNumber: number, repo: RepoInfo, batchData: BatchPrData, verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, changesRequestedCount: number): Promise<ReadyMergeabilityRefresh>;
|
|
13
|
+
export declare function refreshReadyMergeability(prNumber: number, repo: RepoInfo, batchData: BatchPrData, verdict: CiVerdict, unresolvedThreads: number, unresolvedComments: number, changesRequestedCount: number, hasUnreportedRequired?: boolean): Promise<ReadyMergeabilityRefresh>;
|
|
14
14
|
export declare function isBlockedByFilteredCheck(mergeStatus: MergeStatusResult, verdict: CiVerdict): boolean;
|
|
15
15
|
export {};
|
|
@@ -8,10 +8,10 @@ export async function refreshUnknownMergeability(prNumber, repo, batchData) {
|
|
|
8
8
|
}
|
|
9
9
|
return { batchData: await readMergeability(prNumber, repo, batchData), didRefresh: true };
|
|
10
10
|
}
|
|
11
|
-
export async function refreshReadyMergeability(prNumber, repo, batchData, verdict, unresolvedThreads, unresolvedComments, changesRequestedCount) {
|
|
11
|
+
export async function refreshReadyMergeability(prNumber, repo, batchData, verdict, unresolvedThreads, unresolvedComments, changesRequestedCount, hasUnreportedRequired = false) {
|
|
12
12
|
const refreshedBatchData = await readMergeability(prNumber, repo, batchData);
|
|
13
13
|
const mergeStatus = deriveMergeStatus(refreshedBatchData);
|
|
14
|
-
const status = computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedCount);
|
|
14
|
+
const status = computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, changesRequestedCount, hasUnreportedRequired);
|
|
15
15
|
return { batchData: refreshedBatchData, mergeStatus, status };
|
|
16
16
|
}
|
|
17
17
|
export function isBlockedByFilteredCheck(mergeStatus, verdict) {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AutoMinimizedItem, AutoResolvedThread, RuleAutoResolveReport, ShepherdReport } from "../types.mts";
|
|
2
|
+
export declare function uniqueReasons(reasons: readonly string[]): string[];
|
|
3
|
+
export declare function reasonClause(reasons: readonly string[]): string | undefined;
|
|
4
|
+
export declare function ruleAutoResolveBody(counts: {
|
|
5
|
+
threads: number;
|
|
6
|
+
comments: number;
|
|
7
|
+
reviewSummaries: number;
|
|
8
|
+
}): string | undefined;
|
|
9
|
+
export declare function formatRuleAutoResolveJournalItem(input: {
|
|
10
|
+
body: string;
|
|
11
|
+
reasons: readonly string[];
|
|
12
|
+
viewerLogin?: string;
|
|
13
|
+
urls: readonly string[];
|
|
14
|
+
}): string;
|
|
15
|
+
export declare function decorateAutoResolveError(error: string, reasons: readonly string[]): string;
|
|
16
|
+
export declare function reasonsForError(error: string, ids: readonly string[], ruleReasons: ReadonlyMap<string, readonly string[]>): string[];
|
|
17
|
+
export declare function ruleAutoResolveFromReport(report: ShepherdReport): RuleAutoResolveReport | undefined;
|
|
18
|
+
export declare function projectRuleAutoResolve(event: RuleAutoResolveReport): Record<string, unknown>;
|
|
19
|
+
export declare function insertRuleAutoResolveSection(text: string, event: RuleAutoResolveReport | undefined): string;
|
|
20
|
+
export declare function stripReplayedRuleAutoResolve(report: ShepherdReport): ShepherdReport;
|
|
21
|
+
export declare function minimizedItem(kind: AutoMinimizedItem["kind"], id: string, url: string | undefined, reasons: readonly string[]): AutoMinimizedItem;
|
|
22
|
+
export declare function resolvedThread(thread: AutoResolvedThread, reasons: readonly string[]): AutoResolvedThread;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
export function uniqueReasons(reasons) {
|
|
2
|
+
const unique = [];
|
|
3
|
+
for (const reason of reasons) {
|
|
4
|
+
const cleaned = reason.trim();
|
|
5
|
+
if (cleaned && !unique.includes(cleaned))
|
|
6
|
+
unique.push(cleaned);
|
|
7
|
+
}
|
|
8
|
+
return unique;
|
|
9
|
+
}
|
|
10
|
+
export function reasonClause(reasons) {
|
|
11
|
+
const unique = uniqueReasons(reasons);
|
|
12
|
+
if (unique.length === 0)
|
|
13
|
+
return undefined;
|
|
14
|
+
const label = unique.length === 1 ? "rule" : "rules";
|
|
15
|
+
return `(${label}: ${unique.join("; ")})`;
|
|
16
|
+
}
|
|
17
|
+
function joinedReason(reasons) {
|
|
18
|
+
const unique = uniqueReasons(reasons);
|
|
19
|
+
return unique.length > 0 ? unique.join("; ") : undefined;
|
|
20
|
+
}
|
|
21
|
+
function plural(count, singular, pluralForm = `${singular}s`) {
|
|
22
|
+
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
23
|
+
}
|
|
24
|
+
export function ruleAutoResolveBody(counts) {
|
|
25
|
+
const parts = [];
|
|
26
|
+
if (counts.threads > 0)
|
|
27
|
+
parts.push(`auto-resolved ${plural(counts.threads, "thread")}`);
|
|
28
|
+
const minimized = [];
|
|
29
|
+
if (counts.comments > 0)
|
|
30
|
+
minimized.push(plural(counts.comments, "comment"));
|
|
31
|
+
if (counts.reviewSummaries > 0) {
|
|
32
|
+
minimized.push(plural(counts.reviewSummaries, "review summary", "review summaries"));
|
|
33
|
+
}
|
|
34
|
+
if (minimized.length > 0)
|
|
35
|
+
parts.push(`minimized ${minimized.join(" and ")}`);
|
|
36
|
+
return parts.length > 0 ? parts.join(", ") : undefined;
|
|
37
|
+
}
|
|
38
|
+
function withReasonClause(body, reasons) {
|
|
39
|
+
const clause = reasonClause(reasons);
|
|
40
|
+
return clause ? `${body} ${clause}` : body;
|
|
41
|
+
}
|
|
42
|
+
export function formatRuleAutoResolveJournalItem(input) {
|
|
43
|
+
const login = input.viewerLogin?.trim();
|
|
44
|
+
const actor = login ? `as @${login}` : "(token login unavailable)";
|
|
45
|
+
const clause = reasonClause(input.reasons);
|
|
46
|
+
const links = input.urls.map((url) => url.trim()).filter(Boolean);
|
|
47
|
+
const reasonSuffix = clause ? ` ${clause}` : "";
|
|
48
|
+
const linkSuffix = links.length > 0 ? `: ${links.join(", ")}` : "";
|
|
49
|
+
return `- ${input.body} ${actor}${reasonSuffix}${linkSuffix}`;
|
|
50
|
+
}
|
|
51
|
+
export function decorateAutoResolveError(error, reasons) {
|
|
52
|
+
const clause = reasonClause(reasons);
|
|
53
|
+
return clause ? `${error} ${clause}` : error;
|
|
54
|
+
}
|
|
55
|
+
export function reasonsForError(error, ids, ruleReasons) {
|
|
56
|
+
const id = ids.find((candidate) => error.startsWith(`${candidate}:`));
|
|
57
|
+
if (id)
|
|
58
|
+
return [...(ruleReasons.get(id) ?? [])];
|
|
59
|
+
if (!error.startsWith("rate limit:"))
|
|
60
|
+
return [];
|
|
61
|
+
return uniqueReasons(ids.flatMap((candidate) => [...(ruleReasons.get(candidate) ?? [])]));
|
|
62
|
+
}
|
|
63
|
+
function splitJoined(reason) {
|
|
64
|
+
if (!reason)
|
|
65
|
+
return [];
|
|
66
|
+
return reason.split("; ").filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
export function ruleAutoResolveFromReport(report) {
|
|
69
|
+
const threads = report.threads.autoResolved;
|
|
70
|
+
const minimized = report.comments.autoMinimized ?? [];
|
|
71
|
+
const errors = report.threads.autoResolveErrors;
|
|
72
|
+
if (threads.length === 0 && minimized.length === 0 && errors.length === 0)
|
|
73
|
+
return undefined;
|
|
74
|
+
const comments = minimized.filter((item) => item.kind === "pr-comment").length;
|
|
75
|
+
const reviewSummaries = minimized.filter((item) => item.kind === "review-summary").length;
|
|
76
|
+
const success = threads.length + minimized.length > 0;
|
|
77
|
+
const reasons = success
|
|
78
|
+
? uniqueReasons([
|
|
79
|
+
...threads.flatMap((thread) => splitJoined(thread.ruleReason)),
|
|
80
|
+
...minimized.flatMap((item) => splitJoined(item.ruleReason)),
|
|
81
|
+
])
|
|
82
|
+
: uniqueReasons(report.threads.autoResolveErrorReasons ?? []);
|
|
83
|
+
const body = ruleAutoResolveBody({ threads: threads.length, comments, reviewSummaries }) ??
|
|
84
|
+
`auto-resolve failed for ${plural(errors.length, "mutation")}`;
|
|
85
|
+
return {
|
|
86
|
+
summary: withReasonClause(body, reasons),
|
|
87
|
+
...(threads.length > 0 && { threads }),
|
|
88
|
+
...(minimized.length > 0 && { minimized }),
|
|
89
|
+
...(errors.length > 0 && { errors }),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function formatRuleAutoResolveSection(event) {
|
|
93
|
+
if (!event)
|
|
94
|
+
return null;
|
|
95
|
+
const lines = ["## Classification auto-resolve", "", event.summary];
|
|
96
|
+
const links = [
|
|
97
|
+
...(event.threads ?? []).map((thread) => thread.url.trim() || `\`${thread.id}\``),
|
|
98
|
+
...(event.minimized ?? []).map((item) => item.url?.trim() || `\`${item.id}\``),
|
|
99
|
+
];
|
|
100
|
+
if (links.length > 0)
|
|
101
|
+
lines.push("", ...links.map((link) => `- ${link}`));
|
|
102
|
+
if ((event.errors?.length ?? 0) > 0) {
|
|
103
|
+
lines.push("", ...(event.errors ?? []).map((error) => `- ${error}`));
|
|
104
|
+
}
|
|
105
|
+
return lines.join("\n");
|
|
106
|
+
}
|
|
107
|
+
export function projectRuleAutoResolve(event) {
|
|
108
|
+
return {
|
|
109
|
+
summary: event.summary,
|
|
110
|
+
...((event.threads?.length ?? 0) > 0 && { threads: event.threads }),
|
|
111
|
+
...((event.minimized?.length ?? 0) > 0 && { minimized: event.minimized }),
|
|
112
|
+
...((event.errors?.length ?? 0) > 0 && { errors: event.errors }),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export function insertRuleAutoResolveSection(text, event) {
|
|
116
|
+
const section = formatRuleAutoResolveSection(event);
|
|
117
|
+
if (!section)
|
|
118
|
+
return text;
|
|
119
|
+
const index = text.lastIndexOf("## Instructions");
|
|
120
|
+
if (index === -1)
|
|
121
|
+
return `${text}\n\n${section}`;
|
|
122
|
+
return `${text.slice(0, index)}${section}\n\n${text.slice(index)}`;
|
|
123
|
+
}
|
|
124
|
+
export function stripReplayedRuleAutoResolve(report) {
|
|
125
|
+
const hasEvent = report.threads.autoResolved.length > 0 ||
|
|
126
|
+
report.threads.autoResolveErrors.length > 0 ||
|
|
127
|
+
(report.threads.autoResolveErrorReasons?.length ?? 0) > 0 ||
|
|
128
|
+
(report.comments.autoMinimized?.length ?? 0) > 0;
|
|
129
|
+
if (!hasEvent)
|
|
130
|
+
return report;
|
|
131
|
+
const { autoMinimized: _autoMinimized, ...comments } = report.comments;
|
|
132
|
+
const { autoResolveErrorReasons: _autoResolveErrorReasons, ...threads } = report.threads;
|
|
133
|
+
return {
|
|
134
|
+
...report,
|
|
135
|
+
threads: { ...threads, autoResolved: [], autoResolveErrors: [] },
|
|
136
|
+
comments,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function minimizedItem(kind, id, url, reasons) {
|
|
140
|
+
const ruleReason = joinedReason(reasons);
|
|
141
|
+
return {
|
|
142
|
+
id,
|
|
143
|
+
kind,
|
|
144
|
+
...(url?.trim() ? { url: url.trim() } : {}),
|
|
145
|
+
...(ruleReason ? { ruleReason } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export function resolvedThread(thread, reasons) {
|
|
149
|
+
const ruleReason = joinedReason(reasons);
|
|
150
|
+
return { ...thread, isResolved: true, ...(ruleReason ? { ruleReason } : {}) };
|
|
151
|
+
}
|