@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/README.md +72 -48
- package/dist/action.d.mts +13 -13
- package/dist/action.mjs +42 -25
- package/dist/action.mjs.map +1 -1
- package/dist/cli.mjs +2 -2
- package/dist/{fan-out-n-00ppWr.d.mts → fan-out-Bi1v0VaU.d.mts} +314 -307
- package/dist/{github-BgtP7Rdv.mjs → github-C6jrBLA2.mjs} +763 -285
- package/dist/github-C6jrBLA2.mjs.map +1 -0
- package/dist/index.d.mts +84 -126
- package/dist/index.mjs +11 -11
- package/dist/index.mjs.map +1 -1
- package/dist/{providers-BguZK4B_.mjs → providers-2Jao2ZAX.mjs} +173 -585
- package/dist/providers-2Jao2ZAX.mjs.map +1 -0
- package/dist/testing.d.mts +6 -15
- package/dist/testing.mjs +14 -42
- package/dist/testing.mjs.map +1 -1
- package/package.json +2 -2
- package/src/action.ts +58 -34
- package/src/internal/action-entry.ts +0 -1
- package/src/internal/coverage.ts +147 -548
- package/src/internal/factory.ts +33 -54
- package/src/internal/fan-out-scripted.ts +26 -80
- package/src/internal/fan-out.ts +669 -432
- package/src/internal/fingerprint.ts +16 -10
- package/src/internal/fixtures.ts +6 -0
- package/src/internal/github.ts +36 -7
- package/src/internal/profiles.ts +8 -8
- package/src/internal/render.ts +34 -45
- package/src/internal/retirement.ts +3 -1
- package/src/internal/review-state.ts +204 -89
- package/src/internal/review-units.ts +25 -0
- package/src/internal/run.ts +231 -174
- package/src/internal/source.ts +1 -1
- package/dist/github-BgtP7Rdv.mjs.map +0 -1
- package/dist/providers-BguZK4B_.mjs.map +0 -1
package/src/internal/run.ts
CHANGED
|
@@ -10,13 +10,15 @@ import {
|
|
|
10
10
|
import { type Tool } from "effect/unstable/ai";
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
|
-
|
|
13
|
+
assessFlatReview,
|
|
14
|
+
compatibilityCoverage,
|
|
15
|
+
fanOutInputCoverage,
|
|
14
16
|
ReviewAssurance,
|
|
15
17
|
ReviewCoverage,
|
|
16
18
|
ReviewInputCoverage,
|
|
17
|
-
type ReviewShape,
|
|
18
19
|
} from "./coverage.ts";
|
|
19
20
|
import type { ChangedFile } from "./diff.ts";
|
|
21
|
+
import { runFanOutReview, type FileReviewerBinding } from "./fan-out.ts";
|
|
20
22
|
import { computeChangesetFingerprint } from "./fingerprint.ts";
|
|
21
23
|
import { PublishedReview, ReviewPublisher } from "./github.ts";
|
|
22
24
|
import { planPublication, ReviewPublicationPlan } from "./render.ts";
|
|
@@ -30,19 +32,28 @@ import {
|
|
|
30
32
|
import {
|
|
31
33
|
fromStoredConcern,
|
|
32
34
|
fromStoredFinding,
|
|
35
|
+
MAX_STORED_UNREVIEWED_PASSES,
|
|
36
|
+
MAX_STORED_UNREVIEWED_PATHS,
|
|
33
37
|
ReviewExecutionContext,
|
|
34
38
|
ReviewState,
|
|
39
|
+
StoredUnreviewedPass,
|
|
35
40
|
toStoredConcern,
|
|
36
41
|
toStoredFinding,
|
|
37
42
|
} from "./review-state.ts";
|
|
38
|
-
import { rankAndDedupeFindings } from "./review-units.ts";
|
|
43
|
+
import { rankAndDedupeConcerns, rankAndDedupeFindings } from "./review-units.ts";
|
|
39
44
|
import { PullRequestSource, type PullRequestMetadata } from "./source.ts";
|
|
40
45
|
|
|
41
46
|
// ---------------------------------------------------------------------------
|
|
42
|
-
// One review run, end to end: read the pull request, run the bounded
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
47
|
+
// One review run, end to end: read the pull request, run the bounded review
|
|
48
|
+
// (one flat agent, or the host-scheduled fan-out pipeline), validate the
|
|
49
|
+
// review against the real diff, then (optionally) publish. Publication
|
|
50
|
+
// happens strictly AFTER all model work so no model turn can observe or
|
|
51
|
+
// influence the mutation, and a failed run publishes nothing.
|
|
52
|
+
//
|
|
53
|
+
// Continuity is monotone: every completed run that can be signed advances the
|
|
54
|
+
// stored baseline, carrying genuinely-unsettled scope forward explicitly. A
|
|
55
|
+
// flaky pass therefore costs exactly its own scope on the next run — it can
|
|
56
|
+
// never freeze the baseline and reopen everything reviewed since.
|
|
46
57
|
// ---------------------------------------------------------------------------
|
|
47
58
|
|
|
48
59
|
/**
|
|
@@ -59,12 +70,9 @@ export const reviewBudgetLimits = UsageBudgetLimits.make({
|
|
|
59
70
|
});
|
|
60
71
|
|
|
61
72
|
/**
|
|
62
|
-
* Run-level bounds for the fan-out
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* own `AgentPolicy`, never silently by the parent's budget. The duration
|
|
66
|
-
* ceiling is wider because delegation Tool Calls hold the parent turn open
|
|
67
|
-
* while bounded children run.
|
|
73
|
+
* Run-level bounds for the fan-out pipeline. One budget observes EVERY child
|
|
74
|
+
* pass, so the ceiling covers bounded parallel discovery and verification
|
|
75
|
+
* plus the one-retry allowance.
|
|
68
76
|
*/
|
|
69
77
|
export const fanOutReviewBudgetLimits = UsageBudgetLimits.make({
|
|
70
78
|
maxInputTokens: 600_000,
|
|
@@ -87,23 +95,18 @@ export class ReviewRunOutcome extends Schema.Class<ReviewRunOutcome>(
|
|
|
87
95
|
coverage: ReviewCoverage,
|
|
88
96
|
/** Exact path/evidence assignment, distinct from semantic review work. */
|
|
89
97
|
inputCoverage: ReviewInputCoverage,
|
|
90
|
-
/** Settlement of
|
|
98
|
+
/** Settlement of scheduled discovery, specialist, and verification work. */
|
|
91
99
|
assurance: ReviewAssurance,
|
|
100
|
+
/** Retryable scope this run could not settle; carried to the next run. */
|
|
101
|
+
unreviewedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(
|
|
102
|
+
Schema.isMaxLength(300),
|
|
103
|
+
),
|
|
92
104
|
plan: ReviewPublicationPlan,
|
|
93
105
|
published: Schema.optionalKey(PublishedReview),
|
|
94
|
-
turns
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
* the COORDINATOR only — delegated children are bounded and accounted
|
|
98
|
-
* separately by their reservations.
|
|
99
|
-
*/
|
|
106
|
+
/** Total settled model turns (all child passes for the fan-out pipeline). */
|
|
107
|
+
turns: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
108
|
+
/** The run budget's observed usage across the whole run. */
|
|
100
109
|
usage: Schema.optionalKey(UsageTotals),
|
|
101
|
-
/**
|
|
102
|
-
* What `usage` observed: the whole run, or a fan-out coordinator only.
|
|
103
|
-
* Absent when the caller declared no scope — consumers must not present
|
|
104
|
-
* unscoped usage as whole-run totals.
|
|
105
|
-
*/
|
|
106
|
-
usageScope: Schema.optionalKey(Schema.Literals(["run", "coordinator"])),
|
|
107
110
|
reviewMode: Schema.optionalKey(Schema.Literals(["incremental", "full"])),
|
|
108
111
|
reviewReason: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(1_000))),
|
|
109
112
|
state: Schema.optionalKey(ReviewState),
|
|
@@ -114,7 +117,7 @@ export interface ExecuteReviewOptions {
|
|
|
114
117
|
readonly post: boolean;
|
|
115
118
|
/** Map the model's verdict onto APPROVE/REQUEST_CHANGES instead of COMMENT. */
|
|
116
119
|
readonly applyVerdict: boolean;
|
|
117
|
-
/** Run-level usage bounds; defaults to
|
|
120
|
+
/** Run-level usage bounds; defaults to the shape's packaged limits. */
|
|
118
121
|
readonly limits?: UsageBudgetLimits | undefined;
|
|
119
122
|
/**
|
|
120
123
|
* Host-side findings bound (fail-closed backstop for the instruction-level
|
|
@@ -132,15 +135,6 @@ export interface ExecuteReviewOptions {
|
|
|
132
135
|
readonly modelLabel?: string | undefined;
|
|
133
136
|
/** Workflow-run URL rendered into the review footer. */
|
|
134
137
|
readonly runUrl?: string | undefined;
|
|
135
|
-
/**
|
|
136
|
-
* What the run budget observes: the whole run, or a fan-out coordinator
|
|
137
|
-
* only. Without a declared scope the footer omits usage entirely — this
|
|
138
|
-
* generic path cannot know what a caller's binding shape observes, and an
|
|
139
|
-
* unlabeled number would read as whole-run totals.
|
|
140
|
-
*/
|
|
141
|
-
readonly usageScope?: "run" | "coordinator" | undefined;
|
|
142
|
-
/** Host-owned coverage shape; defaults to the flat reviewer. */
|
|
143
|
-
readonly reviewShape?: ReviewShape | undefined;
|
|
144
138
|
}
|
|
145
139
|
|
|
146
140
|
/** Build the mission one review run frames from the source's snapshot. */
|
|
@@ -173,109 +167,43 @@ export const enforceFindingsBound = (review: CodeReview, maxFindings: number): C
|
|
|
173
167
|
const findingKey = (finding: ReviewFinding): string =>
|
|
174
168
|
`${finding.path}\u0000${finding.startLine}\u0000${finding.endLine}\u0000${finding.severity}\u0000${finding.title}`;
|
|
175
169
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const byContent = new Map<string, ReviewConcern>();
|
|
186
|
-
for (const concern of concerns) {
|
|
187
|
-
const key = `${concern.title}\u0000${concern.body}`;
|
|
188
|
-
const previous = byContent.get(key);
|
|
189
|
-
if (
|
|
190
|
-
previous === undefined ||
|
|
191
|
-
severityRank[concern.severity] < severityRank[previous.severity]
|
|
192
|
-
) {
|
|
193
|
-
byContent.set(key, concern);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return [...byContent.values()]
|
|
197
|
-
.sort((left, right) => severityRank[left.severity] - severityRank[right.severity])
|
|
198
|
-
.slice(0, 10);
|
|
199
|
-
};
|
|
170
|
+
/** One shape-specific review result, before the shared settlement tail. */
|
|
171
|
+
interface ReviewCore {
|
|
172
|
+
readonly review: CodeReview;
|
|
173
|
+
readonly inputCoverage: ReviewInputCoverage;
|
|
174
|
+
readonly assurance: ReviewAssurance;
|
|
175
|
+
readonly unreviewedPaths: ReadonlyArray<string>;
|
|
176
|
+
readonly unreviewedPasses?: ReadonlyArray<StoredUnreviewedPass> | undefined;
|
|
177
|
+
readonly turns: number;
|
|
178
|
+
}
|
|
200
179
|
|
|
201
180
|
/**
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
* Layer's requirements stay visible in this Effect's `R`.
|
|
181
|
+
* The shared settlement tail: carry unchanged prior scope, decide whether
|
|
182
|
+
* this run's continuity state can be signed, plan the exact publication, and
|
|
183
|
+
* (optionally) post it. Continuity requires only that the run COMPLETED with
|
|
184
|
+
* a trustworthy full-surface fingerprint — never that every pass settled;
|
|
185
|
+
* unsettled scope travels inside the state instead of freezing it.
|
|
208
186
|
*/
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
typeof CodeReview,
|
|
219
|
-
Instructions,
|
|
220
|
-
Tools,
|
|
221
|
-
Provider,
|
|
222
|
-
ModelProvides,
|
|
223
|
-
ModelRequires
|
|
224
|
-
>,
|
|
187
|
+
const settleReviewRun = (
|
|
188
|
+
core: ReviewCore,
|
|
189
|
+
context: {
|
|
190
|
+
readonly metadata: PullRequestMetadata;
|
|
191
|
+
readonly files: ReadonlyArray<ChangedFile>;
|
|
192
|
+
readonly anchorFiles: ReadonlyArray<ChangedFile>;
|
|
193
|
+
readonly fingerprint: string | undefined;
|
|
194
|
+
readonly usage: UsageTotals | undefined;
|
|
195
|
+
},
|
|
225
196
|
options: ExecuteReviewOptions,
|
|
226
197
|
) =>
|
|
227
198
|
Effect.gen(function* () {
|
|
228
|
-
const
|
|
229
|
-
const metadata = yield* source.metadata;
|
|
230
|
-
const files = yield* source.changedFiles;
|
|
231
|
-
const anchorFiles = yield* source.anchorFiles;
|
|
199
|
+
const { metadata, files, anchorFiles, fingerprint, usage } = context;
|
|
232
200
|
const executionContext = Option.getOrUndefined(
|
|
233
201
|
yield* Effect.serviceOption(ReviewExecutionContext),
|
|
234
202
|
);
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
options.signature === undefined
|
|
239
|
-
? undefined
|
|
240
|
-
: yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
|
|
241
|
-
|
|
242
|
-
const budget = yield* makeUsageBudget(options.limits ?? reviewBudgetLimits);
|
|
243
|
-
const detached = yield* AgentRuntime.start(binding, mission, {
|
|
244
|
-
budget: toRunBudgetHook(budget),
|
|
245
|
-
estimateCostMicrousd: () => Effect.succeed(500),
|
|
246
|
-
});
|
|
247
|
-
const result = yield* detached.await;
|
|
248
|
-
const events = yield* detached.events;
|
|
249
|
-
|
|
250
|
-
// The engine validated the terminal JSON against the output schema; this
|
|
251
|
-
// decode recovers the typed value on this side of the generic boundary.
|
|
252
|
-
const decoded = yield* Schema.decodeUnknownEffect(CodeReview)(result.output);
|
|
203
|
+
const review = enforceFindingsBound(core.review, clampMaxFindings(options.maxFindings));
|
|
204
|
+
const { inputCoverage, assurance } = core;
|
|
205
|
+
const unreviewedPaths = [...new Set(core.unreviewedPaths)].sort();
|
|
253
206
|
const reviewTotalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
|
|
254
|
-
const pipeline = assessReviewPipeline({
|
|
255
|
-
shape: options.reviewShape ?? "flat",
|
|
256
|
-
files,
|
|
257
|
-
totalFiles: reviewTotalFiles,
|
|
258
|
-
anchorFiles,
|
|
259
|
-
totalAnchorFiles: metadata.totalChangedFiles,
|
|
260
|
-
events,
|
|
261
|
-
});
|
|
262
|
-
// The coordinator owns prose only. Fan-out findings and concerns are
|
|
263
|
-
// reconstructed from exact verifier-confirmed discovery candidates; an
|
|
264
|
-
// unsupported or coordinator-invented candidate cannot reach publication.
|
|
265
|
-
const verifiedReview =
|
|
266
|
-
options.reviewShape !== "fan-out"
|
|
267
|
-
? decoded
|
|
268
|
-
: CodeReview.make({
|
|
269
|
-
summary: decoded.summary,
|
|
270
|
-
verdict: decoded.verdict,
|
|
271
|
-
findings: rankAndDedupeFindings(pipeline.confirmedFindings),
|
|
272
|
-
...(pipeline.confirmedConcerns.length === 0
|
|
273
|
-
? {}
|
|
274
|
-
: { concerns: rankAndDedupeConcerns(pipeline.confirmedConcerns) }),
|
|
275
|
-
...(pipeline.walkthrough.length === 0 ? {} : { walkthrough: pipeline.walkthrough }),
|
|
276
|
-
});
|
|
277
|
-
const review = enforceFindingsBound(verifiedReview, clampMaxFindings(options.maxFindings));
|
|
278
|
-
const usage = yield* budget.snapshot;
|
|
279
207
|
const affectedPaths = new Set(
|
|
280
208
|
executionContext?.affectedPaths ??
|
|
281
209
|
files.flatMap((file) =>
|
|
@@ -315,16 +243,25 @@ export const executeReview = <
|
|
|
315
243
|
const key = `${concern.title}\u0000${concern.body}`;
|
|
316
244
|
return activeConcernKeys.has(key) && !currentConcernKeys.has(key);
|
|
317
245
|
});
|
|
318
|
-
const
|
|
246
|
+
const settled =
|
|
247
|
+
inputCoverage.status === "complete" &&
|
|
248
|
+
assurance.status !== "incomplete" &&
|
|
249
|
+
unreviewedPaths.length === 0;
|
|
250
|
+
// The fingerprint marker is standalone skip authority for fingerprint-only
|
|
251
|
+
// harnesses, so it is embedded only for a fully settled run.
|
|
252
|
+
const skipFingerprint = settled ? fingerprint : undefined;
|
|
253
|
+
const carriedScopeFits = unreviewedPaths.length <= MAX_STORED_UNREVIEWED_PATHS;
|
|
319
254
|
const stateCandidate =
|
|
320
255
|
executionContext !== undefined &&
|
|
321
|
-
inputCoverage.status === "complete" &&
|
|
322
|
-
assurance.status === "settled" &&
|
|
323
256
|
fingerprint !== undefined &&
|
|
324
257
|
metadata.baseSha !== undefined &&
|
|
258
|
+
// The fingerprint and stored baseline describe the FULL pull-request
|
|
259
|
+
// surface; a truncated anchor surface cannot make either claim.
|
|
260
|
+
anchorFiles.length >= metadata.totalChangedFiles &&
|
|
261
|
+
carriedScopeFits &&
|
|
325
262
|
executionContext.stateAuthenticator?.status === "available"
|
|
326
263
|
? ReviewState.make({
|
|
327
|
-
version:
|
|
264
|
+
version: 2,
|
|
328
265
|
repository: metadata.repository,
|
|
329
266
|
pullRequestNumber: metadata.number,
|
|
330
267
|
baseRef: metadata.baseRef,
|
|
@@ -336,6 +273,9 @@ export const executeReview = <
|
|
|
336
273
|
reviewedPathCount: anchorFiles.length,
|
|
337
274
|
unresolvedFindings: activeFindings.map(toStoredFinding),
|
|
338
275
|
unresolvedConcerns: activeConcerns.map(toStoredConcern),
|
|
276
|
+
unreviewedPaths,
|
|
277
|
+
unreviewedPasses: (core.unreviewedPasses ?? []).slice(0, MAX_STORED_UNREVIEWED_PASSES),
|
|
278
|
+
settled,
|
|
339
279
|
lastReviewMode: executionContext.mode,
|
|
340
280
|
})
|
|
341
281
|
: undefined;
|
|
@@ -345,12 +285,12 @@ export const executeReview = <
|
|
|
345
285
|
state: undefined,
|
|
346
286
|
marker: undefined,
|
|
347
287
|
notice:
|
|
348
|
-
executionContext
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
288
|
+
executionContext !== undefined && !carriedScopeFits
|
|
289
|
+
? `carried unreviewed scope (${unreviewedPaths.length} paths) exceeded the ${MAX_STORED_UNREVIEWED_PATHS}-path continuity bound`
|
|
290
|
+
: executionContext?.stateAuthenticator?.status === "unavailable"
|
|
291
|
+
? (executionContext.stateAuthenticator.unavailableReason ??
|
|
292
|
+
"authenticated continuity state is unavailable")
|
|
293
|
+
: undefined,
|
|
354
294
|
}
|
|
355
295
|
: yield* executionContext.stateAuthenticator.render(stateCandidate).pipe(
|
|
356
296
|
Effect.match({
|
|
@@ -374,14 +314,10 @@ export const executeReview = <
|
|
|
374
314
|
modelLabel: options.modelLabel,
|
|
375
315
|
runUrl: options.runUrl,
|
|
376
316
|
usage,
|
|
377
|
-
|
|
378
|
-
fingerprint:
|
|
379
|
-
inputCoverage.status === "complete" && assurance.status === "settled"
|
|
380
|
-
? fingerprint
|
|
381
|
-
: undefined,
|
|
382
|
-
coverage,
|
|
317
|
+
fingerprint: skipFingerprint,
|
|
383
318
|
inputCoverage,
|
|
384
319
|
assurance,
|
|
320
|
+
unreviewedPaths,
|
|
385
321
|
carriedFindings,
|
|
386
322
|
carriedConcerns,
|
|
387
323
|
reviewMode: executionContext?.mode,
|
|
@@ -392,44 +328,165 @@ export const executeReview = <
|
|
|
392
328
|
stateMarker: continuity.marker,
|
|
393
329
|
stateNotice: continuity.notice,
|
|
394
330
|
});
|
|
395
|
-
|
|
396
|
-
const scope =
|
|
397
|
-
options.usageScope === undefined ? {} : ({ usageScope: options.usageScope } as const);
|
|
398
|
-
if (!options.post) {
|
|
399
|
-
return ReviewRunOutcome.make({
|
|
400
|
-
review,
|
|
401
|
-
activeFindings,
|
|
402
|
-
activeConcerns,
|
|
403
|
-
coverage,
|
|
404
|
-
inputCoverage,
|
|
405
|
-
assurance,
|
|
406
|
-
plan,
|
|
407
|
-
turns: result.turns,
|
|
408
|
-
usage,
|
|
409
|
-
...scope,
|
|
410
|
-
...(executionContext === undefined
|
|
411
|
-
? {}
|
|
412
|
-
: { reviewMode: executionContext.mode, reviewReason: executionContext.reason }),
|
|
413
|
-
...(continuity.state === undefined ? {} : { state: continuity.state }),
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
const publisher = yield* ReviewPublisher;
|
|
417
|
-
const published = yield* publisher.publish(plan);
|
|
418
|
-
return ReviewRunOutcome.make({
|
|
331
|
+
const shared = {
|
|
419
332
|
review,
|
|
420
333
|
activeFindings,
|
|
421
334
|
activeConcerns,
|
|
422
|
-
coverage,
|
|
335
|
+
coverage: compatibilityCoverage(inputCoverage, assurance),
|
|
423
336
|
inputCoverage,
|
|
424
337
|
assurance,
|
|
338
|
+
unreviewedPaths,
|
|
425
339
|
plan,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
usage,
|
|
429
|
-
...scope,
|
|
340
|
+
turns: core.turns,
|
|
341
|
+
...(usage === undefined ? {} : { usage }),
|
|
430
342
|
...(executionContext === undefined
|
|
431
343
|
? {}
|
|
432
344
|
: { reviewMode: executionContext.mode, reviewReason: executionContext.reason }),
|
|
433
345
|
...(continuity.state === undefined ? {} : { state: continuity.state }),
|
|
346
|
+
};
|
|
347
|
+
if (!options.post) return ReviewRunOutcome.make(shared);
|
|
348
|
+
const publisher = yield* ReviewPublisher;
|
|
349
|
+
const published = yield* publisher.publish(plan);
|
|
350
|
+
return ReviewRunOutcome.make({ ...shared, published });
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Execute one flat review with any explicit Agent Binding whose contract is
|
|
355
|
+
* `ReviewMission -> CodeReview`. The binding stays a parameter (D-027): tests
|
|
356
|
+
* pass scripted models, hosts pass live provider bindings, and the model
|
|
357
|
+
* Layer's requirements stay visible in this Effect's `R`.
|
|
358
|
+
*/
|
|
359
|
+
export const executeReview = <
|
|
360
|
+
Instructions,
|
|
361
|
+
Tools extends Record<string, Tool.Any>,
|
|
362
|
+
Provider,
|
|
363
|
+
ModelProvides,
|
|
364
|
+
ModelRequires,
|
|
365
|
+
>(
|
|
366
|
+
binding: RuntimeBinding<
|
|
367
|
+
typeof ReviewMission,
|
|
368
|
+
typeof CodeReview,
|
|
369
|
+
Instructions,
|
|
370
|
+
Tools,
|
|
371
|
+
Provider,
|
|
372
|
+
ModelProvides,
|
|
373
|
+
ModelRequires
|
|
374
|
+
>,
|
|
375
|
+
options: ExecuteReviewOptions,
|
|
376
|
+
) =>
|
|
377
|
+
Effect.gen(function* () {
|
|
378
|
+
const source = yield* PullRequestSource;
|
|
379
|
+
const metadata = yield* source.metadata;
|
|
380
|
+
const files = yield* source.changedFiles;
|
|
381
|
+
const anchorFiles = yield* source.anchorFiles;
|
|
382
|
+
const executionContext = Option.getOrUndefined(
|
|
383
|
+
yield* Effect.serviceOption(ReviewExecutionContext),
|
|
384
|
+
);
|
|
385
|
+
const mission = buildReviewMission(metadata, files);
|
|
386
|
+
const fullMission = buildReviewMission(metadata, anchorFiles);
|
|
387
|
+
const fingerprint =
|
|
388
|
+
options.signature === undefined
|
|
389
|
+
? undefined
|
|
390
|
+
: yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
|
|
391
|
+
|
|
392
|
+
const budget = yield* makeUsageBudget(options.limits ?? reviewBudgetLimits);
|
|
393
|
+
const detached = yield* AgentRuntime.start(binding, mission, {
|
|
394
|
+
budget: toRunBudgetHook(budget),
|
|
395
|
+
estimateCostMicrousd: () => Effect.succeed(500),
|
|
396
|
+
});
|
|
397
|
+
const result = yield* detached.await;
|
|
398
|
+
const events = yield* detached.events;
|
|
399
|
+
|
|
400
|
+
// The engine validated the terminal JSON against the output schema; this
|
|
401
|
+
// decode recovers the typed value on this side of the generic boundary.
|
|
402
|
+
const review = yield* Schema.decodeUnknownEffect(CodeReview)(result.output);
|
|
403
|
+
const assessment = assessFlatReview({
|
|
404
|
+
files,
|
|
405
|
+
totalFiles: executionContext?.totalFiles ?? metadata.totalChangedFiles,
|
|
406
|
+
anchorFiles,
|
|
407
|
+
totalAnchorFiles: metadata.totalChangedFiles,
|
|
408
|
+
events,
|
|
434
409
|
});
|
|
410
|
+
const usage = yield* budget.snapshot;
|
|
411
|
+
return yield* settleReviewRun(
|
|
412
|
+
{
|
|
413
|
+
review,
|
|
414
|
+
inputCoverage: assessment.inputCoverage,
|
|
415
|
+
assurance: assessment.assurance,
|
|
416
|
+
unreviewedPaths: assessment.unreviewedPaths,
|
|
417
|
+
turns: result.turns,
|
|
418
|
+
},
|
|
419
|
+
{ metadata, files, anchorFiles, fingerprint, usage },
|
|
420
|
+
options,
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Execute one host-scheduled fan-out review: deterministic planning,
|
|
426
|
+
* independent discovery and verification child passes with bounded retries,
|
|
427
|
+
* and a host-composed review from verifier-confirmed candidates only. One
|
|
428
|
+
* budget observes every child pass, so the reported usage is whole-run.
|
|
429
|
+
*/
|
|
430
|
+
export const executeFanOutReview = <Provider, ModelProvides, ModelRequires>(
|
|
431
|
+
binding: FileReviewerBinding<Provider, ModelProvides, ModelRequires>,
|
|
432
|
+
options: ExecuteReviewOptions,
|
|
433
|
+
) =>
|
|
434
|
+
Effect.gen(function* () {
|
|
435
|
+
const source = yield* PullRequestSource;
|
|
436
|
+
const metadata = yield* source.metadata;
|
|
437
|
+
const files = yield* source.changedFiles;
|
|
438
|
+
const anchorFiles = yield* source.anchorFiles;
|
|
439
|
+
const executionContext = Option.getOrUndefined(
|
|
440
|
+
yield* Effect.serviceOption(ReviewExecutionContext),
|
|
441
|
+
);
|
|
442
|
+
const fullMission = buildReviewMission(metadata, anchorFiles);
|
|
443
|
+
const fingerprint =
|
|
444
|
+
options.signature === undefined
|
|
445
|
+
? undefined
|
|
446
|
+
: yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
|
|
447
|
+
|
|
448
|
+
const budget = yield* makeUsageBudget(options.limits ?? fanOutReviewBudgetLimits);
|
|
449
|
+
const totalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
|
|
450
|
+
const pipeline = yield* runFanOutReview(binding, {
|
|
451
|
+
files,
|
|
452
|
+
anchorFiles,
|
|
453
|
+
totalChangedFiles: totalFiles,
|
|
454
|
+
maxFindings: options.maxFindings,
|
|
455
|
+
budget: toRunBudgetHook(budget),
|
|
456
|
+
...(executionContext !== undefined && executionContext.retryPaths.length > 0
|
|
457
|
+
? {
|
|
458
|
+
retry: {
|
|
459
|
+
paths: executionContext.retryPaths,
|
|
460
|
+
stages: executionContext.retryStages,
|
|
461
|
+
},
|
|
462
|
+
}
|
|
463
|
+
: {}),
|
|
464
|
+
});
|
|
465
|
+
const inputCoverage = fanOutInputCoverage({
|
|
466
|
+
plan: pipeline.plan,
|
|
467
|
+
files,
|
|
468
|
+
totalFiles,
|
|
469
|
+
anchorFiles,
|
|
470
|
+
totalAnchorFiles: metadata.totalChangedFiles,
|
|
471
|
+
});
|
|
472
|
+
const usage = yield* budget.snapshot;
|
|
473
|
+
return yield* settleReviewRun(
|
|
474
|
+
{
|
|
475
|
+
review: pipeline.review,
|
|
476
|
+
inputCoverage,
|
|
477
|
+
assurance: pipeline.assurance,
|
|
478
|
+
unreviewedPaths: pipeline.unreviewedPaths,
|
|
479
|
+
unreviewedPasses: pipeline.unreviewedPasses
|
|
480
|
+
.slice(0, MAX_STORED_UNREVIEWED_PASSES)
|
|
481
|
+
.map((pass) =>
|
|
482
|
+
StoredUnreviewedPass.make({
|
|
483
|
+
stage: pass.stage,
|
|
484
|
+
paths: pass.paths.slice(0, 12),
|
|
485
|
+
}),
|
|
486
|
+
),
|
|
487
|
+
turns: pipeline.turns,
|
|
488
|
+
},
|
|
489
|
+
{ metadata, files, anchorFiles, fingerprint, usage },
|
|
490
|
+
options,
|
|
491
|
+
);
|
|
435
492
|
});
|
package/src/internal/source.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context, Effect, Schema } from "effect";
|
|
2
2
|
|
|
3
|
-
import { ChangedFile } from "./diff.ts";
|
|
3
|
+
import type { ChangedFile } from "./diff.ts";
|
|
4
4
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
// The pull-request source port: everything the review tools may observe about
|