@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/README.md +83 -202
- package/dist/action.d.mts +25 -16
- package/dist/action.mjs +51 -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-Bi1v0VaU.d.mts → fan-out-CMEsbFLk.d.mts} +432 -206
- package/dist/{github-C6jrBLA2.mjs → github-NjgxGqwM.mjs} +2141 -1658
- package/dist/github-NjgxGqwM.mjs.map +1 -0
- package/dist/index.d.mts +20 -12
- package/dist/index.mjs +3 -3
- package/dist/{providers-2Jao2ZAX.mjs → providers-CODZQCmL.mjs} +192 -79
- package/dist/providers-CODZQCmL.mjs.map +1 -0
- package/dist/testing.d.mts +1 -1
- package/dist/testing.mjs +1 -1
- package/package.json +2 -2
- package/src/action.ts +123 -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/fan-out.ts +56 -4
- package/src/internal/github-env.ts +9 -0
- package/src/internal/github.ts +207 -0
- 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 +161 -42
- package/src/internal/review-units.ts +10 -9
- package/src/internal/run.ts +178 -64
- package/dist/github-C6jrBLA2.mjs.map +0 -1
- package/dist/providers-2Jao2ZAX.mjs.map +0 -1
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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 (
|
|
377
|
-
* `incomplete` with reasons that explicitly say the failure is
|
|
378
|
-
* uncertainty carried forward for retry, never an invitation to
|
|
379
|
-
*
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
545
|
+
reviewer.snapshot,
|
|
508
546
|
reviewer.profileFingerprint,
|
|
509
|
-
reviewer.fingerprint
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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.
|
|
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:
|
|
661
|
-
reviewReason:
|
|
662
|
-
filesInScope:
|
|
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
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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(
|
|
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";
|