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