@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.
Files changed (37) hide show
  1. package/README.md +83 -195
  2. package/dist/action.d.mts +27 -18
  3. package/dist/action.mjs +60 -39
  4. package/dist/action.mjs.map +1 -1
  5. package/dist/cli.mjs +3 -3
  6. package/dist/cli.mjs.map +1 -1
  7. package/dist/{fan-out-BJBTAYuh.d.mts → fan-out-CMEsbFLk.d.mts} +455 -177
  8. package/dist/{github-BbwYzNrC.mjs → github-NjgxGqwM.mjs} +2163 -1518
  9. package/dist/github-NjgxGqwM.mjs.map +1 -0
  10. package/dist/index.d.mts +30 -20
  11. package/dist/index.mjs +3 -3
  12. package/dist/{providers-NyP-4rS6.mjs → providers-CODZQCmL.mjs} +202 -80
  13. package/dist/providers-CODZQCmL.mjs.map +1 -0
  14. package/dist/testing.d.mts +3 -1
  15. package/dist/testing.mjs +3 -2
  16. package/dist/testing.mjs.map +1 -1
  17. package/package.json +2 -2
  18. package/src/action.ts +141 -78
  19. package/src/cli.ts +6 -1
  20. package/src/index.ts +1 -0
  21. package/src/internal/adjudication.ts +415 -0
  22. package/src/internal/coverage.ts +41 -60
  23. package/src/internal/factory.ts +4 -4
  24. package/src/internal/fan-out.ts +208 -14
  25. package/src/internal/fingerprint.ts +16 -10
  26. package/src/internal/fixtures.ts +6 -0
  27. package/src/internal/github-env.ts +9 -0
  28. package/src/internal/github.ts +243 -7
  29. package/src/internal/progress.ts +1 -1
  30. package/src/internal/render.ts +186 -42
  31. package/src/internal/retirement.ts +16 -17
  32. package/src/internal/review-agent.ts +39 -4
  33. package/src/internal/review-state.ts +315 -105
  34. package/src/internal/review-units.ts +10 -9
  35. package/src/internal/run.ts +197 -63
  36. package/dist/github-BbwYzNrC.mjs.map +0 -1
  37. 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-v\d+:[A-Za-z0-9+/]+={0,2}\.[0-9a-f]{64} -->/g;
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
- // Accepts both the pre-category first line (`**[⚠️ important] Title**`) and
118
- // the current one carrying an optional category chip (`… · security]`), so
119
- // retirement keeps matching inline comments posted by older package versions.
120
- const INLINE_FINDING_TITLE_PATTERN =
121
- /^\*\*\[(?:🛑 blocking|⚠️ important|💅 nit)(?: · [a-z-]+)?\] ([^\n]+)\*\*$/;
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) => !current.has(findingIdentity(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 never as an inline comment, so it needs no anchor and
365
- * is never demoted.
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; report none when none exist.',
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");