@effect-agent/pr-review 0.1.0-beta.9 → 0.1.0-beta.90
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/NOTICE +26 -0
- package/README.md +170 -168
- package/dist/Review.d.mts +294 -0
- package/dist/Review.mjs +702 -0
- package/dist/Review.mjs.map +1 -0
- package/dist/ReviewRepository-Wd_4qCaO.d.mts +71 -0
- package/dist/ReviewRepository.d.mts +2 -0
- package/dist/ReviewRepository.mjs +15 -0
- package/dist/ReviewRepository.mjs.map +1 -0
- package/dist/index.d.mts +3 -717
- package/dist/index.mjs +3 -66
- package/dist/repository-D7NN3225.mjs +101 -0
- package/dist/repository-D7NN3225.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -54
- package/src/Review.ts +1052 -0
- package/src/ReviewRepository.ts +9 -0
- package/src/index.ts +2 -21
- package/src/internal/repository.ts +156 -0
- package/dist/action.d.mts +0 -190
- package/dist/action.mjs +0 -423
- package/dist/action.mjs.map +0 -1
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -103
- package/dist/cli.mjs.map +0 -1
- package/dist/fan-out-C6gq3CFg.d.mts +0 -1081
- package/dist/github-Lfa_ox-u.mjs +0 -1673
- package/dist/github-Lfa_ox-u.mjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/providers-CaOnz7mK.mjs +0 -987
- package/dist/providers-CaOnz7mK.mjs.map +0 -1
- package/dist/testing.d.mts +0 -131
- package/dist/testing.mjs +0 -230
- package/dist/testing.mjs.map +0 -1
- package/src/action.ts +0 -697
- package/src/cli.ts +0 -214
- package/src/internal/action-entry.ts +0 -42
- package/src/internal/coverage.ts +0 -245
- package/src/internal/diff.ts +0 -134
- package/src/internal/effort.ts +0 -86
- package/src/internal/factory.ts +0 -374
- package/src/internal/fan-out-scripted.ts +0 -164
- package/src/internal/fan-out.ts +0 -450
- package/src/internal/fingerprint.ts +0 -74
- package/src/internal/fixtures.ts +0 -131
- package/src/internal/github-env.ts +0 -142
- package/src/internal/github.ts +0 -761
- package/src/internal/ignore.ts +0 -88
- package/src/internal/profiles.ts +0 -79
- package/src/internal/providers.ts +0 -91
- package/src/internal/render.ts +0 -428
- package/src/internal/retirement.ts +0 -332
- package/src/internal/review-agent.ts +0 -385
- package/src/internal/review-state.ts +0 -488
- package/src/internal/review-units.ts +0 -167
- package/src/internal/run.ts +0 -397
- package/src/internal/scripted.ts +0 -108
- package/src/internal/source.ts +0 -110
- package/src/testing.ts +0 -8
package/src/internal/run.ts
DELETED
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
import { Effect, Option, Schema } from "effect";
|
|
2
|
-
import {
|
|
3
|
-
makeUsageBudget,
|
|
4
|
-
toRunBudgetHook,
|
|
5
|
-
UsageBudgetLimits,
|
|
6
|
-
UsageTotals,
|
|
7
|
-
AgentRuntime,
|
|
8
|
-
type RuntimeBinding,
|
|
9
|
-
} from "effect-agent";
|
|
10
|
-
import { type Tool } from "effect/unstable/ai";
|
|
11
|
-
|
|
12
|
-
import { assessReviewCoverage, ReviewCoverage, type ReviewShape } from "./coverage.ts";
|
|
13
|
-
import type { ChangedFile } from "./diff.ts";
|
|
14
|
-
import { computeChangesetFingerprint } from "./fingerprint.ts";
|
|
15
|
-
import { PublishedReview, ReviewPublisher } from "./github.ts";
|
|
16
|
-
import { planPublication, ReviewPublicationPlan } from "./render.ts";
|
|
17
|
-
import {
|
|
18
|
-
clampMaxFindings,
|
|
19
|
-
CodeReview,
|
|
20
|
-
ReviewConcern,
|
|
21
|
-
ReviewFinding,
|
|
22
|
-
ReviewMission,
|
|
23
|
-
} from "./review-agent.ts";
|
|
24
|
-
import {
|
|
25
|
-
fromStoredConcern,
|
|
26
|
-
fromStoredFinding,
|
|
27
|
-
ReviewExecutionContext,
|
|
28
|
-
ReviewState,
|
|
29
|
-
toStoredConcern,
|
|
30
|
-
toStoredFinding,
|
|
31
|
-
} from "./review-state.ts";
|
|
32
|
-
import { rankAndDedupeFindings } from "./review-units.ts";
|
|
33
|
-
import { PullRequestSource, type PullRequestMetadata } from "./source.ts";
|
|
34
|
-
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
// One review run, end to end: read the pull request, run the bounded agent,
|
|
37
|
-
// validate the review against the real diff, then (optionally) publish.
|
|
38
|
-
// Publication happens strictly AFTER the agent loop so no model turn can
|
|
39
|
-
// observe or influence the mutation, and a failed run publishes nothing.
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Run-level usage bounds on top of the definition's AgentPolicy. Real diffs
|
|
44
|
-
* are token-heavy, so the input budget is research-sized with cost as the
|
|
45
|
-
* safety net.
|
|
46
|
-
*/
|
|
47
|
-
export const reviewBudgetLimits = UsageBudgetLimits.make({
|
|
48
|
-
maxInputTokens: 400_000,
|
|
49
|
-
maxOutputTokens: 16_000,
|
|
50
|
-
maxToolCalls: 24,
|
|
51
|
-
maxCostMicrousd: 2_000_000,
|
|
52
|
-
maxDurationMillis: 480_000,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Run-level bounds for the fan-out coordinator. This budget observes only
|
|
57
|
-
* the COORDINATOR'S own usage — delegated children are bounded separately by
|
|
58
|
-
* the delegation's `SubagentPolicy` reservation and the child definition's
|
|
59
|
-
* own `AgentPolicy`, never silently by the parent's budget. The duration
|
|
60
|
-
* ceiling is wider because delegation Tool Calls hold the parent turn open
|
|
61
|
-
* while bounded children run.
|
|
62
|
-
*/
|
|
63
|
-
export const fanOutReviewBudgetLimits = UsageBudgetLimits.make({
|
|
64
|
-
maxInputTokens: 400_000,
|
|
65
|
-
maxOutputTokens: 16_000,
|
|
66
|
-
maxToolCalls: 24,
|
|
67
|
-
maxCostMicrousd: 2_000_000,
|
|
68
|
-
maxDurationMillis: 900_000,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
/** Everything one review run produced, publication receipt included. */
|
|
72
|
-
export class ReviewRunOutcome extends Schema.Class<ReviewRunOutcome>(
|
|
73
|
-
"@effect-agent/pr-review/ReviewRunOutcome",
|
|
74
|
-
)({
|
|
75
|
-
review: CodeReview,
|
|
76
|
-
/** All currently unresolved findings, including unchanged carried scope. */
|
|
77
|
-
activeFindings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(20)),
|
|
78
|
-
/** All currently unresolved concerns, including concerns carried to final audit. */
|
|
79
|
-
activeConcerns: Schema.Array(ReviewConcern).check(Schema.isMaxLength(10)),
|
|
80
|
-
/** Host-owned structural coverage used by the Actions check conclusion. */
|
|
81
|
-
coverage: ReviewCoverage,
|
|
82
|
-
plan: ReviewPublicationPlan,
|
|
83
|
-
published: Schema.optionalKey(PublishedReview),
|
|
84
|
-
turns: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
85
|
-
/**
|
|
86
|
-
* The run budget's observed usage. For the fan-out reviewer this observes
|
|
87
|
-
* the COORDINATOR only — delegated children are bounded and accounted
|
|
88
|
-
* separately by their reservations.
|
|
89
|
-
*/
|
|
90
|
-
usage: Schema.optionalKey(UsageTotals),
|
|
91
|
-
/**
|
|
92
|
-
* What `usage` observed: the whole run, or a fan-out coordinator only.
|
|
93
|
-
* Absent when the caller declared no scope — consumers must not present
|
|
94
|
-
* unscoped usage as whole-run totals.
|
|
95
|
-
*/
|
|
96
|
-
usageScope: Schema.optionalKey(Schema.Literals(["run", "coordinator"])),
|
|
97
|
-
reviewMode: Schema.optionalKey(Schema.Literals(["incremental", "full"])),
|
|
98
|
-
reviewReason: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(1_000))),
|
|
99
|
-
state: Schema.optionalKey(ReviewState),
|
|
100
|
-
}) {}
|
|
101
|
-
|
|
102
|
-
export interface ExecuteReviewOptions {
|
|
103
|
-
/** Post the review to GitHub; `false` stops after planning (dry run). */
|
|
104
|
-
readonly post: boolean;
|
|
105
|
-
/** Map the model's verdict onto APPROVE/REQUEST_CHANGES instead of COMMENT. */
|
|
106
|
-
readonly applyVerdict: boolean;
|
|
107
|
-
/** Run-level usage bounds; defaults to `reviewBudgetLimits`. */
|
|
108
|
-
readonly limits?: UsageBudgetLimits | undefined;
|
|
109
|
-
/**
|
|
110
|
-
* Host-side findings bound (fail-closed backstop for the instruction-level
|
|
111
|
-
* bound): a review carrying more findings is ranked by severity, deduped by
|
|
112
|
-
* anchor, and trimmed — never published oversized. Clamped to the schema cap.
|
|
113
|
-
*/
|
|
114
|
-
readonly maxFindings?: number | undefined;
|
|
115
|
-
/**
|
|
116
|
-
* Prompt signature for changeset fingerprinting. When present, the
|
|
117
|
-
* changeset fingerprint is computed and embedded invisibly in the review
|
|
118
|
-
* body so later runs can skip an unchanged changeset.
|
|
119
|
-
*/
|
|
120
|
-
readonly signature?: ((mission: ReviewMission) => string) | undefined;
|
|
121
|
-
/** Provider binding descriptor rendered into the review footer. */
|
|
122
|
-
readonly modelLabel?: string | undefined;
|
|
123
|
-
/** Workflow-run URL rendered into the review footer. */
|
|
124
|
-
readonly runUrl?: string | undefined;
|
|
125
|
-
/**
|
|
126
|
-
* What the run budget observes: the whole run, or a fan-out coordinator
|
|
127
|
-
* only. Without a declared scope the footer omits usage entirely — this
|
|
128
|
-
* generic path cannot know what a caller's binding shape observes, and an
|
|
129
|
-
* unlabeled number would read as whole-run totals.
|
|
130
|
-
*/
|
|
131
|
-
readonly usageScope?: "run" | "coordinator" | undefined;
|
|
132
|
-
/** Host-owned coverage shape; defaults to the flat reviewer. */
|
|
133
|
-
readonly reviewShape?: ReviewShape | undefined;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** Build the mission one review run frames from the source's snapshot. */
|
|
137
|
-
export const buildReviewMission = (
|
|
138
|
-
metadata: PullRequestMetadata,
|
|
139
|
-
files: ReadonlyArray<ChangedFile>,
|
|
140
|
-
): ReviewMission =>
|
|
141
|
-
ReviewMission.make({
|
|
142
|
-
repository: metadata.repository,
|
|
143
|
-
number: metadata.number,
|
|
144
|
-
title: metadata.title,
|
|
145
|
-
body: metadata.body,
|
|
146
|
-
baseRef: metadata.baseRef,
|
|
147
|
-
headRef: metadata.headRef,
|
|
148
|
-
changedFileCount: files.length,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
/** Enforce the configured findings bound on an already-validated review. */
|
|
152
|
-
export const enforceFindingsBound = (review: CodeReview, maxFindings: number): CodeReview =>
|
|
153
|
-
review.findings.length <= maxFindings
|
|
154
|
-
? review
|
|
155
|
-
: CodeReview.make({
|
|
156
|
-
summary: review.summary,
|
|
157
|
-
verdict: review.verdict,
|
|
158
|
-
findings: rankAndDedupeFindings(review.findings).slice(0, maxFindings),
|
|
159
|
-
...(review.concerns !== undefined ? { concerns: review.concerns } : {}),
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
const findingKey = (finding: ReviewFinding): string =>
|
|
163
|
-
`${finding.path}\u0000${finding.startLine}\u0000${finding.endLine}\u0000${finding.severity}\u0000${finding.title}`;
|
|
164
|
-
|
|
165
|
-
const severityRank: Record<ReviewConcern["severity"], number> = {
|
|
166
|
-
blocking: 0,
|
|
167
|
-
important: 1,
|
|
168
|
-
nit: 2,
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const rankAndDedupeConcerns = (
|
|
172
|
-
concerns: ReadonlyArray<ReviewConcern>,
|
|
173
|
-
): ReadonlyArray<ReviewConcern> => {
|
|
174
|
-
const byContent = new Map<string, ReviewConcern>();
|
|
175
|
-
for (const concern of concerns) {
|
|
176
|
-
const key = `${concern.title}\u0000${concern.body}`;
|
|
177
|
-
const previous = byContent.get(key);
|
|
178
|
-
if (
|
|
179
|
-
previous === undefined ||
|
|
180
|
-
severityRank[concern.severity] < severityRank[previous.severity]
|
|
181
|
-
) {
|
|
182
|
-
byContent.set(key, concern);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return [...byContent.values()]
|
|
186
|
-
.sort((left, right) => severityRank[left.severity] - severityRank[right.severity])
|
|
187
|
-
.slice(0, 10);
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Execute one review with any explicit Agent Binding whose contract is
|
|
192
|
-
* `ReviewMission -> CodeReview` — the flat reviewer or the fan-out
|
|
193
|
-
* coordinator; the toolkit stays generic because publication only depends on
|
|
194
|
-
* the shared output contract. The binding stays a parameter (D-027): tests
|
|
195
|
-
* pass scripted models, hosts pass live provider bindings, and the model
|
|
196
|
-
* Layer's requirements stay visible in this Effect's `R`.
|
|
197
|
-
*/
|
|
198
|
-
export const executeReview = <
|
|
199
|
-
Instructions,
|
|
200
|
-
Tools extends Record<string, Tool.Any>,
|
|
201
|
-
Provider,
|
|
202
|
-
ModelProvides,
|
|
203
|
-
ModelRequires,
|
|
204
|
-
>(
|
|
205
|
-
binding: RuntimeBinding<
|
|
206
|
-
typeof ReviewMission,
|
|
207
|
-
typeof CodeReview,
|
|
208
|
-
Instructions,
|
|
209
|
-
Tools,
|
|
210
|
-
Provider,
|
|
211
|
-
ModelProvides,
|
|
212
|
-
ModelRequires
|
|
213
|
-
>,
|
|
214
|
-
options: ExecuteReviewOptions,
|
|
215
|
-
) =>
|
|
216
|
-
Effect.gen(function* () {
|
|
217
|
-
const source = yield* PullRequestSource;
|
|
218
|
-
const metadata = yield* source.metadata;
|
|
219
|
-
const files = yield* source.changedFiles;
|
|
220
|
-
const anchorFiles = yield* source.anchorFiles;
|
|
221
|
-
const executionContext = Option.getOrUndefined(
|
|
222
|
-
yield* Effect.serviceOption(ReviewExecutionContext),
|
|
223
|
-
);
|
|
224
|
-
const mission = buildReviewMission(metadata, files);
|
|
225
|
-
const fullMission = buildReviewMission(metadata, anchorFiles);
|
|
226
|
-
const fingerprint =
|
|
227
|
-
options.signature === undefined
|
|
228
|
-
? undefined
|
|
229
|
-
: yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
|
|
230
|
-
|
|
231
|
-
const budget = yield* makeUsageBudget(options.limits ?? reviewBudgetLimits);
|
|
232
|
-
const detached = yield* AgentRuntime.start(binding, mission, {
|
|
233
|
-
budget: toRunBudgetHook(budget),
|
|
234
|
-
estimateCostMicrousd: () => Effect.succeed(500),
|
|
235
|
-
});
|
|
236
|
-
const result = yield* detached.await;
|
|
237
|
-
const events = yield* detached.events;
|
|
238
|
-
|
|
239
|
-
// The engine validated the terminal JSON against the output schema; this
|
|
240
|
-
// decode recovers the typed value on this side of the generic boundary.
|
|
241
|
-
const decoded = yield* Schema.decodeUnknownEffect(CodeReview)(result.output);
|
|
242
|
-
const review = enforceFindingsBound(decoded, clampMaxFindings(options.maxFindings));
|
|
243
|
-
const usage = yield* budget.snapshot;
|
|
244
|
-
const affectedPaths = new Set(
|
|
245
|
-
executionContext?.affectedPaths ??
|
|
246
|
-
files.flatMap((file) =>
|
|
247
|
-
file.previousPath === undefined ? [file.path] : [file.path, file.previousPath],
|
|
248
|
-
),
|
|
249
|
-
);
|
|
250
|
-
const priorState =
|
|
251
|
-
executionContext?.mode === "incremental" ? executionContext.priorState : undefined;
|
|
252
|
-
const carriedCandidates =
|
|
253
|
-
priorState?.unresolvedFindings
|
|
254
|
-
.filter((finding) => !affectedPaths.has(finding.path))
|
|
255
|
-
.map(fromStoredFinding) ?? [];
|
|
256
|
-
const activeFindings = rankAndDedupeFindings([...carriedCandidates, ...review.findings]).slice(
|
|
257
|
-
0,
|
|
258
|
-
clampMaxFindings(options.maxFindings),
|
|
259
|
-
);
|
|
260
|
-
const activeFindingKeys = new Set(activeFindings.map(findingKey));
|
|
261
|
-
const currentFindingKeys = new Set(review.findings.map(findingKey));
|
|
262
|
-
const carriedFindings = carriedCandidates.filter(
|
|
263
|
-
(finding) =>
|
|
264
|
-
activeFindingKeys.has(findingKey(finding)) && !currentFindingKeys.has(findingKey(finding)),
|
|
265
|
-
);
|
|
266
|
-
// Non-anchored concerns cannot be mapped safely to one affected path, so
|
|
267
|
-
// incremental runs carry them conservatively until the explicit final audit.
|
|
268
|
-
const carriedConcernCandidates = priorState?.unresolvedConcerns.map(fromStoredConcern) ?? [];
|
|
269
|
-
const activeConcerns = rankAndDedupeConcerns([
|
|
270
|
-
...carriedConcernCandidates,
|
|
271
|
-
...(review.concerns ?? []),
|
|
272
|
-
]);
|
|
273
|
-
const currentConcernKeys = new Set(
|
|
274
|
-
(review.concerns ?? []).map((concern) => `${concern.title}\u0000${concern.body}`),
|
|
275
|
-
);
|
|
276
|
-
const activeConcernKeys = new Set(
|
|
277
|
-
activeConcerns.map((concern) => `${concern.title}\u0000${concern.body}`),
|
|
278
|
-
);
|
|
279
|
-
const carriedConcerns = carriedConcernCandidates.filter((concern) => {
|
|
280
|
-
const key = `${concern.title}\u0000${concern.body}`;
|
|
281
|
-
return activeConcernKeys.has(key) && !currentConcernKeys.has(key);
|
|
282
|
-
});
|
|
283
|
-
const reviewTotalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
|
|
284
|
-
const coverage = assessReviewCoverage({
|
|
285
|
-
shape: options.reviewShape ?? "flat",
|
|
286
|
-
files,
|
|
287
|
-
totalFiles: reviewTotalFiles,
|
|
288
|
-
anchorFiles,
|
|
289
|
-
totalAnchorFiles: metadata.totalChangedFiles,
|
|
290
|
-
events,
|
|
291
|
-
});
|
|
292
|
-
const stateCandidate =
|
|
293
|
-
executionContext !== undefined &&
|
|
294
|
-
coverage.status === "complete" &&
|
|
295
|
-
fingerprint !== undefined &&
|
|
296
|
-
metadata.baseSha !== undefined &&
|
|
297
|
-
executionContext.stateAuthenticator?.status === "available"
|
|
298
|
-
? ReviewState.make({
|
|
299
|
-
version: 1,
|
|
300
|
-
repository: metadata.repository,
|
|
301
|
-
pullRequestNumber: metadata.number,
|
|
302
|
-
baseRef: metadata.baseRef,
|
|
303
|
-
baseSha: metadata.baseSha,
|
|
304
|
-
headRef: metadata.headRef,
|
|
305
|
-
reviewedHeadSha: metadata.headSha,
|
|
306
|
-
profileFingerprint: executionContext.profileFingerprint,
|
|
307
|
-
acceptedScopeFingerprint: fingerprint,
|
|
308
|
-
reviewedPathCount: anchorFiles.length,
|
|
309
|
-
unresolvedFindings: activeFindings.map(toStoredFinding),
|
|
310
|
-
unresolvedConcerns: activeConcerns.map(toStoredConcern),
|
|
311
|
-
lastReviewMode: executionContext.mode,
|
|
312
|
-
})
|
|
313
|
-
: undefined;
|
|
314
|
-
const continuity =
|
|
315
|
-
stateCandidate === undefined || executionContext?.stateAuthenticator === undefined
|
|
316
|
-
? {
|
|
317
|
-
state: undefined,
|
|
318
|
-
marker: undefined,
|
|
319
|
-
notice:
|
|
320
|
-
executionContext?.stateAuthenticator?.status === "unavailable" &&
|
|
321
|
-
coverage.status === "complete"
|
|
322
|
-
? (executionContext.stateAuthenticator.unavailableReason ??
|
|
323
|
-
"authenticated continuity state is unavailable")
|
|
324
|
-
: undefined,
|
|
325
|
-
}
|
|
326
|
-
: yield* executionContext.stateAuthenticator.render(stateCandidate).pipe(
|
|
327
|
-
Effect.match({
|
|
328
|
-
onFailure: (error) => ({
|
|
329
|
-
state: undefined,
|
|
330
|
-
marker: undefined,
|
|
331
|
-
notice:
|
|
332
|
-
error._tag === "ReviewStateMarkerTooLarge"
|
|
333
|
-
? `authenticated continuity state exceeded its ${error.maximumChars}-character bound`
|
|
334
|
-
: `authenticated continuity state could not be signed: ${error.reason}`,
|
|
335
|
-
}),
|
|
336
|
-
onSuccess: (marker) => ({ state: stateCandidate, marker, notice: undefined }),
|
|
337
|
-
}),
|
|
338
|
-
);
|
|
339
|
-
const plan = planPublication(review, anchorFiles, {
|
|
340
|
-
applyVerdict: options.applyVerdict,
|
|
341
|
-
headSha: metadata.headSha,
|
|
342
|
-
totalChangedFiles: metadata.totalChangedFiles,
|
|
343
|
-
baseRef: metadata.baseRef,
|
|
344
|
-
headRef: metadata.headRef,
|
|
345
|
-
modelLabel: options.modelLabel,
|
|
346
|
-
runUrl: options.runUrl,
|
|
347
|
-
usage,
|
|
348
|
-
usageScope: options.usageScope,
|
|
349
|
-
fingerprint: coverage.status === "complete" ? fingerprint : undefined,
|
|
350
|
-
coverage,
|
|
351
|
-
carriedFindings,
|
|
352
|
-
carriedConcerns,
|
|
353
|
-
reviewMode: executionContext?.mode,
|
|
354
|
-
reviewReason: executionContext?.reason,
|
|
355
|
-
baselineSha: executionContext?.baselineSha,
|
|
356
|
-
reviewFilesVisible: files.length,
|
|
357
|
-
reviewTotalFiles,
|
|
358
|
-
stateMarker: continuity.marker,
|
|
359
|
-
stateNotice: continuity.notice,
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
const scope =
|
|
363
|
-
options.usageScope === undefined ? {} : ({ usageScope: options.usageScope } as const);
|
|
364
|
-
if (!options.post) {
|
|
365
|
-
return ReviewRunOutcome.make({
|
|
366
|
-
review,
|
|
367
|
-
activeFindings,
|
|
368
|
-
activeConcerns,
|
|
369
|
-
coverage,
|
|
370
|
-
plan,
|
|
371
|
-
turns: result.turns,
|
|
372
|
-
usage,
|
|
373
|
-
...scope,
|
|
374
|
-
...(executionContext === undefined
|
|
375
|
-
? {}
|
|
376
|
-
: { reviewMode: executionContext.mode, reviewReason: executionContext.reason }),
|
|
377
|
-
...(continuity.state === undefined ? {} : { state: continuity.state }),
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
const publisher = yield* ReviewPublisher;
|
|
381
|
-
const published = yield* publisher.publish(plan);
|
|
382
|
-
return ReviewRunOutcome.make({
|
|
383
|
-
review,
|
|
384
|
-
activeFindings,
|
|
385
|
-
activeConcerns,
|
|
386
|
-
coverage,
|
|
387
|
-
plan,
|
|
388
|
-
published,
|
|
389
|
-
turns: result.turns,
|
|
390
|
-
usage,
|
|
391
|
-
...scope,
|
|
392
|
-
...(executionContext === undefined
|
|
393
|
-
? {}
|
|
394
|
-
: { reviewMode: executionContext.mode, reviewReason: executionContext.reason }),
|
|
395
|
-
...(continuity.state === undefined ? {} : { state: continuity.state }),
|
|
396
|
-
});
|
|
397
|
-
});
|
package/src/internal/scripted.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { Effect, Layer, Ref, Schema, Stream } from "effect";
|
|
2
|
-
import { LanguageModel, Model, type Response } from "effect/unstable/ai";
|
|
3
|
-
|
|
4
|
-
import { CodeReview } from "./review-agent.ts";
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Deterministic offline model for the flat reviewer: a prompt-aware scripted
|
|
8
|
-
// model that walks the real tool surface — list, diff, read — then returns
|
|
9
|
-
// the scripted review as its terminal JSON. Decisions key on committed
|
|
10
|
-
// history in the prompt, never on call order, so replays stay honest.
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
|
|
13
|
-
export const OFFLINE_LIST_CALL_ID = "list-1";
|
|
14
|
-
export const OFFLINE_DIFF_CALL_ID = "diff-1";
|
|
15
|
-
export const OFFLINE_READ_CALL_ID = "read-1";
|
|
16
|
-
|
|
17
|
-
/** Usage attached to EVERY scripted model turn; tests pin exact aggregates. */
|
|
18
|
-
export const SCRIPTED_TURN_USAGE = { inputTokens: 64, outputTokens: 48 } as const;
|
|
19
|
-
|
|
20
|
-
const scriptedUsage = {
|
|
21
|
-
inputTokens: { total: SCRIPTED_TURN_USAGE.inputTokens },
|
|
22
|
-
outputTokens: { total: SCRIPTED_TURN_USAGE.outputTokens },
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export const scriptedToolTurn = (
|
|
26
|
-
...calls: ReadonlyArray<Response.StreamPartEncoded>
|
|
27
|
-
): ReadonlyArray<Response.StreamPartEncoded> => [
|
|
28
|
-
...calls,
|
|
29
|
-
{ type: "finish", reason: "tool-calls", usage: scriptedUsage },
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
export const scriptedFinalParts = (text: string): ReadonlyArray<Response.StreamPartEncoded> => [
|
|
33
|
-
{ type: "text-start", id: "code-review" },
|
|
34
|
-
{ type: "text-delta", id: "code-review", delta: text },
|
|
35
|
-
{ type: "text-end", id: "code-review" },
|
|
36
|
-
{ type: "finish", reason: "stop", usage: scriptedUsage },
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
/** A prompt-keyed scripted LanguageModel with call and prompt observability. */
|
|
40
|
-
export const makePromptKeyedModel = (
|
|
41
|
-
name: string,
|
|
42
|
-
decide: (promptJson: string) => ReadonlyArray<Response.StreamPartEncoded>,
|
|
43
|
-
) =>
|
|
44
|
-
Effect.gen(function* () {
|
|
45
|
-
const calls = yield* Ref.make(0);
|
|
46
|
-
const prompts = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
47
|
-
const model = Model.make(
|
|
48
|
-
"scripted",
|
|
49
|
-
name,
|
|
50
|
-
Layer.effect(
|
|
51
|
-
LanguageModel.LanguageModel,
|
|
52
|
-
LanguageModel.make({
|
|
53
|
-
generateText: () => Effect.succeed([]),
|
|
54
|
-
streamText: (request) =>
|
|
55
|
-
Stream.unwrap(
|
|
56
|
-
Effect.gen(function* () {
|
|
57
|
-
yield* Ref.update(calls, (value) => value + 1);
|
|
58
|
-
const promptJson = JSON.stringify(request.prompt);
|
|
59
|
-
yield* Ref.update(prompts, (previous) => [...previous, promptJson]);
|
|
60
|
-
return Stream.fromIterable(decide(promptJson));
|
|
61
|
-
}),
|
|
62
|
-
),
|
|
63
|
-
}),
|
|
64
|
-
),
|
|
65
|
-
);
|
|
66
|
-
return { model, calls: Ref.get(calls), prompts: Ref.get(prompts) };
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Build the offline scripted reviewer model. Turn 1 lists the changeset,
|
|
71
|
-
* Turn 2 reads one file diff, Turn 3 reads head context, Turn 4 returns the
|
|
72
|
-
* scripted review JSON.
|
|
73
|
-
*/
|
|
74
|
-
export const makeOfflineReviewerModel = (script: {
|
|
75
|
-
readonly diffPath: string;
|
|
76
|
-
readonly readPath: string;
|
|
77
|
-
readonly review: CodeReview;
|
|
78
|
-
}) =>
|
|
79
|
-
makePromptKeyedModel("pr-review-offline", (promptJson) => {
|
|
80
|
-
if (promptJson.includes(OFFLINE_READ_CALL_ID)) {
|
|
81
|
-
return scriptedFinalParts(JSON.stringify(Schema.encodeSync(CodeReview)(script.review)));
|
|
82
|
-
}
|
|
83
|
-
if (promptJson.includes(OFFLINE_DIFF_CALL_ID)) {
|
|
84
|
-
return scriptedToolTurn({
|
|
85
|
-
type: "tool-call",
|
|
86
|
-
id: OFFLINE_READ_CALL_ID,
|
|
87
|
-
name: "read_file",
|
|
88
|
-
params: { path: script.readPath },
|
|
89
|
-
providerExecuted: false,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
if (promptJson.includes(OFFLINE_LIST_CALL_ID)) {
|
|
93
|
-
return scriptedToolTurn({
|
|
94
|
-
type: "tool-call",
|
|
95
|
-
id: OFFLINE_DIFF_CALL_ID,
|
|
96
|
-
name: "read_file_diff",
|
|
97
|
-
params: { path: script.diffPath },
|
|
98
|
-
providerExecuted: false,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
return scriptedToolTurn({
|
|
102
|
-
type: "tool-call",
|
|
103
|
-
id: OFFLINE_LIST_CALL_ID,
|
|
104
|
-
name: "list_changed_files",
|
|
105
|
-
params: { scope: "all" },
|
|
106
|
-
providerExecuted: false,
|
|
107
|
-
});
|
|
108
|
-
});
|
package/src/internal/source.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { Context, Effect, Schema } from "effect";
|
|
2
|
-
|
|
3
|
-
import { ChangedFile } from "./diff.ts";
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// The pull-request source port: everything the review tools may observe about
|
|
7
|
-
// one pull request. The live adapter speaks the GitHub REST API; the fixture
|
|
8
|
-
// adapter (testing entry) serves an in-memory pull request so ordinary gates
|
|
9
|
-
// and dry runs need no network or credential.
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
/** Reading a file head version larger than this is refused, never truncated silently. */
|
|
13
|
-
export const MAX_FILE_CHARS = 200_000;
|
|
14
|
-
|
|
15
|
-
/** The changeset surface is bounded; larger pull requests fail typed. */
|
|
16
|
-
export const MAX_CHANGED_FILES = 300;
|
|
17
|
-
|
|
18
|
-
/** Pull-request identity and framing shown to the agent as its mission. */
|
|
19
|
-
export class PullRequestMetadata extends Schema.Class<PullRequestMetadata>(
|
|
20
|
-
"@effect-agent/pr-review/PullRequestMetadata",
|
|
21
|
-
)({
|
|
22
|
-
/** `owner/name`, exactly as GitHub renders it. */
|
|
23
|
-
repository: Schema.NonEmptyString.check(Schema.isMaxLength(200)),
|
|
24
|
-
number: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
25
|
-
title: Schema.String.check(Schema.isMaxLength(400)),
|
|
26
|
-
/** Author-provided description; empty when the author left none. */
|
|
27
|
-
body: Schema.String.check(Schema.isMaxLength(20_000)),
|
|
28
|
-
baseRef: Schema.NonEmptyString.check(Schema.isMaxLength(300)),
|
|
29
|
-
/** Exact base commit used to validate persisted incremental-review lineage. */
|
|
30
|
-
baseSha: Schema.optionalKey(Schema.NonEmptyString.check(Schema.isMaxLength(64))),
|
|
31
|
-
headRef: Schema.NonEmptyString.check(Schema.isMaxLength(300)),
|
|
32
|
-
headSha: Schema.NonEmptyString.check(Schema.isMaxLength(64)),
|
|
33
|
-
/** GitHub's own changed-file total; may exceed what `changedFiles` returns. */
|
|
34
|
-
totalChangedFiles: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
|
|
35
|
-
}) {}
|
|
36
|
-
|
|
37
|
-
/** The upstream source failed: API error, network fault, or malformed payload. */
|
|
38
|
-
export class PullRequestSourceFailure extends Schema.TaggedError<PullRequestSourceFailure>()(
|
|
39
|
-
"PullRequestSourceFailure",
|
|
40
|
-
{
|
|
41
|
-
operation: Schema.String,
|
|
42
|
-
reason: Schema.String,
|
|
43
|
-
},
|
|
44
|
-
) {
|
|
45
|
-
override get message() {
|
|
46
|
-
return `Pull-request source operation '${this.operation}' failed: ${this.reason}`;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** A model-supplied path or range was invalid; always fail-closed (SEC-007). */
|
|
51
|
-
export class ReviewInputViolation extends Schema.TaggedError<ReviewInputViolation>()(
|
|
52
|
-
"ReviewInputViolation",
|
|
53
|
-
{
|
|
54
|
-
input: Schema.String,
|
|
55
|
-
reason: Schema.String,
|
|
56
|
-
},
|
|
57
|
-
) {
|
|
58
|
-
override get message() {
|
|
59
|
-
return `Rejected review input '${this.input}': ${this.reason}`;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const BACKSLASH = String.fromCharCode(92);
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Normalize and validate one model-supplied repository-relative path.
|
|
67
|
-
* Absolute paths, drive letters, backslashes, empty segments, `.` and `..`
|
|
68
|
-
* segments are all violations — never silently fixed. The changeset list is
|
|
69
|
-
* the real allowlist; this check is defense in depth for URL construction.
|
|
70
|
-
*/
|
|
71
|
-
export const normalizeRepoRelativePath = (
|
|
72
|
-
path: string,
|
|
73
|
-
): Effect.Effect<string, ReviewInputViolation> => {
|
|
74
|
-
const fail = (reason: string) => Effect.fail(ReviewInputViolation.make({ input: path, reason }));
|
|
75
|
-
if (path.length === 0 || path.length > 512) {
|
|
76
|
-
return fail("Path length is out of bounds.");
|
|
77
|
-
}
|
|
78
|
-
if (path.includes(BACKSLASH)) {
|
|
79
|
-
return fail("Path contains a forbidden backslash.");
|
|
80
|
-
}
|
|
81
|
-
if (path.startsWith("/") || /^[A-Za-z]:/.test(path)) {
|
|
82
|
-
return fail("Path must be repository-relative, not absolute.");
|
|
83
|
-
}
|
|
84
|
-
const segments = path.split("/");
|
|
85
|
-
for (const segment of segments) {
|
|
86
|
-
if (segment === "" || segment === "." || segment === "..") {
|
|
87
|
-
return fail("Path segments must not be empty, '.', or '..'.");
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
return Effect.succeed(segments.join("/"));
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
/** Read-only view of one pull request; the only repository access tools get. */
|
|
94
|
-
export class PullRequestSource extends Context.Service<
|
|
95
|
-
PullRequestSource,
|
|
96
|
-
{
|
|
97
|
-
readonly metadata: Effect.Effect<PullRequestMetadata, PullRequestSourceFailure>;
|
|
98
|
-
/** Files exposed to the model for this run (full PR or selected delta). */
|
|
99
|
-
readonly changedFiles: Effect.Effect<ReadonlyArray<ChangedFile>, PullRequestSourceFailure>;
|
|
100
|
-
/** Full current PR diff used only for host-side anchor/state validation. */
|
|
101
|
-
readonly anchorFiles: Effect.Effect<ReadonlyArray<ChangedFile>, PullRequestSourceFailure>;
|
|
102
|
-
/**
|
|
103
|
-
* The head-version content of one CHANGED file. Paths outside the
|
|
104
|
-
* changeset are violations: the reviewer reads the change, not the tree.
|
|
105
|
-
*/
|
|
106
|
-
readonly readFile: (
|
|
107
|
-
path: string,
|
|
108
|
-
) => Effect.Effect<string, PullRequestSourceFailure | ReviewInputViolation>;
|
|
109
|
-
}
|
|
110
|
-
>()("@effect-agent/pr-review/PullRequestSource") {}
|
package/src/testing.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
// Deterministic test helpers: in-memory adapters for both ports and
|
|
2
|
-
// prompt-keyed scripted models that walk the real tool surfaces. Everything
|
|
3
|
-
// here runs with no network and no credentials, so consumers can test their
|
|
4
|
-
// adaptations — guidance, ignore globs, extra tools, custom ports — on every
|
|
5
|
-
// ordinary gate.
|
|
6
|
-
export * from "./internal/fan-out-scripted.ts";
|
|
7
|
-
export * from "./internal/fixtures.ts";
|
|
8
|
-
export * from "./internal/scripted.ts";
|