@effect-agent/pr-review 0.1.0-beta.21 → 0.1.0-beta.23
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/README.md +63 -46
- package/dist/action.d.mts +13 -13
- package/dist/action.mjs +33 -25
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-D5xrmadQ.d.mts → fan-out-BJBTAYuh.d.mts} +278 -303
- package/dist/{github-DSqZp3Ce.mjs → github-BbwYzNrC.mjs} +588 -243
- package/dist/github-BbwYzNrC.mjs.map +1 -0
- package/dist/index.d.mts +72 -116
- package/dist/index.mjs +11 -11
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-CblG1G9b.mjs → providers-NyP-4rS6.mjs} +162 -578
- package/dist/providers-NyP-4rS6.mjs.map +1 -0
- package/dist/testing.d.mts +4 -15
- package/dist/testing.mjs +12 -41
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +40 -34
- package/src/internal/action-entry.ts +0 -1
- package/src/internal/coverage.ts +147 -532
- package/src/internal/factory.ts +29 -50
- package/src/internal/fan-out-scripted.ts +26 -80
- package/src/internal/fan-out.ts +577 -426
- package/src/internal/profiles.ts +8 -8
- package/src/internal/render.ts +34 -45
- package/src/internal/retirement.ts +3 -1
- package/src/internal/review-agent.ts +6 -1
- package/src/internal/review-state.ts +43 -19
- package/src/internal/review-units.ts +25 -0
- package/src/internal/run.ts +211 -174
- package/src/internal/source.ts +1 -1
- package/dist/github-DSqZp3Ce.mjs.map +0 -1
- package/dist/providers-CblG1G9b.mjs.map +0 -1
package/src/internal/profiles.ts
CHANGED
|
@@ -38,18 +38,18 @@ export const pullRequestReviewerProfile = PullRequestReviewerProfile.make({
|
|
|
38
38
|
export class FanOutReviewerProfile extends Schema.Class<FanOutReviewerProfile>(
|
|
39
39
|
"@effect-agent/pr-review/FanOutReviewerProfile",
|
|
40
40
|
)({
|
|
41
|
-
/** Ephemeral runtime:
|
|
41
|
+
/** Ephemeral runtime: bounded host-scheduled child runs per invocation. */
|
|
42
42
|
deploymentClass: Schema.Literal("E"),
|
|
43
|
-
/** Every model-callable
|
|
43
|
+
/** Every model-callable surface is evidence-only; children expose no tools. */
|
|
44
44
|
readOnlyToolSurface: Schema.Literal(true),
|
|
45
45
|
/** The review is posted by the host AFTER the run settles, never by a tool. */
|
|
46
46
|
publicationOutsideAgentLoop: Schema.Literal(true),
|
|
47
47
|
/** Child findings are untrusted; anchors are validated against the parsed diff. */
|
|
48
48
|
anchorsValidatedBeforePublication: Schema.Literal(true),
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
/** A failed
|
|
52
|
-
|
|
49
|
+
/** Host code schedules every pass from the deterministic plan; no coordinator model. */
|
|
50
|
+
hostScheduledPasses: Schema.Literal(true),
|
|
51
|
+
/** A failed pass is retried once, then reported and carried as unreviewed scope. */
|
|
52
|
+
failedPassesRetriedOnceThenCarried: Schema.Literal(true),
|
|
53
53
|
/** Risk categories and required specialist passes are pure host policy. */
|
|
54
54
|
hostOwnedRiskClassification: Schema.Literal(true),
|
|
55
55
|
/** Every host-classified high-risk unit receives a fresh specialist pass. */
|
|
@@ -69,8 +69,8 @@ export const fanOutReviewerProfile = FanOutReviewerProfile.make({
|
|
|
69
69
|
readOnlyToolSurface: true,
|
|
70
70
|
publicationOutsideAgentLoop: true,
|
|
71
71
|
anchorsValidatedBeforePublication: true,
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
hostScheduledPasses: true,
|
|
73
|
+
failedPassesRetriedOnceThenCarried: true,
|
|
74
74
|
hostOwnedRiskClassification: true,
|
|
75
75
|
redundantHighRiskDiscovery: true,
|
|
76
76
|
independentCandidateVerification: true,
|
package/src/internal/render.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Schema } from "effect";
|
|
|
2
2
|
|
|
3
3
|
import { anchorViolation } from "./anchors.ts";
|
|
4
4
|
export { anchorViolation } from "./anchors.ts";
|
|
5
|
-
import type { ReviewAssurance,
|
|
5
|
+
import type { ReviewAssurance, ReviewInputCoverage } from "./coverage.ts";
|
|
6
6
|
import type { ChangedFile } from "./diff.ts";
|
|
7
7
|
import { renderFingerprintMarker } from "./fingerprint.ts";
|
|
8
8
|
import {
|
|
@@ -215,27 +215,27 @@ const renderVerdictCallout = (
|
|
|
215
215
|
options: {
|
|
216
216
|
readonly carriedFindings?: ReadonlyArray<ReviewFinding> | undefined;
|
|
217
217
|
readonly carriedConcerns?: ReadonlyArray<ReviewConcern> | undefined;
|
|
218
|
-
readonly coverage?: ReviewCoverage | undefined;
|
|
219
218
|
readonly inputCoverage?: ReviewInputCoverage | undefined;
|
|
220
219
|
readonly assurance?: ReviewAssurance | undefined;
|
|
220
|
+
readonly unreviewedPaths?: ReadonlyArray<string> | undefined;
|
|
221
221
|
},
|
|
222
222
|
): string => {
|
|
223
223
|
const counts = severityCounts(review, options.carriedFindings, options.carriedConcerns);
|
|
224
|
+
// Code findings outrank machinery gaps: a blocking finding is the
|
|
225
|
+
// actionable signal, and unsettled reviewer-side work is carried forward.
|
|
226
|
+
if (counts.blocking > 0) {
|
|
227
|
+
return `> [!CAUTION]\n> ${countNoun(counts.blocking, "blocking finding")} — do not merge before addressing ${counts.blocking === 1 ? "it" : "them"}.`;
|
|
228
|
+
}
|
|
229
|
+
const carried = options.unreviewedPaths?.length ?? 0;
|
|
224
230
|
if (
|
|
225
231
|
options.inputCoverage?.status === "incomplete" ||
|
|
226
|
-
|
|
232
|
+
options.assurance?.status === "incomplete"
|
|
227
233
|
) {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const suffix =
|
|
234
|
-
counts.blocking > 0 ? ` It also has ${countNoun(counts.blocking, "blocking finding")}.` : "";
|
|
235
|
-
return `> [!CAUTION]\n> Configured review assurance did not settle — the check must not pass.${suffix}`;
|
|
236
|
-
}
|
|
237
|
-
if (counts.blocking > 0) {
|
|
238
|
-
return `> [!CAUTION]\n> ${countNoun(counts.blocking, "blocking finding")} — do not merge before addressing ${counts.blocking === 1 ? "it" : "them"}.`;
|
|
234
|
+
const carriedNote =
|
|
235
|
+
carried > 0
|
|
236
|
+
? ` ${countNoun(carried, "affected path")} ${carried === 1 ? "is" : "are"} carried forward and retried automatically on the next run.`
|
|
237
|
+
: "";
|
|
238
|
+
return `> [!WARNING]\n> Review infrastructure did not settle — a reviewer-side gap, NOT a request to change code.${carriedNote} The check reports "incomplete" until a run settles.`;
|
|
239
239
|
}
|
|
240
240
|
if (counts.important > 0) {
|
|
241
241
|
return `> [!IMPORTANT]\n> ${countNoun(counts.important, "important finding")} to address before merging.`;
|
|
@@ -393,22 +393,20 @@ export const planPublication = (
|
|
|
393
393
|
readonly modelLabel?: string | undefined;
|
|
394
394
|
/** Workflow-run URL rendered into the footer. */
|
|
395
395
|
readonly runUrl?: string | undefined;
|
|
396
|
-
/** Observed run usage rendered into the footer. */
|
|
396
|
+
/** Observed whole-run usage rendered into the footer. */
|
|
397
397
|
readonly usage?: { readonly inputTokens: number; readonly outputTokens: number } | undefined;
|
|
398
|
-
/** What the usage observed: the whole run, or the coordinator only. */
|
|
399
|
-
readonly usageScope?: "run" | "coordinator" | undefined;
|
|
400
398
|
/**
|
|
401
399
|
* Changeset fingerprint embedded invisibly in the review body so a later
|
|
402
400
|
* run can skip re-reviewing an unchanged changeset.
|
|
403
401
|
*/
|
|
404
402
|
readonly fingerprint?: string | undefined;
|
|
405
|
-
/** Host-owned coverage; incomplete coverage is rendered and fails the check. */
|
|
406
|
-
readonly coverage?: ReviewCoverage | undefined;
|
|
407
403
|
/** Host-owned path/evidence assignment, separate from review assurance. */
|
|
408
404
|
readonly inputCoverage?: ReviewInputCoverage | undefined;
|
|
409
405
|
/** Host-owned discovery/specialist/verification settlement. */
|
|
410
406
|
readonly assurance?: ReviewAssurance | undefined;
|
|
411
|
-
/**
|
|
407
|
+
/** Retryable scope this run could not settle; carried to the next run. */
|
|
408
|
+
readonly unreviewedPaths?: ReadonlyArray<string> | undefined;
|
|
409
|
+
/** Unchanged unresolved items carried from the prior reviewed baseline. */
|
|
412
410
|
readonly carriedFindings?: ReadonlyArray<ReviewFinding> | undefined;
|
|
413
411
|
readonly carriedConcerns?: ReadonlyArray<ReviewConcern> | undefined;
|
|
414
412
|
/** Selected review scope, made visible whenever orchestration chose it. */
|
|
@@ -452,14 +450,8 @@ export const planPublication = (
|
|
|
452
450
|
|
|
453
451
|
const footerParts = ["Automated review by @effect-agent/pr-review"];
|
|
454
452
|
if (options.modelLabel !== undefined) footerParts.push(options.modelLabel);
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
// coordinator, and omitting the number is honest where mislabeling is not.
|
|
458
|
-
if (options.usage !== undefined && options.usageScope !== undefined) {
|
|
459
|
-
const scope = options.usageScope === "coordinator" ? " (coordinator)" : "";
|
|
460
|
-
footerParts.push(
|
|
461
|
-
`${options.usage.inputTokens} in / ${options.usage.outputTokens} out tokens${scope}`,
|
|
462
|
-
);
|
|
453
|
+
if (options.usage !== undefined) {
|
|
454
|
+
footerParts.push(`${options.usage.inputTokens} in / ${options.usage.outputTokens} out tokens`);
|
|
463
455
|
}
|
|
464
456
|
if (options.runUrl !== undefined) footerParts.push(`[run](${options.runUrl})`);
|
|
465
457
|
footerParts.push(`reviewed at ${options.headSha.slice(0, 7)}`);
|
|
@@ -490,9 +482,9 @@ export const planPublication = (
|
|
|
490
482
|
renderVerdictCallout(review, {
|
|
491
483
|
carriedFindings,
|
|
492
484
|
carriedConcerns,
|
|
493
|
-
coverage: options.coverage,
|
|
494
485
|
inputCoverage: options.inputCoverage,
|
|
495
486
|
assurance: options.assurance,
|
|
487
|
+
unreviewedPaths: options.unreviewedPaths,
|
|
496
488
|
}),
|
|
497
489
|
];
|
|
498
490
|
if (options.reviewMode !== undefined && options.reviewReason !== undefined) {
|
|
@@ -513,7 +505,7 @@ export const planPublication = (
|
|
|
513
505
|
if (options.inputCoverage !== undefined && options.assurance !== undefined) {
|
|
514
506
|
parts.push(
|
|
515
507
|
"",
|
|
516
|
-
`**Input coverage:** ${options.inputCoverage.status} (${options.inputCoverage.assignedPaths.length}/${options.inputCoverage.requiredPaths.length} paths assigned, ${options.inputCoverage.partialPaths.length} partial) · **Review assurance:** ${options.assurance.status} (${options.assurance.completedGeneralDiscoveryPasses}/${options.assurance.requiredGeneralDiscoveryPasses} general discovery, ${options.assurance.completedSpecialistPasses}/${options.assurance.requiredSpecialistPasses} specialist, ${options.assurance.completedVerificationPasses}/${options.assurance.requiredVerificationPasses} verification; ${options.assurance.confirmedCandidates} confirmed / ${options.assurance.rejectedCandidates} rejected / ${options.assurance.unsettledCandidates} unsettled candidates)`,
|
|
508
|
+
`**Input coverage:** ${options.inputCoverage.status} (${options.inputCoverage.assignedPaths.length}/${options.inputCoverage.requiredPaths.length} paths assigned, ${options.inputCoverage.partialPaths.length} partial) · **Review assurance:** ${options.assurance.status} (${options.assurance.completedGeneralDiscoveryPasses}/${options.assurance.requiredGeneralDiscoveryPasses} general discovery, ${options.assurance.completedSpecialistPasses}/${options.assurance.requiredSpecialistPasses} specialist, ${options.assurance.completedVerificationPasses}/${options.assurance.requiredVerificationPasses} verification; ${options.assurance.confirmedCandidates} confirmed / ${options.assurance.rejectedCandidates} rejected / ${options.assurance.unsettledCandidates} unsettled${options.assurance.discardedInvalidFindings > 0 ? ` / ${options.assurance.discardedInvalidFindings} discarded` : ""} candidates)`,
|
|
517
509
|
);
|
|
518
510
|
}
|
|
519
511
|
parts.push("", review.summary);
|
|
@@ -525,22 +517,17 @@ export const planPublication = (
|
|
|
525
517
|
if (options.inputCoverage?.status === "incomplete") {
|
|
526
518
|
parts.push(
|
|
527
519
|
"",
|
|
528
|
-
"###
|
|
520
|
+
"### ⚠️ Incomplete input coverage",
|
|
529
521
|
"",
|
|
530
522
|
...options.inputCoverage.reasons.map((reason) => `- ${reason}`),
|
|
531
523
|
);
|
|
532
|
-
} else if (options.inputCoverage === undefined && options.coverage?.status === "incomplete") {
|
|
533
|
-
parts.push(
|
|
534
|
-
"",
|
|
535
|
-
"### 🛑 Incomplete coverage",
|
|
536
|
-
"",
|
|
537
|
-
...options.coverage.reasons.map((reason) => `- ${reason}`),
|
|
538
|
-
);
|
|
539
524
|
}
|
|
540
|
-
if (options.assurance
|
|
525
|
+
if (options.assurance?.status === "incomplete") {
|
|
541
526
|
parts.push(
|
|
542
527
|
"",
|
|
543
|
-
"###
|
|
528
|
+
"### ⚠️ Unsettled review passes",
|
|
529
|
+
"",
|
|
530
|
+
"The passes below failed on the reviewer's side after a bounded retry. Their paths are carried forward and re-reviewed automatically on the next run — do not change code to satisfy this section.",
|
|
544
531
|
"",
|
|
545
532
|
...options.assurance.reasons.map((reason) => `- ${reason}`),
|
|
546
533
|
);
|
|
@@ -613,14 +600,16 @@ export const planPublication = (
|
|
|
613
600
|
options.carriedFindings ?? [],
|
|
614
601
|
options.carriedConcerns ?? [],
|
|
615
602
|
);
|
|
603
|
+
// Machinery gaps (incomplete input or unsettled passes) block APPROVE but
|
|
604
|
+
// never REQUEST_CHANGES: requesting changes for a reviewer-side fault would
|
|
605
|
+
// tell the author to edit code nobody reviewed.
|
|
606
|
+
const unclean =
|
|
607
|
+
options.inputCoverage?.status === "incomplete" || options.assurance?.status === "incomplete";
|
|
616
608
|
const event: ReviewEvent = !options.applyVerdict
|
|
617
609
|
? "COMMENT"
|
|
618
|
-
:
|
|
619
|
-
(options.inputCoverage === undefined && options.coverage?.status === "incomplete") ||
|
|
620
|
-
(options.assurance !== undefined && options.assurance.status !== "settled") ||
|
|
621
|
-
counts.blocking > 0
|
|
610
|
+
: counts.blocking > 0
|
|
622
611
|
? "REQUEST_CHANGES"
|
|
623
|
-
: review.verdict === "approve" && counts.important === 0
|
|
612
|
+
: review.verdict === "approve" && counts.important === 0 && !unclean
|
|
624
613
|
? "APPROVE"
|
|
625
614
|
: "COMMENT";
|
|
626
615
|
|
|
@@ -102,8 +102,10 @@ const findingIdentity = (finding: {
|
|
|
102
102
|
|
|
103
103
|
const REVIEW_METADATA_PATTERN = /<!-- effect-agent-pr-review metadata\n[\s\S]*?\n-->/g;
|
|
104
104
|
const FINGERPRINT_PATTERN = /<!-- effect-agent-pr-review fingerprint=sha256:[0-9a-f]{64} -->/g;
|
|
105
|
+
// Version-agnostic on purpose: retirement only RECOGNIZES machine markers to
|
|
106
|
+
// keep them byte-identical through an edit; it never authenticates them.
|
|
105
107
|
const STATE_PATTERN =
|
|
106
|
-
/<!-- effect-agent-pr-review state-
|
|
108
|
+
/<!-- effect-agent-pr-review state-v\d+:[A-Za-z0-9+/]+={0,2}\.[0-9a-f]{64} -->/g;
|
|
107
109
|
const RETIRED_ORIGINAL_PATTERN =
|
|
108
110
|
/<!-- effect-agent-pr-review retired-original:start -->\n([\s\S]*?)\n<!-- effect-agent-pr-review retired-original:end -->/;
|
|
109
111
|
const MACHINE_COMMENT_PATTERN = new RegExp(
|
|
@@ -346,7 +346,12 @@ export class ReviewFinding extends Schema.Class<ReviewFinding>(
|
|
|
346
346
|
title: Schema.NonEmptyString.check(Schema.isMaxLength(120)),
|
|
347
347
|
body: Schema.NonEmptyString.check(Schema.isMaxLength(2_000)),
|
|
348
348
|
/** Replacement for exactly lines startLine..endLine; omit when unsure. */
|
|
349
|
-
suggestion: Schema.optionalKey(
|
|
349
|
+
suggestion: Schema.optionalKey(
|
|
350
|
+
Schema.String.annotate({
|
|
351
|
+
description:
|
|
352
|
+
"Committable replacement source code for exactly lines startLine..endLine: the full replacement for every line in the range and nothing else — never prose describing the change, which belongs in body.",
|
|
353
|
+
}).check(Schema.isMaxLength(2_000)),
|
|
354
|
+
),
|
|
350
355
|
}) {}
|
|
351
356
|
|
|
352
357
|
export const ReviewVerdict = Schema.Literals(["approve", "comment", "request-changes"]);
|
|
@@ -46,16 +46,22 @@ export class StoredReviewConcern extends Schema.Class<StoredReviewConcern>(
|
|
|
46
46
|
body: StoredText,
|
|
47
47
|
}) {}
|
|
48
48
|
|
|
49
|
+
/** The carried-scope bound; a run that cannot fit its leftovers publishes no state. */
|
|
50
|
+
export const MAX_STORED_UNREVIEWED_PATHS = 100;
|
|
51
|
+
|
|
49
52
|
/**
|
|
50
|
-
* Versioned state embedded
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* Versioned state embedded after EVERY completed run that can be signed. The
|
|
54
|
+
* head plus full-scope fingerprint forms an incremental baseline; an absent
|
|
55
|
+
* unresolved item never means the path is defect-free. `unreviewedPaths`
|
|
56
|
+
* carries retryable review gaps (failed passes) forward so the next
|
|
57
|
+
* incremental run re-reviews exactly them plus the new delta — the baseline
|
|
58
|
+
* advances monotonically instead of freezing on one flaky pass and reopening
|
|
59
|
+
* the whole post-baseline scope. The `acceptedScopeFingerprint` name is
|
|
60
|
+
* retained for wire compatibility. Storing hundreds of path strings
|
|
61
|
+
* separately would not fit GitHub's bounded review body in the worst case.
|
|
56
62
|
*/
|
|
57
63
|
export class ReviewState extends Schema.Class<ReviewState>("@effect-agent/pr-review/ReviewState")({
|
|
58
|
-
version: Schema.Literal(
|
|
64
|
+
version: Schema.Literal(2),
|
|
59
65
|
repository: Schema.NonEmptyString.check(Schema.isMaxLength(200)),
|
|
60
66
|
pullRequestNumber: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
61
67
|
baseRef: Schema.NonEmptyString.check(Schema.isMaxLength(300)),
|
|
@@ -67,6 +73,14 @@ export class ReviewState extends Schema.Class<ReviewState>("@effect-agent/pr-rev
|
|
|
67
73
|
reviewedPathCount: Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 300 })),
|
|
68
74
|
unresolvedFindings: Schema.Array(StoredReviewFinding).check(Schema.isMaxLength(20)),
|
|
69
75
|
unresolvedConcerns: Schema.Array(StoredReviewConcern).check(Schema.isMaxLength(10)),
|
|
76
|
+
/** Retryable review gaps carried into the next incremental run's scope. */
|
|
77
|
+
unreviewedPaths: Schema.Array(ChangedPath).check(Schema.isMaxLength(MAX_STORED_UNREVIEWED_PATHS)),
|
|
78
|
+
/**
|
|
79
|
+
* True only when the producing run had complete input coverage, no
|
|
80
|
+
* unsettled pass, and nothing carried. Skip-unchanged authority: an
|
|
81
|
+
* unchanged patch may skip re-review only over a settled state.
|
|
82
|
+
*/
|
|
83
|
+
settled: Schema.Boolean,
|
|
70
84
|
lastReviewMode: ReviewScopeMode,
|
|
71
85
|
}) {}
|
|
72
86
|
|
|
@@ -100,15 +114,15 @@ export const toStoredConcern = (concern: ReviewConcern): StoredReviewConcern =>
|
|
|
100
114
|
export const fromStoredConcern = (concern: StoredReviewConcern): ReviewConcern =>
|
|
101
115
|
ReviewConcern.make({ severity: concern.severity, title: concern.title, body: concern.body });
|
|
102
116
|
|
|
103
|
-
const STATE_MARKER_PREFIX = "<!-- effect-agent-pr-review state-
|
|
117
|
+
const STATE_MARKER_PREFIX = "<!-- effect-agent-pr-review state-v2:";
|
|
104
118
|
const STATE_MARKER_SUFFIX = " -->";
|
|
105
119
|
const STATE_MARKER_PATTERN =
|
|
106
|
-
/(?:^|\n)<!-- effect-agent-pr-review state-
|
|
107
|
-
const STATE_SIGNATURE_DOMAIN = "effect-agent-pr-review/state-
|
|
120
|
+
/(?:^|\n)<!-- effect-agent-pr-review state-v2:([A-Za-z0-9+/]+={0,2})\.([0-9a-f]{64}) -->$/;
|
|
121
|
+
const STATE_SIGNATURE_DOMAIN = "effect-agent-pr-review/state-v2\u0000";
|
|
108
122
|
export const MAX_REVIEW_STATE_MARKER_CHARS = 24_000;
|
|
109
123
|
export const ReviewStateMarker = Schema.NonEmptyString.check(
|
|
110
124
|
Schema.isMaxLength(MAX_REVIEW_STATE_MARKER_CHARS),
|
|
111
|
-
Schema.isPattern(/^<!-- effect-agent-pr-review state-
|
|
125
|
+
Schema.isPattern(/^<!-- effect-agent-pr-review state-v2:[A-Za-z0-9+/]+={0,2}\.[0-9a-f]{64} -->$/),
|
|
112
126
|
).pipe(Schema.brand("@effect-agent/pr-review/ReviewStateMarker"));
|
|
113
127
|
export type ReviewStateMarker = typeof ReviewStateMarker.Type;
|
|
114
128
|
|
|
@@ -402,22 +416,32 @@ export const selectReviewRange = (input: {
|
|
|
402
416
|
selectedByPath.set(file.path, file);
|
|
403
417
|
}
|
|
404
418
|
}
|
|
405
|
-
|
|
419
|
+
// Retryable gaps carried by the prior state re-enter this run's scope; a
|
|
420
|
+
// carried path reverted out of the pull request has nothing left to review.
|
|
421
|
+
const carriedPaths = new Set(
|
|
422
|
+
input.priorState.unreviewedPaths.filter((path) => currentPaths.has(path)),
|
|
423
|
+
);
|
|
424
|
+
for (const path of carriedPaths) affectedPaths.add(path);
|
|
425
|
+
const rescuePaths = input.priorState.baseSha !== input.current.baseSha;
|
|
426
|
+
if (rescuePaths || carriedPaths.size > 0) {
|
|
406
427
|
for (const file of input.fullFiles) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
428
|
+
const affected =
|
|
429
|
+
(rescuePaths &&
|
|
430
|
+
(affectedPaths.has(file.path) ||
|
|
431
|
+
(file.previousPath !== undefined && affectedPaths.has(file.previousPath)))) ||
|
|
432
|
+
carriedPaths.has(file.path) ||
|
|
433
|
+
(file.previousPath !== undefined && carriedPaths.has(file.previousPath));
|
|
434
|
+
if (affected) selectedByPath.set(file.path, file);
|
|
413
435
|
}
|
|
414
436
|
}
|
|
415
437
|
const selectedFiles = [...selectedByPath.values()].sort((left, right) =>
|
|
416
438
|
left.path < right.path ? -1 : left.path > right.path ? 1 : 0,
|
|
417
439
|
);
|
|
440
|
+
const carriedReason =
|
|
441
|
+
carriedPaths.size === 0 ? "" : `; retrying ${carriedPaths.size} carried unreviewed path(s)`;
|
|
418
442
|
return {
|
|
419
443
|
mode: "incremental",
|
|
420
|
-
reason: `changes since
|
|
444
|
+
reason: `changes since reviewed head ${input.priorState.reviewedHeadSha.slice(0, 7)}${baseReason}${carriedReason}`,
|
|
421
445
|
files: selectedFiles,
|
|
422
446
|
affectedPaths: [...affectedPaths].sort(),
|
|
423
447
|
totalFiles: selectedFiles.length,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
fileReviewEvidenceChunks,
|
|
7
7
|
type FindingSeverity,
|
|
8
8
|
MAX_PATCH_CHARS,
|
|
9
|
+
type ReviewConcern,
|
|
9
10
|
type ReviewFinding,
|
|
10
11
|
} from "./review-agent.ts";
|
|
11
12
|
|
|
@@ -465,3 +466,27 @@ export const rankAndDedupeFindings = (
|
|
|
465
466
|
})
|
|
466
467
|
.slice(0, MAX_MERGED_FINDINGS);
|
|
467
468
|
};
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* The concern analogue of `rankAndDedupeFindings`: dedupe by exact content
|
|
472
|
+
* keeping the most severe duplicate, rank by severity, and cap at the
|
|
473
|
+
* `CodeReview` concerns bound.
|
|
474
|
+
*/
|
|
475
|
+
export const rankAndDedupeConcerns = (
|
|
476
|
+
concerns: ReadonlyArray<ReviewConcern>,
|
|
477
|
+
): ReadonlyArray<ReviewConcern> => {
|
|
478
|
+
const byContent = new Map<string, ReviewConcern>();
|
|
479
|
+
for (const concern of concerns) {
|
|
480
|
+
const key = `${concern.title}\u0000${concern.body}`;
|
|
481
|
+
const previous = byContent.get(key);
|
|
482
|
+
if (
|
|
483
|
+
previous === undefined ||
|
|
484
|
+
severityRank[concern.severity] < severityRank[previous.severity]
|
|
485
|
+
) {
|
|
486
|
+
byContent.set(key, concern);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return [...byContent.values()]
|
|
490
|
+
.sort((left, right) => severityRank[left.severity] - severityRank[right.severity])
|
|
491
|
+
.slice(0, 10);
|
|
492
|
+
};
|