@effect-agent/pr-review 0.1.0-beta.8 → 0.1.0-beta.80
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 -158
- package/dist/Review.d.mts +295 -0
- package/dist/Review.mjs +704 -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 -716
- package/dist/index.mjs +3 -66
- package/dist/repository-BzSG74vX.mjs +101 -0
- package/dist/repository-BzSG74vX.mjs.map +1 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -54
- package/src/Review.ts +1058 -0
- package/src/ReviewRepository.ts +9 -0
- package/src/index.ts +2 -20
- package/src/internal/repository.ts +156 -0
- package/dist/action.d.mts +0 -185
- package/dist/action.mjs +0 -406
- package/dist/action.mjs.map +0 -1
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -102
- package/dist/cli.mjs.map +0 -1
- package/dist/fan-out-BBEATQwc.d.mts +0 -997
- package/dist/github-BZNzmxao.mjs +0 -1372
- package/dist/github-BZNzmxao.mjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/providers-J6BKHyHe.mjs +0 -986
- package/dist/providers-J6BKHyHe.mjs.map +0 -1
- package/dist/testing.d.mts +0 -131
- package/dist/testing.mjs +0 -228
- package/dist/testing.mjs.map +0 -1
- package/src/action.ts +0 -666
- package/src/cli.ts +0 -213
- package/src/internal/action-entry.ts +0 -41
- 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 -127
- package/src/internal/github-env.ts +0 -128
- package/src/internal/github.ts +0 -531
- 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/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
|
@@ -1,986 +0,0 @@
|
|
|
1
|
-
import { A as toStoredConcern, Bt as ReviewConcern, Ct as planReviewUnits, D as fromStoredFinding, Dt as CodeReview, E as fromStoredConcern, G as FileReviewDelegationFailure, Ht as ReviewMission, I as computeChangesetFingerprint, Kt as clampMaxFindings, Nt as ListChangedFiles, Ot as FileDiffQuery, Qt as resolveGuidance, R as renderFingerprintMarker, Rt as ReadFile, T as computeProfileFingerprint, V as FanOutCoordinatorToolkitLayer, Vt as ReviewFinding, Wt as ReviewToolkitLayer, Y as FileReviewToolkitLayer, Yt as makeReviewInstructions, Z as FileReviewUnitResult, _ as ReviewState, a as PublishedReview, an as ReviewInputViolation, c as gitHubPriorReviewsLayer, dn as commentableLines, j as toStoredFinding, l as gitHubPullRequestSourceLayer, n as GitHubReviewTarget, nn as PullRequestMetadata, o as ReviewPublisher, p as ReviewExecutionContext, pt as makeFanOutReviewSuite, q as FileReviewRequest, qt as defaultReviewPolicy, rn as PullRequestSource, st as fanOutHandlersLayerFor, u as gitHubReviewPublisherLayer, w as buildProfileMission, wt as rankAndDedupeFindings, zt as ReadFileDiff } from "./github-BZNzmxao.mjs";
|
|
2
|
-
import { Config, Effect, FileSystem, Layer, Option, Schema } from "effect";
|
|
3
|
-
import { Agent, AgentPolicy, AgentRuntime, IdGenerator, SubagentReservationsMemoryLive, UsageBudgetLimits, UsageTotals, getToolExecutionClass, makeUsageBudget, toRunBudgetHook } from "effect-agent";
|
|
4
|
-
import { Toolkit } from "effect/unstable/ai";
|
|
5
|
-
import { FetchHttpClient } from "effect/unstable/http";
|
|
6
|
-
import { AnthropicClient, AnthropicLanguageModel } from "@effect/ai-anthropic";
|
|
7
|
-
import { OpenAiClient, OpenAiLanguageModel } from "@effect/ai-openai";
|
|
8
|
-
//#region src/internal/coverage.ts
|
|
9
|
-
const ReviewShape = Schema.Literals(["flat", "fan-out"]);
|
|
10
|
-
var FailedReviewUnit = class extends Schema.Class("@effect-agent/pr-review/FailedReviewUnit")({
|
|
11
|
-
unitId: Schema.NonEmptyString.check(Schema.isMaxLength(32)),
|
|
12
|
-
errorTag: Schema.NonEmptyString.check(Schema.isMaxLength(256))
|
|
13
|
-
}) {};
|
|
14
|
-
var ReviewCoverage = class extends Schema.Class("@effect-agent/pr-review/ReviewCoverage")({
|
|
15
|
-
status: Schema.Literals(["complete", "incomplete"]),
|
|
16
|
-
requiredPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
|
|
17
|
-
reviewedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
|
|
18
|
-
unreviewedPaths: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(512))).check(Schema.isMaxLength(300)),
|
|
19
|
-
failedUnits: Schema.Array(FailedReviewUnit).check(Schema.isMaxLength(8)),
|
|
20
|
-
reasons: Schema.Array(Schema.NonEmptyString.check(Schema.isMaxLength(1e3))).check(Schema.isMaxLength(20))
|
|
21
|
-
}) {};
|
|
22
|
-
const toolTrace = (events) => {
|
|
23
|
-
const declared = /* @__PURE__ */ new Map();
|
|
24
|
-
const succeeded = /* @__PURE__ */ new Map();
|
|
25
|
-
const failed = /* @__PURE__ */ new Map();
|
|
26
|
-
for (const event of events) {
|
|
27
|
-
if (event._tag === "ToolCallDeclared") declared.set(event.toolCallId, event);
|
|
28
|
-
if (event._tag === "ToolCallSucceeded") succeeded.set(event.toolCallId, event);
|
|
29
|
-
if (event._tag === "ToolCallFailed") failed.set(event.toolCallId, event);
|
|
30
|
-
}
|
|
31
|
-
return {
|
|
32
|
-
declared,
|
|
33
|
-
succeeded,
|
|
34
|
-
failed
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
const sortedUnique = (values) => [...new Set(values)].sort((left, right) => left < right ? -1 : left > right ? 1 : 0);
|
|
38
|
-
const boundedListReason = (label, values) => {
|
|
39
|
-
const items = sortedUnique(values);
|
|
40
|
-
let rendered = `${label} (${items.length}): `;
|
|
41
|
-
for (let index = 0; index < items.length; index += 1) {
|
|
42
|
-
const item = items[index] ?? "";
|
|
43
|
-
const separator = index === 0 ? "" : ", ";
|
|
44
|
-
const omitted = items.length - index - 1;
|
|
45
|
-
const suffix = omitted === 0 ? "" : ` … (+${omitted} more)`;
|
|
46
|
-
if (`${rendered}${separator}${item}${suffix}`.length > 1e3) {
|
|
47
|
-
const omission = `… (+${items.length - index} more)`;
|
|
48
|
-
return `${rendered.slice(0, 1e3 - omission.length)}${omission}`;
|
|
49
|
-
}
|
|
50
|
-
rendered = `${rendered}${separator}${item}`;
|
|
51
|
-
}
|
|
52
|
-
return rendered;
|
|
53
|
-
};
|
|
54
|
-
const flatCoverage = (files, totalFiles, trace) => {
|
|
55
|
-
const requiredPaths = sortedUnique(files.map((file) => file.path));
|
|
56
|
-
const reviewed = /* @__PURE__ */ new Set();
|
|
57
|
-
const failedPaths = /* @__PURE__ */ new Set();
|
|
58
|
-
for (const [toolCallId, declaration] of trace.declared) {
|
|
59
|
-
if (declaration.toolName !== "read_file_diff") continue;
|
|
60
|
-
const query = Schema.decodeUnknownOption(FileDiffQuery)(declaration.parameters);
|
|
61
|
-
if (Option.isNone(query)) continue;
|
|
62
|
-
if (trace.succeeded.has(toolCallId)) reviewed.add(query.value.path);
|
|
63
|
-
if (trace.failed.has(toolCallId)) failedPaths.add(query.value.path);
|
|
64
|
-
}
|
|
65
|
-
const undiffable = files.filter((file) => file.patch === void 0).map((file) => file.path);
|
|
66
|
-
const unreviewed = requiredPaths.filter((path) => !reviewed.has(path) || undiffable.includes(path) || failedPaths.has(path));
|
|
67
|
-
const reasons = [];
|
|
68
|
-
if (files.length < totalFiles) reasons.push(`review range exposed ${files.length} of ${totalFiles} required files`);
|
|
69
|
-
if (undiffable.length > 0) reasons.push(boundedListReason("required paths have no textual diff", undiffable));
|
|
70
|
-
if (failedPaths.size > 0) reasons.push(boundedListReason("diff reads failed", failedPaths));
|
|
71
|
-
if (unreviewed.length > 0) reasons.push(boundedListReason("required paths were not successfully reviewed", unreviewed));
|
|
72
|
-
return ReviewCoverage.make({
|
|
73
|
-
status: reasons.length === 0 ? "complete" : "incomplete",
|
|
74
|
-
requiredPaths,
|
|
75
|
-
reviewedPaths: sortedUnique(reviewed),
|
|
76
|
-
unreviewedPaths: sortedUnique(unreviewed),
|
|
77
|
-
failedUnits: [],
|
|
78
|
-
reasons
|
|
79
|
-
});
|
|
80
|
-
};
|
|
81
|
-
const fanOutCoverage = (files, totalFiles, trace) => {
|
|
82
|
-
const plan = planReviewUnits(files, { totalChangedFiles: totalFiles });
|
|
83
|
-
const declarationsByUnit = /* @__PURE__ */ new Map();
|
|
84
|
-
for (const [toolCallId, declaration] of trace.declared) {
|
|
85
|
-
if (declaration.toolName !== "delegate_file_review") continue;
|
|
86
|
-
const request = Schema.decodeUnknownOption(FileReviewRequest)(declaration.parameters);
|
|
87
|
-
if (Option.isNone(request)) continue;
|
|
88
|
-
const declarations = declarationsByUnit.get(request.value.unitId) ?? [];
|
|
89
|
-
declarations.push({
|
|
90
|
-
id: toolCallId,
|
|
91
|
-
paths: request.value.paths
|
|
92
|
-
});
|
|
93
|
-
declarationsByUnit.set(request.value.unitId, declarations);
|
|
94
|
-
}
|
|
95
|
-
const reviewed = /* @__PURE__ */ new Set();
|
|
96
|
-
const unreviewed = /* @__PURE__ */ new Set([...plan.undiffablePaths, ...plan.unassignedPaths]);
|
|
97
|
-
const failedUnits = [];
|
|
98
|
-
const reasons = [];
|
|
99
|
-
for (const unit of plan.units) {
|
|
100
|
-
const declarations = declarationsByUnit.get(unit.unitId) ?? [];
|
|
101
|
-
const expectedPaths = [...unit.paths];
|
|
102
|
-
const exact = declarations.filter((declaration) => declaration.paths.length === expectedPaths.length && declaration.paths.every((path, index) => path === expectedPaths[index]));
|
|
103
|
-
const successful = exact.filter((declaration) => {
|
|
104
|
-
const event = trace.succeeded.get(declaration.id);
|
|
105
|
-
if (event === void 0 || trace.failed.has(declaration.id)) return false;
|
|
106
|
-
const result = Schema.decodeUnknownOption(FileReviewUnitResult)(event.result);
|
|
107
|
-
return Option.isSome(result) && result.value.unitId === unit.unitId;
|
|
108
|
-
});
|
|
109
|
-
if (declarations.length === 1 && exact.length === 1 && successful.length === 1) {
|
|
110
|
-
for (const path of unit.paths) reviewed.add(path);
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
for (const path of unit.paths) unreviewed.add(path);
|
|
114
|
-
const failure = declarations.map((declaration) => trace.failed.get(declaration.id)).find((event) => event !== void 0);
|
|
115
|
-
const returnedFailure = declarations.map((declaration) => trace.succeeded.get(declaration.id)).filter((event) => event !== void 0).map((event) => Schema.decodeUnknownOption(FileReviewDelegationFailure)(event.result)).find(Option.isSome);
|
|
116
|
-
failedUnits.push(FailedReviewUnit.make({
|
|
117
|
-
unitId: unit.unitId,
|
|
118
|
-
errorTag: failure?.errorTag ?? (returnedFailure !== void 0 ? returnedFailure.value._tag === "FileReviewUnitFailed" ? `${returnedFailure.value._tag}:${returnedFailure.value.childErrorTag}` : returnedFailure.value._tag : void 0) ?? (declarations.length === 0 ? "UnitNotAssigned" : declarations.length > 1 ? "UnitAssignedMultipleTimes" : exact.length === 0 ? "UnitAssignmentMismatch" : "UnitDidNotSettleSuccessfully")
|
|
119
|
-
}));
|
|
120
|
-
}
|
|
121
|
-
if (plan.truncated) reasons.push(`review range exposed ${files.length} of ${totalFiles} required files`);
|
|
122
|
-
if (plan.undiffablePaths.length > 0) reasons.push(boundedListReason("required paths have no textual diff", plan.undiffablePaths));
|
|
123
|
-
if (plan.unassignedPaths.length > 0) reasons.push(boundedListReason("fan-out capacity left paths unassigned", plan.unassignedPaths));
|
|
124
|
-
if (failedUnits.length > 0) reasons.push(boundedListReason("review units did not complete", failedUnits.map((unit) => `${unit.unitId} (${unit.errorTag})`)));
|
|
125
|
-
return ReviewCoverage.make({
|
|
126
|
-
status: reasons.length === 0 ? "complete" : "incomplete",
|
|
127
|
-
requiredPaths: sortedUnique(files.map((file) => file.path)),
|
|
128
|
-
reviewedPaths: sortedUnique(reviewed),
|
|
129
|
-
unreviewedPaths: sortedUnique(unreviewed),
|
|
130
|
-
failedUnits,
|
|
131
|
-
reasons
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
|
-
/** Assess one settled run without trusting its prose summary or verdict. */
|
|
135
|
-
const assessReviewCoverage = (input) => {
|
|
136
|
-
const trace = toolTrace(input.events);
|
|
137
|
-
const coverage = input.shape === "fan-out" ? fanOutCoverage(input.files, input.totalFiles, trace) : flatCoverage(input.files, input.totalFiles, trace);
|
|
138
|
-
if (input.anchorFiles.length >= input.totalAnchorFiles) return coverage;
|
|
139
|
-
return ReviewCoverage.make({
|
|
140
|
-
...coverage,
|
|
141
|
-
status: "incomplete",
|
|
142
|
-
reasons: [...coverage.reasons, `full pull-request anchor surface exposed ${input.anchorFiles.length} of ${input.totalAnchorFiles} required files`]
|
|
143
|
-
});
|
|
144
|
-
};
|
|
145
|
-
//#endregion
|
|
146
|
-
//#region src/internal/effort.ts
|
|
147
|
-
/**
|
|
148
|
-
* Names accepted on user-facing surfaces (the action input, the CLI flag),
|
|
149
|
-
* mapped to fixed points on the axis. These same names anchor every offered
|
|
150
|
-
* rung during resolution, so a named input always lands on its same-named
|
|
151
|
-
* rung when the provider offers it — `high` never resolves to `medium` just
|
|
152
|
-
* because a ladder is short.
|
|
153
|
-
*/
|
|
154
|
-
const EFFORT_ALIASES = {
|
|
155
|
-
low: 0,
|
|
156
|
-
medium: .25,
|
|
157
|
-
high: .5,
|
|
158
|
-
xhigh: .75,
|
|
159
|
-
max: 1
|
|
160
|
-
};
|
|
161
|
-
const aliasPosition = EFFORT_ALIASES;
|
|
162
|
-
/** An effort input that is neither a known name nor a number on [0, 1]. */
|
|
163
|
-
var InvalidEffortInput = class extends Schema.TaggedError()("InvalidEffortInput", { input: Schema.String }) {
|
|
164
|
-
get message() {
|
|
165
|
-
return `Invalid effort '${this.input}': expected one of ${Object.keys(EFFORT_ALIASES).join(", ")} or a number between 0 and 1.`;
|
|
166
|
-
}
|
|
167
|
-
};
|
|
168
|
-
const isEffortPosition = (value) => Number.isFinite(value) && value >= 0 && value <= 1;
|
|
169
|
-
/**
|
|
170
|
-
* Parse a user-supplied effort into a position: a name (`high`) or a bare
|
|
171
|
-
* number (`0.75`). Returns undefined for anything else so the caller can fail
|
|
172
|
-
* typed — a typo must stay visible, never silently become a level.
|
|
173
|
-
*/
|
|
174
|
-
const parseEffortPosition = (raw) => {
|
|
175
|
-
const normalized = raw.trim().toLowerCase();
|
|
176
|
-
const named = aliasPosition[normalized];
|
|
177
|
-
if (named !== void 0) return named;
|
|
178
|
-
if (normalized === "") return void 0;
|
|
179
|
-
const numeric = Number(normalized);
|
|
180
|
-
return isEffortPosition(numeric) ? numeric : void 0;
|
|
181
|
-
};
|
|
182
|
-
/**
|
|
183
|
-
* Land a position on one provider's offered ladder: the highest offered rung
|
|
184
|
-
* whose canonical alias position is at or below the requested position.
|
|
185
|
-
* Anchoring on the alias positions (instead of scaling by ladder index) keeps
|
|
186
|
-
* two properties at once: a named input lands on its same-named rung whenever
|
|
187
|
-
* the provider offers it, and anything between rungs rounds DOWN so
|
|
188
|
-
* resolution never costs more than was asked for.
|
|
189
|
-
*/
|
|
190
|
-
const resolveEffortRung = (position, rungs) => {
|
|
191
|
-
const clamped = Math.min(1, Math.max(0, position));
|
|
192
|
-
let selected = rungs[0];
|
|
193
|
-
for (const rung of rungs) if (EFFORT_ALIASES[rung] <= clamped) selected = rung;
|
|
194
|
-
return selected;
|
|
195
|
-
};
|
|
196
|
-
//#endregion
|
|
197
|
-
//#region src/internal/ignore.ts
|
|
198
|
-
const REGEX_SPECIALS = /[.+^${}()|[\]\\]/g;
|
|
199
|
-
const CROSSING_SLASH = "\0";
|
|
200
|
-
const CROSSING = "";
|
|
201
|
-
/**
|
|
202
|
-
* The supported glob vocabulary is deliberately minimal: `**` crosses
|
|
203
|
-
* directory separators, `*` and `?` stay within one path segment, everything
|
|
204
|
-
* else is literal. Every string compiles — there is no invalid pattern.
|
|
205
|
-
*/
|
|
206
|
-
const globToRegExpSource = (pattern) => pattern.replace(REGEX_SPECIALS, String.raw`\$&`).replaceAll("**/", CROSSING_SLASH).replaceAll("**", CROSSING).replaceAll("*", "[^/]*").replaceAll("?", "[^/]").replaceAll(CROSSING_SLASH, "(?:.*/)?").replaceAll(CROSSING, ".*");
|
|
207
|
-
/** Compile ignore globs into one predicate over repository-relative paths. */
|
|
208
|
-
const compileIgnoreGlobs = (patterns) => {
|
|
209
|
-
if (patterns.length === 0) return () => false;
|
|
210
|
-
const expressions = patterns.map((pattern) => new RegExp(`^(?:${globToRegExpSource(pattern)})$`));
|
|
211
|
-
return (path) => expressions.some((expression) => expression.test(path));
|
|
212
|
-
};
|
|
213
|
-
/**
|
|
214
|
-
* Decorate the ambient PullRequestSource with configured ignore globs. The
|
|
215
|
-
* resulting Layer requires the undecorated source, so callers provide their
|
|
216
|
-
* real adapter beneath it. Metadata's changed-file total is reduced by the
|
|
217
|
-
* ignored count: from the reviewer's perspective the ignored files do not
|
|
218
|
-
* exist, and truncation reporting stays about the reviewer's own bound.
|
|
219
|
-
*/
|
|
220
|
-
const ignoringPullRequestSourceLayer = (patterns) => Layer.effect(PullRequestSource)(Effect.gen(function* () {
|
|
221
|
-
const source = yield* PullRequestSource;
|
|
222
|
-
const ignored = compileIgnoreGlobs(patterns);
|
|
223
|
-
const changedFiles = source.changedFiles.pipe(Effect.map((files) => files.filter((file) => !ignored(file.path))));
|
|
224
|
-
const anchorFiles = source.anchorFiles.pipe(Effect.map((files) => files.filter((file) => !ignored(file.path))));
|
|
225
|
-
const metadata = Effect.gen(function* () {
|
|
226
|
-
const [meta, files] = yield* Effect.all([source.metadata, source.anchorFiles]);
|
|
227
|
-
const ignoredCount = files.filter((file) => ignored(file.path)).length;
|
|
228
|
-
return PullRequestMetadata.make({
|
|
229
|
-
...meta,
|
|
230
|
-
totalChangedFiles: Math.max(0, meta.totalChangedFiles - ignoredCount)
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
return PullRequestSource.of({
|
|
234
|
-
metadata,
|
|
235
|
-
changedFiles,
|
|
236
|
-
anchorFiles,
|
|
237
|
-
readFile: (path) => ignored(path) ? Effect.fail(ReviewInputViolation.make({
|
|
238
|
-
input: path,
|
|
239
|
-
reason: "Path is excluded from this review by configuration."
|
|
240
|
-
})) : source.readFile(path)
|
|
241
|
-
});
|
|
242
|
-
}));
|
|
243
|
-
//#endregion
|
|
244
|
-
//#region src/internal/render.ts
|
|
245
|
-
const ReviewEvent = Schema.Literals([
|
|
246
|
-
"COMMENT",
|
|
247
|
-
"APPROVE",
|
|
248
|
-
"REQUEST_CHANGES"
|
|
249
|
-
]);
|
|
250
|
-
/** One inline comment exactly as the GitHub review API accepts it. */
|
|
251
|
-
var ReviewCommentDraft = class extends Schema.Class("@effect-agent/pr-review/ReviewCommentDraft")({
|
|
252
|
-
path: Schema.NonEmptyString,
|
|
253
|
-
/** The last (or only) commented line, RIGHT side of the diff. */
|
|
254
|
-
line: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
255
|
-
/** Present only for multi-line comments; strictly less than `line`. */
|
|
256
|
-
startLine: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
257
|
-
body: Schema.NonEmptyString
|
|
258
|
-
}) {};
|
|
259
|
-
/** The complete, validated review ready for one GitHub reviews API call. */
|
|
260
|
-
var ReviewPublicationPlan = class extends Schema.Class("@effect-agent/pr-review/ReviewPublicationPlan")({
|
|
261
|
-
event: ReviewEvent,
|
|
262
|
-
body: Schema.String.check(Schema.isMaxLength(6e4)),
|
|
263
|
-
comments: Schema.Array(ReviewCommentDraft),
|
|
264
|
-
/** Findings whose anchors failed diff validation; folded into `body`. */
|
|
265
|
-
demoted: Schema.Array(ReviewFinding),
|
|
266
|
-
/** The head commit the diffs were fetched at; pins the posted review. */
|
|
267
|
-
commitSha: Schema.NonEmptyString.check(Schema.isMaxLength(64))
|
|
268
|
-
}) {};
|
|
269
|
-
const severityEmoji = {
|
|
270
|
-
blocking: "🛑",
|
|
271
|
-
important: "⚠️",
|
|
272
|
-
nit: "💅"
|
|
273
|
-
};
|
|
274
|
-
const severityRank$1 = {
|
|
275
|
-
blocking: 0,
|
|
276
|
-
important: 1,
|
|
277
|
-
nit: 2
|
|
278
|
-
};
|
|
279
|
-
const severityLabel = {
|
|
280
|
-
blocking: `${severityEmoji.blocking} blocking`,
|
|
281
|
-
important: `${severityEmoji.important} important`,
|
|
282
|
-
nit: `${severityEmoji.nit} nit`
|
|
283
|
-
};
|
|
284
|
-
/** A fence long enough that the suggestion content can never close it early. */
|
|
285
|
-
const suggestionFence = (suggestion) => {
|
|
286
|
-
let fence = "```";
|
|
287
|
-
while (suggestion.includes(fence)) fence = `${fence}\``;
|
|
288
|
-
return fence;
|
|
289
|
-
};
|
|
290
|
-
const renderCommentBody = (finding) => {
|
|
291
|
-
const parts = [
|
|
292
|
-
`**[${severityLabel[finding.severity]}] ${finding.title}**`,
|
|
293
|
-
"",
|
|
294
|
-
finding.body
|
|
295
|
-
];
|
|
296
|
-
if (finding.suggestion !== void 0) {
|
|
297
|
-
const fence = suggestionFence(finding.suggestion);
|
|
298
|
-
parts.push("", `${fence}suggestion`, finding.suggestion, fence);
|
|
299
|
-
}
|
|
300
|
-
return parts.join("\n");
|
|
301
|
-
};
|
|
302
|
-
const renderDemoted = (finding, reason) => {
|
|
303
|
-
return `- ${`\`${finding.path}:${finding.startLine}${finding.endLine !== finding.startLine ? `-${finding.endLine}` : ""}\``} **[${severityLabel[finding.severity]}] ${finding.title}** — ${finding.body} _(demoted: ${reason})_`;
|
|
304
|
-
};
|
|
305
|
-
const countNoun = (count, noun) => `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
306
|
-
/** The validated finding + concern severities, tallied for callout and event. */
|
|
307
|
-
const severityCounts = (review, carriedFindings = [], carriedConcerns = []) => {
|
|
308
|
-
const severities = [
|
|
309
|
-
...review.findings.map((finding) => finding.severity),
|
|
310
|
-
...(review.concerns ?? []).map((concern) => concern.severity),
|
|
311
|
-
...carriedFindings.map((finding) => finding.severity),
|
|
312
|
-
...carriedConcerns.map((concern) => concern.severity)
|
|
313
|
-
];
|
|
314
|
-
return {
|
|
315
|
-
blocking: severities.filter((severity) => severity === "blocking").length,
|
|
316
|
-
important: severities.filter((severity) => severity === "important").length,
|
|
317
|
-
total: severities.length
|
|
318
|
-
};
|
|
319
|
-
};
|
|
320
|
-
/**
|
|
321
|
-
* The opening callout: the review's overall tier, derived HOST-SIDE from the
|
|
322
|
-
* validated severities (never from model prose), described by what GitHub
|
|
323
|
-
* renders it as. `[!CAUTION]` is a red banner, `[!IMPORTANT]` a purple one;
|
|
324
|
-
* the blockquote tiers read as informational.
|
|
325
|
-
*/
|
|
326
|
-
const renderVerdictCallout = (review, options) => {
|
|
327
|
-
const counts = severityCounts(review, options.carriedFindings, options.carriedConcerns);
|
|
328
|
-
if (options.coverage?.status === "incomplete") return `> [!CAUTION]\n> Review coverage is incomplete — the check must not pass.${counts.blocking > 0 ? ` It also has ${countNoun(counts.blocking, "blocking finding")}.` : ""}`;
|
|
329
|
-
if (counts.blocking > 0) return `> [!CAUTION]\n> ${countNoun(counts.blocking, "blocking finding")} — do not merge before addressing ${counts.blocking === 1 ? "it" : "them"}.`;
|
|
330
|
-
if (counts.important > 0) return `> [!IMPORTANT]\n> ${countNoun(counts.important, "important finding")} to address before merging.`;
|
|
331
|
-
if (counts.total > 0) return "> ℹ️ Minor suggestions only — mergeable as-is.";
|
|
332
|
-
return review.verdict === "approve" ? "> ✅ No issues found." : "> ℹ️ No findings — see the summary.";
|
|
333
|
-
};
|
|
334
|
-
const renderConcern = (concern) => [
|
|
335
|
-
`### ${severityEmoji[concern.severity]} ${concern.title}`,
|
|
336
|
-
"",
|
|
337
|
-
concern.body
|
|
338
|
-
].join("\n");
|
|
339
|
-
const renderCarriedFinding = (finding) => `- \`${finding.path}:${finding.startLine}${finding.endLine === finding.startLine ? "" : `-${finding.endLine}`}\` **[${severityLabel[finding.severity]}] ${finding.title}** — ${finding.body}`;
|
|
340
|
-
/** HTML comments must not contain `--`; interpolated values are sanitized. */
|
|
341
|
-
const commentSafe = (value) => value.replaceAll("--", "- -");
|
|
342
|
-
/**
|
|
343
|
-
* The invisible staleness note addressed to whoever reads the review later —
|
|
344
|
-
* a human or a downstream agent: which commit the findings were written
|
|
345
|
-
* against, and that line callouts age the moment new commits land.
|
|
346
|
-
*/
|
|
347
|
-
const renderReviewMetadata = (options) => [
|
|
348
|
-
"<!-- effect-agent-pr-review metadata",
|
|
349
|
-
`reviewed-head: ${commentSafe(options.headSha)}`,
|
|
350
|
-
...options.baseRef !== void 0 && options.headRef !== void 0 ? [`base-ref: ${commentSafe(options.baseRef)}`, `head-ref: ${commentSafe(options.headRef)}`] : [],
|
|
351
|
-
`files-visible: ${options.filesVisible} of ${options.totalChangedFiles}`,
|
|
352
|
-
...options.reviewMode === void 0 ? [] : [`review-mode: ${options.reviewMode}`],
|
|
353
|
-
...options.baselineSha === void 0 ? [] : [`incremental-baseline: ${commentSafe(options.baselineSha)}`],
|
|
354
|
-
"Findings were written against the head commit above; if commits have landed",
|
|
355
|
-
"since, treat file and line callouts as potentially stale and re-diff first.",
|
|
356
|
-
"-->"
|
|
357
|
-
].join("\n");
|
|
358
|
-
/**
|
|
359
|
-
* Why one finding cannot become an inline comment, or undefined when it can.
|
|
360
|
-
* Exported so tests can pin each rule individually.
|
|
361
|
-
*/
|
|
362
|
-
const anchorViolation = (finding, files) => {
|
|
363
|
-
const file = files.find((candidate) => candidate.path === finding.path);
|
|
364
|
-
if (file === void 0) return "path is not part of the changeset";
|
|
365
|
-
if (file.patch === void 0) return "file has no textual diff";
|
|
366
|
-
if (finding.endLine < finding.startLine) return "endLine precedes startLine";
|
|
367
|
-
if (finding.endLine - finding.startLine + 1 > 100) return "range is implausibly large";
|
|
368
|
-
const anchors = commentableLines(file.patch);
|
|
369
|
-
for (let line = finding.startLine; line <= finding.endLine; line += 1) if (!anchors.has(line)) return `line ${line} is not part of the diff`;
|
|
370
|
-
};
|
|
371
|
-
/**
|
|
372
|
-
* Turn one validated review into the exact GitHub publication payload.
|
|
373
|
-
* `applyVerdict: false` (the safe default) always posts a COMMENT review;
|
|
374
|
-
* `true` maps the model's verdict onto APPROVE / REQUEST_CHANGES.
|
|
375
|
-
*/
|
|
376
|
-
const planPublication = (review, files, options) => {
|
|
377
|
-
const comments = [];
|
|
378
|
-
const demoted = [];
|
|
379
|
-
for (const finding of review.findings) {
|
|
380
|
-
const violation = anchorViolation(finding, files);
|
|
381
|
-
if (violation === void 0) comments.push(ReviewCommentDraft.make({
|
|
382
|
-
path: finding.path,
|
|
383
|
-
line: finding.endLine,
|
|
384
|
-
...finding.endLine > finding.startLine ? { startLine: finding.startLine } : {},
|
|
385
|
-
body: renderCommentBody(finding)
|
|
386
|
-
}));
|
|
387
|
-
else demoted.push({
|
|
388
|
-
finding,
|
|
389
|
-
reason: violation
|
|
390
|
-
});
|
|
391
|
-
}
|
|
392
|
-
const sortedConcerns = [...review.concerns ?? []].sort((a, b) => severityRank$1[a.severity] - severityRank$1[b.severity]);
|
|
393
|
-
const sortedDemoted = [...demoted].sort((a, b) => severityRank$1[a.finding.severity] - severityRank$1[b.finding.severity]);
|
|
394
|
-
const footerParts = ["Automated review by @effect-agent/pr-review"];
|
|
395
|
-
if (options.modelLabel !== void 0) footerParts.push(options.modelLabel);
|
|
396
|
-
if (options.usage !== void 0 && options.usageScope !== void 0) {
|
|
397
|
-
const scope = options.usageScope === "coordinator" ? " (coordinator)" : "";
|
|
398
|
-
footerParts.push(`${options.usage.inputTokens} in / ${options.usage.outputTokens} out tokens${scope}`);
|
|
399
|
-
}
|
|
400
|
-
if (options.runUrl !== void 0) footerParts.push(`[run](${options.runUrl})`);
|
|
401
|
-
footerParts.push(`reviewed at ${options.headSha.slice(0, 7)}`);
|
|
402
|
-
const footer = `_${footerParts.join(" · ")}._`;
|
|
403
|
-
const renderHead = (concernsKept, demotedKept, omitted) => {
|
|
404
|
-
const carriedFindings = options.carriedFindings ?? [];
|
|
405
|
-
const carriedConcerns = options.carriedConcerns ?? [];
|
|
406
|
-
const parts = [renderVerdictCallout(review, {
|
|
407
|
-
carriedFindings,
|
|
408
|
-
carriedConcerns,
|
|
409
|
-
coverage: options.coverage
|
|
410
|
-
})];
|
|
411
|
-
if (options.reviewMode !== void 0 && options.reviewReason !== void 0) parts.push("", options.reviewMode === "incremental" ? `**Incremental scope:** reviewed ${options.reviewFilesVisible ?? files.length} file(s) ${options.reviewReason}. Unchanged accepted scope was preserved and not reopened.` : `**Full-diff scope:** ${options.reviewReason}.`);
|
|
412
|
-
if (options.stateNotice !== void 0) parts.push("", `⚠️ Continuity state was not stored (${options.stateNotice.slice(0, 1e3)}); the next run will safely review the full diff.`);
|
|
413
|
-
parts.push("", review.summary);
|
|
414
|
-
if (options.coverage?.status === "incomplete") parts.push("", "### 🛑 Incomplete coverage", "", ...options.coverage.reasons.map((reason) => `- ${reason}`));
|
|
415
|
-
if (carriedFindings.length > 0) parts.push("", "### Unresolved findings carried from unchanged scope", "", ...carriedFindings.map(renderCarriedFinding));
|
|
416
|
-
if (carriedConcerns.length > 0) {
|
|
417
|
-
parts.push("", "### Unresolved concerns carried to the final audit");
|
|
418
|
-
for (const concern of carriedConcerns) parts.push("", renderConcern(concern));
|
|
419
|
-
}
|
|
420
|
-
for (const concern of sortedConcerns.slice(0, concernsKept)) parts.push("", renderConcern(concern));
|
|
421
|
-
if (files.length < options.totalChangedFiles) parts.push("", `⚠️ Reviewed ${files.length} of ${options.totalChangedFiles} changed files — the changeset exceeded the reviewer's file bound.`);
|
|
422
|
-
if (demotedKept > 0) parts.push("", "### Findings without a valid diff anchor", ...sortedDemoted.slice(0, demotedKept).map(({ finding, reason }) => renderDemoted(finding, reason)));
|
|
423
|
-
if (omitted > 0) parts.push("", `⚠️ ${countNoun(omitted, "review item")} omitted — the body exceeded GitHub's review size cap.`);
|
|
424
|
-
parts.push("", footer);
|
|
425
|
-
return parts.join("\n");
|
|
426
|
-
};
|
|
427
|
-
const counts = severityCounts(review, options.carriedFindings ?? [], options.carriedConcerns ?? []);
|
|
428
|
-
const event = !options.applyVerdict ? "COMMENT" : options.coverage?.status === "incomplete" || counts.blocking > 0 ? "REQUEST_CHANGES" : review.verdict === "approve" && counts.important === 0 ? "APPROVE" : "COMMENT";
|
|
429
|
-
const tail = [
|
|
430
|
-
renderReviewMetadata({
|
|
431
|
-
headSha: options.headSha,
|
|
432
|
-
baseRef: options.baseRef,
|
|
433
|
-
headRef: options.headRef,
|
|
434
|
-
filesVisible: options.reviewFilesVisible ?? files.length,
|
|
435
|
-
totalChangedFiles: options.reviewTotalFiles ?? options.totalChangedFiles,
|
|
436
|
-
reviewMode: options.reviewMode,
|
|
437
|
-
baselineSha: options.baselineSha
|
|
438
|
-
}),
|
|
439
|
-
...options.fingerprint === void 0 ? [] : [renderFingerprintMarker(options.fingerprint)],
|
|
440
|
-
...options.stateMarker === void 0 ? [] : [options.stateMarker]
|
|
441
|
-
].join("\n");
|
|
442
|
-
const headBudget = 6e4 - tail.length - 1;
|
|
443
|
-
let concernsKept = sortedConcerns.length;
|
|
444
|
-
let demotedKept = sortedDemoted.length;
|
|
445
|
-
let omitted = 0;
|
|
446
|
-
let head = renderHead(concernsKept, demotedKept, omitted);
|
|
447
|
-
while (head.length > headBudget && (demotedKept > 0 || concernsKept > 0)) {
|
|
448
|
-
if (demotedKept > 0) demotedKept -= 1;
|
|
449
|
-
else concernsKept -= 1;
|
|
450
|
-
omitted += 1;
|
|
451
|
-
head = renderHead(concernsKept, demotedKept, omitted);
|
|
452
|
-
}
|
|
453
|
-
const body = `${head.slice(0, headBudget)}\n${tail}`;
|
|
454
|
-
return ReviewPublicationPlan.make({
|
|
455
|
-
event,
|
|
456
|
-
body,
|
|
457
|
-
comments,
|
|
458
|
-
demoted: demoted.map(({ finding }) => finding),
|
|
459
|
-
commitSha: options.headSha
|
|
460
|
-
});
|
|
461
|
-
};
|
|
462
|
-
//#endregion
|
|
463
|
-
//#region src/internal/run.ts
|
|
464
|
-
/**
|
|
465
|
-
* Run-level usage bounds on top of the definition's AgentPolicy. Real diffs
|
|
466
|
-
* are token-heavy, so the input budget is research-sized with cost as the
|
|
467
|
-
* safety net.
|
|
468
|
-
*/
|
|
469
|
-
const reviewBudgetLimits = UsageBudgetLimits.make({
|
|
470
|
-
maxInputTokens: 4e5,
|
|
471
|
-
maxOutputTokens: 16e3,
|
|
472
|
-
maxToolCalls: 24,
|
|
473
|
-
maxCostMicrousd: 2e6,
|
|
474
|
-
maxDurationMillis: 48e4
|
|
475
|
-
});
|
|
476
|
-
/**
|
|
477
|
-
* Run-level bounds for the fan-out coordinator. This budget observes only
|
|
478
|
-
* the COORDINATOR'S own usage — delegated children are bounded separately by
|
|
479
|
-
* the delegation's `SubagentPolicy` reservation and the child definition's
|
|
480
|
-
* own `AgentPolicy`, never silently by the parent's budget. The duration
|
|
481
|
-
* ceiling is wider because delegation Tool Calls hold the parent turn open
|
|
482
|
-
* while bounded children run.
|
|
483
|
-
*/
|
|
484
|
-
const fanOutReviewBudgetLimits = UsageBudgetLimits.make({
|
|
485
|
-
maxInputTokens: 4e5,
|
|
486
|
-
maxOutputTokens: 16e3,
|
|
487
|
-
maxToolCalls: 24,
|
|
488
|
-
maxCostMicrousd: 2e6,
|
|
489
|
-
maxDurationMillis: 9e5
|
|
490
|
-
});
|
|
491
|
-
/** Everything one review run produced, publication receipt included. */
|
|
492
|
-
var ReviewRunOutcome = class extends Schema.Class("@effect-agent/pr-review/ReviewRunOutcome")({
|
|
493
|
-
review: CodeReview,
|
|
494
|
-
/** All currently unresolved findings, including unchanged carried scope. */
|
|
495
|
-
activeFindings: Schema.Array(ReviewFinding).check(Schema.isMaxLength(20)),
|
|
496
|
-
/** All currently unresolved concerns, including concerns carried to final audit. */
|
|
497
|
-
activeConcerns: Schema.Array(ReviewConcern).check(Schema.isMaxLength(10)),
|
|
498
|
-
/** Host-owned structural coverage used by the Actions check conclusion. */
|
|
499
|
-
coverage: ReviewCoverage,
|
|
500
|
-
plan: ReviewPublicationPlan,
|
|
501
|
-
published: Schema.optionalKey(PublishedReview),
|
|
502
|
-
turns: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
503
|
-
/**
|
|
504
|
-
* The run budget's observed usage. For the fan-out reviewer this observes
|
|
505
|
-
* the COORDINATOR only — delegated children are bounded and accounted
|
|
506
|
-
* separately by their reservations.
|
|
507
|
-
*/
|
|
508
|
-
usage: Schema.optionalKey(UsageTotals),
|
|
509
|
-
/**
|
|
510
|
-
* What `usage` observed: the whole run, or a fan-out coordinator only.
|
|
511
|
-
* Absent when the caller declared no scope — consumers must not present
|
|
512
|
-
* unscoped usage as whole-run totals.
|
|
513
|
-
*/
|
|
514
|
-
usageScope: Schema.optionalKey(Schema.Literals(["run", "coordinator"])),
|
|
515
|
-
reviewMode: Schema.optionalKey(Schema.Literals(["incremental", "full"])),
|
|
516
|
-
reviewReason: Schema.optionalKey(Schema.String.check(Schema.isMaxLength(1e3))),
|
|
517
|
-
state: Schema.optionalKey(ReviewState)
|
|
518
|
-
}) {};
|
|
519
|
-
/** Build the mission one review run frames from the source's snapshot. */
|
|
520
|
-
const buildReviewMission = (metadata, files) => ReviewMission.make({
|
|
521
|
-
repository: metadata.repository,
|
|
522
|
-
number: metadata.number,
|
|
523
|
-
title: metadata.title,
|
|
524
|
-
body: metadata.body,
|
|
525
|
-
baseRef: metadata.baseRef,
|
|
526
|
-
headRef: metadata.headRef,
|
|
527
|
-
changedFileCount: files.length
|
|
528
|
-
});
|
|
529
|
-
/** Enforce the configured findings bound on an already-validated review. */
|
|
530
|
-
const enforceFindingsBound = (review, maxFindings) => review.findings.length <= maxFindings ? review : CodeReview.make({
|
|
531
|
-
summary: review.summary,
|
|
532
|
-
verdict: review.verdict,
|
|
533
|
-
findings: rankAndDedupeFindings(review.findings).slice(0, maxFindings),
|
|
534
|
-
...review.concerns !== void 0 ? { concerns: review.concerns } : {}
|
|
535
|
-
});
|
|
536
|
-
const findingKey = (finding) => `${finding.path}\u0000${finding.startLine}\u0000${finding.endLine}\u0000${finding.severity}\u0000${finding.title}`;
|
|
537
|
-
const severityRank = {
|
|
538
|
-
blocking: 0,
|
|
539
|
-
important: 1,
|
|
540
|
-
nit: 2
|
|
541
|
-
};
|
|
542
|
-
const rankAndDedupeConcerns = (concerns) => {
|
|
543
|
-
const byContent = /* @__PURE__ */ new Map();
|
|
544
|
-
for (const concern of concerns) {
|
|
545
|
-
const key = `${concern.title}\u0000${concern.body}`;
|
|
546
|
-
const previous = byContent.get(key);
|
|
547
|
-
if (previous === void 0 || severityRank[concern.severity] < severityRank[previous.severity]) byContent.set(key, concern);
|
|
548
|
-
}
|
|
549
|
-
return [...byContent.values()].sort((left, right) => severityRank[left.severity] - severityRank[right.severity]).slice(0, 10);
|
|
550
|
-
};
|
|
551
|
-
/**
|
|
552
|
-
* Execute one review with any explicit Agent Binding whose contract is
|
|
553
|
-
* `ReviewMission -> CodeReview` — the flat reviewer or the fan-out
|
|
554
|
-
* coordinator; the toolkit stays generic because publication only depends on
|
|
555
|
-
* the shared output contract. The binding stays a parameter (D-027): tests
|
|
556
|
-
* pass scripted models, hosts pass live provider bindings, and the model
|
|
557
|
-
* Layer's requirements stay visible in this Effect's `R`.
|
|
558
|
-
*/
|
|
559
|
-
const executeReview = (binding, options) => Effect.gen(function* () {
|
|
560
|
-
const source = yield* PullRequestSource;
|
|
561
|
-
const metadata = yield* source.metadata;
|
|
562
|
-
const files = yield* source.changedFiles;
|
|
563
|
-
const anchorFiles = yield* source.anchorFiles;
|
|
564
|
-
const executionContext = Option.getOrUndefined(yield* Effect.serviceOption(ReviewExecutionContext));
|
|
565
|
-
const mission = buildReviewMission(metadata, files);
|
|
566
|
-
const fullMission = buildReviewMission(metadata, anchorFiles);
|
|
567
|
-
const fingerprint = options.signature === void 0 ? void 0 : yield* computeChangesetFingerprint(anchorFiles, options.signature(fullMission));
|
|
568
|
-
const budget = yield* makeUsageBudget(options.limits ?? reviewBudgetLimits);
|
|
569
|
-
const detached = yield* AgentRuntime.start(binding, mission, {
|
|
570
|
-
budget: toRunBudgetHook(budget),
|
|
571
|
-
estimateCostMicrousd: () => Effect.succeed(500)
|
|
572
|
-
});
|
|
573
|
-
const result = yield* detached.await;
|
|
574
|
-
const events = yield* detached.events;
|
|
575
|
-
const decoded = yield* Schema.decodeUnknownEffect(CodeReview)(result.output);
|
|
576
|
-
const review = enforceFindingsBound(decoded, clampMaxFindings(options.maxFindings));
|
|
577
|
-
const usage = yield* budget.snapshot;
|
|
578
|
-
const affectedPaths = new Set(executionContext?.affectedPaths ?? files.flatMap((file) => file.previousPath === void 0 ? [file.path] : [file.path, file.previousPath]));
|
|
579
|
-
const priorState = executionContext?.mode === "incremental" ? executionContext.priorState : void 0;
|
|
580
|
-
const carriedCandidates = priorState?.unresolvedFindings.filter((finding) => !affectedPaths.has(finding.path)).map(fromStoredFinding) ?? [];
|
|
581
|
-
const activeFindings = rankAndDedupeFindings([...carriedCandidates, ...review.findings]).slice(0, clampMaxFindings(options.maxFindings));
|
|
582
|
-
const activeFindingKeys = new Set(activeFindings.map(findingKey));
|
|
583
|
-
const currentFindingKeys = new Set(review.findings.map(findingKey));
|
|
584
|
-
const carriedFindings = carriedCandidates.filter((finding) => activeFindingKeys.has(findingKey(finding)) && !currentFindingKeys.has(findingKey(finding)));
|
|
585
|
-
const carriedConcernCandidates = priorState?.unresolvedConcerns.map(fromStoredConcern) ?? [];
|
|
586
|
-
const activeConcerns = rankAndDedupeConcerns([...carriedConcernCandidates, ...review.concerns ?? []]);
|
|
587
|
-
const currentConcernKeys = new Set((review.concerns ?? []).map((concern) => `${concern.title}\u0000${concern.body}`));
|
|
588
|
-
const activeConcernKeys = new Set(activeConcerns.map((concern) => `${concern.title}\u0000${concern.body}`));
|
|
589
|
-
const carriedConcerns = carriedConcernCandidates.filter((concern) => {
|
|
590
|
-
const key = `${concern.title}\u0000${concern.body}`;
|
|
591
|
-
return activeConcernKeys.has(key) && !currentConcernKeys.has(key);
|
|
592
|
-
});
|
|
593
|
-
const reviewTotalFiles = executionContext?.totalFiles ?? metadata.totalChangedFiles;
|
|
594
|
-
const coverage = assessReviewCoverage({
|
|
595
|
-
shape: options.reviewShape ?? "flat",
|
|
596
|
-
files,
|
|
597
|
-
totalFiles: reviewTotalFiles,
|
|
598
|
-
anchorFiles,
|
|
599
|
-
totalAnchorFiles: metadata.totalChangedFiles,
|
|
600
|
-
events
|
|
601
|
-
});
|
|
602
|
-
const stateCandidate = executionContext !== void 0 && coverage.status === "complete" && fingerprint !== void 0 && metadata.baseSha !== void 0 && executionContext.stateAuthenticator?.status === "available" ? ReviewState.make({
|
|
603
|
-
version: 1,
|
|
604
|
-
repository: metadata.repository,
|
|
605
|
-
pullRequestNumber: metadata.number,
|
|
606
|
-
baseRef: metadata.baseRef,
|
|
607
|
-
baseSha: metadata.baseSha,
|
|
608
|
-
headRef: metadata.headRef,
|
|
609
|
-
reviewedHeadSha: metadata.headSha,
|
|
610
|
-
profileFingerprint: executionContext.profileFingerprint,
|
|
611
|
-
acceptedScopeFingerprint: fingerprint,
|
|
612
|
-
reviewedPathCount: anchorFiles.length,
|
|
613
|
-
unresolvedFindings: activeFindings.map(toStoredFinding),
|
|
614
|
-
unresolvedConcerns: activeConcerns.map(toStoredConcern),
|
|
615
|
-
lastReviewMode: executionContext.mode
|
|
616
|
-
}) : void 0;
|
|
617
|
-
const continuity = stateCandidate === void 0 || executionContext?.stateAuthenticator === void 0 ? {
|
|
618
|
-
state: void 0,
|
|
619
|
-
marker: void 0,
|
|
620
|
-
notice: executionContext?.stateAuthenticator?.status === "unavailable" && coverage.status === "complete" ? executionContext.stateAuthenticator.unavailableReason ?? "authenticated continuity state is unavailable" : void 0
|
|
621
|
-
} : yield* executionContext.stateAuthenticator.render(stateCandidate).pipe(Effect.match({
|
|
622
|
-
onFailure: (error) => ({
|
|
623
|
-
state: void 0,
|
|
624
|
-
marker: void 0,
|
|
625
|
-
notice: error._tag === "ReviewStateMarkerTooLarge" ? `authenticated continuity state exceeded its ${error.maximumChars}-character bound` : `authenticated continuity state could not be signed: ${error.reason}`
|
|
626
|
-
}),
|
|
627
|
-
onSuccess: (marker) => ({
|
|
628
|
-
state: stateCandidate,
|
|
629
|
-
marker,
|
|
630
|
-
notice: void 0
|
|
631
|
-
})
|
|
632
|
-
}));
|
|
633
|
-
const plan = planPublication(review, anchorFiles, {
|
|
634
|
-
applyVerdict: options.applyVerdict,
|
|
635
|
-
headSha: metadata.headSha,
|
|
636
|
-
totalChangedFiles: metadata.totalChangedFiles,
|
|
637
|
-
baseRef: metadata.baseRef,
|
|
638
|
-
headRef: metadata.headRef,
|
|
639
|
-
modelLabel: options.modelLabel,
|
|
640
|
-
runUrl: options.runUrl,
|
|
641
|
-
usage,
|
|
642
|
-
usageScope: options.usageScope,
|
|
643
|
-
fingerprint: coverage.status === "complete" ? fingerprint : void 0,
|
|
644
|
-
coverage,
|
|
645
|
-
carriedFindings,
|
|
646
|
-
carriedConcerns,
|
|
647
|
-
reviewMode: executionContext?.mode,
|
|
648
|
-
reviewReason: executionContext?.reason,
|
|
649
|
-
baselineSha: executionContext?.baselineSha,
|
|
650
|
-
reviewFilesVisible: files.length,
|
|
651
|
-
reviewTotalFiles,
|
|
652
|
-
stateMarker: continuity.marker,
|
|
653
|
-
stateNotice: continuity.notice
|
|
654
|
-
});
|
|
655
|
-
const scope = options.usageScope === void 0 ? {} : { usageScope: options.usageScope };
|
|
656
|
-
if (!options.post) return ReviewRunOutcome.make({
|
|
657
|
-
review,
|
|
658
|
-
activeFindings,
|
|
659
|
-
activeConcerns,
|
|
660
|
-
coverage,
|
|
661
|
-
plan,
|
|
662
|
-
turns: result.turns,
|
|
663
|
-
usage,
|
|
664
|
-
...scope,
|
|
665
|
-
...executionContext === void 0 ? {} : {
|
|
666
|
-
reviewMode: executionContext.mode,
|
|
667
|
-
reviewReason: executionContext.reason
|
|
668
|
-
},
|
|
669
|
-
...continuity.state === void 0 ? {} : { state: continuity.state }
|
|
670
|
-
});
|
|
671
|
-
const published = yield* (yield* ReviewPublisher).publish(plan);
|
|
672
|
-
return ReviewRunOutcome.make({
|
|
673
|
-
review,
|
|
674
|
-
activeFindings,
|
|
675
|
-
activeConcerns,
|
|
676
|
-
coverage,
|
|
677
|
-
plan,
|
|
678
|
-
published,
|
|
679
|
-
turns: result.turns,
|
|
680
|
-
usage,
|
|
681
|
-
...scope,
|
|
682
|
-
...executionContext === void 0 ? {} : {
|
|
683
|
-
reviewMode: executionContext.mode,
|
|
684
|
-
reviewReason: executionContext.reason
|
|
685
|
-
},
|
|
686
|
-
...continuity.state === void 0 ? {} : { state: continuity.state }
|
|
687
|
-
});
|
|
688
|
-
});
|
|
689
|
-
//#endregion
|
|
690
|
-
//#region src/internal/factory.ts
|
|
691
|
-
const EMPTY_TOOLS = [];
|
|
692
|
-
const requireReadonly = (tools) => {
|
|
693
|
-
for (const tool of tools) {
|
|
694
|
-
const executionClass = getToolExecutionClass(tool);
|
|
695
|
-
if (executionClass !== "readonly") throw new Error(`PrReview.make: extra tool '${tool.name}' declares execution class '${executionClass}'. The packaged reviewer's tool surface is read-only; annotate the tool with ToolExecutionClass "readonly" or run it outside the reviewer.`);
|
|
696
|
-
}
|
|
697
|
-
};
|
|
698
|
-
const provideIgnore = (effect, ignore) => ignore !== void 0 && ignore.length > 0 ? effect.pipe(Effect.provide(ignoringPullRequestSourceLayer(ignore))) : effect;
|
|
699
|
-
/**
|
|
700
|
-
* The changeset fingerprint of what this reviewer WOULD review right now:
|
|
701
|
-
* the ignore-filtered changeset hashed with the prompt signature. Identical
|
|
702
|
-
* fingerprints mean an identical review input surface — the basis for
|
|
703
|
-
* skipping re-reviews after content-free head changes (base auto-merges,
|
|
704
|
-
* equivalent rebases).
|
|
705
|
-
*/
|
|
706
|
-
const makeFingerprint = (signature, ignore) => provideIgnore(Effect.gen(function* () {
|
|
707
|
-
const source = yield* PullRequestSource;
|
|
708
|
-
const metadata = yield* source.metadata;
|
|
709
|
-
const files = yield* source.changedFiles;
|
|
710
|
-
return yield* computeChangesetFingerprint(files, signature(buildReviewMission(metadata, files)));
|
|
711
|
-
}), ignore);
|
|
712
|
-
const makeProfileFingerprint = (signature, ignore) => provideIgnore(Effect.gen(function* () {
|
|
713
|
-
const source = yield* PullRequestSource;
|
|
714
|
-
return yield* computeProfileFingerprint(signature(buildProfileMission(yield* source.metadata, yield* source.anchorFiles)));
|
|
715
|
-
}), ignore);
|
|
716
|
-
const makeReviewSnapshot = (ignore) => provideIgnore(Effect.gen(function* () {
|
|
717
|
-
const source = yield* PullRequestSource;
|
|
718
|
-
return {
|
|
719
|
-
metadata: yield* source.metadata,
|
|
720
|
-
files: yield* source.anchorFiles
|
|
721
|
-
};
|
|
722
|
-
}), ignore);
|
|
723
|
-
/**
|
|
724
|
-
* Build the flat reviewer: one bounded read-only agent over the whole
|
|
725
|
-
* changeset. Returns the model-agnostic definition, the explicit binding, and
|
|
726
|
-
* a `run` whose error and requirement channels stay fully inferred — the
|
|
727
|
-
* pull-request source, the publisher, extra tool handlers, and the Model
|
|
728
|
-
* Layer's requirements all remain visible to the caller.
|
|
729
|
-
*/
|
|
730
|
-
const make = (options) => {
|
|
731
|
-
const extraTools = options.extraTools ?? EMPTY_TOOLS;
|
|
732
|
-
requireReadonly(extraTools);
|
|
733
|
-
const definition = Agent.define("pr-reviewer", {
|
|
734
|
-
input: ReviewMission,
|
|
735
|
-
output: CodeReview,
|
|
736
|
-
instructions: makeReviewInstructions({
|
|
737
|
-
guidance: options.guidance,
|
|
738
|
-
maxFindings: options.maxFindings
|
|
739
|
-
}),
|
|
740
|
-
toolkit: Toolkit.make(ListChangedFiles, ReadFileDiff, ReadFile, ...extraTools),
|
|
741
|
-
policy: options.policy === void 0 ? defaultReviewPolicy : AgentPolicy.make(options.policy),
|
|
742
|
-
description: "Review one pull request read-only: list the changeset, read annotated diffs and head-file context, and return a structured, line-anchored code review.",
|
|
743
|
-
metadata: {
|
|
744
|
-
deploymentClass: "E",
|
|
745
|
-
surface: "read-only"
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
const binding = Object.freeze({
|
|
749
|
-
definition,
|
|
750
|
-
model: options.model
|
|
751
|
-
});
|
|
752
|
-
const signature = (mission) => [
|
|
753
|
-
definition.instructions(mission),
|
|
754
|
-
`applyVerdict=${String(options.applyVerdict ?? false)}`,
|
|
755
|
-
...options.modelLabel === void 0 ? [] : [`model=${options.modelLabel}`]
|
|
756
|
-
].join("\0");
|
|
757
|
-
const profileSignature = (mission) => [
|
|
758
|
-
"pr-review-profile-v1-flat",
|
|
759
|
-
JSON.stringify(resolveGuidance(options.guidance, mission)),
|
|
760
|
-
JSON.stringify(options.policy ?? {}),
|
|
761
|
-
JSON.stringify(extraTools.map((tool) => tool.name)),
|
|
762
|
-
JSON.stringify(options.ignore ?? []),
|
|
763
|
-
`maxFindings=${clampMaxFindings(options.maxFindings)}`,
|
|
764
|
-
`applyVerdict=${String(options.applyVerdict ?? false)}`,
|
|
765
|
-
...options.modelLabel === void 0 ? [] : [`model=${options.modelLabel}`]
|
|
766
|
-
].join("\0");
|
|
767
|
-
const run = (runOptions = {}) => provideIgnore(executeReview(binding, {
|
|
768
|
-
post: runOptions.post ?? false,
|
|
769
|
-
applyVerdict: options.applyVerdict ?? false,
|
|
770
|
-
limits: options.budget ?? reviewBudgetLimits,
|
|
771
|
-
maxFindings: clampMaxFindings(options.maxFindings),
|
|
772
|
-
signature,
|
|
773
|
-
modelLabel: options.modelLabel,
|
|
774
|
-
runUrl: runOptions.runUrl,
|
|
775
|
-
usageScope: "run",
|
|
776
|
-
reviewShape: "flat"
|
|
777
|
-
}).pipe(Effect.provide(Layer.mergeAll(ReviewToolkitLayer, IdGenerator.layer)), Effect.scoped), options.ignore);
|
|
778
|
-
return {
|
|
779
|
-
definition,
|
|
780
|
-
binding,
|
|
781
|
-
run,
|
|
782
|
-
fingerprint: makeFingerprint(signature, options.ignore),
|
|
783
|
-
profileFingerprint: makeProfileFingerprint(profileSignature, options.ignore),
|
|
784
|
-
snapshot: makeReviewSnapshot(options.ignore),
|
|
785
|
-
filterFiles: (files) => {
|
|
786
|
-
const ignored = compileIgnoreGlobs(options.ignore ?? []);
|
|
787
|
-
return files.filter((file) => !ignored(file.path));
|
|
788
|
-
}
|
|
789
|
-
};
|
|
790
|
-
};
|
|
791
|
-
/**
|
|
792
|
-
* Build the fan-out reviewer: a coordinator that delegates bounded per-unit
|
|
793
|
-
* file reviews to attached ephemeral children and merges their findings under
|
|
794
|
-
* the same output contract and the same fail-closed publication path as the
|
|
795
|
-
* flat reviewer. Child and coordinator execution bounds are packaged and not
|
|
796
|
-
* configurable here — the delegation reservation mirrors the child policy,
|
|
797
|
-
* and letting the two drift apart is a published-API hazard.
|
|
798
|
-
*/
|
|
799
|
-
const makeFanOut = (options) => {
|
|
800
|
-
const suite = makeFanOutReviewSuite({
|
|
801
|
-
guidance: options.guidance,
|
|
802
|
-
maxFindings: options.maxFindings
|
|
803
|
-
});
|
|
804
|
-
const binding = Object.freeze({
|
|
805
|
-
definition: suite.parent,
|
|
806
|
-
model: options.model
|
|
807
|
-
});
|
|
808
|
-
const childBinding = Object.freeze({
|
|
809
|
-
definition: suite.child,
|
|
810
|
-
model: options.model
|
|
811
|
-
});
|
|
812
|
-
const guidanceLines = options.guidance === void 0 ? [] : typeof options.guidance === "string" ? [options.guidance] : options.guidance;
|
|
813
|
-
const signature = (mission) => [
|
|
814
|
-
suite.parent.instructions(mission),
|
|
815
|
-
`childGuidance=${JSON.stringify(guidanceLines)}`,
|
|
816
|
-
`applyVerdict=${String(options.applyVerdict ?? false)}`,
|
|
817
|
-
...options.modelLabel === void 0 ? [] : [`model=${options.modelLabel}`]
|
|
818
|
-
].join(" ");
|
|
819
|
-
const profileSignature = (_mission) => [
|
|
820
|
-
"pr-review-profile-v1-fan-out",
|
|
821
|
-
JSON.stringify(guidanceLines),
|
|
822
|
-
JSON.stringify(options.ignore ?? []),
|
|
823
|
-
`maxFindings=${clampMaxFindings(options.maxFindings)}`,
|
|
824
|
-
`applyVerdict=${String(options.applyVerdict ?? false)}`,
|
|
825
|
-
...options.modelLabel === void 0 ? [] : [`model=${options.modelLabel}`]
|
|
826
|
-
].join("\0");
|
|
827
|
-
const delegationLayer = fanOutHandlersLayerFor(suite.delegation)(childBinding).pipe(Layer.provide(Layer.mergeAll(FileReviewToolkitLayer, SubagentReservationsMemoryLive, IdGenerator.layer)));
|
|
828
|
-
const run = (runOptions = {}) => provideIgnore(executeReview(binding, {
|
|
829
|
-
post: runOptions.post ?? false,
|
|
830
|
-
applyVerdict: options.applyVerdict ?? false,
|
|
831
|
-
limits: options.budget ?? fanOutReviewBudgetLimits,
|
|
832
|
-
maxFindings: clampMaxFindings(options.maxFindings),
|
|
833
|
-
signature,
|
|
834
|
-
modelLabel: options.modelLabel,
|
|
835
|
-
runUrl: runOptions.runUrl,
|
|
836
|
-
usageScope: "coordinator",
|
|
837
|
-
reviewShape: "fan-out"
|
|
838
|
-
}).pipe(Effect.provide(Layer.mergeAll(FanOutCoordinatorToolkitLayer, delegationLayer, IdGenerator.layer)), Effect.scoped), options.ignore);
|
|
839
|
-
return {
|
|
840
|
-
definition: suite.parent,
|
|
841
|
-
binding,
|
|
842
|
-
childBinding,
|
|
843
|
-
run,
|
|
844
|
-
fingerprint: makeFingerprint(signature, options.ignore),
|
|
845
|
-
profileFingerprint: makeProfileFingerprint(profileSignature, options.ignore),
|
|
846
|
-
snapshot: makeReviewSnapshot(options.ignore),
|
|
847
|
-
filterFiles: (files) => {
|
|
848
|
-
const ignored = compileIgnoreGlobs(options.ignore ?? []);
|
|
849
|
-
return files.filter((file) => !ignored(file.path));
|
|
850
|
-
}
|
|
851
|
-
};
|
|
852
|
-
};
|
|
853
|
-
/**
|
|
854
|
-
* The packaged pull-request reviewer factory.
|
|
855
|
-
*
|
|
856
|
-
* - `make` — one flat reviewer over the whole changeset.
|
|
857
|
-
* - `makeFanOut` — a coordinator delegating bounded per-unit child reviews.
|
|
858
|
-
*/
|
|
859
|
-
const PrReview = {
|
|
860
|
-
make,
|
|
861
|
-
makeFanOut
|
|
862
|
-
};
|
|
863
|
-
//#endregion
|
|
864
|
-
//#region src/internal/github-env.ts
|
|
865
|
-
/** The pull request could not be resolved from options or the environment. */
|
|
866
|
-
var ReviewTargetUnresolved = class extends Schema.TaggedError()("ReviewTargetUnresolved", { reason: Schema.String }) {
|
|
867
|
-
get message() {
|
|
868
|
-
return this.reason;
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
|
-
/** The slice of a GitHub Actions event payload this package understands. */
|
|
872
|
-
const GitHubEventWire = Schema.Struct({
|
|
873
|
-
pull_request: Schema.optionalKey(Schema.Struct({
|
|
874
|
-
number: Schema.Int,
|
|
875
|
-
draft: Schema.optionalKey(Schema.Boolean)
|
|
876
|
-
})),
|
|
877
|
-
repository: Schema.optionalKey(Schema.Struct({ full_name: Schema.String }))
|
|
878
|
-
});
|
|
879
|
-
const decodeEvent = Schema.decodeUnknownEffect(Schema.fromJsonString(GitHubEventWire));
|
|
880
|
-
/** Read and decode the GITHUB_EVENT_PATH payload, or none outside Actions. */
|
|
881
|
-
const readGitHubEvent = Effect.fn("readGitHubEvent")(function* () {
|
|
882
|
-
const eventPath = yield* Config.string("GITHUB_EVENT_PATH").pipe(Config.withDefault(""));
|
|
883
|
-
if (eventPath === "") return Option.none();
|
|
884
|
-
const raw = yield* (yield* FileSystem.FileSystem).readFileString(eventPath).pipe(Effect.mapError((error) => ReviewTargetUnresolved.make({ reason: `Cannot read event payload: ${error.message}` })));
|
|
885
|
-
const event = yield* decodeEvent(raw).pipe(Effect.mapError((error) => ReviewTargetUnresolved.make({ reason: `Cannot decode event payload: ${error.message}` })));
|
|
886
|
-
return Option.some(event);
|
|
887
|
-
});
|
|
888
|
-
/**
|
|
889
|
-
* Resolve the review target: explicit values win, then GITHUB_REPOSITORY and
|
|
890
|
-
* the pull_request event payload. Fails typed when no target can be named.
|
|
891
|
-
*/
|
|
892
|
-
const resolveReviewTarget = Effect.fn("resolveReviewTarget")(function* (options) {
|
|
893
|
-
let repository = options.repository ?? "";
|
|
894
|
-
if (repository === "") repository = yield* Config.string("GITHUB_REPOSITORY").pipe(Config.withDefault(""));
|
|
895
|
-
let number = options.number;
|
|
896
|
-
if (number === void 0 || repository === "") {
|
|
897
|
-
const event = yield* readGitHubEvent();
|
|
898
|
-
if (Option.isSome(event)) {
|
|
899
|
-
number ??= event.value.pull_request?.number;
|
|
900
|
-
if (repository === "") repository = event.value.repository?.full_name ?? "";
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
if (repository === "" || number === void 0) return yield* ReviewTargetUnresolved.make({ reason: "No pull request to review: pass an explicit repository and number, or run inside a GitHub Actions pull_request event." });
|
|
904
|
-
return {
|
|
905
|
-
repository,
|
|
906
|
-
number
|
|
907
|
-
};
|
|
908
|
-
});
|
|
909
|
-
/**
|
|
910
|
-
* Build the GitHub source and publisher Layers for one resolved target,
|
|
911
|
-
* reading GITHUB_API_URL and GITHUB_TOKEN from configuration. The returned
|
|
912
|
-
* Layer is the complete GitHub side of a review run.
|
|
913
|
-
*/
|
|
914
|
-
const gitHubReviewLayers = (target) => Layer.unwrap(Effect.gen(function* () {
|
|
915
|
-
const apiUrl = yield* Config.string("GITHUB_API_URL").pipe(Config.withDefault("https://api.github.com"));
|
|
916
|
-
const token = yield* Config.option(Config.redacted("GITHUB_TOKEN"));
|
|
917
|
-
const targetLayer = GitHubReviewTarget.layer({
|
|
918
|
-
apiUrl,
|
|
919
|
-
repository: target.repository,
|
|
920
|
-
number: target.number,
|
|
921
|
-
token
|
|
922
|
-
});
|
|
923
|
-
const deps = Layer.merge(targetLayer, FetchHttpClient.layer);
|
|
924
|
-
return Layer.mergeAll(gitHubPullRequestSourceLayer.pipe(Layer.provide(deps)), gitHubReviewPublisherLayer.pipe(Layer.provide(deps)), gitHubPriorReviewsLayer.pipe(Layer.provide(deps)));
|
|
925
|
-
}));
|
|
926
|
-
//#endregion
|
|
927
|
-
//#region src/internal/providers.ts
|
|
928
|
-
const DEFAULT_PROVIDER = "openai";
|
|
929
|
-
const DEFAULT_MODEL = {
|
|
930
|
-
openai: "gpt-5.6-sol",
|
|
931
|
-
anthropic: "claude-sonnet-5"
|
|
932
|
-
};
|
|
933
|
-
const PROVIDER_CREDENTIAL_ENV = {
|
|
934
|
-
openai: "OPENAI_API_KEY",
|
|
935
|
-
anthropic: "ANTHROPIC_API_KEY"
|
|
936
|
-
};
|
|
937
|
-
/**
|
|
938
|
-
* Each provider's offered reasoning-effort ladder, cheapest first. The rungs
|
|
939
|
-
* that turn reasoning off (`none`, `minimal`) are deliberately not offered —
|
|
940
|
-
* no review run wants them. An `EffortPosition` resolves into the running
|
|
941
|
-
* provider's own ladder, so the same stored position survives a provider or
|
|
942
|
-
* model change.
|
|
943
|
-
*/
|
|
944
|
-
const PROVIDER_EFFORT_RUNGS = {
|
|
945
|
-
openai: [
|
|
946
|
-
"low",
|
|
947
|
-
"medium",
|
|
948
|
-
"high",
|
|
949
|
-
"xhigh"
|
|
950
|
-
],
|
|
951
|
-
anthropic: [
|
|
952
|
-
"low",
|
|
953
|
-
"medium",
|
|
954
|
-
"high"
|
|
955
|
-
]
|
|
956
|
-
};
|
|
957
|
-
/** One OpenAI review model binding with the package's structured-output settings. */
|
|
958
|
-
const makeOpenAiReviewModel = (model, effort) => OpenAiLanguageModel.model(model ?? DEFAULT_MODEL.openai, {
|
|
959
|
-
max_output_tokens: 8e3,
|
|
960
|
-
store: false,
|
|
961
|
-
strictJsonSchema: true,
|
|
962
|
-
...effort === void 0 ? {} : { reasoning: { effort: resolveEffortRung(effort, PROVIDER_EFFORT_RUNGS.openai) } }
|
|
963
|
-
});
|
|
964
|
-
/** One Anthropic review model binding with the package's output settings. */
|
|
965
|
-
const makeAnthropicReviewModel = (model, effort) => AnthropicLanguageModel.model(model ?? DEFAULT_MODEL.anthropic, {
|
|
966
|
-
max_tokens: 8e3,
|
|
967
|
-
...effort === void 0 ? {} : { output_config: { effort: resolveEffortRung(effort, PROVIDER_EFFORT_RUNGS.anthropic) } }
|
|
968
|
-
});
|
|
969
|
-
/**
|
|
970
|
-
* The human-readable descriptor of one provider binding, e.g.
|
|
971
|
-
* `openai/gpt-5.6-sol (effort high)`. Rendered into the review footer and
|
|
972
|
-
* included in the changeset-fingerprint signature, so a provider, model, or
|
|
973
|
-
* effort change re-reviews instead of skipping.
|
|
974
|
-
*/
|
|
975
|
-
const describeReviewModel = (provider, model, effort) => {
|
|
976
|
-
const base = `${provider}/${model ?? DEFAULT_MODEL[provider]}`;
|
|
977
|
-
return effort === void 0 ? base : `${base} (effort ${resolveEffortRung(effort, PROVIDER_EFFORT_RUNGS[provider])})`;
|
|
978
|
-
};
|
|
979
|
-
/** The OpenAI client Layer, credential from `OPENAI_API_KEY`. */
|
|
980
|
-
const openAiClientLayer = OpenAiClient.layerConfig({ apiKey: Config.redacted(PROVIDER_CREDENTIAL_ENV.openai) }).pipe(Layer.provide(FetchHttpClient.layer));
|
|
981
|
-
/** The Anthropic client Layer, credential from `ANTHROPIC_API_KEY`. */
|
|
982
|
-
const anthropicClientLayer = AnthropicClient.layerConfig({ apiKey: Config.redacted(PROVIDER_CREDENTIAL_ENV.anthropic) }).pipe(Layer.provide(FetchHttpClient.layer));
|
|
983
|
-
//#endregion
|
|
984
|
-
export { InvalidEffortInput as A, ReviewEvent as C, compileIgnoreGlobs as D, planPublication as E, ReviewCoverage as F, ReviewShape as I, assessReviewCoverage as L, parseEffortPosition as M, resolveEffortRung as N, ignoringPullRequestSourceLayer as O, FailedReviewUnit as P, ReviewCommentDraft as S, anchorViolation as T, buildReviewMission as _, anthropicClientLayer as a, fanOutReviewBudgetLimits as b, makeOpenAiReviewModel as c, ReviewTargetUnresolved as d, gitHubReviewLayers as f, ReviewRunOutcome as g, PrReview as h, PROVIDER_EFFORT_RUNGS as i, isEffortPosition as j, EFFORT_ALIASES as k, openAiClientLayer as l, resolveReviewTarget as m, DEFAULT_PROVIDER as n, describeReviewModel as o, readGitHubEvent as p, PROVIDER_CREDENTIAL_ENV as r, makeAnthropicReviewModel as s, DEFAULT_MODEL as t, GitHubEventWire as u, enforceFindingsBound as v, ReviewPublicationPlan as w, reviewBudgetLimits as x, executeReview as y };
|
|
985
|
-
|
|
986
|
-
//# sourceMappingURL=providers-J6BKHyHe.mjs.map
|