@effect-agent/pr-review 0.1.0-beta.11 → 0.1.0-beta.111
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 +181 -172
- package/dist/Review.d.mts +294 -0
- package/dist/Review.mjs +738 -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 -718
- 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 +1111 -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-TrA9EUCr.d.mts +0 -1085
- package/dist/github-5TCFrxfX.mjs +0 -1676
- package/dist/github-5TCFrxfX.mjs.map +0 -1
- package/dist/index.mjs.map +0 -1
- package/dist/providers-DobNWMUn.mjs +0 -990
- package/dist/providers-DobNWMUn.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 -43
- 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 -148
- package/src/internal/github.ts +0 -774
- package/src/internal/ignore.ts +0 -88
- package/src/internal/profiles.ts +0 -79
- package/src/internal/providers.ts +0 -94
- 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/render.ts
DELETED
|
@@ -1,428 +0,0 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
2
|
-
|
|
3
|
-
import type { ReviewCoverage } from "./coverage.ts";
|
|
4
|
-
import { commentableLines, type ChangedFile } from "./diff.ts";
|
|
5
|
-
import { renderFingerprintMarker } from "./fingerprint.ts";
|
|
6
|
-
import { ReviewFinding, type CodeReview, type ReviewConcern } from "./review-agent.ts";
|
|
7
|
-
import type { ReviewScopeMode, ReviewStateMarker } from "./review-state.ts";
|
|
8
|
-
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Publication planning: pure, deterministic, and fail-closed. Model output is
|
|
11
|
-
// untrusted input, so every finding anchor is validated against the parsed
|
|
12
|
-
// diff before it may become an inline comment; findings that fail validation
|
|
13
|
-
// are demoted into the review body instead of being dropped or trusted. This
|
|
14
|
-
// module is deliberately not configurable — customization widens what goes
|
|
15
|
-
// into a review, never what leaves it unvalidated.
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
export const ReviewEvent = Schema.Literals(["COMMENT", "APPROVE", "REQUEST_CHANGES"]);
|
|
19
|
-
export type ReviewEvent = typeof ReviewEvent.Type;
|
|
20
|
-
|
|
21
|
-
/** One inline comment exactly as the GitHub review API accepts it. */
|
|
22
|
-
export class ReviewCommentDraft extends Schema.Class<ReviewCommentDraft>(
|
|
23
|
-
"@effect-agent/pr-review/ReviewCommentDraft",
|
|
24
|
-
)({
|
|
25
|
-
path: Schema.NonEmptyString,
|
|
26
|
-
/** The last (or only) commented line, RIGHT side of the diff. */
|
|
27
|
-
line: Schema.Int.check(Schema.isGreaterThan(0)),
|
|
28
|
-
/** Present only for multi-line comments; strictly less than `line`. */
|
|
29
|
-
startLine: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
30
|
-
body: Schema.NonEmptyString,
|
|
31
|
-
}) {}
|
|
32
|
-
|
|
33
|
-
/** The complete, validated review ready for one GitHub reviews API call. */
|
|
34
|
-
export class ReviewPublicationPlan extends Schema.Class<ReviewPublicationPlan>(
|
|
35
|
-
"@effect-agent/pr-review/ReviewPublicationPlan",
|
|
36
|
-
)({
|
|
37
|
-
event: ReviewEvent,
|
|
38
|
-
body: Schema.String.check(Schema.isMaxLength(60_000)),
|
|
39
|
-
comments: Schema.Array(ReviewCommentDraft),
|
|
40
|
-
/** Findings whose anchors failed diff validation; folded into `body`. */
|
|
41
|
-
demoted: Schema.Array(ReviewFinding),
|
|
42
|
-
/** The head commit the diffs were fetched at; pins the posted review. */
|
|
43
|
-
commitSha: Schema.NonEmptyString.check(Schema.isMaxLength(64)),
|
|
44
|
-
}) {}
|
|
45
|
-
|
|
46
|
-
const severityEmoji: Record<ReviewFinding["severity"], string> = {
|
|
47
|
-
blocking: "🛑",
|
|
48
|
-
important: "⚠️",
|
|
49
|
-
nit: "💅",
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const severityRank: Record<ReviewFinding["severity"], number> = {
|
|
53
|
-
blocking: 0,
|
|
54
|
-
important: 1,
|
|
55
|
-
nit: 2,
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const severityLabel: Record<ReviewFinding["severity"], string> = {
|
|
59
|
-
blocking: `${severityEmoji.blocking} blocking`,
|
|
60
|
-
important: `${severityEmoji.important} important`,
|
|
61
|
-
nit: `${severityEmoji.nit} nit`,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
/** A fence long enough that the suggestion content can never close it early. */
|
|
65
|
-
const suggestionFence = (suggestion: string): string => {
|
|
66
|
-
let fence = "```";
|
|
67
|
-
while (suggestion.includes(fence)) fence = `${fence}\``;
|
|
68
|
-
return fence;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const renderCommentBody = (finding: ReviewFinding): string => {
|
|
72
|
-
const parts = [`**[${severityLabel[finding.severity]}] ${finding.title}**`, "", finding.body];
|
|
73
|
-
if (finding.suggestion !== undefined) {
|
|
74
|
-
const fence = suggestionFence(finding.suggestion);
|
|
75
|
-
parts.push("", `${fence}suggestion`, finding.suggestion, fence);
|
|
76
|
-
}
|
|
77
|
-
return parts.join("\n");
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const renderDemoted = (finding: ReviewFinding, reason: string): string => {
|
|
81
|
-
const location = `\`${finding.path}:${finding.startLine}${
|
|
82
|
-
finding.endLine !== finding.startLine ? `-${finding.endLine}` : ""
|
|
83
|
-
}\``;
|
|
84
|
-
return `- ${location} **[${severityLabel[finding.severity]}] ${finding.title}** — ${finding.body} _(demoted: ${reason})_`;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const countNoun = (count: number, noun: string): string =>
|
|
88
|
-
`${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
89
|
-
|
|
90
|
-
/** The validated finding + concern severities, tallied for callout and event. */
|
|
91
|
-
const severityCounts = (
|
|
92
|
-
review: CodeReview,
|
|
93
|
-
carriedFindings: ReadonlyArray<ReviewFinding> = [],
|
|
94
|
-
carriedConcerns: ReadonlyArray<ReviewConcern> = [],
|
|
95
|
-
) => {
|
|
96
|
-
const severities = [
|
|
97
|
-
...review.findings.map((finding) => finding.severity),
|
|
98
|
-
...(review.concerns ?? []).map((concern) => concern.severity),
|
|
99
|
-
...carriedFindings.map((finding) => finding.severity),
|
|
100
|
-
...carriedConcerns.map((concern) => concern.severity),
|
|
101
|
-
];
|
|
102
|
-
return {
|
|
103
|
-
blocking: severities.filter((severity) => severity === "blocking").length,
|
|
104
|
-
important: severities.filter((severity) => severity === "important").length,
|
|
105
|
-
total: severities.length,
|
|
106
|
-
};
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* The opening callout: the review's overall tier, derived HOST-SIDE from the
|
|
111
|
-
* validated severities (never from model prose), described by what GitHub
|
|
112
|
-
* renders it as. `[!CAUTION]` is a red banner, `[!IMPORTANT]` a purple one;
|
|
113
|
-
* the blockquote tiers read as informational.
|
|
114
|
-
*/
|
|
115
|
-
const renderVerdictCallout = (
|
|
116
|
-
review: CodeReview,
|
|
117
|
-
options: {
|
|
118
|
-
readonly carriedFindings?: ReadonlyArray<ReviewFinding> | undefined;
|
|
119
|
-
readonly carriedConcerns?: ReadonlyArray<ReviewConcern> | undefined;
|
|
120
|
-
readonly coverage?: ReviewCoverage | undefined;
|
|
121
|
-
},
|
|
122
|
-
): string => {
|
|
123
|
-
const counts = severityCounts(review, options.carriedFindings, options.carriedConcerns);
|
|
124
|
-
if (options.coverage?.status === "incomplete") {
|
|
125
|
-
const suffix =
|
|
126
|
-
counts.blocking > 0 ? ` It also has ${countNoun(counts.blocking, "blocking finding")}.` : "";
|
|
127
|
-
return `> [!CAUTION]\n> Review coverage is incomplete — the check must not pass.${suffix}`;
|
|
128
|
-
}
|
|
129
|
-
if (counts.blocking > 0) {
|
|
130
|
-
return `> [!CAUTION]\n> ${countNoun(counts.blocking, "blocking finding")} — do not merge before addressing ${counts.blocking === 1 ? "it" : "them"}.`;
|
|
131
|
-
}
|
|
132
|
-
if (counts.important > 0) {
|
|
133
|
-
return `> [!IMPORTANT]\n> ${countNoun(counts.important, "important finding")} to address before merging.`;
|
|
134
|
-
}
|
|
135
|
-
if (counts.total > 0) {
|
|
136
|
-
return "> ℹ️ Minor suggestions only — mergeable as-is.";
|
|
137
|
-
}
|
|
138
|
-
return review.verdict === "approve"
|
|
139
|
-
? "> ✅ No issues found."
|
|
140
|
-
: "> ℹ️ No findings — see the summary.";
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const renderConcern = (concern: ReviewConcern): string =>
|
|
144
|
-
[`### ${severityEmoji[concern.severity]} ${concern.title}`, "", concern.body].join("\n");
|
|
145
|
-
|
|
146
|
-
const renderCarriedFinding = (finding: ReviewFinding): string =>
|
|
147
|
-
`- \`${finding.path}:${finding.startLine}${finding.endLine === finding.startLine ? "" : `-${finding.endLine}`}\` **[${severityLabel[finding.severity]}] ${finding.title}** — ${finding.body}`;
|
|
148
|
-
|
|
149
|
-
/** HTML comments must not contain `--`; interpolated values are sanitized. */
|
|
150
|
-
const commentSafe = (value: string): string => value.replaceAll("--", "- -");
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* The invisible staleness note addressed to whoever reads the review later —
|
|
154
|
-
* a human or a downstream agent: which commit the findings were written
|
|
155
|
-
* against, and that line callouts age the moment new commits land.
|
|
156
|
-
*/
|
|
157
|
-
const renderReviewMetadata = (options: {
|
|
158
|
-
readonly headSha: string;
|
|
159
|
-
readonly baseRef?: string | undefined;
|
|
160
|
-
readonly headRef?: string | undefined;
|
|
161
|
-
readonly filesVisible: number;
|
|
162
|
-
readonly totalChangedFiles: number;
|
|
163
|
-
readonly reviewMode?: ReviewScopeMode | undefined;
|
|
164
|
-
readonly baselineSha?: string | undefined;
|
|
165
|
-
}): string =>
|
|
166
|
-
[
|
|
167
|
-
"<!-- effect-agent-pr-review metadata",
|
|
168
|
-
`reviewed-head: ${commentSafe(options.headSha)}`,
|
|
169
|
-
...(options.baseRef !== undefined && options.headRef !== undefined
|
|
170
|
-
? [`base-ref: ${commentSafe(options.baseRef)}`, `head-ref: ${commentSafe(options.headRef)}`]
|
|
171
|
-
: []),
|
|
172
|
-
// The observation surface, not a coverage claim: the host cannot know
|
|
173
|
-
// which visible files the model actually examined, and the summary is
|
|
174
|
-
// where unreviewed units are named.
|
|
175
|
-
`files-visible: ${options.filesVisible} of ${options.totalChangedFiles}`,
|
|
176
|
-
...(options.reviewMode === undefined ? [] : [`review-mode: ${options.reviewMode}`]),
|
|
177
|
-
...(options.baselineSha === undefined
|
|
178
|
-
? []
|
|
179
|
-
: [`incremental-baseline: ${commentSafe(options.baselineSha)}`]),
|
|
180
|
-
"Findings were written against the head commit above; if commits have landed",
|
|
181
|
-
"since, treat file and line callouts as potentially stale and re-diff first.",
|
|
182
|
-
"-->",
|
|
183
|
-
].join("\n");
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Why one finding cannot become an inline comment, or undefined when it can.
|
|
187
|
-
* Exported so tests can pin each rule individually.
|
|
188
|
-
*/
|
|
189
|
-
export const anchorViolation = (
|
|
190
|
-
finding: ReviewFinding,
|
|
191
|
-
files: ReadonlyArray<ChangedFile>,
|
|
192
|
-
): string | undefined => {
|
|
193
|
-
const file = files.find((candidate) => candidate.path === finding.path);
|
|
194
|
-
if (file === undefined) return "path is not part of the changeset";
|
|
195
|
-
if (file.patch === undefined) return "file has no textual diff";
|
|
196
|
-
if (finding.endLine < finding.startLine) return "endLine precedes startLine";
|
|
197
|
-
if (finding.endLine - finding.startLine + 1 > 100) return "range is implausibly large";
|
|
198
|
-
const anchors = commentableLines(file.patch);
|
|
199
|
-
for (let line = finding.startLine; line <= finding.endLine; line += 1) {
|
|
200
|
-
if (!anchors.has(line)) return `line ${line} is not part of the diff`;
|
|
201
|
-
}
|
|
202
|
-
return undefined;
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Turn one validated review into the exact GitHub publication payload.
|
|
207
|
-
* `applyVerdict: false` (the safe default) always posts a COMMENT review;
|
|
208
|
-
* `true` maps the model's verdict onto APPROVE / REQUEST_CHANGES.
|
|
209
|
-
*/
|
|
210
|
-
export const planPublication = (
|
|
211
|
-
review: CodeReview,
|
|
212
|
-
files: ReadonlyArray<ChangedFile>,
|
|
213
|
-
options: {
|
|
214
|
-
readonly applyVerdict: boolean;
|
|
215
|
-
/** Head commit the changeset was fetched at (pins the posted review). */
|
|
216
|
-
readonly headSha: string;
|
|
217
|
-
/** GitHub's changed-file total, for honest truncation reporting. */
|
|
218
|
-
readonly totalChangedFiles: number;
|
|
219
|
-
/** Base/head refs for the staleness metadata comment. */
|
|
220
|
-
readonly baseRef?: string | undefined;
|
|
221
|
-
readonly headRef?: string | undefined;
|
|
222
|
-
/** Provider binding descriptor rendered into the footer. */
|
|
223
|
-
readonly modelLabel?: string | undefined;
|
|
224
|
-
/** Workflow-run URL rendered into the footer. */
|
|
225
|
-
readonly runUrl?: string | undefined;
|
|
226
|
-
/** Observed run usage rendered into the footer. */
|
|
227
|
-
readonly usage?: { readonly inputTokens: number; readonly outputTokens: number } | undefined;
|
|
228
|
-
/** What the usage observed: the whole run, or the coordinator only. */
|
|
229
|
-
readonly usageScope?: "run" | "coordinator" | undefined;
|
|
230
|
-
/**
|
|
231
|
-
* Changeset fingerprint embedded invisibly in the review body so a later
|
|
232
|
-
* run can skip re-reviewing an unchanged changeset.
|
|
233
|
-
*/
|
|
234
|
-
readonly fingerprint?: string | undefined;
|
|
235
|
-
/** Host-owned coverage; incomplete coverage is rendered and fails the check. */
|
|
236
|
-
readonly coverage?: ReviewCoverage | undefined;
|
|
237
|
-
/** Unchanged unresolved items carried from the prior successfully reviewed head. */
|
|
238
|
-
readonly carriedFindings?: ReadonlyArray<ReviewFinding> | undefined;
|
|
239
|
-
readonly carriedConcerns?: ReadonlyArray<ReviewConcern> | undefined;
|
|
240
|
-
/** Selected review scope, made visible whenever orchestration chose it. */
|
|
241
|
-
readonly reviewMode?: ReviewScopeMode | undefined;
|
|
242
|
-
readonly reviewReason?: string | undefined;
|
|
243
|
-
readonly baselineSha?: string | undefined;
|
|
244
|
-
readonly reviewFilesVisible?: number | undefined;
|
|
245
|
-
readonly reviewTotalFiles?: number | undefined;
|
|
246
|
-
/** Authenticated continuity state is emitted only after complete host-owned coverage. */
|
|
247
|
-
readonly stateMarker?: ReviewStateMarker | undefined;
|
|
248
|
-
/** Visible reason continuity state was omitted; the next run will review fully. */
|
|
249
|
-
readonly stateNotice?: string | undefined;
|
|
250
|
-
},
|
|
251
|
-
): ReviewPublicationPlan => {
|
|
252
|
-
const comments: Array<ReviewCommentDraft> = [];
|
|
253
|
-
const demoted: Array<{ readonly finding: ReviewFinding; readonly reason: string }> = [];
|
|
254
|
-
for (const finding of review.findings) {
|
|
255
|
-
const violation = anchorViolation(finding, files);
|
|
256
|
-
if (violation === undefined) {
|
|
257
|
-
comments.push(
|
|
258
|
-
ReviewCommentDraft.make({
|
|
259
|
-
path: finding.path,
|
|
260
|
-
line: finding.endLine,
|
|
261
|
-
...(finding.endLine > finding.startLine ? { startLine: finding.startLine } : {}),
|
|
262
|
-
body: renderCommentBody(finding),
|
|
263
|
-
}),
|
|
264
|
-
);
|
|
265
|
-
} else {
|
|
266
|
-
demoted.push({ finding, reason: violation });
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// Rendered most-severe first so the size cap below sheds the least severe.
|
|
271
|
-
const sortedConcerns = [...(review.concerns ?? [])].sort(
|
|
272
|
-
(a, b) => severityRank[a.severity] - severityRank[b.severity],
|
|
273
|
-
);
|
|
274
|
-
const sortedDemoted = [...demoted].sort(
|
|
275
|
-
(a, b) => severityRank[a.finding.severity] - severityRank[b.finding.severity],
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
const footerParts = ["Automated review by @effect-agent/pr-review"];
|
|
279
|
-
if (options.modelLabel !== undefined) footerParts.push(options.modelLabel);
|
|
280
|
-
// Usage renders only under an EXPLICIT scope: this planner cannot know
|
|
281
|
-
// whether a budget snapshot observed the whole run or only a fan-out
|
|
282
|
-
// coordinator, and omitting the number is honest where mislabeling is not.
|
|
283
|
-
if (options.usage !== undefined && options.usageScope !== undefined) {
|
|
284
|
-
const scope = options.usageScope === "coordinator" ? " (coordinator)" : "";
|
|
285
|
-
footerParts.push(
|
|
286
|
-
`${options.usage.inputTokens} in / ${options.usage.outputTokens} out tokens${scope}`,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
if (options.runUrl !== undefined) footerParts.push(`[run](${options.runUrl})`);
|
|
290
|
-
footerParts.push(`reviewed at ${options.headSha.slice(0, 7)}`);
|
|
291
|
-
const footer = `_${footerParts.join(" · ")}._`;
|
|
292
|
-
|
|
293
|
-
const renderHead = (concernsKept: number, demotedKept: number, omitted: number): string => {
|
|
294
|
-
const carriedFindings = options.carriedFindings ?? [];
|
|
295
|
-
const carriedConcerns = options.carriedConcerns ?? [];
|
|
296
|
-
const parts = [
|
|
297
|
-
renderVerdictCallout(review, {
|
|
298
|
-
carriedFindings,
|
|
299
|
-
carriedConcerns,
|
|
300
|
-
coverage: options.coverage,
|
|
301
|
-
}),
|
|
302
|
-
];
|
|
303
|
-
if (options.reviewMode !== undefined && options.reviewReason !== undefined) {
|
|
304
|
-
parts.push(
|
|
305
|
-
"",
|
|
306
|
-
options.reviewMode === "incremental"
|
|
307
|
-
? `**Incremental scope:** reviewed ${options.reviewFilesVisible ?? files.length} file(s) ${options.reviewReason}. Unchanged accepted scope was preserved and not reopened.`
|
|
308
|
-
: `**Full-diff scope:** ${options.reviewReason}.`,
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
if (options.stateNotice !== undefined) {
|
|
312
|
-
parts.push(
|
|
313
|
-
"",
|
|
314
|
-
`⚠️ Continuity state was not stored (${options.stateNotice.slice(0, 1_000)}); the next run will safely review the full diff.`,
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
parts.push("", review.summary);
|
|
318
|
-
if (options.coverage?.status === "incomplete") {
|
|
319
|
-
parts.push(
|
|
320
|
-
"",
|
|
321
|
-
"### 🛑 Incomplete coverage",
|
|
322
|
-
"",
|
|
323
|
-
...options.coverage.reasons.map((reason) => `- ${reason}`),
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
if (carriedFindings.length > 0) {
|
|
327
|
-
parts.push(
|
|
328
|
-
"",
|
|
329
|
-
"### Unresolved findings carried from unchanged scope",
|
|
330
|
-
"",
|
|
331
|
-
...carriedFindings.map(renderCarriedFinding),
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
if (carriedConcerns.length > 0) {
|
|
335
|
-
parts.push("", "### Unresolved concerns carried to the final audit");
|
|
336
|
-
for (const concern of carriedConcerns) parts.push("", renderConcern(concern));
|
|
337
|
-
}
|
|
338
|
-
for (const concern of sortedConcerns.slice(0, concernsKept)) {
|
|
339
|
-
parts.push("", renderConcern(concern));
|
|
340
|
-
}
|
|
341
|
-
if (files.length < options.totalChangedFiles) {
|
|
342
|
-
parts.push(
|
|
343
|
-
"",
|
|
344
|
-
`⚠️ Reviewed ${files.length} of ${options.totalChangedFiles} changed files — the changeset exceeded the reviewer's file bound.`,
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
if (demotedKept > 0) {
|
|
348
|
-
parts.push(
|
|
349
|
-
"",
|
|
350
|
-
"### Findings without a valid diff anchor",
|
|
351
|
-
...sortedDemoted
|
|
352
|
-
.slice(0, demotedKept)
|
|
353
|
-
.map(({ finding, reason }) => renderDemoted(finding, reason)),
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
if (omitted > 0) {
|
|
357
|
-
parts.push(
|
|
358
|
-
"",
|
|
359
|
-
`⚠️ ${countNoun(omitted, "review item")} omitted — the body exceeded GitHub's review size cap.`,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
parts.push("", footer);
|
|
363
|
-
return parts.join("\n");
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
// The model's verdict may not contradict the reported severities (model
|
|
367
|
-
// output is untrusted input): any blocking item forces REQUEST_CHANGES, a
|
|
368
|
-
// review with no blocking item can never REQUEST_CHANGES, and an approval
|
|
369
|
-
// is honored only when nothing blocking or important was reported — the
|
|
370
|
-
// event always agrees with the callout tier. Demoted findings and concerns
|
|
371
|
-
// count like anchored findings: anchor validation validates LOCATIONS, not
|
|
372
|
-
// truth, so severity is equally model-claimed for all three, and counting
|
|
373
|
-
// them only ever moves the event toward the closed direction.
|
|
374
|
-
const counts = severityCounts(
|
|
375
|
-
review,
|
|
376
|
-
options.carriedFindings ?? [],
|
|
377
|
-
options.carriedConcerns ?? [],
|
|
378
|
-
);
|
|
379
|
-
const event: ReviewEvent = !options.applyVerdict
|
|
380
|
-
? "COMMENT"
|
|
381
|
-
: options.coverage?.status === "incomplete" || counts.blocking > 0
|
|
382
|
-
? "REQUEST_CHANGES"
|
|
383
|
-
: review.verdict === "approve" && counts.important === 0
|
|
384
|
-
? "APPROVE"
|
|
385
|
-
: "COMMENT";
|
|
386
|
-
|
|
387
|
-
// The invisible tail (metadata + fingerprint marker) must survive the body
|
|
388
|
-
// cap, so the cap reserves exactly the room it needs.
|
|
389
|
-
const tail = [
|
|
390
|
-
renderReviewMetadata({
|
|
391
|
-
headSha: options.headSha,
|
|
392
|
-
baseRef: options.baseRef,
|
|
393
|
-
headRef: options.headRef,
|
|
394
|
-
filesVisible: options.reviewFilesVisible ?? files.length,
|
|
395
|
-
totalChangedFiles: options.reviewTotalFiles ?? options.totalChangedFiles,
|
|
396
|
-
reviewMode: options.reviewMode,
|
|
397
|
-
baselineSha: options.baselineSha,
|
|
398
|
-
}),
|
|
399
|
-
...(options.fingerprint === undefined ? [] : [renderFingerprintMarker(options.fingerprint)]),
|
|
400
|
-
...(options.stateMarker === undefined ? [] : [options.stateMarker]),
|
|
401
|
-
].join("\n");
|
|
402
|
-
const headBudget = 60_000 - tail.length - 1;
|
|
403
|
-
|
|
404
|
-
// Shed whole trailing items — demoted bullets first (they already failed
|
|
405
|
-
// validation), then concerns — instead of slicing markdown mid-block. Every
|
|
406
|
-
// omission is announced, and `plan.demoted` keeps the full data regardless.
|
|
407
|
-
let concernsKept = sortedConcerns.length;
|
|
408
|
-
let demotedKept = sortedDemoted.length;
|
|
409
|
-
let omitted = 0;
|
|
410
|
-
let head = renderHead(concernsKept, demotedKept, omitted);
|
|
411
|
-
while (head.length > headBudget && (demotedKept > 0 || concernsKept > 0)) {
|
|
412
|
-
if (demotedKept > 0) demotedKept -= 1;
|
|
413
|
-
else concernsKept -= 1;
|
|
414
|
-
omitted += 1;
|
|
415
|
-
head = renderHead(concernsKept, demotedKept, omitted);
|
|
416
|
-
}
|
|
417
|
-
// Last resort for a pathological summary; unreachable while the CodeReview
|
|
418
|
-
// schema caps the summary well below the budget.
|
|
419
|
-
const body = `${head.slice(0, headBudget)}\n${tail}`;
|
|
420
|
-
|
|
421
|
-
return ReviewPublicationPlan.make({
|
|
422
|
-
event,
|
|
423
|
-
body,
|
|
424
|
-
comments,
|
|
425
|
-
demoted: demoted.map(({ finding }) => finding),
|
|
426
|
-
commitSha: options.headSha,
|
|
427
|
-
});
|
|
428
|
-
};
|