@effect-agent/pr-review 0.1.0-beta.22 → 0.1.0-beta.24

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/src/action.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  ReviewExecutionContext,
25
25
  ReviewHeadComparison,
26
26
  ReviewStateAuthenticator,
27
+ isLineageAncestor,
27
28
  type ReviewMode,
28
29
  type ReviewState,
29
30
  selectReviewRange,
@@ -51,10 +52,6 @@ import {
51
52
  // nothing.
52
53
  // ---------------------------------------------------------------------------
53
54
 
54
- /** Deprecated compatibility input; host-derived conclusions are unconditional. */
55
- export const FailOnPolicy = Schema.Literals(["never", "request-changes"]);
56
- export type FailOnPolicy = typeof FailOnPolicy.Type;
57
-
58
55
  export const ReviewCheckConclusion = Schema.Literals(["success", "blocking", "incomplete"]);
59
56
  export type ReviewCheckConclusion = typeof ReviewCheckConclusion.Type;
60
57
 
@@ -96,8 +93,6 @@ export interface ResolvedActionInputs {
96
93
  readonly maxFindings: number | undefined;
97
94
  readonly maxDurationMinutes: number | undefined;
98
95
  readonly reviewMode: ReviewMode;
99
- /** Deprecated compatibility input; conclusions are always conservative. */
100
- readonly failOn: FailOnPolicy;
101
96
  readonly skipUnchanged: boolean;
102
97
  readonly retireStaleReviews: boolean;
103
98
  readonly progressComment: boolean;
@@ -135,9 +130,6 @@ export const resolveActionInputs = Effect.fn("resolveActionInputs")(function* ()
135
130
  const reviewMode = yield* Config.literals(["incremental", "final"], "PR_REVIEW_MODE").pipe(
136
131
  Config.withDefault<ReviewMode>("incremental"),
137
132
  );
138
- const failOn = yield* Config.literals(["never", "request-changes"], "PR_REVIEW_FAIL_ON").pipe(
139
- Config.withDefault<FailOnPolicy>("never"),
140
- );
141
133
  const skipUnchanged = yield* Config.boolean("PR_REVIEW_SKIP_UNCHANGED").pipe(
142
134
  Config.withDefault(true),
143
135
  );
@@ -163,7 +155,6 @@ export const resolveActionInputs = Effect.fn("resolveActionInputs")(function* ()
163
155
  maxFindings: Option.getOrUndefined(maxFindings),
164
156
  maxDurationMinutes,
165
157
  reviewMode,
166
- failOn,
167
158
  skipUnchanged,
168
159
  retireStaleReviews,
169
160
  progressComment,
@@ -276,6 +267,7 @@ const outcomeOutputs = (
276
267
  ["inline-comments", String(outcome.plan.comments.length)],
277
268
  ["demoted-findings", String(outcome.plan.demoted.length)],
278
269
  ["concerns", String(outcome.review.concerns?.length ?? 0)],
270
+ ["unreviewed-paths", String(outcome.unreviewedPaths.length)],
279
271
  ...(outcome.usage === undefined
280
272
  ? []
281
273
  : ([
@@ -296,14 +288,15 @@ const outcomeSummary = (
296
288
  `- Input coverage: **${outcome.inputCoverage.status}** · scope: ${outcome.reviewMode ?? "full"}`,
297
289
  `- Review assurance: **${outcome.assurance.status}** · general discovery ${outcome.assurance.completedGeneralDiscoveryPasses}/${outcome.assurance.requiredGeneralDiscoveryPasses} · specialist ${outcome.assurance.completedSpecialistPasses}/${outcome.assurance.requiredSpecialistPasses} · verification ${outcome.assurance.completedVerificationPasses}/${outcome.assurance.requiredVerificationPasses}`,
298
290
  `- Inline comments: ${outcome.plan.comments.length} · demoted findings: ${outcome.plan.demoted.length} · concerns: ${outcome.review.concerns?.length ?? 0}`,
299
- ...(modelLabel === undefined ? [] : [`- Model: \`${modelLabel}\``]),
300
- ...(outcome.usage === undefined
291
+ ...(outcome.unreviewedPaths.length === 0
301
292
  ? []
302
293
  : [
303
- `- Tokens: ${outcome.usage.inputTokens} in / ${outcome.usage.outputTokens} out${
304
- outcome.usageScope === "coordinator" ? " (coordinator)" : ""
305
- }`,
294
+ `- Carried forward: ${outcome.unreviewedPaths.length} unreviewed path(s) retried automatically on the next run (reviewer-side gap, not a code defect)`,
306
295
  ]),
296
+ ...(modelLabel === undefined ? [] : [`- Model: \`${modelLabel}\``]),
297
+ ...(outcome.usage === undefined
298
+ ? []
299
+ : [`- Tokens: ${outcome.usage.inputTokens} in / ${outcome.usage.outputTokens} out`]),
307
300
  ...(outcome.published === undefined
308
301
  ? ["- Dry run: nothing posted"]
309
302
  : [`- Posted: ${outcome.published.url}`]),
@@ -376,32 +369,44 @@ const blockingReasons = (input: {
376
369
  .map((concern) => `blocking concern: ${concern.title}`),
377
370
  ];
378
371
 
379
- /** Host-derived check conclusion; model verdict prose cannot weaken it. */
372
+ /**
373
+ * Host-derived check conclusion; model verdict prose cannot weaken it.
374
+ *
375
+ * Blocking code findings outrank machinery gaps — they are the actionable
376
+ * signal. A machinery gap (incomplete input, unsettled passes) concludes
377
+ * `incomplete` with reasons that explicitly say the failure is reviewer-side
378
+ * uncertainty carried forward for retry, never an invitation to change code.
379
+ * The flat reviewer's constant `unverified` assurance is not a gap.
380
+ */
380
381
  export const concludeReviewOutcome = (
381
382
  outcome: ReviewRunOutcome,
382
383
  ): {
383
384
  readonly conclusion: ReviewCheckConclusion;
384
385
  readonly reasons: ReadonlyArray<string>;
385
386
  } => {
386
- if (outcome.inputCoverage.status === "incomplete") {
387
- return { conclusion: "incomplete", reasons: outcome.inputCoverage.reasons };
388
- }
389
- if (outcome.assurance.status !== "settled") {
390
- return {
391
- conclusion: "incomplete",
392
- reasons:
393
- outcome.assurance.reasons.length > 0
394
- ? outcome.assurance.reasons
395
- : ["configured discovery and verification work did not settle"],
396
- };
387
+ const machinery: Array<string> = [];
388
+ if (outcome.inputCoverage.status === "incomplete" || outcome.assurance.status === "incomplete") {
389
+ machinery.push(
390
+ outcome.unreviewedPaths.length > 0
391
+ ? `review infrastructure did not settle — a reviewer-side gap, not a code defect; ${outcome.unreviewedPaths.length} path(s) are carried forward and retried automatically on the next run`
392
+ : "review infrastructure did not settle — a reviewer-side gap, not a code defect",
393
+ );
394
+ if (outcome.inputCoverage.status === "incomplete") {
395
+ machinery.push(...outcome.inputCoverage.reasons);
396
+ }
397
+ if (outcome.assurance.status === "incomplete") {
398
+ machinery.push(...outcome.assurance.reasons);
399
+ }
397
400
  }
398
- const reasons = blockingReasons({
401
+ const blocking = blockingReasons({
399
402
  findings: outcome.activeFindings,
400
403
  concerns: outcome.activeConcerns,
401
404
  });
402
- return reasons.length > 0
403
- ? { conclusion: "blocking", reasons }
404
- : { conclusion: "success", reasons };
405
+ if (blocking.length > 0) {
406
+ return { conclusion: "blocking", reasons: [...blocking, ...machinery] };
407
+ }
408
+ if (machinery.length > 0) return { conclusion: "incomplete", reasons: machinery };
409
+ return { conclusion: "success", reasons: [] };
405
410
  };
406
411
 
407
412
  const concludeReviewState = (state: ReviewState) => {
@@ -462,7 +467,6 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
462
467
  reviewer: HarnessedReviewer<E, R, FingerprintE, FingerprintR>,
463
468
  options: {
464
469
  readonly post?: boolean | undefined;
465
- readonly failOn?: FailOnPolicy | undefined;
466
470
  /** Skip model execution when the current head already has complete stored coverage. */
467
471
  readonly skipUnchanged?: boolean | undefined;
468
472
  /** Incremental by default; `final` deliberately re-reviews the full PR diff. */
@@ -533,6 +537,9 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
533
537
  (options.reviewMode ?? "incremental") === "incremental" &&
534
538
  options.skipUnchanged !== false &&
535
539
  recovered.state !== undefined &&
540
+ // Only a fully settled run may be skipped over: an unsettled state
541
+ // carries retryable scope the next run must actually retry.
542
+ recovered.state.settled &&
536
543
  currentFingerprint !== undefined &&
537
544
  validateReviewState(recovered.state, metadata, profileFingerprint) === undefined &&
538
545
  recovered.state.acceptedScopeFingerprint === currentFingerprint
@@ -552,6 +559,7 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
552
559
  }
553
560
  let comparison: ReviewHeadComparison | undefined;
554
561
  let baseComparison: ReviewHeadComparison | undefined;
562
+ let contentComparison: ReviewHeadComparison | undefined;
555
563
  if (
556
564
  (options.reviewMode ?? "incremental") === "incremental" &&
557
565
  recovered.state !== undefined
@@ -587,6 +595,21 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
587
595
  });
588
596
  }
589
597
  }
598
+ if (
599
+ recovered.state.reviewedHeadSha !== metadata.headSha &&
600
+ (comparison === undefined ||
601
+ !isLineageAncestor(comparison, recovered.state, metadata.headSha))
602
+ ) {
603
+ contentComparison = yield* history
604
+ .compareTrees(recovered.state.reviewedHeadSha, metadata.headSha)
605
+ .pipe(Effect.orElseSucceed(() => undefined));
606
+ if (contentComparison !== undefined && reviewer.filterFiles !== undefined) {
607
+ contentComparison = ReviewHeadComparison.make({
608
+ ...contentComparison,
609
+ files: reviewer.filterFiles(contentComparison.files),
610
+ });
611
+ }
612
+ }
590
613
  }
591
614
  selection = {
592
615
  ...selectReviewRange({
@@ -597,6 +620,7 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
597
620
  priorState: recovered.state,
598
621
  comparison,
599
622
  baseComparison,
623
+ contentComparison,
600
624
  lookupFailure: recovered.failure,
601
625
  }),
602
626
  stateAuthenticator,
@@ -605,7 +629,8 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
605
629
  options.skipUnchanged !== false &&
606
630
  selection.mode === "incremental" &&
607
631
  selection.files.length === 0 &&
608
- selection.priorState !== undefined
632
+ selection.priorState !== undefined &&
633
+ selection.priorState.settled
609
634
  ) {
610
635
  const reason = "no changed review scope since the last settled review head";
611
636
  return yield* skipCoveredReview({
@@ -747,7 +772,6 @@ export const reviewActionProgram = Effect.gen(function* () {
747
772
  };
748
773
  const harness = {
749
774
  post: inputs.post,
750
- failOn: inputs.failOn,
751
775
  skipUnchanged: inputs.skipUnchanged,
752
776
  reviewMode: inputs.reviewMode,
753
777
  retireStaleReviews: inputs.retireStaleReviews,
@@ -23,7 +23,6 @@ const INPUT_TO_ENV: ReadonlyArray<readonly [input: string, env: string]> = [
23
23
  ["INPUT_IGNORE", "PR_REVIEW_IGNORE"],
24
24
  ["INPUT_MAX-FINDINGS", "PR_REVIEW_MAX_FINDINGS"],
25
25
  ["INPUT_REVIEW-MODE", "PR_REVIEW_MODE"],
26
- ["INPUT_FAIL-ON", "PR_REVIEW_FAIL_ON"],
27
26
  ["INPUT_SKIP-UNCHANGED", "PR_REVIEW_SKIP_UNCHANGED"],
28
27
  ["INPUT_RETIRE-STALE-REVIEWS", "PR_REVIEW_RETIRE_STALE_REVIEWS"],
29
28
  ["INPUT_PROGRESS-COMMENT", "PR_REVIEW_PROGRESS_COMMENT"],