@effect-agent/pr-review 0.1.0-beta.25 → 0.1.0-beta.27
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 +26 -3
- package/dist/action.d.mts +11 -8
- package/dist/action.mjs +24 -9
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +7 -4
- package/dist/cli.mjs.map +1 -1
- package/dist/{fan-out-CMEsbFLk.d.mts → fan-out-C3yG1cx3.d.mts} +45 -20
- package/dist/{github-NjgxGqwM.mjs → github-CCuLgyqb.mjs} +254 -81
- package/dist/github-CCuLgyqb.mjs.map +1 -0
- package/dist/index.d.mts +164 -13
- package/dist/index.mjs +3 -3
- package/dist/{providers-CODZQCmL.mjs → providers-Br9FRn7j.mjs} +35 -12
- package/dist/providers-Br9FRn7j.mjs.map +1 -0
- package/dist/testing.d.mts +3 -3
- package/dist/testing.mjs +1 -1
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +48 -11
- package/src/cli.ts +14 -2
- package/src/internal/action-entry.ts +1 -0
- package/src/internal/factory.ts +2 -1
- package/src/internal/fan-out.ts +150 -66
- package/src/internal/fixtures.ts +3 -3
- package/src/internal/github.ts +169 -22
- package/src/internal/providers.ts +47 -8
- package/src/internal/review-state.ts +85 -47
- package/src/internal/run.ts +8 -3
- package/dist/github-NjgxGqwM.mjs.map +0 -1
- package/dist/providers-CODZQCmL.mjs.map +0 -1
package/src/action.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
makeAnthropicReviewModel,
|
|
20
20
|
makeOpenAiReviewModel,
|
|
21
21
|
openAiClientLayer,
|
|
22
|
+
validateReviewServiceTier,
|
|
23
|
+
type OpenAiServiceTier,
|
|
22
24
|
type ReviewProvider,
|
|
23
25
|
} from "./internal/providers.ts";
|
|
24
26
|
import { retireStaleReviews } from "./internal/retirement.ts";
|
|
@@ -30,6 +32,7 @@ import {
|
|
|
30
32
|
ReviewExecutionContext,
|
|
31
33
|
ReviewHeadComparison,
|
|
32
34
|
ReviewStateAuthenticator,
|
|
35
|
+
type ReviewTreeComparison,
|
|
33
36
|
isLineageAncestor,
|
|
34
37
|
type ReviewMode,
|
|
35
38
|
type ReviewState,
|
|
@@ -90,6 +93,7 @@ export interface ResolvedActionInputs {
|
|
|
90
93
|
readonly provider: ReviewProvider;
|
|
91
94
|
readonly model: string | undefined;
|
|
92
95
|
readonly effort: EffortPosition | undefined;
|
|
96
|
+
readonly serviceTier: OpenAiServiceTier | undefined;
|
|
93
97
|
readonly post: boolean;
|
|
94
98
|
readonly applyVerdict: boolean;
|
|
95
99
|
readonly fanOut: boolean;
|
|
@@ -118,6 +122,12 @@ export const resolveActionInputs = Effect.fn("resolveActionInputs")(function* ()
|
|
|
118
122
|
return yield* InvalidEffortInput.make({ input: effortRaw.value });
|
|
119
123
|
}
|
|
120
124
|
}
|
|
125
|
+
const serviceTier = yield* validateReviewServiceTier(
|
|
126
|
+
provider,
|
|
127
|
+
Option.getOrUndefined(
|
|
128
|
+
yield* Config.option(Config.literals(["fast"], "PR_REVIEW_SERVICE_TIER")),
|
|
129
|
+
),
|
|
130
|
+
);
|
|
121
131
|
const maxDurationMinutes = Option.getOrUndefined(
|
|
122
132
|
yield* Config.option(Config.int("PR_REVIEW_MAX_DURATION_MINUTES")),
|
|
123
133
|
);
|
|
@@ -149,6 +159,7 @@ export const resolveActionInputs = Effect.fn("resolveActionInputs")(function* ()
|
|
|
149
159
|
provider,
|
|
150
160
|
model: Option.getOrUndefined(model),
|
|
151
161
|
effort,
|
|
162
|
+
serviceTier,
|
|
152
163
|
post,
|
|
153
164
|
applyVerdict,
|
|
154
165
|
fanOut,
|
|
@@ -594,7 +605,8 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
|
|
|
594
605
|
}
|
|
595
606
|
let comparison: ReviewHeadComparison | undefined;
|
|
596
607
|
let baseComparison: ReviewHeadComparison | undefined;
|
|
597
|
-
let contentComparison:
|
|
608
|
+
let contentComparison: ReviewTreeComparison | undefined;
|
|
609
|
+
let contentComparisonFailure: string | undefined;
|
|
598
610
|
if (
|
|
599
611
|
(options.reviewMode ?? "incremental") === "incremental" &&
|
|
600
612
|
recovered.state !== undefined
|
|
@@ -635,15 +647,34 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
|
|
|
635
647
|
(comparison === undefined ||
|
|
636
648
|
!isLineageAncestor(comparison, recovered.state, metadata.headSha))
|
|
637
649
|
) {
|
|
638
|
-
|
|
639
|
-
.
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
650
|
+
const comparisonPaths = new Set(
|
|
651
|
+
fullFiles.flatMap((file) =>
|
|
652
|
+
file.previousPath === undefined ? [file.path] : [file.path, file.previousPath],
|
|
653
|
+
),
|
|
654
|
+
);
|
|
655
|
+
for (const finding of recovered.state.unresolvedFindings) {
|
|
656
|
+
comparisonPaths.add(finding.path);
|
|
657
|
+
}
|
|
658
|
+
for (const concern of recovered.state.unresolvedConcerns) {
|
|
659
|
+
for (const path of concern.evidencePaths ?? []) comparisonPaths.add(path);
|
|
646
660
|
}
|
|
661
|
+
for (const path of recovered.state.unreviewedPaths) comparisonPaths.add(path);
|
|
662
|
+
const treeResult = yield* history
|
|
663
|
+
.compareTrees(recovered.state.reviewedHeadSha, metadata.headSha, [...comparisonPaths])
|
|
664
|
+
.pipe(
|
|
665
|
+
Effect.match({
|
|
666
|
+
onFailure: (failure) => ({
|
|
667
|
+
comparison: undefined,
|
|
668
|
+
failure: failure.reason,
|
|
669
|
+
}),
|
|
670
|
+
onSuccess: (treeComparison) => ({
|
|
671
|
+
comparison: treeComparison,
|
|
672
|
+
failure: undefined,
|
|
673
|
+
}),
|
|
674
|
+
}),
|
|
675
|
+
);
|
|
676
|
+
contentComparison = treeResult.comparison;
|
|
677
|
+
contentComparisonFailure = treeResult.failure;
|
|
647
678
|
}
|
|
648
679
|
}
|
|
649
680
|
selection = {
|
|
@@ -656,6 +687,7 @@ export const runReviewAction = <E, R, FingerprintE = never, FingerprintR = never
|
|
|
656
687
|
comparison,
|
|
657
688
|
baseComparison,
|
|
658
689
|
contentComparison,
|
|
690
|
+
contentComparisonFailure,
|
|
659
691
|
lookupFailure: recovered.failure,
|
|
660
692
|
}),
|
|
661
693
|
stateAuthenticator,
|
|
@@ -798,7 +830,12 @@ export const reviewActionProgram = Effect.gen(function* () {
|
|
|
798
830
|
onSome: webCryptoReviewStateAuthenticatorLayer,
|
|
799
831
|
});
|
|
800
832
|
const guidance = yield* resolveGuidance(inputs);
|
|
801
|
-
const modelLabel = describeReviewModel(
|
|
833
|
+
const modelLabel = describeReviewModel(
|
|
834
|
+
inputs.provider,
|
|
835
|
+
inputs.model,
|
|
836
|
+
inputs.effort,
|
|
837
|
+
inputs.serviceTier,
|
|
838
|
+
);
|
|
802
839
|
const defaults = inputs.fanOut ? fanOutReviewBudgetLimits : reviewBudgetLimits;
|
|
803
840
|
const budget =
|
|
804
841
|
inputs.maxDurationMinutes === undefined
|
|
@@ -832,7 +869,7 @@ export const reviewActionProgram = Effect.gen(function* () {
|
|
|
832
869
|
Effect.provide(Layer.merge(stateAuthenticatorLayer, anthropicClientLayer)),
|
|
833
870
|
);
|
|
834
871
|
}
|
|
835
|
-
const model = makeOpenAiReviewModel(inputs.model, inputs.effort);
|
|
872
|
+
const model = makeOpenAiReviewModel(inputs.model, inputs.effort, inputs.serviceTier);
|
|
836
873
|
const reviewer = inputs.fanOut
|
|
837
874
|
? PrReview.makeFanOut({ ...shared, model })
|
|
838
875
|
: PrReview.make({ ...shared, model });
|
package/src/cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
makeAnthropicReviewModel,
|
|
17
17
|
makeOpenAiReviewModel,
|
|
18
18
|
openAiClientLayer,
|
|
19
|
+
validateReviewServiceTier,
|
|
19
20
|
type ReviewProvider,
|
|
20
21
|
} from "./internal/providers.ts";
|
|
21
22
|
import { ReviewPublicationPlan } from "./internal/render.ts";
|
|
@@ -53,6 +54,12 @@ const effortFlag = Flag.string("effort").pipe(
|
|
|
53
54
|
'Reasoning effort: "low", "medium", "high", "xhigh", "max", or a number in [0, 1] resolved onto the provider\'s own ladder.',
|
|
54
55
|
),
|
|
55
56
|
);
|
|
57
|
+
const serviceTierFlag = Flag.choice("service-tier", ["fast"]).pipe(
|
|
58
|
+
Flag.optional,
|
|
59
|
+
Flag.withDescription(
|
|
60
|
+
'OpenAI Responses service tier. The only supported value is "fast"; omit it to use the OpenAI project default.',
|
|
61
|
+
),
|
|
62
|
+
);
|
|
56
63
|
const postFlag = Flag.boolean("post").pipe(
|
|
57
64
|
Flag.withDefault(false),
|
|
58
65
|
Flag.withDescription("Post the review to GitHub; without it the plan prints to stdout."),
|
|
@@ -108,6 +115,7 @@ const command = CliCommand.make(
|
|
|
108
115
|
provider: providerFlag,
|
|
109
116
|
model: modelFlag,
|
|
110
117
|
effort: effortFlag,
|
|
118
|
+
serviceTier: serviceTierFlag,
|
|
111
119
|
post: postFlag,
|
|
112
120
|
applyVerdict: applyVerdictFlag,
|
|
113
121
|
fanOut: fanOutFlag,
|
|
@@ -118,6 +126,10 @@ const command = CliCommand.make(
|
|
|
118
126
|
(flags) =>
|
|
119
127
|
Effect.gen(function* () {
|
|
120
128
|
const provider = yield* decodeProvider(flags.provider);
|
|
129
|
+
const serviceTier = yield* validateReviewServiceTier(
|
|
130
|
+
provider,
|
|
131
|
+
Option.getOrUndefined(flags.serviceTier),
|
|
132
|
+
);
|
|
121
133
|
const target = yield* resolveReviewTarget({
|
|
122
134
|
repository: Option.getOrUndefined(flags.repo),
|
|
123
135
|
number: Option.getOrUndefined(flags.pr),
|
|
@@ -138,7 +150,7 @@ const command = CliCommand.make(
|
|
|
138
150
|
.map((pattern) => pattern.trim())
|
|
139
151
|
.filter((pattern) => pattern.length > 0),
|
|
140
152
|
maxFindings: Option.getOrUndefined(flags.maxFindings),
|
|
141
|
-
modelLabel: describeReviewModel(provider, model, effort),
|
|
153
|
+
modelLabel: describeReviewModel(provider, model, effort, serviceTier),
|
|
142
154
|
};
|
|
143
155
|
|
|
144
156
|
yield* Console.log(
|
|
@@ -176,7 +188,7 @@ const command = CliCommand.make(
|
|
|
176
188
|
Effect.provide(Layer.merge(githubLayers, anthropicClientLayer)),
|
|
177
189
|
);
|
|
178
190
|
} else {
|
|
179
|
-
const boundModel = makeOpenAiReviewModel(model, effort);
|
|
191
|
+
const boundModel = makeOpenAiReviewModel(model, effort, serviceTier);
|
|
180
192
|
const reviewer = flags.fanOut
|
|
181
193
|
? PrReview.makeFanOut({ ...shared, model: boundModel })
|
|
182
194
|
: PrReview.make({ ...shared, model: boundModel });
|
|
@@ -14,6 +14,7 @@ const INPUT_TO_ENV: ReadonlyArray<readonly [input: string, env: string]> = [
|
|
|
14
14
|
["INPUT_PROVIDER", "PR_REVIEW_PROVIDER"],
|
|
15
15
|
["INPUT_MODEL", "PR_REVIEW_MODEL"],
|
|
16
16
|
["INPUT_EFFORT", "PR_REVIEW_EFFORT"],
|
|
17
|
+
["INPUT_SERVICE-TIER", "PR_REVIEW_SERVICE_TIER"],
|
|
17
18
|
["INPUT_MAX-DURATION-MINUTES", "PR_REVIEW_MAX_DURATION_MINUTES"],
|
|
18
19
|
["INPUT_POST", "PR_REVIEW_POST"],
|
|
19
20
|
["INPUT_APPLY-VERDICT", "PR_REVIEW_APPLY_VERDICT"],
|
package/src/internal/factory.ts
CHANGED
|
@@ -61,7 +61,8 @@ export interface PrReviewSharedOptions {
|
|
|
61
61
|
/** Run-level usage bounds; defaults to the shape's packaged limits. */
|
|
62
62
|
readonly budget?: UsageBudgetLimits | undefined;
|
|
63
63
|
/**
|
|
64
|
-
* Human-readable descriptor of the bound model (provider, model id, effort
|
|
64
|
+
* Human-readable descriptor of the bound model (provider, model id, effort,
|
|
65
|
+
* and request profile such as service tier)
|
|
65
66
|
* rendered into the review footer and included in the fingerprint
|
|
66
67
|
* signature, so changing the binding re-reviews instead of skipping.
|
|
67
68
|
*/
|
package/src/internal/fan-out.ts
CHANGED
|
@@ -403,13 +403,20 @@ export interface FanOutPipelineInput {
|
|
|
403
403
|
/** Shared run budget observed by every child pass. */
|
|
404
404
|
readonly budget?: RunBudgetHook<BudgetExceeded | BudgetAdapterError> | undefined;
|
|
405
405
|
/**
|
|
406
|
-
* Unchanged
|
|
407
|
-
*
|
|
406
|
+
* Unchanged leftovers from prior failed passes. Every stage stays attached
|
|
407
|
+
* to its own paths; failed verification reopens both discovery perspectives
|
|
408
|
+
* for only those paths because candidate payloads are not persisted.
|
|
408
409
|
*/
|
|
409
410
|
readonly retry?:
|
|
410
411
|
| {
|
|
411
|
-
readonly
|
|
412
|
-
|
|
412
|
+
readonly passes?: ReadonlyArray<{
|
|
413
|
+
readonly stage: FailedReviewPass["stage"];
|
|
414
|
+
readonly paths: ReadonlyArray<string>;
|
|
415
|
+
}>;
|
|
416
|
+
/** @deprecated Pass path-bound `passes`; this flat form cannot preserve ownership. */
|
|
417
|
+
readonly paths?: ReadonlyArray<string>;
|
|
418
|
+
/** @deprecated Pass path-bound `passes`; this flat form cannot preserve ownership. */
|
|
419
|
+
readonly stages?: ReadonlyArray<FailedReviewPass["stage"]>;
|
|
413
420
|
}
|
|
414
421
|
| undefined;
|
|
415
422
|
/**
|
|
@@ -840,11 +847,38 @@ const scheduleFanOutWork = (
|
|
|
840
847
|
): {
|
|
841
848
|
readonly plan: ReviewUnitPlan;
|
|
842
849
|
readonly passesByUnit: Map<string, ReadonlyArray<ReviewDiscoveryPass>>;
|
|
843
|
-
readonly
|
|
850
|
+
readonly overflowRetryPasses: ReadonlyArray<{
|
|
851
|
+
readonly stage: FailedReviewPass["stage"];
|
|
852
|
+
readonly paths: ReadonlyArray<string>;
|
|
853
|
+
}>;
|
|
844
854
|
} => {
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
855
|
+
const requestedRetryPasses =
|
|
856
|
+
input.retry?.passes ??
|
|
857
|
+
input.retry?.stages?.map((stage) => ({ stage, paths: input.retry?.paths ?? [] })) ??
|
|
858
|
+
[];
|
|
859
|
+
const requestedStagesByPath = new Map<string, Set<FailedReviewPass["stage"]>>();
|
|
860
|
+
for (const pass of requestedRetryPasses) {
|
|
861
|
+
for (const path of pass.paths) {
|
|
862
|
+
const stages = requestedStagesByPath.get(path) ?? new Set<FailedReviewPass["stage"]>();
|
|
863
|
+
stages.add(pass.stage);
|
|
864
|
+
requestedStagesByPath.set(path, stages);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
const canonicalPathByKnownPath = new Map<string, string>();
|
|
868
|
+
const retryStagesByPath = new Map<string, Set<FailedReviewPass["stage"]>>();
|
|
869
|
+
for (const file of input.files) {
|
|
870
|
+
canonicalPathByKnownPath.set(file.path, file.path);
|
|
871
|
+
if (file.previousPath !== undefined) {
|
|
872
|
+
canonicalPathByKnownPath.set(file.previousPath, file.path);
|
|
873
|
+
}
|
|
874
|
+
const requested = [
|
|
875
|
+
requestedStagesByPath.get(file.path),
|
|
876
|
+
...(file.previousPath === undefined ? [] : [requestedStagesByPath.get(file.previousPath)]),
|
|
877
|
+
];
|
|
878
|
+
const stages = new Set(requested.flatMap((entry) => [...(entry ?? [])]));
|
|
879
|
+
if (stages.size > 0) retryStagesByPath.set(file.path, stages);
|
|
880
|
+
}
|
|
881
|
+
if (retryStagesByPath.size === 0) {
|
|
848
882
|
const plan = planReviewUnits(input.files, { totalChangedFiles: input.totalChangedFiles });
|
|
849
883
|
const passesByUnit = new Map<string, Array<ReviewDiscoveryPass>>();
|
|
850
884
|
for (const pass of plan.discoveryPasses) {
|
|
@@ -852,65 +886,96 @@ const scheduleFanOutWork = (
|
|
|
852
886
|
passes.push(pass);
|
|
853
887
|
passesByUnit.set(pass.unitId, passes);
|
|
854
888
|
}
|
|
855
|
-
return { plan, passesByUnit,
|
|
889
|
+
return { plan, passesByUnit, overflowRetryPasses: [] };
|
|
856
890
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
Math.max(0, MAX_REVIEW_UNITS - acceptedFresh.length),
|
|
868
|
-
);
|
|
869
|
-
const overflowRetryPaths = retryPlan.units
|
|
870
|
-
.slice(acceptedRetry.length)
|
|
871
|
-
.flatMap((unit) => [...unit.paths]);
|
|
872
|
-
const retryPassFilter = (pass: ReviewDiscoveryPass): boolean => {
|
|
873
|
-
const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
|
|
874
|
-
return retryStages.has(stage);
|
|
891
|
+
|
|
892
|
+
type DiscoveryStage = "discovery" | "specialist";
|
|
893
|
+
const discoveryStagesFor = (
|
|
894
|
+
stages: ReadonlySet<FailedReviewPass["stage"]>,
|
|
895
|
+
): ReadonlyArray<DiscoveryStage> => {
|
|
896
|
+
if (stages.has("verification")) return ["discovery", "specialist"];
|
|
897
|
+
return [
|
|
898
|
+
...(stages.has("discovery") ? (["discovery"] as const) : []),
|
|
899
|
+
...(stages.has("specialist") ? (["specialist"] as const) : []),
|
|
900
|
+
];
|
|
875
901
|
};
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
902
|
+
const freshFiles = input.files.filter((file) => !retryStagesByPath.has(file.path));
|
|
903
|
+
const retryGroups = new Map<
|
|
904
|
+
string,
|
|
905
|
+
{ readonly stages: ReadonlyArray<DiscoveryStage>; readonly files: Array<ChangedFile> }
|
|
906
|
+
>();
|
|
907
|
+
for (const file of input.files) {
|
|
908
|
+
const retryStages = retryStagesByPath.get(file.path);
|
|
909
|
+
if (retryStages === undefined) continue;
|
|
910
|
+
const stages = discoveryStagesFor(retryStages);
|
|
911
|
+
const key = stages.join("|");
|
|
912
|
+
const group = retryGroups.get(key) ?? { stages, files: [] };
|
|
913
|
+
group.files.push(file);
|
|
914
|
+
retryGroups.set(key, group);
|
|
886
915
|
}
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
|
|
916
|
+
|
|
917
|
+
const batches: ReadonlyArray<{
|
|
918
|
+
readonly stages: ReadonlyArray<DiscoveryStage>;
|
|
919
|
+
readonly files: ReadonlyArray<ChangedFile>;
|
|
920
|
+
}> = [
|
|
921
|
+
...(freshFiles.length === 0
|
|
922
|
+
? []
|
|
923
|
+
: [{ stages: ["discovery", "specialist"] as const, files: freshFiles }]),
|
|
924
|
+
...[...retryGroups.entries()]
|
|
925
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
926
|
+
.map(([, group]) => group),
|
|
927
|
+
];
|
|
928
|
+
const subplans: Array<ReviewUnitPlan> = [];
|
|
929
|
+
const acceptedUnits: Array<ReviewUnit> = [];
|
|
930
|
+
const rejectedUnits: Array<ReviewUnit> = [];
|
|
931
|
+
const discoveryPasses: Array<ReviewDiscoveryPass> = [];
|
|
932
|
+
for (const batch of batches) {
|
|
933
|
+
const batchPlan = remapPlanUnitIds(
|
|
934
|
+
planReviewUnits(batch.files, { totalChangedFiles: batch.files.length }),
|
|
935
|
+
acceptedUnits.length,
|
|
936
|
+
);
|
|
937
|
+
subplans.push(batchPlan);
|
|
938
|
+
const accepted = batchPlan.units.slice(0, Math.max(0, MAX_REVIEW_UNITS - acceptedUnits.length));
|
|
939
|
+
acceptedUnits.push(...accepted);
|
|
940
|
+
rejectedUnits.push(...batchPlan.units.slice(accepted.length));
|
|
941
|
+
const acceptedIds = new Set(accepted.map((unit) => unit.unitId));
|
|
942
|
+
discoveryPasses.push(
|
|
943
|
+
...batchPlan.discoveryPasses.filter((pass) => {
|
|
944
|
+
const stage = pass.perspective === "risk-specialist" ? "specialist" : "discovery";
|
|
945
|
+
return acceptedIds.has(pass.unitId) && batch.stages.includes(stage);
|
|
946
|
+
}),
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const acceptedPaths = new Set(acceptedUnits.flatMap((unit) => unit.paths));
|
|
951
|
+
const incompletePlannedPaths = new Set([
|
|
952
|
+
...subplans.flatMap((plan) => plan.partialEvidencePaths),
|
|
953
|
+
...subplans.flatMap((plan) => plan.unassignedPaths),
|
|
954
|
+
...rejectedUnits.flatMap((unit) => unit.paths),
|
|
955
|
+
]);
|
|
956
|
+
const partialEvidencePaths = [...incompletePlannedPaths]
|
|
957
|
+
.filter((path) => acceptedPaths.has(path))
|
|
958
|
+
.sort();
|
|
959
|
+
const unassignedPaths = [...incompletePlannedPaths]
|
|
960
|
+
.filter((path) => !acceptedPaths.has(path))
|
|
961
|
+
.sort();
|
|
962
|
+
const rejectedEvidenceShards = rejectedUnits.flatMap((unit) => unit.evidenceShards);
|
|
963
|
+
const undiffablePaths = [...new Set(subplans.flatMap((plan) => plan.undiffablePaths))].sort();
|
|
890
964
|
const plan = ReviewUnitPlan.make({
|
|
891
965
|
totalFiles: input.files.length,
|
|
892
|
-
truncated:
|
|
893
|
-
units:
|
|
966
|
+
truncated: input.files.length < input.totalChangedFiles,
|
|
967
|
+
units: acceptedUnits,
|
|
894
968
|
discoveryPasses,
|
|
895
|
-
undiffablePaths
|
|
896
|
-
|
|
897
|
-
].sort(),
|
|
898
|
-
partialEvidencePaths: [
|
|
899
|
-
...new Set([...freshPlan.partialEvidencePaths, ...retryPlan.partialEvidencePaths]),
|
|
900
|
-
].sort(),
|
|
969
|
+
undiffablePaths,
|
|
970
|
+
partialEvidencePaths,
|
|
901
971
|
unassignedEvidenceShardCount:
|
|
902
|
-
|
|
972
|
+
subplans.reduce((total, item) => total + item.unassignedEvidenceShardCount, 0) +
|
|
973
|
+
rejectedEvidenceShards.length,
|
|
903
974
|
unassignedEvidenceShardIds: [
|
|
904
|
-
...
|
|
905
|
-
...
|
|
975
|
+
...subplans.flatMap((item) => item.unassignedEvidenceShardIds),
|
|
976
|
+
...rejectedEvidenceShards.map((shard) => shard.shardId),
|
|
906
977
|
].slice(0, MAX_REPORTED_UNASSIGNED_EVIDENCE_SHARDS),
|
|
907
|
-
unassignedPaths
|
|
908
|
-
...new Set([
|
|
909
|
-
...freshPlan.unassignedPaths,
|
|
910
|
-
...retryPlan.unassignedPaths,
|
|
911
|
-
...overflowRetryPaths,
|
|
912
|
-
]),
|
|
913
|
-
].sort(),
|
|
978
|
+
unassignedPaths,
|
|
914
979
|
});
|
|
915
980
|
const passesByUnit = new Map<string, Array<ReviewDiscoveryPass>>();
|
|
916
981
|
for (const pass of discoveryPasses) {
|
|
@@ -918,7 +983,31 @@ const scheduleFanOutWork = (
|
|
|
918
983
|
passes.push(pass);
|
|
919
984
|
passesByUnit.set(pass.unitId, passes);
|
|
920
985
|
}
|
|
921
|
-
|
|
986
|
+
const incompletePaths = new Set([
|
|
987
|
+
...partialEvidencePaths,
|
|
988
|
+
...unassignedPaths,
|
|
989
|
+
...undiffablePaths,
|
|
990
|
+
]);
|
|
991
|
+
const overflowRetryPathsByStage = new Map<FailedReviewPass["stage"], Set<string>>();
|
|
992
|
+
for (const pass of requestedRetryPasses) {
|
|
993
|
+
for (const path of pass.paths) {
|
|
994
|
+
const canonicalPath = canonicalPathByKnownPath.get(path);
|
|
995
|
+
if (canonicalPath === undefined || !incompletePaths.has(canonicalPath)) continue;
|
|
996
|
+
const paths = overflowRetryPathsByStage.get(pass.stage) ?? new Set<string>();
|
|
997
|
+
paths.add(canonicalPath);
|
|
998
|
+
overflowRetryPathsByStage.set(pass.stage, paths);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
const overflowRetryPasses = (["discovery", "specialist", "verification"] as const).flatMap(
|
|
1002
|
+
(stage) => {
|
|
1003
|
+
const paths = [...(overflowRetryPathsByStage.get(stage) ?? [])].sort();
|
|
1004
|
+
return Array.from({ length: Math.ceil(paths.length / MAX_UNIT_FILES) }, (_, index) => ({
|
|
1005
|
+
stage,
|
|
1006
|
+
paths: paths.slice(index * MAX_UNIT_FILES, (index + 1) * MAX_UNIT_FILES),
|
|
1007
|
+
}));
|
|
1008
|
+
},
|
|
1009
|
+
);
|
|
1010
|
+
return { plan, passesByUnit, overflowRetryPasses };
|
|
922
1011
|
};
|
|
923
1012
|
|
|
924
1013
|
/**
|
|
@@ -932,7 +1021,7 @@ export const runFanOutReview = <Provider, ModelProvides, ModelRequires>(
|
|
|
932
1021
|
input: FanOutPipelineInput,
|
|
933
1022
|
) =>
|
|
934
1023
|
Effect.gen(function* () {
|
|
935
|
-
const { plan, passesByUnit,
|
|
1024
|
+
const { plan, passesByUnit, overflowRetryPasses } = scheduleFanOutWork(input);
|
|
936
1025
|
const outcomes = yield* Effect.forEach(
|
|
937
1026
|
plan.units,
|
|
938
1027
|
(unit) => reviewUnit(binding, unit, passesByUnit.get(unit.unitId) ?? [], input),
|
|
@@ -1052,12 +1141,7 @@ export const runFanOutReview = <Provider, ModelProvides, ModelRequires>(
|
|
|
1052
1141
|
].sort(),
|
|
1053
1142
|
unreviewedPasses: [
|
|
1054
1143
|
...outcomes.flatMap((outcome) => outcome.unreviewedPasses),
|
|
1055
|
-
...
|
|
1056
|
-
? []
|
|
1057
|
-
: (input.retry?.stages.length
|
|
1058
|
-
? input.retry.stages
|
|
1059
|
-
: (["discovery", "specialist", "verification"] as const)
|
|
1060
|
-
).map((stage) => ({ stage, paths: overflowRetryPaths }))),
|
|
1144
|
+
...overflowRetryPasses,
|
|
1061
1145
|
],
|
|
1062
1146
|
turns: outcomes.reduce((total, outcome) => total + outcome.turns, 0),
|
|
1063
1147
|
} satisfies FanOutPipelineOutcome;
|
package/src/internal/fixtures.ts
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
ReviewPublisher,
|
|
9
9
|
} from "./github.ts";
|
|
10
10
|
import type { ReviewPublicationPlan } from "./render.ts";
|
|
11
|
-
import type { ReviewHeadComparison, ReviewState } from "./review-state.ts";
|
|
11
|
+
import type { ReviewHeadComparison, ReviewState, ReviewTreeComparison } from "./review-state.ts";
|
|
12
12
|
import {
|
|
13
13
|
MAX_CHANGED_FILES,
|
|
14
14
|
MAX_FILE_CHARS,
|
|
@@ -120,7 +120,7 @@ export const staticPriorReviews = (
|
|
|
120
120
|
options: {
|
|
121
121
|
readonly state?: Option.Option<ReviewState> | undefined;
|
|
122
122
|
readonly comparison?: ReviewHeadComparison | undefined;
|
|
123
|
-
readonly treeComparison?:
|
|
123
|
+
readonly treeComparison?: ReviewTreeComparison | undefined;
|
|
124
124
|
} = {},
|
|
125
125
|
): PriorReviews["Service"] =>
|
|
126
126
|
PriorReviews.of({
|
|
@@ -142,7 +142,7 @@ export const staticPriorReviewsLayer = (
|
|
|
142
142
|
options: {
|
|
143
143
|
readonly state?: Option.Option<ReviewState> | undefined;
|
|
144
144
|
readonly comparison?: ReviewHeadComparison | undefined;
|
|
145
|
-
readonly treeComparison?:
|
|
145
|
+
readonly treeComparison?: ReviewTreeComparison | undefined;
|
|
146
146
|
} = {},
|
|
147
147
|
): Layer.Layer<PriorReviews> =>
|
|
148
148
|
Layer.succeed(PriorReviews)(staticPriorReviews(fingerprint, options));
|