@effect-agent/pr-review 0.1.0-beta.18 → 0.1.0-beta.20

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/dist/testing.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { Cn as ChangedFile, Rt as CodeReview, Sn as normalizeRepoRelativePath, _n as MAX_FILE_CHARS, a as PriorReviews, i as PriorReviewLookupFailure, o as PublishedReview, rt as FileReviewReport, s as ReviewPublisher, vn as PullRequestMetadata, xn as ReviewInputViolation, yn as PullRequestSource } from "./github-mmanX6hk.mjs";
1
+ import { Cn as ChangedFile, Rt as CodeReview, Sn as normalizeRepoRelativePath, _n as MAX_FILE_CHARS, a as PriorReviews, i as PriorReviewLookupFailure, o as PublishedReview, rt as FileReviewReport, s as ReviewPublisher, vn as PullRequestMetadata, xn as ReviewInputViolation, yn as PullRequestSource } from "./github-DnenG3be.mjs";
2
2
  import { DateTime, Effect, Layer, Option, Ref, Schema, Stream } from "effect";
3
3
  import { LanguageModel, Model } from "effect/unstable/ai";
4
4
  //#region src/internal/scripted.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-agent/pr-review",
3
- "version": "0.1.0-beta.18",
3
+ "version": "0.1.0-beta.20",
4
4
  "exports": {
5
5
  ".": {
6
6
  "types": "./dist/index.d.mts",
@@ -20,11 +20,11 @@
20
20
  }
21
21
  },
22
22
  "dependencies": {
23
- "@effect/ai-anthropic": "4.0.0-beta.107",
24
- "@effect/ai-openai": "4.0.0-beta.107",
25
- "@effect/platform-node": "4.0.0-beta.107",
26
- "effect": "4.0.0-beta.107",
27
- "effect-agent": "0.1.0-beta.18"
23
+ "@effect/ai-anthropic": "4.0.0-rc.110",
24
+ "@effect/ai-openai": "4.0.0-rc.110",
25
+ "@effect/platform-node": "4.0.0-rc.110",
26
+ "effect": "4.0.0-rc.110",
27
+ "effect-agent": "0.1.0-beta.20"
28
28
  },
29
29
  "description": "Bounded, fail-closed GitHub pull-request reviewer built on the effect-agent public surface: schema-first review contracts, read-only tools, GitHub adapters, a configuration factory, and CLI + GitHub Actions entrypoints.",
30
30
  "license": "MIT",
@@ -47,7 +47,7 @@
47
47
  "test": "vp test --passWithNoTests"
48
48
  },
49
49
  "devDependencies": {
50
- "@effect/vitest": "4.0.0-beta.107",
50
+ "@effect/vitest": "4.0.0-rc.110",
51
51
  "typescript": "7.0.2",
52
52
  "vite-plus": "0.2.6"
53
53
  }
package/src/action.ts CHANGED
@@ -28,6 +28,7 @@ import {
28
28
  selectReviewRange,
29
29
  selectedPullRequestSourceLayer,
30
30
  unavailableReviewStateAuthenticatorLayer,
31
+ validateReviewState,
31
32
  webCryptoReviewStateAuthenticatorLayer,
32
33
  } from "./internal/review-state.ts";
33
34
  import { fanOutReviewBudgetLimits, reviewBudgetLimits } from "./internal/run.ts";
@@ -391,6 +392,39 @@ const concludeReviewState = (state: ReviewState) => {
391
392
  : ({ conclusion: "success", reasons: [] } as const);
392
393
  };
393
394
 
395
+ const skipCoveredReview = Effect.fn("skipCoveredReview")(function* (input: {
396
+ readonly repository: string;
397
+ readonly pullRequestNumber: number;
398
+ readonly reason: string;
399
+ readonly state: ReviewState;
400
+ readonly fingerprint?: string | undefined;
401
+ }) {
402
+ const result = concludeReviewState(input.state);
403
+ yield* Console.log(
404
+ `Skipping review of ${input.repository}#${input.pullRequestNumber}: ${input.reason}.`,
405
+ );
406
+ yield* writeActionOutputs([
407
+ ["skipped", "true"],
408
+ ["skip-reason", input.reason],
409
+ ...(input.fingerprint === undefined ? [] : ([["fingerprint", input.fingerprint]] as const)),
410
+ ["conclusion", result.conclusion],
411
+ ["coverage", "complete"],
412
+ ["review-mode", "incremental"],
413
+ ]);
414
+ yield* writeStepSummary([
415
+ "### Pull-request review skipped",
416
+ `- Reason: ${input.reason}`,
417
+ `- Preserved check conclusion: **${result.conclusion}**`,
418
+ ]);
419
+ if (result.conclusion === "blocking") {
420
+ return yield* ReviewGateFailed.make({
421
+ conclusion: "blocking",
422
+ reasons: result.reasons,
423
+ });
424
+ }
425
+ return { _tag: "Skipped", reason: input.reason } satisfies ReviewActionResult;
426
+ });
427
+
394
428
  /**
395
429
  * Harness one already-built reviewer inside the Actions environment: resolve
396
430
  * the target from the event, provide the GitHub source/publisher/prior
@@ -441,9 +475,15 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
441
475
  let selection: ReturnType<typeof selectReviewRange> | undefined;
442
476
  if (reviewer.profileFingerprint !== undefined) {
443
477
  const source = yield* PullRequestSource;
444
- const [snapshot, profileFingerprint] = yield* Effect.all([
478
+ const [snapshot, profileFingerprint, currentFingerprint] = yield* Effect.all([
445
479
  reviewer.snapshot ?? Effect.all({ metadata: source.metadata, files: source.anchorFiles }),
446
480
  reviewer.profileFingerprint,
481
+ reviewer.fingerprint === undefined
482
+ ? Effect.succeed(undefined)
483
+ : reviewer.fingerprint.pipe(
484
+ Effect.map((fingerprint): string | undefined => fingerprint),
485
+ Effect.orElseSucceed(() => undefined),
486
+ ),
447
487
  ]);
448
488
  const { metadata, files: fullFiles } = snapshot;
449
489
  const history = options.priorReviews ?? (yield* PriorReviews);
@@ -465,6 +505,27 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
465
505
  }),
466
506
  }),
467
507
  );
508
+ const equivalentPatchState =
509
+ (options.reviewMode ?? "incremental") === "incremental" &&
510
+ options.skipUnchanged !== false &&
511
+ recovered.state !== undefined &&
512
+ currentFingerprint !== undefined &&
513
+ validateReviewState(recovered.state, metadata, profileFingerprint) === undefined &&
514
+ recovered.state.acceptedScopeFingerprint === currentFingerprint
515
+ ? recovered.state
516
+ : undefined;
517
+ if (equivalentPatchState !== undefined) {
518
+ return yield* skipCoveredReview({
519
+ repository: target.repository,
520
+ pullRequestNumber: target.number,
521
+ reason:
522
+ equivalentPatchState.reviewedHeadSha === metadata.headSha
523
+ ? "the current head already has complete stored review coverage"
524
+ : "the effective pull-request patch is unchanged since the last complete review",
525
+ state: equivalentPatchState,
526
+ fingerprint: currentFingerprint,
527
+ });
528
+ }
468
529
  let comparison: ReviewHeadComparison | undefined;
469
530
  let baseComparison: ReviewHeadComparison | undefined;
470
531
  if (
@@ -522,30 +583,13 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
522
583
  selection.files.length === 0 &&
523
584
  selection.priorState !== undefined
524
585
  ) {
525
- const result = concludeReviewState(selection.priorState);
526
586
  const reason = "no changed review scope since the last successfully reviewed head";
527
- yield* Console.log(
528
- `Skipping review of ${target.repository}#${target.number}: ${reason}.`,
529
- );
530
- yield* writeActionOutputs([
531
- ["skipped", "true"],
532
- ["skip-reason", reason],
533
- ["conclusion", result.conclusion],
534
- ["coverage", "complete"],
535
- ["review-mode", "incremental"],
536
- ]);
537
- yield* writeStepSummary([
538
- "### Pull-request review skipped",
539
- `- Reason: ${reason}`,
540
- `- Preserved check conclusion: **${result.conclusion}**`,
541
- ]);
542
- if (result.conclusion === "blocking") {
543
- return yield* ReviewGateFailed.make({
544
- conclusion: "blocking",
545
- reasons: result.reasons,
546
- });
547
- }
548
- return { _tag: "Skipped", reason } satisfies ReviewActionResult;
587
+ return yield* skipCoveredReview({
588
+ repository: target.repository,
589
+ pullRequestNumber: target.number,
590
+ reason,
591
+ state: selection.priorState,
592
+ });
549
593
  }
550
594
  } else if (options.skipUnchanged !== false && reviewer.fingerprint !== undefined) {
551
595
  // Preserve the pre-state custom harness contract explicitly. The
package/src/cli.ts CHANGED
@@ -53,14 +53,17 @@ const effortFlag = Flag.string("effort").pipe(
53
53
  ),
54
54
  );
55
55
  const postFlag = Flag.boolean("post").pipe(
56
+ Flag.withDefault(false),
56
57
  Flag.withDescription("Post the review to GitHub; without it the plan prints to stdout."),
57
58
  );
58
59
  const applyVerdictFlag = Flag.boolean("apply-verdict").pipe(
60
+ Flag.withDefault(false),
59
61
  Flag.withDescription(
60
62
  "Map the model verdict onto APPROVE/REQUEST_CHANGES instead of always COMMENT.",
61
63
  ),
62
64
  );
63
65
  const fanOutFlag = Flag.boolean("fan-out").pipe(
66
+ Flag.withDefault(false),
64
67
  Flag.withDescription(
65
68
  "Fan the review out to bounded per-unit subagent reviewers (S1 attached delegation) instead of one flat reviewer.",
66
69
  ),
@@ -76,6 +79,7 @@ const maxFindingsFlag = Flag.integer("max-findings").pipe(
76
79
  Flag.withDescription("Findings bound (1-20); the schema cap of 20 applies regardless."),
77
80
  );
78
81
  const skipUnchangedFlag = Flag.boolean("skip-unchanged").pipe(
82
+ Flag.withDefault(false),
79
83
  Flag.withDescription(
80
84
  "Skip when the changeset fingerprint matches the last posted review (explicit runs review unconditionally by default).",
81
85
  ),
@@ -49,6 +49,15 @@ const FIELD = "\u0000";
49
49
  const RECORD = "\u0001";
50
50
  const SECTION = "\u0002";
51
51
 
52
+ /**
53
+ * Unified-diff hunk coordinates describe where a patch applies, not what it
54
+ * changes. A content-equivalent rebase can shift both coordinates while
55
+ * leaving every context/addition/deletion line unchanged, so exclude only
56
+ * those coordinates from the canonical patch representation.
57
+ */
58
+ const canonicalPatch = (patch: string): string =>
59
+ patch.replace(/^@@ -\d+(?:,\d+)? \+\d+(?:,\d+)? @@/gm, "@@ -_ +_ @@");
60
+
52
61
  /**
53
62
  * Canonical changeset encoding: sorted by path so provider ordering never
54
63
  * matters, with every review-relevant field of every file.
@@ -57,7 +66,7 @@ const canonicalChangeset = (files: ReadonlyArray<ChangedFile>): string =>
57
66
  files
58
67
  .map(
59
68
  (file) =>
60
- `${file.path}${FIELD}${file.status}${FIELD}${String(file.additions)}${FIELD}${String(file.deletions)}${FIELD}${file.patch ?? ""}${FIELD}${file.reviewBaseContent ?? ""}${FIELD}${file.reviewHeadContent ?? ""}`,
69
+ `${file.path}${FIELD}${file.status}${FIELD}${String(file.additions)}${FIELD}${String(file.deletions)}${FIELD}${file.patch === undefined ? "" : canonicalPatch(file.patch)}${FIELD}${file.reviewBaseContent ?? ""}${FIELD}${file.reviewHeadContent ?? ""}`,
61
70
  )
62
71
  .sort()
63
72
  .join(RECORD);