@effect-agent/pr-review 0.1.0-beta.24 → 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/src/action.ts CHANGED
@@ -3,6 +3,8 @@ import { Config, Console, Effect, FileSystem, Layer, Option, Redacted, Schema }
3
3
  import { BudgetExceeded, UsageBudgetLimits } from "effect-agent";
4
4
  import { FetchHttpClient } from "effect/unstable/http";
5
5
 
6
+ import { collectReviewAdjudications } from "./internal/adjudication.ts";
7
+ import { splitCarriedScope } from "./internal/coverage.ts";
6
8
  import type { ChangedFile } from "./internal/diff.ts";
7
9
  import { InvalidEffortInput, parseEffortPosition, type EffortPosition } from "./internal/effort.ts";
8
10
  import { PrReview, type RunReviewOptions } from "./internal/factory.ts";
@@ -21,6 +23,10 @@ import {
21
23
  } from "./internal/providers.ts";
22
24
  import { retireStaleReviews } from "./internal/retirement.ts";
23
25
  import {
26
+ adjudicationIdentity,
27
+ concernIdentity,
28
+ findingIdentity,
29
+ fullReviewSelection,
24
30
  ReviewExecutionContext,
25
31
  ReviewHeadComparison,
26
32
  ReviewStateAuthenticator,
@@ -260,8 +266,6 @@ const outcomeOutputs = (
260
266
  ["verdict", outcome.review.verdict],
261
267
  ["input-coverage", outcome.inputCoverage.status],
262
268
  ["review-assurance", outcome.assurance.status],
263
- // Compatibility output: the precise outputs above own new integrations.
264
- ["coverage", outcome.coverage.status],
265
269
  ["review-mode", outcome.reviewMode ?? "full"],
266
270
  ["review-reason", outcome.reviewReason ?? "direct full review"],
267
271
  ["inline-comments", String(outcome.plan.comments.length)],
@@ -281,26 +285,34 @@ const outcomeSummary = (
281
285
  outcome: ReviewRunOutcome,
282
286
  modelLabel: string | undefined,
283
287
  conclusion: ReviewCheckConclusion,
284
- ): ReadonlyArray<string> => [
285
- "### Pull-request review",
286
- `- Check conclusion: **${conclusion}**`,
287
- `- Verdict: **${outcome.review.verdict}**`,
288
- `- Input coverage: **${outcome.inputCoverage.status}** · scope: ${outcome.reviewMode ?? "full"}`,
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}`,
290
- `- Inline comments: ${outcome.plan.comments.length} · demoted findings: ${outcome.plan.demoted.length} · concerns: ${outcome.review.concerns?.length ?? 0}`,
291
- ...(outcome.unreviewedPaths.length === 0
292
- ? []
293
- : [
294
- `- Carried forward: ${outcome.unreviewedPaths.length} unreviewed path(s) retried automatically on the next run (reviewer-side gap, not a code defect)`,
295
- ]),
296
- ...(modelLabel === undefined ? [] : [`- Model: \`${modelLabel}\``]),
297
- ...(outcome.usage === undefined
298
- ? []
299
- : [`- Tokens: ${outcome.usage.inputTokens} in / ${outcome.usage.outputTokens} out`]),
300
- ...(outcome.published === undefined
301
- ? ["- Dry run: nothing posted"]
302
- : [`- Posted: ${outcome.published.url}`]),
303
- ];
288
+ ): ReadonlyArray<string> => {
289
+ const scope = splitCarriedScope(outcome);
290
+ return [
291
+ "### Pull-request review",
292
+ `- Check conclusion: **${conclusion}**`,
293
+ `- Verdict: **${outcome.review.verdict}**`,
294
+ `- Input coverage: **${outcome.inputCoverage.status}** · scope: ${outcome.reviewMode ?? "full"}`,
295
+ `- 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}`,
296
+ `- Inline comments: ${outcome.plan.comments.length} · demoted findings: ${outcome.plan.demoted.length} · concerns: ${outcome.review.concerns?.length ?? 0}`,
297
+ ...(scope.retryablePaths.length === 0
298
+ ? []
299
+ : [
300
+ `- Carried forward: ${scope.retryablePaths.length} unreviewed path(s) retried automatically on the next run (reviewer-side gap, not a code defect)`,
301
+ ]),
302
+ ...(scope.undiffablePaths.length === 0
303
+ ? []
304
+ : [
305
+ `- Unreviewable: ${scope.undiffablePaths.length} path(s) with no reviewable diff (binary or oversized) — remove them from the pull request or exclude them with ignore globs`,
306
+ ]),
307
+ ...(modelLabel === undefined ? [] : [`- Model: \`${modelLabel}\``]),
308
+ ...(outcome.usage === undefined
309
+ ? []
310
+ : [`- Tokens: ${outcome.usage.inputTokens} in / ${outcome.usage.outputTokens} out`]),
311
+ ...(outcome.published === undefined
312
+ ? ["- Dry run: nothing posted"]
313
+ : [`- Posted: ${outcome.published.url}`]),
314
+ ];
315
+ };
304
316
 
305
317
  /** The result of one action invocation: a typed skip or a settled review. */
306
318
  export type ReviewActionResult =
@@ -333,30 +345,41 @@ const resolveRunUrl = Effect.fn("resolveRunUrl")(function* () {
333
345
  * The reviewer surface the action harness drives. `PrReview.make` and
334
346
  * `PrReview.makeFanOut` provide the state-selection effects. A fingerprint is
335
347
  * skip authority only when a profile fingerprint and authenticated review
336
- * state bind it to settled assurance; a fingerprint-only custom harness
337
- * remains source-compatible but deliberately runs instead of claiming green
338
- * assurance from unauthenticated prior-review text.
348
+ * state bind it to settled assurance.
339
349
  */
340
- export interface HarnessedReviewer<E, R, FingerprintE, FingerprintR> {
341
- readonly run: (runOptions?: RunReviewOptions) => Effect.Effect<ReviewRunOutcome, E, R>;
342
- /** Current effective changeset fingerprint; not standalone skip authority. */
343
- readonly fingerprint?: Effect.Effect<string, FingerprintE, FingerprintR> | undefined;
344
- readonly profileFingerprint?: Effect.Effect<string, FingerprintE, FingerprintR> | undefined;
345
- readonly snapshot?:
346
- | Effect.Effect<
347
- {
348
- readonly metadata: PullRequestMetadata;
349
- readonly files: ReadonlyArray<ChangedFile>;
350
- },
351
- FingerprintE,
352
- FingerprintR
353
- >
354
- | undefined;
355
- readonly filterFiles?:
356
- | ((files: ReadonlyArray<ChangedFile>) => ReadonlyArray<ChangedFile>)
357
- | undefined;
350
+ interface HarnessedReviewerBase<E, R> {
351
+ /** The action composition root always supplies the selected run context. */
352
+ readonly run: (
353
+ runOptions?: RunReviewOptions,
354
+ ) => Effect.Effect<ReviewRunOutcome, E, R | ReviewExecutionContext>;
358
355
  }
359
356
 
357
+ export type HarnessedReviewer<E, R, FingerprintE, FingerprintR> = HarnessedReviewerBase<E, R> &
358
+ (
359
+ | {
360
+ readonly fingerprint?: undefined;
361
+ readonly profileFingerprint?: undefined;
362
+ readonly snapshot?: undefined;
363
+ readonly filterFiles?: undefined;
364
+ }
365
+ | {
366
+ /** Current effective changeset fingerprint; not standalone skip authority. */
367
+ readonly fingerprint: Effect.Effect<string, FingerprintE, FingerprintR>;
368
+ readonly profileFingerprint: Effect.Effect<string, FingerprintE, FingerprintR>;
369
+ readonly snapshot: Effect.Effect<
370
+ {
371
+ readonly metadata: PullRequestMetadata;
372
+ readonly files: ReadonlyArray<ChangedFile>;
373
+ },
374
+ FingerprintE,
375
+ FingerprintR
376
+ >;
377
+ readonly filterFiles?:
378
+ | ((files: ReadonlyArray<ChangedFile>) => ReadonlyArray<ChangedFile>)
379
+ | undefined;
380
+ }
381
+ );
382
+
360
383
  const blockingReasons = (input: {
361
384
  readonly findings: ReadonlyArray<{ readonly severity: string; readonly title: string }>;
362
385
  readonly concerns: ReadonlyArray<{ readonly severity: string; readonly title: string }>;
@@ -373,10 +396,13 @@ const blockingReasons = (input: {
373
396
  * Host-derived check conclusion; model verdict prose cannot weaken it.
374
397
  *
375
398
  * 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.
399
+ * signal. A retryable machinery gap (failed passes, capacity overflow)
400
+ * concludes `incomplete` with reasons that explicitly say the failure is
401
+ * reviewer-side uncertainty carried forward for retry, never an invitation to
402
+ * change code. Undiffable files are the deliberate exception: no retry can
403
+ * settle them, so their reason instructs removal or ignore globs instead of
404
+ * promising an automatic retry. The flat reviewer's constant `unverified`
405
+ * assurance is not a gap.
380
406
  */
381
407
  export const concludeReviewOutcome = (
382
408
  outcome: ReviewRunOutcome,
@@ -386,11 +412,19 @@ export const concludeReviewOutcome = (
386
412
  } => {
387
413
  const machinery: Array<string> = [];
388
414
  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
- );
415
+ const scope = splitCarriedScope(outcome);
416
+ if (scope.retryableGap) {
417
+ machinery.push(
418
+ scope.retryablePaths.length > 0
419
+ ? `review infrastructure did not settle — a reviewer-side gap, not a code defect; ${scope.retryablePaths.length} path(s) are carried forward and retried automatically on the next run`
420
+ : "review infrastructure did not settle — a reviewer-side gap, not a code defect",
421
+ );
422
+ }
423
+ if (scope.undiffablePaths.length > 0) {
424
+ machinery.push(
425
+ `${scope.undiffablePaths.length} path(s) have no reviewable diff (binary or oversized) and no retry can settle them — remove them from the pull request or exclude them with ignore globs`,
426
+ );
427
+ }
394
428
  if (outcome.inputCoverage.status === "incomplete") {
395
429
  machinery.push(...outcome.inputCoverage.reasons);
396
430
  }
@@ -409,10 +443,14 @@ export const concludeReviewOutcome = (
409
443
  return { conclusion: "success", reasons: [] };
410
444
  };
411
445
 
412
- const concludeReviewState = (state: ReviewState) => {
446
+ const concludeReviewState = (state: ReviewState, adjudicated: ReadonlySet<string>) => {
413
447
  const reasons = blockingReasons({
414
- findings: state.unresolvedFindings,
415
- concerns: state.unresolvedConcerns,
448
+ findings: state.unresolvedFindings.filter(
449
+ (finding) => !adjudicated.has(findingIdentity(finding)),
450
+ ),
451
+ concerns: state.unresolvedConcerns.filter(
452
+ (concern) => !adjudicated.has(concernIdentity(concern)),
453
+ ),
416
454
  });
417
455
  return reasons.length > 0
418
456
  ? ({ conclusion: "blocking", reasons } as const)
@@ -424,20 +462,21 @@ const skipCoveredReview = Effect.fn("skipCoveredReview")(function* (input: {
424
462
  readonly pullRequestNumber: number;
425
463
  readonly reason: string;
426
464
  readonly state: ReviewState;
427
- readonly fingerprint?: string | undefined;
428
465
  }) {
429
- const result = concludeReviewState(input.state);
466
+ // A maintainer adjudication must lift a preserved blocking conclusion
467
+ // without a code push, so the skip path re-reads adjudications (fail-open;
468
+ // stored ones survive a listing fault) before enforcing the stored state.
469
+ const adjudications = yield* collectReviewAdjudications(input.state.adjudications ?? []);
470
+ const result = concludeReviewState(input.state, new Set(adjudications.map(adjudicationIdentity)));
430
471
  yield* Console.log(
431
472
  `Skipping review of ${input.repository}#${input.pullRequestNumber}: ${input.reason}.`,
432
473
  );
433
474
  yield* writeActionOutputs([
434
475
  ["skipped", "true"],
435
476
  ["skip-reason", input.reason],
436
- ...(input.fingerprint === undefined ? [] : ([["fingerprint", input.fingerprint]] as const)),
437
477
  ["conclusion", result.conclusion],
438
478
  ["input-coverage", "complete"],
439
479
  ["review-assurance", "settled"],
440
- ["coverage", "complete"],
441
480
  ["review-mode", "incremental"],
442
481
  ]);
443
482
  yield* writeStepSummary([
@@ -502,16 +541,13 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
502
541
  return yield* Effect.gen(function* () {
503
542
  let selection: ReturnType<typeof selectReviewRange> | undefined;
504
543
  if (reviewer.profileFingerprint !== undefined) {
505
- const source = yield* PullRequestSource;
506
544
  const [snapshot, profileFingerprint, currentFingerprint] = yield* Effect.all([
507
- reviewer.snapshot ?? Effect.all({ metadata: source.metadata, files: source.anchorFiles }),
545
+ reviewer.snapshot,
508
546
  reviewer.profileFingerprint,
509
- reviewer.fingerprint === undefined
510
- ? Effect.succeed(undefined)
511
- : reviewer.fingerprint.pipe(
512
- Effect.map((fingerprint): string | undefined => fingerprint),
513
- Effect.orElseSucceed(() => undefined),
514
- ),
547
+ reviewer.fingerprint.pipe(
548
+ Effect.map((fingerprint): string | undefined => fingerprint),
549
+ Effect.orElseSucceed(() => undefined),
550
+ ),
515
551
  ]);
516
552
  const { metadata, files: fullFiles } = snapshot;
517
553
  const history = options.priorReviews ?? (yield* PriorReviews);
@@ -542,7 +578,7 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
542
578
  recovered.state.settled &&
543
579
  currentFingerprint !== undefined &&
544
580
  validateReviewState(recovered.state, metadata, profileFingerprint) === undefined &&
545
- recovered.state.acceptedScopeFingerprint === currentFingerprint
581
+ recovered.state.settledScopeFingerprint === currentFingerprint
546
582
  ? recovered.state
547
583
  : undefined;
548
584
  if (equivalentPatchState !== undefined) {
@@ -554,7 +590,6 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
554
590
  ? "the current head already has settled stored review assurance"
555
591
  : "the effective pull-request patch is unchanged since the last settled review",
556
592
  state: equivalentPatchState,
557
- fingerprint: currentFingerprint,
558
593
  });
559
594
  }
560
595
  let comparison: ReviewHeadComparison | undefined;
@@ -641,6 +676,17 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
641
676
  });
642
677
  }
643
678
  }
679
+ const executionContext =
680
+ selection ??
681
+ (yield* Effect.gen(function* () {
682
+ const source = yield* PullRequestSource;
683
+ const [metadata, files] = yield* Effect.all([source.metadata, source.changedFiles]);
684
+ return fullReviewSelection({
685
+ reason: "explicit custom-reviewer full review without continuity selection",
686
+ files,
687
+ totalFiles: metadata.totalChangedFiles,
688
+ });
689
+ }));
644
690
  yield* Console.log(
645
691
  `Reviewing ${target.repository}#${target.number} (${options.post === false ? "dry run" : "posting"})...`,
646
692
  );
@@ -657,9 +703,9 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
657
703
  const headMetadata = yield* source.metadata.pipe(Effect.orElseSucceed(() => undefined));
658
704
  yield* progress.value.begin({
659
705
  headSha: headMetadata?.headSha,
660
- reviewMode: selection?.mode,
661
- reviewReason: selection?.reason,
662
- filesInScope: selection?.files.length,
706
+ reviewMode: executionContext.mode,
707
+ reviewReason: executionContext.reason,
708
+ filesInScope: executionContext.files.length,
663
709
  modelLabel: options.modelLabel,
664
710
  runUrl,
665
711
  });
@@ -676,12 +722,11 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
676
722
  ),
677
723
  )
678
724
  : runReview;
679
- const outcome = yield* selection === undefined
680
- ? reviewEffect
681
- : reviewEffect.pipe(
682
- Effect.provide(selectedPullRequestSourceLayer(selection)),
683
- Effect.provideService(ReviewExecutionContext, selection),
684
- );
725
+ const executionLayer = Layer.merge(
726
+ Layer.succeed(ReviewExecutionContext)(executionContext),
727
+ selectedPullRequestSourceLayer(executionContext),
728
+ );
729
+ const outcome = yield* reviewEffect.pipe(Effect.provide(executionLayer));
685
730
  yield* Console.log(
686
731
  `Review finished in ${outcome.turns} turn(s): verdict ${outcome.review.verdict}, ` +
687
732
  `${outcome.plan.comments.length} inline comment(s), ${outcome.plan.demoted.length} demoted finding(s).`,
package/src/cli.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  type ReviewProvider,
20
20
  } from "./internal/providers.ts";
21
21
  import { ReviewPublicationPlan } from "./internal/render.ts";
22
+ import { fullReviewExecutionContextLayer } from "./internal/review-state.ts";
22
23
  import type { ReviewRunOutcome } from "./internal/run.ts";
23
24
 
24
25
  // ---------------------------------------------------------------------------
@@ -158,7 +159,11 @@ const command = CliCommand.make(
158
159
  return Option.none<ReviewRunOutcome>();
159
160
  }
160
161
  }
161
- return Option.some(yield* reviewer.run({ post: flags.post }));
162
+ return Option.some(
163
+ yield* reviewer
164
+ .run({ post: flags.post })
165
+ .pipe(Effect.provide(fullReviewExecutionContextLayer("explicit CLI full review"))),
166
+ );
162
167
  });
163
168
 
164
169
  let result: Option.Option<ReviewRunOutcome>;
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // The @effect-agent/pr-review public surface. Platform-free: host entrypoints
2
2
  // live behind the "./action" and "./cli" subpaths, deterministic test helpers
3
3
  // behind "./testing".
4
+ export * from "./internal/adjudication.ts";
4
5
  export * from "./internal/anchors.ts";
5
6
  export * from "./internal/diff.ts";
6
7
  export * from "./internal/coverage.ts";