@effect-agent/pr-review 0.1.0-beta.23 → 0.1.0-beta.25
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 +83 -195
- package/dist/action.d.mts +27 -18
- package/dist/action.mjs +60 -39
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +3 -3
- package/dist/cli.mjs.map +1 -1
- package/dist/{fan-out-BJBTAYuh.d.mts → fan-out-CMEsbFLk.d.mts} +455 -177
- package/dist/{github-BbwYzNrC.mjs → github-NjgxGqwM.mjs} +2163 -1518
- package/dist/github-NjgxGqwM.mjs.map +1 -0
- package/dist/index.d.mts +30 -20
- package/dist/index.mjs +3 -3
- package/dist/{providers-NyP-4rS6.mjs → providers-CODZQCmL.mjs} +202 -80
- package/dist/providers-CODZQCmL.mjs.map +1 -0
- package/dist/testing.d.mts +3 -1
- package/dist/testing.mjs +3 -2
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +141 -78
- package/src/cli.ts +6 -1
- package/src/index.ts +1 -0
- package/src/internal/adjudication.ts +415 -0
- package/src/internal/coverage.ts +41 -60
- package/src/internal/factory.ts +4 -4
- package/src/internal/fan-out.ts +208 -14
- package/src/internal/fingerprint.ts +16 -10
- package/src/internal/fixtures.ts +6 -0
- package/src/internal/github-env.ts +9 -0
- package/src/internal/github.ts +243 -7
- package/src/internal/progress.ts +1 -1
- package/src/internal/render.ts +186 -42
- package/src/internal/retirement.ts +16 -17
- package/src/internal/review-agent.ts +39 -4
- package/src/internal/review-state.ts +315 -105
- package/src/internal/review-units.ts +10 -9
- package/src/internal/run.ts +197 -63
- package/dist/github-BbwYzNrC.mjs.map +0 -1
- package/dist/providers-NyP-4rS6.mjs.map +0 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Context, DateTime, Effect, Option, Schema } from "effect";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
adjudicationIdentity,
|
|
5
|
+
findingIdentity,
|
|
4
6
|
ReviewStateAuthenticator,
|
|
5
7
|
type ReviewState,
|
|
6
8
|
type StoredReviewFinding,
|
|
@@ -92,20 +94,10 @@ export interface ReviewRetirementDecision {
|
|
|
92
94
|
readonly priorFindingCount: number;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
const findingIdentity = (finding: {
|
|
96
|
-
readonly path: string;
|
|
97
|
-
readonly startLine: number;
|
|
98
|
-
readonly endLine: number;
|
|
99
|
-
readonly title: string;
|
|
100
|
-
}): string =>
|
|
101
|
-
`${finding.path}\u0000${finding.startLine}\u0000${finding.endLine}\u0000${finding.title}`;
|
|
102
|
-
|
|
103
97
|
const REVIEW_METADATA_PATTERN = /<!-- effect-agent-pr-review metadata\n[\s\S]*?\n-->/g;
|
|
104
98
|
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.
|
|
107
99
|
const STATE_PATTERN =
|
|
108
|
-
/<!-- effect-agent-pr-review state-
|
|
100
|
+
/<!-- effect-agent-pr-review state-v1:[A-Za-z0-9+/]+={0,2}\.[0-9a-f]{64} -->/g;
|
|
109
101
|
const RETIRED_ORIGINAL_PATTERN =
|
|
110
102
|
/<!-- effect-agent-pr-review retired-original:start -->\n([\s\S]*?)\n<!-- effect-agent-pr-review retired-original:end -->/;
|
|
111
103
|
const MACHINE_COMMENT_PATTERN = new RegExp(
|
|
@@ -114,11 +106,12 @@ const MACHINE_COMMENT_PATTERN = new RegExp(
|
|
|
114
106
|
);
|
|
115
107
|
const VERDICT_CALLOUT_PATTERN =
|
|
116
108
|
/^(?:> \[!(?:CAUTION|IMPORTANT)\]\n> [^\n]*(?:\n> [^\n]*)*|> (?:ℹ️|✅)[^\n]*)\n*/;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
109
|
+
/**
|
|
110
|
+
* The first line of every inline finding comment this package posts. Shared
|
|
111
|
+
* with adjudication so both parse the identical title shape.
|
|
112
|
+
*/
|
|
113
|
+
export const INLINE_FINDING_TITLE_PATTERN =
|
|
114
|
+
/^\*\*\[(?:🛑 blocking|⚠️ important|💅 nit) · [a-z-]+\] ([^\n]+)\*\*$/;
|
|
122
115
|
const MAX_REVIEW_BODY_CHARS = 60_000;
|
|
123
116
|
|
|
124
117
|
/** The host-authored metadata marker is the authority gate for any edit. */
|
|
@@ -191,8 +184,14 @@ export const decideReviewRetirement = (input: {
|
|
|
191
184
|
readonly currentReviewUrl: string;
|
|
192
185
|
}): ReviewRetirementDecision => {
|
|
193
186
|
const current = new Set(input.currentState.unresolvedFindings.map(findingIdentity));
|
|
187
|
+
// Adjudicated identities are distinct from resolved: a maintainer verdict
|
|
188
|
+
// is not a fix, so retirement neither strikes nor minimizes them.
|
|
189
|
+
const adjudicated = new Set(
|
|
190
|
+
(input.currentState.adjudications ?? []).map((entry) => adjudicationIdentity(entry)),
|
|
191
|
+
);
|
|
194
192
|
const resolvedFindings = input.priorState.unresolvedFindings.filter(
|
|
195
|
-
(finding) =>
|
|
193
|
+
(finding) =>
|
|
194
|
+
!current.has(findingIdentity(finding)) && !adjudicated.has(findingIdentity(finding)),
|
|
196
195
|
);
|
|
197
196
|
return {
|
|
198
197
|
body: renderRetiredBody({ ...input, resolvedFindings }),
|
|
@@ -300,6 +300,11 @@ export const ReviewToolkitLayer = ReviewToolkit.toLayer({
|
|
|
300
300
|
// Mission input and review output contracts.
|
|
301
301
|
// ---------------------------------------------------------------------------
|
|
302
302
|
|
|
303
|
+
/** Bounded prior-review context lines injected into reviewer instructions. */
|
|
304
|
+
const ReviewContextLines = Schema.Array(Schema.String.check(Schema.isMaxLength(1_200))).check(
|
|
305
|
+
Schema.isMaxLength(20),
|
|
306
|
+
);
|
|
307
|
+
|
|
303
308
|
export class ReviewMission extends Schema.Class<ReviewMission>(
|
|
304
309
|
"@effect-agent/pr-review/ReviewMission",
|
|
305
310
|
)({
|
|
@@ -310,6 +315,18 @@ export class ReviewMission extends Schema.Class<ReviewMission>(
|
|
|
310
315
|
baseRef: Schema.NonEmptyString.check(Schema.isMaxLength(300)),
|
|
311
316
|
headRef: Schema.NonEmptyString.check(Schema.isMaxLength(300)),
|
|
312
317
|
changedFileCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
318
|
+
/**
|
|
319
|
+
* Maintainer-adjudicated identities rendered as bounded context lines; the
|
|
320
|
+
* reviewer must not re-raise them without materially new evidence. Absent
|
|
321
|
+
* from fingerprint missions so an adjudication never invalidates the
|
|
322
|
+
* skip-unchanged authority.
|
|
323
|
+
*/
|
|
324
|
+
adjudicatedContext: Schema.optionalKey(ReviewContextLines),
|
|
325
|
+
/**
|
|
326
|
+
* Prior-round findings on re-reviewed scope, rendered as bounded context
|
|
327
|
+
* lines; each must be confirmed, declared fixed, or explicitly withdrawn.
|
|
328
|
+
*/
|
|
329
|
+
priorFindingContext: Schema.optionalKey(ReviewContextLines),
|
|
313
330
|
}) {}
|
|
314
331
|
|
|
315
332
|
export const FindingSeverity = Schema.Literals(["blocking", "important", "nit"]);
|
|
@@ -361,12 +378,18 @@ export type ReviewVerdict = typeof ReviewVerdict.Type;
|
|
|
361
378
|
* A concern with no diff line to anchor to: a missing deletion or cleanup,
|
|
362
379
|
* rollout or migration sequencing, a coverage gap the diff implies but does
|
|
363
380
|
* not add, or a scope question only the author can answer. Rendered as a
|
|
364
|
-
* review-body section
|
|
365
|
-
*
|
|
381
|
+
* review-body section instead of an inline comment. `evidencePaths` binds the
|
|
382
|
+
* concern to changed files so a later incremental review can invalidate and
|
|
383
|
+
* recheck it when any supporting path changes. It remains optional only for
|
|
384
|
+
* decoding review output and continuity state written before path binding was
|
|
385
|
+
* introduced; a pathless concern cannot authorize incremental continuity.
|
|
366
386
|
*/
|
|
367
387
|
export class ReviewConcern extends Schema.Class<ReviewConcern>(
|
|
368
388
|
"@effect-agent/pr-review/ReviewConcern",
|
|
369
389
|
)({
|
|
390
|
+
evidencePaths: Schema.optionalKey(
|
|
391
|
+
Schema.Array(ChangedPath).check(Schema.isMinLength(1)).check(Schema.isMaxLength(3)),
|
|
392
|
+
),
|
|
370
393
|
severity: FindingSeverity,
|
|
371
394
|
title: Schema.NonEmptyString.check(Schema.isMaxLength(120)),
|
|
372
395
|
body: Schema.NonEmptyString.check(Schema.isMaxLength(2_000)),
|
|
@@ -448,6 +471,18 @@ export const makeReviewInstructions =
|
|
|
448
471
|
? `Author description:\n${mission.body}`
|
|
449
472
|
: "The author provided no description.",
|
|
450
473
|
...resolveGuidance(options.guidance, mission),
|
|
474
|
+
...(mission.adjudicatedContext === undefined || mission.adjudicatedContext.length === 0
|
|
475
|
+
? []
|
|
476
|
+
: [
|
|
477
|
+
"A maintainer has adjudicated these previously raised review items (disposition, reason). Do not re-raise them unless you have materially new evidence, and if you do, say explicitly what changed since the adjudication:",
|
|
478
|
+
...mission.adjudicatedContext.map((line) => `- ${line}`),
|
|
479
|
+
]),
|
|
480
|
+
...(mission.priorFindingContext === undefined || mission.priorFindingContext.length === 0
|
|
481
|
+
? []
|
|
482
|
+
: [
|
|
483
|
+
"Your previous review raised these findings on the scope you are re-reviewing. For each, either confirm it still holds, state that it is fixed, or withdraw it; do not demand the opposite of your own prior guidance without explicitly acknowledging the reversal:",
|
|
484
|
+
...mission.priorFindingContext.map((line) => `- ${line}`),
|
|
485
|
+
]),
|
|
451
486
|
"Work in this order:",
|
|
452
487
|
"1. Call list_changed_files once to see the selected input scope. In incremental reviews it is deliberately a subset of the pull request's full diff (totalFiles counts the whole pull request); omitted paths belong to settled prior scope or explicit host exclusions, not to this run.",
|
|
453
488
|
"2. Call read_file_diff for every listed file. A normal diff marks new-version anchors as R<number>; only those numbers are valid startLine/endLine values. When GitHub omitted a diff, the tool may return bounded base/head content marked B/H instead. Review that content, but report its defects as non-anchored concerns because B/H lines cannot anchor GitHub comments. Never anchor a finding to a removed (-), B, or H line.",
|
|
@@ -456,9 +491,9 @@ export const makeReviewInstructions =
|
|
|
456
491
|
"When the diff adds or changes a test, check that it can actually fail: a test that would still pass with the bug present is theatre, not coverage. The usual tell is a loose assertion standing where an exact one belongs — >= or a truthiness check over an expected value, or a snapshot that absorbs whatever it is handed.",
|
|
457
492
|
"Go shallow only when the diff has no behavioral surface at all: doc typos, formatting, lockfile or generated-code regeneration, a mechanical rename. Line count is not the signal — a one-line change to auth, money, SQL, a comparison operator, or a config default is not trivial.",
|
|
458
493
|
"Drop bloat-shaped findings before reporting: defensive checks for cases that cannot happen, abstractions used once, comments restating obvious code, tests asserting tautologies, just-in-case guards. A finding must be sound, correct, and worth acting on; prefer an explicit keep over an invented finding.",
|
|
459
|
-
'5. After collecting anchored findings, deliberately scan for concerns with NO line to point at: deletion or cleanup plans for code the diff replaces, rollout or migration sequencing, coverage gaps the diff implies but does not add, scope questions only the author can answer. Report each as a "concern", never as a finding with an invented anchor
|
|
494
|
+
'5. After collecting anchored findings, deliberately scan for concerns with NO line to point at: deletion or cleanup plans for code the diff replaces, rollout or migration sequencing, coverage gaps the diff implies but does not add, scope questions only the author can answer. Report each as a "concern", never as a finding with an invented anchor. Every concern must list 1-3 exact changed evidencePaths that support it so later incremental reviews can recheck it when those files change. Report none when none exist, and never split one root concern into differently worded restatements.',
|
|
460
495
|
`6. Write a walkthrough: for every file whose evidence you examined, one factual sentence (<= ${MAX_WALKTHROUGH_SUMMARY_CHARS} chars) describing what changed in that file — written for a reader scanning the pull request, never restating the diff line by line. Use only paths from list_changed_files; invented paths are dropped.`,
|
|
461
|
-
'7. Then return ONLY a JSON object — no Markdown fences, no prose before or after — exactly this shape: {"summary": <string, 1-3 paragraphs of overall assessment>, "verdict": <"approve" | "comment" | "request-changes">, "findings": [{"path": <string, a changed file path>, "startLine": <integer, an R-marked new-file line>, "endLine": <integer, >= startLine, same file, R-marked>, "severity": <"blocking" | "important" | "nit">, "category": <OPTIONAL: "correctness" | "security" | "concurrency" | "performance" | "resources" | "error-handling" | "testing" | "maintainability" | "style" | "docs">, "title": <string, <= 120 chars>, "body": <string, why it matters and what to do>, "suggestion": <string, OPTIONAL: replacement text for exactly lines startLine..endLine, ready to commit>}], "concerns": <array, OPTIONAL: [{"severity": <"blocking" | "important" | "nit">, "title": <string, <= 120 chars>, "body": <string>}], only for step-5 concerns with no valid anchor — never duplicate a finding here>, "walkthrough": <array, OPTIONAL: [{"path": <string, a changed file path>, "summary": <string, the step-6 sentence>}], one entry per reviewed file>}.',
|
|
496
|
+
'7. Then return ONLY a JSON object — no Markdown fences, no prose before or after — exactly this shape: {"summary": <string, 1-3 paragraphs of overall assessment>, "verdict": <"approve" | "comment" | "request-changes">, "findings": [{"path": <string, a changed file path>, "startLine": <integer, an R-marked new-file line>, "endLine": <integer, >= startLine, same file, R-marked>, "severity": <"blocking" | "important" | "nit">, "category": <OPTIONAL: "correctness" | "security" | "concurrency" | "performance" | "resources" | "error-handling" | "testing" | "maintainability" | "style" | "docs">, "title": <string, <= 120 chars>, "body": <string, why it matters and what to do>, "suggestion": <string, OPTIONAL: replacement text for exactly lines startLine..endLine, ready to commit>}], "concerns": <array, OPTIONAL: [{"evidencePaths": <array of 1-3 exact changed file paths>, "severity": <"blocking" | "important" | "nit">, "title": <string, <= 120 chars>, "body": <string>}], only for step-5 concerns with no valid anchor — never duplicate a finding here>, "walkthrough": <array, OPTIONAL: [{"path": <string, a changed file path>, "summary": <string, the step-6 sentence>}], one entry per reviewed file>}.',
|
|
462
497
|
`Report at most ${maxFindings} findings and at most ${MAX_CONCERNS} concerns; prefer the most important ones. An empty findings array with verdict "approve" is a valid review. Include "suggestion" only when you are confident the replacement compiles and preserves intent; its text must contain the full replacement for every line in the range and nothing else.`,
|
|
463
498
|
'Use verdict "request-changes" only when at least one finding or concern is "blocking". Line anchors you invent will be discarded, so copy R-numbers from read_file_diff output.',
|
|
464
499
|
].join("\n");
|